SwiftUI框架詳細解析 (十六) —— 基于SwiftUI簡單App的Dependency Injection應(yīng)用(一)

版本記錄

版本號 時間
V1.0 2021.01.02 星期六

前言

今天翻閱蘋果的API文檔觅赊,發(fā)現(xiàn)多了一個框架SwiftUI责静,這里我們就一起來看一下這個框架角撞。感興趣的看下面幾篇文章贮乳。
1. SwiftUI框架詳細解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細解析 (四) —— 使用SwiftUI進行蘋果登錄(一)
5. SwiftUI框架詳細解析 (五) —— 使用SwiftUI進行蘋果登錄(二)
6. SwiftUI框架詳細解析 (六) —— 基于SwiftUI的導(dǎo)航的實現(xiàn)(一)
7. SwiftUI框架詳細解析 (七) —— 基于SwiftUI的導(dǎo)航的實現(xiàn)(二)
8. SwiftUI框架詳細解析 (八) —— 基于SwiftUI的動畫的實現(xiàn)(一)
9. SwiftUI框架詳細解析 (九) —— 基于SwiftUI的動畫的實現(xiàn)(二)
10. SwiftUI框架詳細解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
13. SwiftUI框架詳細解析 (十三) —— 基于SwiftUI創(chuàng)建Mind-Map UI(二)
14. SwiftUI框架詳細解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
15. SwiftUI框架詳細解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)

開始

首先看下主要內(nèi)容:

在本教程中,您將在SwiftUI中創(chuàng)建社交媒體應(yīng)用的個人資料頁面時涤伐,學(xué)習(xí)有關(guān)iOS的Dependency Injection的信息。內(nèi)容來自翻譯罪既。

接著看下寫作環(huán)境

Swift 5, iOS 14, Xcode 12

接著就是正文啦腹暖。

程序員開發(fā)了許多體系結(jié)構(gòu)汇在,設(shè)計模式和編程風格。 雖然它們各自解決不同的問題脏答,但它們都有助于使代碼更具可讀性糕殉,可測試性和靈活性。

Inversion of Control 因其效率而廣受歡迎殖告。 在本教程中阿蝶,您將通過Dependency Injection, or DI模式應(yīng)用此原理。 您無需編寫第三方框架黄绩,而是編寫自己的小型Dependency Injection解決方案羡洁,并使用它來重構(gòu)應(yīng)用程序并添加一些新功能。

如果您不知道IoCDI的全部含義爽丹,那么沒問題筑煮,您將很快了解更多。

注意:這是涉及SwiftUI的中級iOS教程粤蝎。 如果您不熟悉SwiftUI真仲,請查看SwiftUI視頻課程SwiftUI video course

打開入門項目初澎。 打開入門項目并運行該應(yīng)用程序:

您會從社交媒體應(yīng)用程序中看到一個配置文件屏幕秸应,其中包含許多用戶數(shù)據(jù):個人簡歷,朋友,照片和信息灸眼。 與任何社交網(wǎng)絡(luò)一樣卧檐,用戶隱私和互聯(lián)網(wǎng)安全至關(guān)重要。

您的目標是讓用戶控制他們與其他用戶共享的信息焰宣。 另外霉囚,您還可以讓他們根據(jù)他們與給定用戶的關(guān)系來調(diào)整隱私規(guī)則。

在使他們能夠控制并了解有關(guān)依賴注入(Dependency Injection)及其如何幫助您的更多信息之前匕积,您需要確定問題盈罐。

1. Identifying the Issue

打開ProfileView.swift并仔細查看ProfileViewbody

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")
  }
}

以下是代碼細分:

  • 1) 您將ProfileHeaderView添加到VStack的頂部,并指定僅當用戶是朋友時闪唆,消息和視頻通話選項才可用盅粪。
  • 2) 如果用戶是朋友,則顯示朋友列表悄蕾,照片和帖子票顾。
  • 3) 否則,您將顯示RestrictedAccessView帆调。

ProfileView頂部的privacyLevel值定義查看您的個人資料的用戶的訪問級別奠骄。 將privacyLevel更改為.everyone并運行該應(yīng)用程序以查看您的個人資料,就好像您不在好友列表中一樣:

已經(jīng)有基本的隱私控制措施番刊。 但是含鳞,用戶無法選擇誰可以看到其個人資料的哪些部分。 僅有兩個隱私級別是不夠的芹务。

當前蝉绷,ProfileView根據(jù)隱私級別決定顯示哪些視圖。 由于以下幾個原因枣抱,這不是一個合適的解決方案:

  • 它不是很容易測試熔吗。 盡管可以用UI測試進行覆蓋,但運行起來要比單元測試或集成測試昂貴沃但。
  • 每次您決定擴展或修改應(yīng)用程序的功能時磁滚,ProfileView都需要進行大量調(diào)整。 它與PrivacyLevel緊密結(jié)合宵晚,并承擔了比所需更多的責任垂攘。
  • 隨著應(yīng)用程序的復(fù)雜性和功能的增長,維護此代碼將變得更加困難淤刃。

