版本記錄
| 版本號 | 時(shí)間 |
|---|---|
| V1.0 | 2021.01.02 星期六 |
前言
今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動(dòng)畫的實(shí)現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動(dòng)畫的實(shí)現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細(xì)解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
13. SwiftUI框架詳細(xì)解析 (十三) —— 基于SwiftUI創(chuàng)建Mind-Map UI(二)
14. SwiftUI框架詳細(xì)解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
15. SwiftUI框架詳細(xì)解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)
開始
首先看下主要內(nèi)容:
在本教程中,您將在
SwiftUI中創(chuàng)建社交媒體應(yīng)用的個(gè)人資料頁面時(shí),學(xué)習(xí)有關(guān)iOS的Dependency Injection的信息。內(nèi)容來自翻譯。
接著看下寫作環(huán)境
Swift 5, iOS 14, Xcode 12
接著就是正文啦。
程序員開發(fā)了許多體系結(jié)構(gòu),設(shè)計(jì)模式和編程風(fēng)格。 雖然它們各自解決不同的問題,但它們都有助于使代碼更具可讀性,可測試性和靈活性。
Inversion of Control 因其效率而廣受歡迎。 在本教程中,您將通過Dependency Injection, or DI模式應(yīng)用此原理。 您無需編寫第三方框架,而是編寫自己的小型Dependency Injection解決方案,并使用它來重構(gòu)應(yīng)用程序并添加一些新功能。
如果您不知道IoC或DI的全部含義,那么沒問題,您將很快了解更多。
注意:這是涉及
SwiftUI的中級iOS教程。 如果您不熟悉SwiftUI,請查看SwiftUI視頻課程SwiftUI video course。
打開入門項(xiàng)目。 打開入門項(xiàng)目并運(yùn)行該應(yīng)用程序:

您會從社交媒體應(yīng)用程序中看到一個(gè)配置文件屏幕,其中包含許多用戶數(shù)據(jù):個(gè)人簡歷,朋友,照片和信息。 與任何社交網(wǎng)絡(luò)一樣,用戶隱私和互聯(lián)網(wǎng)安全至關(guān)重要。
您的目標(biāo)是讓用戶控制他們與其他用戶共享的信息。 另外,您還可以讓他們根據(jù)他們與給定用戶的關(guān)系來調(diào)整隱私規(guī)則。
在使他們能夠控制并了解有關(guān)依賴注入(Dependency Injection)及其如何幫助您的更多信息之前,您需要確定問題。
1. Identifying the Issue
打開ProfileView.swift并仔細(xì)查看ProfileView的body:
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: true) {
VStack {
// 1
ProfileHeaderView(
user: user,
canSendMessage: privacyLevel == .friend,
canStartVideoChat: privacyLevel == .friend
)
// 2
if privacyLevel == .friend {
UsersView(title: "Friends", users: user.friends)
PhotosView(photos: user.photos)
HistoryFeedView(posts: user.historyFeed)
} else {
// 3
RestrictedAccessView()
}
}
}.navigationTitle("Profile")
}
}
以下是代碼細(xì)分:
- 1) 您將
ProfileHeaderView添加到VStack的頂部,并指定僅當(dāng)用戶是朋友時(shí),消息和視頻通話選項(xiàng)才可用。 - 2) 如果用戶是朋友,則顯示朋友列表,照片和帖子。
- 3) 否則,您將顯示
RestrictedAccessView。
ProfileView頂部的privacyLevel值定義查看您的個(gè)人資料的用戶的訪問級別。 將privacyLevel更改為.everyone并運(yùn)行該應(yīng)用程序以查看您的個(gè)人資料,就好像您不在好友列表中一樣:

