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

版本記錄

版本號 時(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)用程序并添加一些新功能。

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

注意:這是涉及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ì)查看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")
  }
}

以下是代碼細(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 InjectionInitializer 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
  }
}

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

接下來,您將看到Setter Injection。

2. Setter Injection

Setter InjectionMethod 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 ContainerDI 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并在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變量,因?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_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中的更改,并在用戶選擇新的首選項(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)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容