但是晒他,您可以改善這種情況并使用Dependency Injection無縫添加新功能。


What Are Inversion of Control and Dependency Injection?

Inversion of Control是一種模式逸贾,可讓您反轉(zhuǎn)控制流程陨仅。為此津滞,您將一個類的所有職責(主要職責除外)移到了外部,使其成為從屬對象(dependencies)灼伤。通過抽象触徐,您可以輕松地使依賴項互換。

您的類(DI客戶對象)不知道其依賴項(即DI服務(wù)對象)的實現(xiàn)狐赡。它還不知道如何創(chuàng)建它們撞鹉。通過消除類之間的緊密耦合關(guān)系,這使得代碼可測試和可維護颖侄。

Dependency Injection是幫助應(yīng)用Inversion of Control原理的幾種模式之一鸟雏。您可以通過多種方式實現(xiàn)依賴項注入(Dependency Injection),包括Constructor Injection, Setter Injection and Interface Injection览祖。

一種常見的方法稱為構(gòu)造函數(shù)注入(Constructor Injection)孝鹊。這是您要看的第一個。

1. Constructor Injection

Constructor InjectionInitializer Injection中展蒂,您將所有類依賴項作為構(gòu)造函數(shù)參數(shù)傳遞又活。更容易理解代碼的作用,因為您可以在一處立即看到類需要的所有依賴項锰悼。例如皇钞,查看以下代碼片段:

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
  }
}

在此代碼段中,EngineProtocolTransmissionProtocol是服務(wù)松捉,而Car是客戶端。 由于您劃分了職責并使用了抽象馆里,因此可以創(chuàng)建一個Car實例隘世,該實例具有符合預(yù)期協(xié)議的任何依賴關(guān)系。 您甚至可以通過EngineProtocolTransmissionProtocol的測試實現(xiàn)鸠踪,以對Car進行一些單元測試丙者。

接下來,您將看到Setter Injection营密。

2. Setter Injection

Setter InjectionMethod Injection明顯不同械媒。 如本例所示,它需要依賴項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
  }
}

當您只有幾個依賴項并且一些是可選的時评汰,這是一個好方法纷捞。 但是,很容易忘記設(shè)置必要的依賴項被去,因為沒有什么可以強迫您這樣做主儡。

接下來,您將探索Interface Injection惨缆。

3. Interface Injection

Interface Injection要求客戶端遵守用于inject dependencies的協(xié)議糜值。 看這個例子:

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可能完全不了解客戶的實際執(zhí)行情況寂汇。

Dependency Injection ContainerDI Container是另一個重要的Dependency Injection概念病往。 DI Container負責注冊registering和解決resolving項目中的所有依賴項。 根據(jù)DI容器的復(fù)雜程度骄瓣,它可以處理依賴項的生命周期停巷,并在必要時自行自動注入依賴項。

在下一部分中累贤,您將創(chuàng)建一個基本的DI Container叠穆。


Using Dependency Injection

最后,是時候運用您對模式的知識了臼膏! 使用以下命令創(chuàng)建一個名為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 }
}

盡管此代碼只是一個協(xié)議硼被,但實現(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)在嚷硫,您有一個單獨的提供者,其職責是:根據(jù)隱私級別決定如何顯示用戶個人資料始鱼。

接下來仔掸,切換到ProfileView.swift并在ProfileViewbody屬性上方添加以下代碼:

private let provider: ProfileContentProviderProtocol

init(provider: ProfileContentProviderProtocol, user: User) {
  self.provider = provider
  self.user = user
}

您在其初始值中設(shè)置ProfileViewuser變量,因此刪除Mock.user()值分配医清。

現(xiàn)在起暮,如下更新ProfileViewbody屬性:

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變量会烙,因為它通過其初始化程序構(gòu)造函數(shù)注入獲得了必要的依賴關(guān)系负懦。 從ProfileView中刪除privacyLevel常數(shù)。

注意:您會看到Xcode警告它在ProfileView_Previews中缺少參數(shù)柏腻。 不用擔心 您很快就會解決纸厉。

在這里,您可以開始看到這種方法的美五嫂。 現(xiàn)在颗品,該視圖完全不了解profile content背后的業(yè)務(wù)邏輯。 您可以提供ProfileContentProviderProtocol的任何實現(xiàn)沃缘,包括新的隱私級別躯枢,甚至可以模擬提供程序而無需更改任何代碼!

稍后您將對此進行驗證槐臀。 首先闺金,是時候設(shè)置Dependency Injection Container,以幫助將所有DI infrastructure收集到一個地方峰档。

1. Using a Dependency Injection Container