已經(jīng)有基本的隱私控制措施。 但是,用戶無法選擇誰可以看到其個(gè)人資料的哪些部分。 僅有兩個(gè)隱私級別是不夠的。
當(dāng)前,ProfileView根據(jù)隱私級別決定顯示哪些視圖。 由于以下幾個(gè)原因,這不是一個(gè)合適的解決方案:
- 它不是很容易測試。 盡管可以用UI測試進(jìn)行覆蓋,但運(yùn)行起來要比單元測試或集成測試昂貴。
- 每次您決定擴(kuò)展或修改應(yīng)用程序的功能時(shí),
ProfileView都需要進(jìn)行大量調(diào)整。 它與PrivacyLevel緊密結(jié)合,并承擔(dān)了比所需更多的責(zé)任。 - 隨著應(yīng)用程序的復(fù)雜性和功能的增長,維護(hù)此代碼將變得更加困難。
但是,您可以改善這種情況并使用Dependency Injection無縫添加新功能。
What Are Inversion of Control and Dependency Injection?
Inversion of Control是一種模式,可讓您反轉(zhuǎn)控制流程。為此,您將一個(gè)類的所有職責(zé)(主要職責(zé)除外)移到了外部,使其成為從屬對象(dependencies)。通過抽象,您可以輕松地使依賴項(xiàng)互換。
您的類(DI客戶對象)不知道其依賴項(xiàng)(即DI服務(wù)對象)的實(shí)現(xiàn)。它還不知道如何創(chuàng)建它們。通過消除類之間的緊密耦合關(guān)系,這使得代碼可測試和可維護(hù)。
Dependency Injection是幫助應(yīng)用Inversion of Control原理的幾種模式之一。您可以通過多種方式實(shí)現(xiàn)依賴項(xiàng)注入(Dependency Injection),包括Constructor Injection, Setter Injection and Interface Injection。
一種常見的方法稱為構(gòu)造函數(shù)注入(Constructor Injection)。這是您要看的第一個(gè)。
1. Constructor Injection
在Constructor Injection或Initializer Injection中,您將所有類依賴項(xiàng)作為構(gòu)造函數(shù)參數(shù)傳遞。更容易理解代碼的作用,因?yàn)槟梢栽谝惶幜⒓纯吹筋愋枰乃幸蕾図?xiàng)。例如,查看以下代碼片段:
protocol EngineProtocol {
func start()
func stop()
}
protocol TransmissionProtocol {
func changeGear(gear: Gear)
}
final class Car {
private let engine: EngineProtocol
private let transmission: TransmissionProtocol
init(engine: EngineProtocol, transmission: TransmissionProtocol) {
self.engine = engine
self.transmission = transmission
}
}
在此代碼段中,EngineProtocol和TransmissionProtocol是服務(wù),而Car是客戶端。 由于您劃分了職責(zé)并使用了抽象,因此可以創(chuàng)建一個(gè)Car實(shí)例,該實(shí)例具有符合預(yù)期協(xié)議的任何依賴關(guān)系。 您甚至可以通過EngineProtocol和TransmissionProtocol的測試實(shí)現(xiàn),以對Car進(jìn)行一些單元測試。
接下來,您將看到Setter Injection。
2. Setter Injection
Setter Injection或Method Injection明顯不同。 如本例所示,它需要依賴項(xiàng)setter方法:
final class Car {
private var engine: EngineProtocol?
private var transmission: TransmissionProtocol?
func setEngine(engine: EngineProtocol) {
self.engine = engine
}
func setTransmission(transmission: TransmissionProtocol) {
self.transmission = transmission
}
}
當(dāng)您只有幾個(gè)依賴項(xiàng)并且一些是可選的時(shí),這是一個(gè)好方法。 但是,很容易忘記設(shè)置必要的依賴項(xiàng),因?yàn)闆]有什么可以強(qiáng)迫您這樣做。
接下來,您將探索Interface Injection。
3. Interface Injection
Interface Injection要求客戶端遵守用于inject dependencies的協(xié)議。 看這個(gè)例子:
protocol EngineMountable {
func mountEngine(engine: EngineProtocol)
}
protocol TransmissionMountable {
func mountTransmission(transmission: TransmissionProtocol)
}
final class Car: EngineMountable, TransmissionMountable {
private var engine: EngineProtocol?
private var transmission: TransmissionProtocol?
func mountEngine(engine: EngineProtocol) {
self.engine = engine
}
func mountTransmission(transmission: TransmissionProtocol) {
self.transmission = transmission
}
}
您的代碼更加分離。 此外,injector可能完全不了解客戶的實(shí)際執(zhí)行情況。
Dependency Injection Container或DI Container是另一個(gè)重要的Dependency Injection概念。 DI Container負(fù)責(zé)注冊registering和解決resolving項(xiàng)目中的所有依賴項(xiàng)。 根據(jù)DI容器的復(fù)雜程度,它可以處理依賴項(xiàng)的生命周期,并在必要時(shí)自行自動(dòng)注入依賴項(xiàng)。
在下一部分中,您將創(chuàng)建一個(gè)基本的DI Container。
Using Dependency Injection
最后,是時(shí)候運(yùn)用您對模式的知識了! 使用以下命令創(chuàng)建一個(gè)名為ProfileContentProvider的新Swift文件:
import SwiftUI
protocol ProfileContentProviderProtocol {
var privacyLevel: PrivacyLevel { get }
var canSendMessage: Bool { get }
var canStartVideoChat: Bool { get }
var photosView: AnyView { get }
var feedView: AnyView { get }
var friendsView: AnyView { get }
}
盡管此代碼只是一個(gè)協(xié)議,但實(shí)現(xiàn)方式?jīng)Q定了要提供哪種內(nèi)容。
接下來,在您添加的協(xié)議下方添加以下類:
final class ProfileContentProvider: ProfileContentProviderProtocol {
let privacyLevel: PrivacyLevel
private let user: User
init(privacyLevel: PrivacyLevel, user: User) {
self.privacyLevel = privacyLevel
self.user = user
}
var canSendMessage: Bool {
privacyLevel > .everyone
}
var canStartVideoChat: Bool {
privacyLevel > .everyone
}
var photosView: AnyView {
privacyLevel > .everyone ?
AnyView(PhotosView(photos: user.photos)) :
AnyView(EmptyView())
}
var feedView: AnyView {
privacyLevel > .everyone ?
AnyView(HistoryFeedView(posts: user.historyFeed)) :
AnyView(RestrictedAccessView())
}
var friendsView: AnyView {
privacyLevel > .everyone ?
AnyView(UsersView(title: "Friends", users: user.friends)) :
AnyView(EmptyView())
}
}
現(xiàn)在,您有一個(gè)單獨(dú)的提供者,其職責(zé)是:根據(jù)隱私級別決定如何顯示用戶個(gè)人資料。
接下來,切換到ProfileView.swift并在ProfileView的body屬性上方添加以下代碼:
private let provider: ProfileContentProviderProtocol
init(provider: ProfileContentProviderProtocol, user: User) {
self.provider = provider
self.user = user
}
您在其初始值中設(shè)置ProfileView的user變量,因此刪除Mock.user()值分配。
現(xiàn)在,如下更新ProfileView的body屬性:
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ProfileHeaderView(
user: user,
canSendMessage: provider.canSendMessage,
canStartVideoChat: provider.canStartVideoChat
)
provider.friendsView
provider.photosView
provider.feedView
}
}.navigationTitle("Profile")
}
}
有了這些更改,ProfileView不再依賴privacyLevel變量,因?yàn)樗ㄟ^其初始化程序構(gòu)造函數(shù)注入獲得了必要的依賴關(guān)系。 從ProfileView中刪除privacyLevel常數(shù)。
注意:您會看到Xcode警告它在
ProfileView_Previews中缺少參數(shù)。 不用擔(dān)心 您很快就會解決。
在這里,您可以開始看到這種方法的美。 現(xiàn)在,該視圖完全不了解profile content背后的業(yè)務(wù)邏輯。 您可以提供ProfileContentProviderProtocol的任何實(shí)現(xiàn),包括新的隱私級別,甚至可以模擬提供程序而無需更改任何代碼!
稍后您將對此進(jìn)行驗(yàn)證。 首先,是時(shí)候設(shè)置Dependency Injection Container,以幫助將所有DI infrastructure收集到一個(gè)地方。
1. Using a Dependency Injection Container
現(xiàn)在,創(chuàng)建一個(gè)名為DIContainer.swift的新文件,并添加以下內(nèi)容:
protocol DIContainerProtocol {
func register<Component>(type: Component.Type, component: Any)
func resolve<Component>(type: Component.Type) -> Component?
}
final class DIContainer: DIContainerProtocol {
// 1
static let shared = DIContainer()
// 2
private init() {}
// 3
var components: [String: Any] = [:]
func register<Component>(type: Component.Type, component: Any) {
// 4
components["\(type)"] = component
}
func resolve<Component>(type: Component.Type) -> Component? {
// 5
return components["\(type)"] as? Component
}
}
以下是分步說明:
- 1) 首先,創(chuàng)建一個(gè)類型為
DIContainer的靜態(tài)屬性。 - 2) 由于您將初始化程序標(biāo)記為私有,因此實(shí)質(zhì)上可以確保您的容器是單例的。 這樣可以防止意外使用多個(gè)實(shí)例和意外行為,例如丟失某些依賴項(xiàng)。
- 3) 然后,您創(chuàng)建一個(gè)詞典以保留所有服務(wù)。
- 4) 組件類型的字符串表示形式是字典中的鍵。
- 5) 您可以再次使用該類型來解決必要的依賴關(guān)系。
注意:本質(zhì)上,
DI Container與其他任何模式一樣,是解決編程問題的一種方法。 您可以通過多種方式來實(shí)現(xiàn)它,包括第三方框架。
接下來,要使您的容器處理依賴關(guān)系,請打開ProfileView.swift并更新ProfileView的初始化程序,如下所示:
init(
provider: ProfileContentProviderProtocol =
DIContainer.shared.resolve(type: ProfileContentProviderProtocol.self)!,
user: User = DIContainer.shared.resolve(type: User.self)!
) {
self.provider = provider
self.user = user
}
現(xiàn)在,您的DIContainer默認(rèn)提供了必要的參數(shù)。 但是,您始終可以自行傳遞依賴項(xiàng)以進(jìn)行測試,也可以在容器中注冊模擬的依賴項(xiàng)。
接下來,在ProfileView下面找到ProfileView_Previews并進(jìn)行更新:
struct ProfileView_Previews: PreviewProvider {
private static let user = Mock.user()
static var previews: some View {
ProfileView(
provider: ProfileContentProvider(privacyLevel: .friend, user: user),
user: user)
}
}
打開ProfileContentProvider.swift。 使用相同的方法更新ProfileContentProvider的初始化程序:
init(
privacyLevel: PrivacyLevel =
DIContainer.shared.resolve(type: PrivacyLevel.self)!,
user: User = DIContainer.shared.resolve(type: User.self)!
) {
self.privacyLevel = privacyLevel
self.user = user
}
最后,您必須定義依賴項(xiàng)的初始狀態(tài)以復(fù)制應(yīng)用程序的行為,然后再開始對其進(jìn)行操作。
在SceneDelegate.swift中,在profileView的初始化上方添加以下代碼:
let container = DIContainer.shared
container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
container.register(type: User.self, component: Mock.user())
container.register(
type: ProfileContentProviderProtocol.self,
component: ProfileContentProvider())
構(gòu)建并運(yùn)行。 雖然該應(yīng)用看起來與以前完全一樣,但您知道它的內(nèi)部更漂亮。

