全文共7044字,預計學習時長14分鐘
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
化繁為簡 圖片來源:unsplash.com/@emileseguin
然而,大多數現實生活中的程序都是複雜的,因此初始化函數可能會開始失控。
class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}
初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣板文件。如何避免這種情況?
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
化繁為簡 圖片來源:unsplash.com/@emileseguin
然而,大多數現實生活中的程序都是複雜的,因此初始化函數可能會開始失控。
class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}
初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣板文件。如何避免這種情況?
Swift 5.1和Property Wrappers
幸運的是,Swift 5.1為我們提供了一個新工具稱為Property Wrappers(正式稱為“property delegates”),該工具作為提案SE-0258的一部分在Swift論壇上提出,並添加到Swift 5.1和Xcode 11中。
Property Wrapper的新功能使屬性值能夠使用自定義get / set實現自動包裝,因此得名。
請注意,可以使用屬性值上的自定義getter和setter來執行其中一些操作,但缺點是必須在每個屬性上編寫幾乎相同的代碼,即更多樣板文件。如果每個屬性都需要某種內部支持變量,那就更糟了。 (還有更多樣板文件。)
@Injected Property Wrapper
因此,在get / set對中自動包裝屬性聽起來並不令人興奮,但屬性包裝器將對我們的Swift代碼產生重大影響。
為了演示,我們將創建一個名為@Injected的Property Wrappers並將其添加到代碼庫中。
現在,回到“失控”示例,看看全新的物業包裝給我們帶來了什麼。
class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}
就是這樣。 只需將屬性標記為@Injected,每個屬性將根據需要自動解析(注入),由此初始化功能中的所有樣板代碼都消失了!
此外,現在從@Injected註釋中可以清楚地看出依賴注入系統提供了哪些服務。
這種特殊類型的註釋方案在其他語言上應用時,最明顯的是在Android上的Kotlin中編程以及使用Dagger 2依賴注入框架。
履行
屬性包裝器實現很簡單。 我們使用Service類型定義一個通用結構,並將其標記為@propertyWrapper。
@propertyWrapper
struct Injected<Service> {
private var service: Service?
public var container: Resolver?
public var name: String?
public var value: Service {
mutating get {
if service == nil {
service = (container ?? Resolver.root).resolve(
Service.self,
name: name
)
}
return service!
}
mutating set {
service = newValue
}
}
}
所有屬性包裝器都必須實現一個名為value的變量。
當從變量請求或賦值時,Value提供屬性包裝器使用的getter和setter實現。
在這種情況下,服務被請求時,我們的值“getter”將檢查這是否是第一次被調用。 如果是這樣,當訪問包裝器代碼時,請求Resolver根據泛型類型解析所需服務的實例,將結果存儲到私有變量中供以後使用,並返回該服務。
當想要手動分配服務時,我們還提供了一個setter。 在某些情況下,這可以派上用場,最值得注意的是在進行單元測試時。
該實現還公開了一些額外的參數,如名稱和容器,更多的是在一秒鐘內實現。
更多實例
屬性包裝器的實現很簡單。使用服務類型定義一個通用結構,並將其標記為@propertyWrapper。
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
override func viewDidLoad() {
...
}
}
將ViewModel精簡到最基本的代碼為:
class XYZViewModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
至註冊碼也被簡化,因為構造函數參數被左右刪除......
func setupMyRegistrations {
register { XYZViewModel() }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
命名服務類型
解析器支持命名類型,它允許程序區分相同類型的服務或協議。
這也展示了一個有趣的property wrappers屬性,讓我們來看看這個。
常見的用例可能需要兩種不同視圖模型中的view controller,該選擇取決於它是否已經傳遞數據,因此應該以“添加”或“編輯”模式操作。
註冊可能如下所示,兩個模型都符合XYZViewModel協議或基類。
func setupMyRegistrations {
register(name: "add") { NewXYZViewModel() as XYZViewModel }
register(name: "edit") { EditXYZViewModel() as XYZViewModel }
}
然後在view controller中:
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
var myData: MyData?
override func viewDidLoad() {
$viewModel.name = myData == nil ? "add" : "edit"
viewModel.configure(myData)
...
}
}
請注意viewDidLoad中引用的$ viewModel.name。
在大多數情況下,我們希望Swift假裝包裝的值是屬性的實際值。但是,使用美元符號為屬性包裝器添加前綴使我們可以引用屬性包裝器本身,從而獲得對可能公開的任何公共變量或函數的訪問權限。
在這種情況下,設置name參數,第一次嘗試使用視圖模型時,該參數將傳遞給Resolver。解析器將在解析依賴關係時傳遞該名稱。
簡而言之,在屬性包裝器上使用$前綴可以讓我們操縱和/或引用包裝器本身。你會在SwiftUI中看到很多這樣的東西。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
化繁為簡 圖片來源:unsplash.com/@emileseguin
然而,大多數現實生活中的程序都是複雜的,因此初始化函數可能會開始失控。
class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}
初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣板文件。如何避免這種情況?
Swift 5.1和Property Wrappers
幸運的是,Swift 5.1為我們提供了一個新工具稱為Property Wrappers(正式稱為“property delegates”),該工具作為提案SE-0258的一部分在Swift論壇上提出,並添加到Swift 5.1和Xcode 11中。
Property Wrapper的新功能使屬性值能夠使用自定義get / set實現自動包裝,因此得名。
請注意,可以使用屬性值上的自定義getter和setter來執行其中一些操作,但缺點是必須在每個屬性上編寫幾乎相同的代碼,即更多樣板文件。如果每個屬性都需要某種內部支持變量,那就更糟了。 (還有更多樣板文件。)
@Injected Property Wrapper
因此,在get / set對中自動包裝屬性聽起來並不令人興奮,但屬性包裝器將對我們的Swift代碼產生重大影響。
為了演示,我們將創建一個名為@Injected的Property Wrappers並將其添加到代碼庫中。
現在,回到“失控”示例,看看全新的物業包裝給我們帶來了什麼。
class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}
就是這樣。 只需將屬性標記為@Injected,每個屬性將根據需要自動解析(注入),由此初始化功能中的所有樣板代碼都消失了!
此外,現在從@Injected註釋中可以清楚地看出依賴注入系統提供了哪些服務。
這種特殊類型的註釋方案在其他語言上應用時,最明顯的是在Android上的Kotlin中編程以及使用Dagger 2依賴注入框架。
履行
屬性包裝器實現很簡單。 我們使用Service類型定義一個通用結構,並將其標記為@propertyWrapper。
@propertyWrapper
struct Injected<Service> {
private var service: Service?
public var container: Resolver?
public var name: String?
public var value: Service {
mutating get {
if service == nil {
service = (container ?? Resolver.root).resolve(
Service.self,
name: name
)
}
return service!
}
mutating set {
service = newValue
}
}
}
所有屬性包裝器都必須實現一個名為value的變量。
當從變量請求或賦值時,Value提供屬性包裝器使用的getter和setter實現。
在這種情況下,服務被請求時,我們的值“getter”將檢查這是否是第一次被調用。 如果是這樣,當訪問包裝器代碼時,請求Resolver根據泛型類型解析所需服務的實例,將結果存儲到私有變量中供以後使用,並返回該服務。
當想要手動分配服務時,我們還提供了一個setter。 在某些情況下,這可以派上用場,最值得注意的是在進行單元測試時。
該實現還公開了一些額外的參數,如名稱和容器,更多的是在一秒鐘內實現。
更多實例
屬性包裝器的實現很簡單。使用服務類型定義一個通用結構,並將其標記為@propertyWrapper。
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
override func viewDidLoad() {
...
}
}
將ViewModel精簡到最基本的代碼為:
class XYZViewModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
至註冊碼也被簡化,因為構造函數參數被左右刪除......
func setupMyRegistrations {
register { XYZViewModel() }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
命名服務類型
解析器支持命名類型,它允許程序區分相同類型的服務或協議。
這也展示了一個有趣的property wrappers屬性,讓我們來看看這個。
常見的用例可能需要兩種不同視圖模型中的view controller,該選擇取決於它是否已經傳遞數據,因此應該以“添加”或“編輯”模式操作。
註冊可能如下所示,兩個模型都符合XYZViewModel協議或基類。
func setupMyRegistrations {
register(name: "add") { NewXYZViewModel() as XYZViewModel }
register(name: "edit") { EditXYZViewModel() as XYZViewModel }
}
然後在view controller中:
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
var myData: MyData?
override func viewDidLoad() {
$viewModel.name = myData == nil ? "add" : "edit"
viewModel.configure(myData)
...
}
}
請注意viewDidLoad中引用的$ viewModel.name。
在大多數情況下,我們希望Swift假裝包裝的值是屬性的實際值。但是,使用美元符號為屬性包裝器添加前綴使我們可以引用屬性包裝器本身,從而獲得對可能公開的任何公共變量或函數的訪問權限。
在這種情況下,設置name參數,第一次嘗試使用視圖模型時,該參數將傳遞給Resolver。解析器將在解析依賴關係時傳遞該名稱。
簡而言之,在屬性包裝器上使用$前綴可以讓我們操縱和/或引用包裝器本身。你會在SwiftUI中看到很多這樣的東西。
為什麼是“注入”?
不少人會問:為什麼使用“注入”一詞?既然代碼使用Resolver,為什麼不將它標記為@Resolve?
理由很簡單。我們現在正在使用Resolver,主要是因為我們寫了它。但我們可能想在另一個應用程序中共享或使用我的一些模型或服務代碼,並且該應用程序可能使用不同的系統來管理依賴注入。比如,Swinject Storyboard。
“注入“成為一個更中性的術語,需要做的就是提供一個新版本的@Injected屬性包裝器,使用Swinject作為後端,一旦使用,就可固定化。
其他用例
Property Wrappers將來的更多用途體現在Swift上。
SwiftUI廣泛使用依賴注入,除此之外,Cocoa和UIKit中的標準類提供了一些額外的包裝器也不足為奇。
我們會想到圍繞用戶默認值和鑰匙串訪問的常見包裝器。想象一下用下列代碼包裝任何屬性:
@Keychain(key: "username") var username: String?
並從鑰匙串自動獲取支持你的數據。
過度使用
然而,就像任何酷炫的新錘子一樣,我們冒著過度使用它的風險,因為每個問題看起來都像釘子一樣。
有一次所有東西都變成了協議,然後開始瞭解何時能最好地使用協議(比如數據層代碼),然後再退出。 在此之前,C ++添加了自定義運算符,我們突然試圖找出user1 + user2的結果可能是什麼?
實現Property Wrappers時的關鍵問題是問自己:我是否會在所有代碼庫中廣泛使用這個包裝器? 如果是這樣,那麼Property Wrappers可能是個不錯的選擇。
或者至少減少其佔用的空間。 如果創建一個如上所示的@Keychain包裝器,可以在與KeychainManager類相同的文件中將它實現為fileprivate,從而避免在整個代碼中到處隨意穿插。
畢竟,現在使用它簡單得就像:
@Injected var keychain: KeychainManager
我們不想要每個模型看起來都像這樣的版本:
class MyModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
@Error private var error: String
@Constrain private var myInt: Int
@Status private var x = 0
@Status private var y = 0
}
然後讓下一個查看代碼的開發人員爭先恐後地弄清楚每個包裝器的作用。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
化繁為簡 圖片來源:unsplash.com/@emileseguin
然而,大多數現實生活中的程序都是複雜的,因此初始化函數可能會開始失控。
class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}
初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣板文件。如何避免這種情況?
Swift 5.1和Property Wrappers
幸運的是,Swift 5.1為我們提供了一個新工具稱為Property Wrappers(正式稱為“property delegates”),該工具作為提案SE-0258的一部分在Swift論壇上提出,並添加到Swift 5.1和Xcode 11中。
Property Wrapper的新功能使屬性值能夠使用自定義get / set實現自動包裝,因此得名。
請注意,可以使用屬性值上的自定義getter和setter來執行其中一些操作,但缺點是必須在每個屬性上編寫幾乎相同的代碼,即更多樣板文件。如果每個屬性都需要某種內部支持變量,那就更糟了。 (還有更多樣板文件。)
@Injected Property Wrapper
因此,在get / set對中自動包裝屬性聽起來並不令人興奮,但屬性包裝器將對我們的Swift代碼產生重大影響。
為了演示,我們將創建一個名為@Injected的Property Wrappers並將其添加到代碼庫中。
現在,回到“失控”示例,看看全新的物業包裝給我們帶來了什麼。
class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}
就是這樣。 只需將屬性標記為@Injected,每個屬性將根據需要自動解析(注入),由此初始化功能中的所有樣板代碼都消失了!
此外,現在從@Injected註釋中可以清楚地看出依賴注入系統提供了哪些服務。
這種特殊類型的註釋方案在其他語言上應用時,最明顯的是在Android上的Kotlin中編程以及使用Dagger 2依賴注入框架。
履行
屬性包裝器實現很簡單。 我們使用Service類型定義一個通用結構,並將其標記為@propertyWrapper。
@propertyWrapper
struct Injected<Service> {
private var service: Service?
public var container: Resolver?
public var name: String?
public var value: Service {
mutating get {
if service == nil {
service = (container ?? Resolver.root).resolve(
Service.self,
name: name
)
}
return service!
}
mutating set {
service = newValue
}
}
}
所有屬性包裝器都必須實現一個名為value的變量。
當從變量請求或賦值時,Value提供屬性包裝器使用的getter和setter實現。
在這種情況下,服務被請求時,我們的值“getter”將檢查這是否是第一次被調用。 如果是這樣,當訪問包裝器代碼時,請求Resolver根據泛型類型解析所需服務的實例,將結果存儲到私有變量中供以後使用,並返回該服務。
當想要手動分配服務時,我們還提供了一個setter。 在某些情況下,這可以派上用場,最值得注意的是在進行單元測試時。
該實現還公開了一些額外的參數,如名稱和容器,更多的是在一秒鐘內實現。
更多實例
屬性包裝器的實現很簡單。使用服務類型定義一個通用結構,並將其標記為@propertyWrapper。
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
override func viewDidLoad() {
...
}
}
將ViewModel精簡到最基本的代碼為:
class XYZViewModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
至註冊碼也被簡化,因為構造函數參數被左右刪除......
func setupMyRegistrations {
register { XYZViewModel() }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
命名服務類型
解析器支持命名類型,它允許程序區分相同類型的服務或協議。
這也展示了一個有趣的property wrappers屬性,讓我們來看看這個。
常見的用例可能需要兩種不同視圖模型中的view controller,該選擇取決於它是否已經傳遞數據,因此應該以“添加”或“編輯”模式操作。
註冊可能如下所示,兩個模型都符合XYZViewModel協議或基類。
func setupMyRegistrations {
register(name: "add") { NewXYZViewModel() as XYZViewModel }
register(name: "edit") { EditXYZViewModel() as XYZViewModel }
}
然後在view controller中:
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
var myData: MyData?
override func viewDidLoad() {
$viewModel.name = myData == nil ? "add" : "edit"
viewModel.configure(myData)
...
}
}
請注意viewDidLoad中引用的$ viewModel.name。
在大多數情況下,我們希望Swift假裝包裝的值是屬性的實際值。但是,使用美元符號為屬性包裝器添加前綴使我們可以引用屬性包裝器本身,從而獲得對可能公開的任何公共變量或函數的訪問權限。
在這種情況下,設置name參數,第一次嘗試使用視圖模型時,該參數將傳遞給Resolver。解析器將在解析依賴關係時傳遞該名稱。
簡而言之,在屬性包裝器上使用$前綴可以讓我們操縱和/或引用包裝器本身。你會在SwiftUI中看到很多這樣的東西。
為什麼是“注入”?
不少人會問:為什麼使用“注入”一詞?既然代碼使用Resolver,為什麼不將它標記為@Resolve?
理由很簡單。我們現在正在使用Resolver,主要是因為我們寫了它。但我們可能想在另一個應用程序中共享或使用我的一些模型或服務代碼,並且該應用程序可能使用不同的系統來管理依賴注入。比如,Swinject Storyboard。
“注入“成為一個更中性的術語,需要做的就是提供一個新版本的@Injected屬性包裝器,使用Swinject作為後端,一旦使用,就可固定化。
其他用例
Property Wrappers將來的更多用途體現在Swift上。
SwiftUI廣泛使用依賴注入,除此之外,Cocoa和UIKit中的標準類提供了一些額外的包裝器也不足為奇。
我們會想到圍繞用戶默認值和鑰匙串訪問的常見包裝器。想象一下用下列代碼包裝任何屬性:
@Keychain(key: "username") var username: String?
並從鑰匙串自動獲取支持你的數據。
過度使用
然而,就像任何酷炫的新錘子一樣,我們冒著過度使用它的風險,因為每個問題看起來都像釘子一樣。
有一次所有東西都變成了協議,然後開始瞭解何時能最好地使用協議(比如數據層代碼),然後再退出。 在此之前,C ++添加了自定義運算符,我們突然試圖找出user1 + user2的結果可能是什麼?
實現Property Wrappers時的關鍵問題是問自己:我是否會在所有代碼庫中廣泛使用這個包裝器? 如果是這樣,那麼Property Wrappers可能是個不錯的選擇。
或者至少減少其佔用的空間。 如果創建一個如上所示的@Keychain包裝器,可以在與KeychainManager類相同的文件中將它實現為fileprivate,從而避免在整個代碼中到處隨意穿插。
畢竟,現在使用它簡單得就像:
@Injected var keychain: KeychainManager
我們不想要每個模型看起來都像這樣的版本:
class MyModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
@Error private var error: String
@Constrain private var myInt: Int
@Status private var x = 0
@Status private var y = 0
}
然後讓下一個查看代碼的開發人員爭先恐後地弄清楚每個包裝器的作用。
完成塊
property wrappers只是Swift5.1和Xcode 11中引入的許多功能之一,有望徹底改變編寫Swift應用程序的方式。
SwiftUI和Combine得到了媒體大幅的關注,但特別是在真正開始使用SwiftUI和Combine之前,property wrappers就將大大減少在日常編程中編寫的樣板代碼量。
與SwiftUI和Combine不同,property wrappers可以在早期版本的iOS上使用! 不只是iOS 13。
全文共7044字,預計學習時長14分鐘
圖片來源:unsplash.com/@max_duz
Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減少一半?
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。
背景
現代軟件開發是一種有關項目管理複雜性的練習,架構是我們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解為易於瞭解的層和組件。
因此,我們將軟件分解為可以輕鬆編寫的簡化組件,只做一件事(單一職責原則,SRP),並且可以輕鬆測試。
然而,一旦擁有了一堆部件,就必須將所有部件重新連接在一起,才能形成一個工作應用程序。
以正確的方式將部件連接在一起,就能得到一個由鬆散耦合的組件組成的整潔架構。
但如果連接的方式出錯了,最終只能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運作的方法的信息。
這使組件共享幾乎不可能實現,並且同樣無法輕鬆地將一個組件層換成另一個組件層。
這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。
幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基於一個稱為控制反轉的原理。
依賴注入
本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定組件向系統要求連接到完成其工作所需的所有部件。
這些依賴項將返回到完全成型並準備使用的組件。
例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API組件來獲取一些數據,這些數據又需要訪問身份驗證系統和當前的會話管理器。ViewModel還需要一個具有依賴關係的數據轉換服務。
ViewController不涉及這些東西,也不應該涉及,只需與它完成工作所需的組件進行對話。
為了演示所涉及的技術,本文將使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI框架也可。
如果想了解更多信息,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。
傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------
簡單實例
使用依賴注入的非常基本的視圖模型如下所示:
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
以上列出的是view model需要的組件,以及一個初始化函數,其作用基本上是將傳遞給模型的任何組件分配給模型的實例變量。
這稱為構造函數注入,使用該函數可確保在無法實例化給定組件時不用給它所需的一切。
現在有了view model之後,如何獲得是view controller?
Resolver可以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。
class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}
因此viewModel要求Resolver“resolve(解析)”依賴關係。解析器使用提供的類型信息來查找用於創建所請求類型的對象的實例工廠。
請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,只需依賴注入系統處理所有這些凌亂的小細節。
此外還有其他一些好處。例如,可以運行“Mock”方案,其中數據層被替換為來自應用程序中嵌入的JSON文件的mock數據,這樣的數據在開發,調試和測試時都便於運行。
依賴系統可以在後臺輕鬆處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。
Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------
最後請注意,在依賴注入術語中,依賴關係通常稱為服務。
註冊
為了使典型的依賴注入系統工作,服務必須註冊,需要提供與系統可能要創建的每種類型相關聯的工廠方法。
在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型信息。
因此,解析器中的典型註冊塊可能如下所示:
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
注意第一個註冊函數註冊XYZViewModel並提供一個工廠函數來創建新的實例。 註冊的類型由工廠的返回類型自動推斷。
XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名並依次解析來解決。
第二個函數註冊XYZFetching協議,通過構建具有自己的依賴關係的XYZFetcher實例來滿足該協議。
該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件並執行他們需要的操作。
問題
化繁為簡 圖片來源:unsplash.com/@emileseguin
然而,大多數現實生活中的程序都是複雜的,因此初始化函數可能會開始失控。
class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}
初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣板文件。如何避免這種情況?
Swift 5.1和Property Wrappers
幸運的是,Swift 5.1為我們提供了一個新工具稱為Property Wrappers(正式稱為“property delegates”),該工具作為提案SE-0258的一部分在Swift論壇上提出,並添加到Swift 5.1和Xcode 11中。
Property Wrapper的新功能使屬性值能夠使用自定義get / set實現自動包裝,因此得名。
請注意,可以使用屬性值上的自定義getter和setter來執行其中一些操作,但缺點是必須在每個屬性上編寫幾乎相同的代碼,即更多樣板文件。如果每個屬性都需要某種內部支持變量,那就更糟了。 (還有更多樣板文件。)
@Injected Property Wrapper
因此,在get / set對中自動包裝屬性聽起來並不令人興奮,但屬性包裝器將對我們的Swift代碼產生重大影響。
為了演示,我們將創建一個名為@Injected的Property Wrappers並將其添加到代碼庫中。
現在,回到“失控”示例,看看全新的物業包裝給我們帶來了什麼。
class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}
就是這樣。 只需將屬性標記為@Injected,每個屬性將根據需要自動解析(注入),由此初始化功能中的所有樣板代碼都消失了!
此外,現在從@Injected註釋中可以清楚地看出依賴注入系統提供了哪些服務。
這種特殊類型的註釋方案在其他語言上應用時,最明顯的是在Android上的Kotlin中編程以及使用Dagger 2依賴注入框架。
履行
屬性包裝器實現很簡單。 我們使用Service類型定義一個通用結構,並將其標記為@propertyWrapper。
@propertyWrapper
struct Injected<Service> {
private var service: Service?
public var container: Resolver?
public var name: String?
public var value: Service {
mutating get {
if service == nil {
service = (container ?? Resolver.root).resolve(
Service.self,
name: name
)
}
return service!
}
mutating set {
service = newValue
}
}
}
所有屬性包裝器都必須實現一個名為value的變量。
當從變量請求或賦值時,Value提供屬性包裝器使用的getter和setter實現。
在這種情況下,服務被請求時,我們的值“getter”將檢查這是否是第一次被調用。 如果是這樣,當訪問包裝器代碼時,請求Resolver根據泛型類型解析所需服務的實例,將結果存儲到私有變量中供以後使用,並返回該服務。
當想要手動分配服務時,我們還提供了一個setter。 在某些情況下,這可以派上用場,最值得注意的是在進行單元測試時。
該實現還公開了一些額外的參數,如名稱和容器,更多的是在一秒鐘內實現。
更多實例
屬性包裝器的實現很簡單。使用服務類型定義一個通用結構,並將其標記為@propertyWrapper。
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
override func viewDidLoad() {
...
}
}
將ViewModel精簡到最基本的代碼為:
class XYZViewModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
至註冊碼也被簡化,因為構造函數參數被左右刪除......
func setupMyRegistrations {
register { XYZViewModel() }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}
命名服務類型
解析器支持命名類型,它允許程序區分相同類型的服務或協議。
這也展示了一個有趣的property wrappers屬性,讓我們來看看這個。
常見的用例可能需要兩種不同視圖模型中的view controller,該選擇取決於它是否已經傳遞數據,因此應該以“添加”或“編輯”模式操作。
註冊可能如下所示,兩個模型都符合XYZViewModel協議或基類。
func setupMyRegistrations {
register(name: "add") { NewXYZViewModel() as XYZViewModel }
register(name: "edit") { EditXYZViewModel() as XYZViewModel }
}
然後在view controller中:
class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
var myData: MyData?
override func viewDidLoad() {
$viewModel.name = myData == nil ? "add" : "edit"
viewModel.configure(myData)
...
}
}
請注意viewDidLoad中引用的$ viewModel.name。
在大多數情況下,我們希望Swift假裝包裝的值是屬性的實際值。但是,使用美元符號為屬性包裝器添加前綴使我們可以引用屬性包裝器本身,從而獲得對可能公開的任何公共變量或函數的訪問權限。
在這種情況下,設置name參數,第一次嘗試使用視圖模型時,該參數將傳遞給Resolver。解析器將在解析依賴關係時傳遞該名稱。
簡而言之,在屬性包裝器上使用$前綴可以讓我們操縱和/或引用包裝器本身。你會在SwiftUI中看到很多這樣的東西。
為什麼是“注入”?
不少人會問:為什麼使用“注入”一詞?既然代碼使用Resolver,為什麼不將它標記為@Resolve?
理由很簡單。我們現在正在使用Resolver,主要是因為我們寫了它。但我們可能想在另一個應用程序中共享或使用我的一些模型或服務代碼,並且該應用程序可能使用不同的系統來管理依賴注入。比如,Swinject Storyboard。
“注入“成為一個更中性的術語,需要做的就是提供一個新版本的@Injected屬性包裝器,使用Swinject作為後端,一旦使用,就可固定化。
其他用例
Property Wrappers將來的更多用途體現在Swift上。
SwiftUI廣泛使用依賴注入,除此之外,Cocoa和UIKit中的標準類提供了一些額外的包裝器也不足為奇。
我們會想到圍繞用戶默認值和鑰匙串訪問的常見包裝器。想象一下用下列代碼包裝任何屬性:
@Keychain(key: "username") var username: String?
並從鑰匙串自動獲取支持你的數據。
過度使用
然而,就像任何酷炫的新錘子一樣,我們冒著過度使用它的風險,因為每個問題看起來都像釘子一樣。
有一次所有東西都變成了協議,然後開始瞭解何時能最好地使用協議(比如數據層代碼),然後再退出。 在此之前,C ++添加了自定義運算符,我們突然試圖找出user1 + user2的結果可能是什麼?
實現Property Wrappers時的關鍵問題是問自己:我是否會在所有代碼庫中廣泛使用這個包裝器? 如果是這樣,那麼Property Wrappers可能是個不錯的選擇。
或者至少減少其佔用的空間。 如果創建一個如上所示的@Keychain包裝器,可以在與KeychainManager類相同的文件中將它實現為fileprivate,從而避免在整個代碼中到處隨意穿插。
畢竟,現在使用它簡單得就像:
@Injected var keychain: KeychainManager
我們不想要每個模型看起來都像這樣的版本:
class MyModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
@Error private var error: String
@Constrain private var myInt: Int
@Status private var x = 0
@Status private var y = 0
}
然後讓下一個查看代碼的開發人員爭先恐後地弄清楚每個包裝器的作用。
完成塊
property wrappers只是Swift5.1和Xcode 11中引入的許多功能之一,有望徹底改變編寫Swift應用程序的方式。
SwiftUI和Combine得到了媒體大幅的關注,但特別是在真正開始使用SwiftUI和Combine之前,property wrappers就將大大減少在日常編程中編寫的樣板代碼量。
與SwiftUI和Combine不同,property wrappers可以在早期版本的iOS上使用! 不只是iOS 13。
留言 點贊 關注
我們一起分享AI學習與發展的乾貨