現(xiàn)在败匹,創(chuàng)建一個名為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)建一個類型為DIContainer的靜態(tài)屬性掀亩。
  • 2) 由于您將初始化程序標記為私有舔哪,因此實質(zhì)上可以確保您的容器是單例的。 這樣可以防止意外使用多個實例和意外行為槽棍,例如丟失某些依賴項捉蚤。
  • 3) 然后,您創(chuàng)建一個詞典以保留所有服務(wù)炼七。
  • 4) 組件類型的字符串表示形式是字典中的鍵缆巧。
  • 5) 您可以再次使用該類型來解決必要的依賴關(guān)系。

注意:本質(zhì)上豌拙,DI Container與其他任何模式一樣陕悬,是解決編程問題的一種方法。 您可以通過多種方式來實現(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默認提供了必要的參數(shù)况芒。 但是惜纸,您始終可以自行傳遞依賴項以進行測試,也可以在容器中注冊模擬的依賴項绝骚。

接下來堪簿,在ProfileView下面找到ProfileView_Previews并進行更新:

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
}

最后皮壁,您必須定義依賴項的初始狀態(tài)以復(fù)制應(yīng)用程序的行為,然后再開始對其進行操作哪审。

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īng)用看起來與以前完全一樣湿滓,但您知道它的內(nèi)部更漂亮滴须。

接下來,您將實現(xiàn)新功能叽奥。


Extending the Functionality

有時扔水,用戶希望向其好友列表中的人隱藏某些內(nèi)容或功能。 也許他們張貼聚會中的圖片朝氓,而他們只希望親密的朋友看到魔市。 或者主届,也許他們只想接收親密朋友的視頻通話。

無論出于何種原因待德,賦予密友額外訪問權(quán)限的能力都是一個很大的功能君丁。

要實現(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ù)需要創(chuàng)建任意數(shù)量的隱私級別或組,為ProfileView提供provider丑勤,然后其他所有事務(wù)都將由您處理华嘹。

現(xiàn)在,構(gòu)建并運行:

如您所見确封,.friend隱私級別的視頻通話圖標和“最近的照片”部分現(xiàn)在消失了除呵。 您實現(xiàn)了目標!


Adding User Preferences

是否想嘗試更復(fù)雜的用例爪喘? 如果您需要讓provider根據(jù)用戶的隱私首選項做出決定颜曾,該怎么辦?

為解決此問題秉剑,您將添加一個新屏幕泛豪,用戶可以在其中確定誰可以訪問其個人資料的每個部分,使用UserDefaults保存偏好設(shè)置侦鹏,并在每次更新偏好設(shè)置時重新加載個人資料屏幕诡曙。 您將使用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在要創(chuàng)建的新偏好設(shè)置屏幕上顯示隱私級別選項渊涝。 from(string :)幫助從保存的UserDefaults首選項重新創(chuàng)建PrivacyLevel慎璧。

現(xiàn)在,在項目導(dǎo)航器中右鍵單擊Sociobox文件夾跨释,然后選擇Add Files to “Sociobox”…胸私。選擇PreferencesStore.swift,然后單擊Add鳖谈。打開文件并瀏覽代碼岁疼。

該類負責從UserDefaults中保存和讀取用戶偏好設(shè)置。

您具有五個配置文件部分中的每一個的屬性缆娃,以及用于重置首選項的方法捷绒。 PreferencesStoreProtocol符合ObservableObject協(xié)議瑰排,使您的store擁有一個publisher,只要使用@Published屬性標記的任何屬性發(fā)生更改疙驾,發(fā)布者都將發(fā)出該發(fā)布者凶伙。

進行任何更改時,任何SwiftUI視圖甚至常規(guī)類都可以訂閱PreferencesStoreProtocol并重新加載其內(nèi)容它碎。

接下來函荣,您將添加Preferences Screen

1. Adding the Preferences Screen

現(xiàn)在扳肛,右鍵單擊Views文件夾傻挂,然后再次選擇Add Files to “Sociobox”…,以添加UserPreferencesView.swift挖息。打開它金拒,看看預(yù)覽:

這是新屏幕的外觀。

通過實現(xiàn)PreferencesStoreProtocol套腹,使新屏幕保存用戶首選項绪抛。 將UserPreferencesView的聲明更新為以下內(nèi)容:

struct UserPreferencesView<Store>: View where Store: PreferencesStoreProtocol {

像每種靜態(tài)類型的編程語言一樣,在編譯時定義和檢查類型电禀。 這就是問題所在:您不知道Store在運行時將具有的確切類型幢码,但不要驚慌! 您所知道的是尖飞,Store將符合PreferencesStoreProtocol症副。 因此,您告訴編譯器Store將實現(xiàn)此協(xié)議政基。

編譯器需要知道要用于視圖的特定類型贞铣。 稍后,當您創(chuàng)建UserPreferencesView實例時沮明,需要在尖括號中使用特定類型而不是協(xié)議辕坝,如下所示:

UserPreferencesView<PreferencesStore>()

這樣,可以在編譯時檢查類型荐健。 現(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訪問用戶首選項:

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")
}

以下是代碼細分:

  • 1) 垂直堆棧(vertical stack)中的每個PreferenceView都代表一個不同的配置文件部分挚歧,并帶有一個下拉菜單來選擇一個隱私級別扛稽。
  • 2) 從store中讀取每個首選項的當前值。
  • 3) 當用戶選擇隱私選項時滑负,將新值保存到store在张。

更新UserPreferencesView_PreviewsPreviews屬性用含,以便您可以再次看到預(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中的更改瘟斜,并在用戶選擇新的首選項時立即更新狀態(tài)缸夹。

ProfileContentProvider中,為store添加一個屬性并替換初始化程序:

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的實例螺句。
  • 2) 您使用objectWillChange屬性訂閱PreferencesStoreProtocol的發(fā)布者虽惭。
  • 3) 當store中的屬性發(fā)生更改時,也會使ProfileContentProviderProtocol的發(fā)布者發(fā)出蛇尚。

現(xiàn)在芽唇,更新ProfileContentProvider的屬性以使用store的屬性,而不使用PrivacyLevel枚舉的實例:

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并同時更改ProfileView的聲明:

struct ProfileView<ContentProvider>: View 
  where ContentProvider: ProfileContentProviderProtocol {

更新provider屬性以使用通用屬性:

@ObservedObject private var provider: ContentProvider

SwiftUI視圖中使用@ObservedObject時谱邪,您將訂閱其發(fā)布者炮捧。 該視圖在發(fā)出時會重新加載。

也更新初始化器:

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")
  }
})

您添加了一個導(dǎo)航欄按鈕虾标,該按鈕會將用戶帶到首選項屏幕寓盗。

現(xiàn)在返回SceneDelegate.swift以更新依賴項注冊。 由于您的很多協(xié)議和類都是通用的璧函,因此將它們?nèi)恳黄鹗褂米兊糜悬c難以閱讀傀蚌。

為了簡化操作,請在scene(_:willConnectTo:options:)上方為提供者(provider)創(chuàng)建一個新的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善炫,因為其初始化程序希望隱私級別,用戶和存儲區(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)過艱苦的工作,是時候看看它們?nèi)绾我黄鸸ぷ髁恕?運行應(yīng)用程序以查看結(jié)果:

在本教程中宪萄,您學(xué)習(xí)了Dependency Injection模式以及如何在項目中構(gòu)建和應(yīng)用它艺谆。 根據(jù)您正在從事的項目,您可以考慮使用第三方解決方案拜英。

要了解更多信息静汤,請閱讀我們的Swinject教程。 即使您沒有使用第三方框架進行依賴項注入(dependency injection),您也會發(fā)現(xiàn)一些方便的測試示例虫给。

后記

本篇主要講述了基于SwiftUI簡單App的Dependency Injection應(yīng)用藤抡,感興趣的給個贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抹估,隨后出現(xiàn)的幾起案子缠黍,更是在濱河造成了極大的恐慌,老刑警劉巖药蜻,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓷式,死亡現(xiàn)場離奇詭異,居然都是意外死亡谷暮,警方通過查閱死者的電腦和手機蒿往,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來湿弦,“玉大人瓤漏,你說我怎么就攤上這事〖瞻#” “怎么了蔬充?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長班利。 經(jīng)常有香客問我饥漫,道長,這世上最難降的妖魔是什么罗标? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任庸队,我火速辦了婚禮,結(jié)果婚禮上闯割,老公的妹妹穿的比我還像新娘彻消。我一直安慰自己,他們只是感情好宙拉,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布宾尚。 她就那樣靜靜地躺著,像睡著了一般谢澈。 火紅的嫁衣襯著肌膚如雪煌贴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天锥忿,我揣著相機與錄音牛郑,去河邊找鬼。 笑死敬鬓,一個胖子當著我的面吹牛淹朋,可吹牛的內(nèi)容都是我干的灶似。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼瑞你,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了希痴?” 一聲冷哼從身側(cè)響起者甲,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砌创,沒想到半個月后虏缸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡嫩实,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年刽辙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甲献。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡宰缤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晃洒,到底是詐尸還是另有隱情慨灭,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布球及,位于F島的核電站氧骤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吃引。R本人自食惡果不足惜筹陵,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镊尺。 院中可真熱鬧朦佩,春花似錦、人聲如沸鹅心。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旭愧。三九已至颅筋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間输枯,已是汗流浹背议泵。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桃熄,地道東北人先口。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓型奥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碉京。 傳聞我的和親對象是個殘疾皇子厢汹,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容