接下來,您將實(shí)現(xiàn)新功能。
Extending the Functionality
有時(shí),用戶希望向其好友列表中的人隱藏某些內(nèi)容或功能。 也許他們張貼聚會中的圖片,而他們只希望親密的朋友看到。 或者,也許他們只想接收親密朋友的視頻通話。
無論出于何種原因,賦予密友額外訪問權(quán)限的能力都是一個(gè)很大的功能。
要實(shí)現(xiàn)它,請轉(zhuǎn)到PrivacyLevel.swift并添加另一種case:
enum PrivacyLevel: Comparable {
case everyone, friend, closeFriend
}
接下來,更新將處理新隱私級別的provider。 轉(zhuǎn)到ProfileContentProvider.swift并更新以下屬性:
var canStartVideoChat: Bool {
privacyLevel > .friend
}
var photosView: AnyView {
privacyLevel > .friend ?
AnyView(PhotosView(photos: user.photos)) :
AnyView(EmptyView())
}
使用此代碼,您可以確保只有密友可以訪問照片并發(fā)起視頻通話。 您無需進(jìn)行任何其他更改即可添加其他隱私級別。 您可以根據(jù)需要?jiǎng)?chuàng)建任意數(shù)量的隱私級別或組,為ProfileView提供provider,然后其他所有事務(wù)都將由您處理。
現(xiàn)在,構(gòu)建并運(yùn)行:

如您所見,.friend隱私級別的視頻通話圖標(biāo)和“最近的照片”部分現(xiàn)在消失了。 您實(shí)現(xiàn)了目標(biāo)!
Adding User Preferences
是否想嘗試更復(fù)雜的用例? 如果您需要讓provider根據(jù)用戶的隱私首選項(xiàng)做出決定,該怎么辦?
為解決此問題,您將添加一個(gè)新屏幕,用戶可以在其中確定誰可以訪問其個(gè)人資料的每個(gè)部分,使用UserDefaults保存偏好設(shè)置,并在每次更新偏好設(shè)置時(shí)重新加載個(gè)人資料屏幕。 您將使用Combine框架來使其工作。
注意:如果您想更熟悉
Combine或更新知識,請看一下Combine: Getting Started tutorial教程。
首先,打開PrivacyLevel.swift并將以下屬性和方法添加到PrivacyLevel:
var title: String {
switch self {
case .everyone:
return "Everyone"
case .friend:
return "Friends only"
case .closeFriend:
return "Close friends only"
}
}
static func from(string: String) -> PrivacyLevel? {
switch string {
case everyone.title:
return everyone
case friend.title:
return friend
case closeFriend.title:
return closeFriend
default:
return nil
}
}
您將使用title在要?jiǎng)?chuàng)建的新偏好設(shè)置屏幕上顯示隱私級別選項(xiàng)。 from(string :)幫助從保存的UserDefaults首選項(xiàng)重新創(chuàng)建PrivacyLevel。
現(xiàn)在,在項(xiàng)目導(dǎo)航器中右鍵單擊Sociobox文件夾,然后選擇Add Files to “Sociobox”…。選擇PreferencesStore.swift,然后單擊Add。打開文件并瀏覽代碼。
該類負(fù)責(zé)從UserDefaults中保存和讀取用戶偏好設(shè)置。
您具有五個(gè)配置文件部分中的每一個(gè)的屬性,以及用于重置首選項(xiàng)的方法。 PreferencesStoreProtocol符合ObservableObject協(xié)議,使您的store擁有一個(gè)publisher,只要使用@Published屬性標(biāo)記的任何屬性發(fā)生更改,發(fā)布者都將發(fā)出該發(fā)布者。
進(jìn)行任何更改時(shí),任何SwiftUI視圖甚至常規(guī)類都可以訂閱PreferencesStoreProtocol并重新加載其內(nèi)容。
接下來,您將添加Preferences Screen。
1. Adding the Preferences Screen
現(xiàn)在,右鍵單擊Views文件夾,然后再次選擇Add Files to “Sociobox”…,以添加UserPreferencesView.swift。打開它,看看預(yù)覽:

這是新屏幕的外觀。
通過實(shí)現(xiàn)PreferencesStoreProtocol,使新屏幕保存用戶首選項(xiàng)。 將UserPreferencesView的聲明更新為以下內(nèi)容:
struct UserPreferencesView<Store>: View where Store: PreferencesStoreProtocol {
像每種靜態(tài)類型的編程語言一樣,在編譯時(shí)定義和檢查類型。 這就是問題所在:您不知道Store在運(yùn)行時(shí)將具有的確切類型,但不要驚慌! 您所知道的是,Store將符合PreferencesStoreProtocol。 因此,您告訴編譯器Store將實(shí)現(xiàn)此協(xié)議。
編譯器需要知道要用于視圖的特定類型。 稍后,當(dāng)您創(chuàng)建UserPreferencesView實(shí)例時(shí),需要在尖括號中使用特定類型而不是協(xié)議,如下所示:
UserPreferencesView<PreferencesStore>()
這樣,可以在編譯時(shí)檢查類型。 現(xiàn)在,將以下屬性和初始化程序添加到UserPreferencesView中:
private var store: Store
init(store: Store = DIContainer.shared.resolve(type: Store.self)!) {
self.store = store
}
使用上面的代碼,您可以讓UserPreferencesView接收所需的依賴關(guān)系,而不是自己創(chuàng)建它。
更新body屬性以使用store訪問用戶首選項(xiàng):
var body: some View {
NavigationView {
VStack {
PreferenceView(title: .photos, value: store.photosPreference) { value in
store.photosPreference = value
}
PreferenceView(
title: .friends,
value: store.friendsListPreference
) { value in
store.friendsListPreference = value
}
PreferenceView(title: .feed, value: store.feedPreference) { value in
store.feedPreference = value
}
PreferenceView(
title: .videoCall,
value: store.videoCallsPreference
) { value in
store.videoCallsPreference = value
}
PreferenceView(
title: .message,
value: store.messagePreference
) { value in
store.messagePreference = value
}
Spacer()
}
}.navigationBarTitle("Privacy preferences")
}
以下是代碼細(xì)分:
- 1) 垂直堆棧
(vertical stack)中的每個(gè)PreferenceView都代表一個(gè)不同的配置文件部分,并帶有一個(gè)下拉菜單來選擇一個(gè)隱私級別。 - 2) 從
store中讀取每個(gè)首選項(xiàng)的當(dāng)前值。 - 3) 當(dāng)用戶選擇隱私選項(xiàng)時(shí),將新值保存到
store。
更新UserPreferencesView_Previews的Previews屬性,以便您可以再次看到預(yù)覽:
static var previews: some View {
UserPreferencesView(store: PreferencesStore())
}
在SceneDelegate.swift中,將store dependency注冊到您的容器中:
container.register(type: PreferencesStore.self, component: PreferencesStore())
2. Adding Combine
接下來,轉(zhuǎn)到ProfileContentProvider.swift并在文件頂部導(dǎo)入Combine:
import Combine
然后,像使用UserPreferencesView一樣更新其聲明:
final class ProfileContentProvider<Store>: ProfileContentProviderProtocol
where Store: PreferencesStoreProtocol {
現(xiàn)在,更新ProfileContentProviderProtocol的聲明:
protocol ProfileContentProviderProtocol: ObservableObject {
此代碼使ProfileView可以訂閱ProfileContentProvider中的更改,并在用戶選擇新的首選項(xiàng)時(shí)立即更新狀態(tài)。
在ProfileContentProvider中,為store添加一個(gè)屬性并替換初始化程序:
private var store: Store
private var cancellables: Set<AnyCancellable> = []
init(
privacyLevel: PrivacyLevel =
DIContainer.shared.resolve(type: PrivacyLevel.self)!,
user: User = DIContainer.shared.resolve(type: User.self)!,
// 1
store: Store = DIContainer.shared.resolve(type: Store.self)!
) {
self.privacyLevel = privacyLevel
self.user = user
self.store = store
// 2
store.objectWillChange.sink { _ in
self.objectWillChange.send()
}
.store(in: &cancellables)
}
這是您所做的:
- 1)
DI Container提供PreferencesStore的實(shí)例。 - 2) 您使用
objectWillChange屬性訂閱PreferencesStoreProtocol的發(fā)布者。 - 3) 當(dāng)
store中的屬性發(fā)生更改時(shí),也會使ProfileContentProviderProtocol的發(fā)布者發(fā)出。
現(xiàn)在,更新ProfileContentProvider的屬性以使用store的屬性,而不使用PrivacyLevel枚舉的實(shí)例:
var canSendMessage: Bool {
privacyLevel >= store.messagePreference
}
var canStartVideoChat: Bool {
privacyLevel >= store.videoCallsPreference
}
var photosView: AnyView {
privacyLevel >= store.photosPreference ?
AnyView(PhotosView(photos: user.photos)) :
AnyView(EmptyView())
}
var feedView: AnyView {
privacyLevel >= store.feedPreference ?
AnyView(HistoryFeedView(posts: user.historyFeed)) :
AnyView(EmptyView())
}
var friendsView: AnyView {
privacyLevel >= store.friendsListPreference ?
AnyView(UsersView(title: "Friends", users: user.friends)) :
AnyView(EmptyView())
}
除不再直接使用enum外,其他所有內(nèi)容保持不變。
3. Bringing It All Together
要訂閱provider中的更改,請打開ProfileView.swift并同時(shí)更改ProfileView的聲明:
struct ProfileView<ContentProvider>: View
where ContentProvider: ProfileContentProviderProtocol {
更新provider屬性以使用通用屬性:
@ObservedObject private var provider: ContentProvider
在SwiftUI視圖中使用@ObservedObject時(shí),您將訂閱其發(fā)布者。 該視圖在發(fā)出時(shí)會重新加載。
也更新初始化器:
init(
provider: ContentProvider =
DIContainer.shared.resolve(type: ContentProvider.self)!,
user: User = DIContainer.shared.resolve(type: User.self)!
) {
self.provider = provider
self.user = user
}
然后在body屬性內(nèi)的navigationTitle(“ Profile”)下方添加以下代碼:
.navigationBarItems(trailing: Button(action: {}) {
NavigationLink(destination: UserPreferencesView<PreferencesStore>()) {
Image(systemName: "gear")
}
})
您添加了一個(gè)導(dǎo)航欄按鈕,該按鈕會將用戶帶到首選項(xiàng)屏幕。
現(xiàn)在返回SceneDelegate.swift以更新依賴項(xiàng)注冊。 由于您的很多協(xié)議和類都是通用的,因此將它們?nèi)恳黄鹗褂米兊糜悬c(diǎn)難以閱讀。
為了簡化操作,請?jiān)?code>scene(_:willConnectTo:options:)上方為提供者(provider)創(chuàng)建一個(gè)新的typealias:
typealias Provider = ProfileContentProvider<PreferencesStore>
通過刪除以下內(nèi)容使用新的typealias:
container.register(
type: ProfileContentProviderProtocol.self,
component: ProfileContentProvider())
現(xiàn)在,添加以下_after_調(diào)用以注冊PreferencesStore:
container.register(type: Provider.self, component: Provider())
注意:您必須最后注冊
Provider,因?yàn)槠涑跏蓟绦蛳M[私級別,用戶和存儲區(qū)已經(jīng)存在于DI Container中。
將<Provider>添加到profileView的初始化中:
let profileView = ProfileView<Provider>()
要獲得可用的預(yù)覽,請打開ProfileView.swift并在ProfileView_Previews中添加相同的設(shè)置:
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
typealias Provider = ProfileContentProvider<PreferencesStore>
let container = DIContainer.shared
container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
container.register(type: User.self, component: Mock.user())
container.register(
type: PreferencesStore.self,
component: PreferencesStore())
container.register(type: Provider.self, component: Provider())
return ProfileView<Provider>()
}
}
經(jīng)過艱苦的工作,是時(shí)候看看它們?nèi)绾我黄鸸ぷ髁恕?運(yùn)行應(yīng)用程序以查看結(jié)果:

在本教程中,您學(xué)習(xí)了Dependency Injection模式以及如何在項(xiàng)目中構(gòu)建和應(yīng)用它。 根據(jù)您正在從事的項(xiàng)目,您可以考慮使用第三方解決方案。
要了解更多信息,請閱讀我們的Swinject教程。 即使您沒有使用第三方框架進(jìn)行依賴項(xiàng)注入(dependency injection),您也會發(fā)現(xiàn)一些方便的測試示例。
后記
本篇主要講述了基于SwiftUI簡單App的
Dependency Injection應(yīng)用,感興趣的給個(gè)贊或者關(guān)注~~~
