可組合架構(簡稱 TCA)是一個庫,用於以一致且易於理解的方式建立應用程序,並考慮到組合、測試和人體工學。它可用於 SwiftUI、UIKit 等以及任何 Apple 平台(iOS、macOS、visionOS、tvOS 和 watchOS)。
什麼是可組合架構?
了解更多
範例
基本用法
文件
社群
安裝
翻譯
該庫提供了一些核心工具,可用於建立不同目的和複雜性的應用程式。它提供了引人入勝的故事,您可以遵循這些故事來解決建立應用程式時日常遇到的許多問題,例如:
狀態管理
如何使用簡單的值類型管理應用程式的狀態,並在多個螢幕之間共享狀態,以便可以立即在另一個螢幕中觀察到一個螢幕中的變化。
作品
如何將大型功能分解為更小的組件,這些組件可以提取到自己的獨立模組中,並可以輕鬆地粘合在一起以形成功能。
副作用
如何讓應用程式的某些部分以最可測試和最容易理解的方式與外界對話。
測試
如何不僅測試架構中內建的功能,還為由許多部分組成的功能編寫整合測試,並編寫端到端測試以了解副作用如何影響應用程式。這使您能夠強有力地保證您的業務邏輯按照您期望的方式運作。
人體工學
如何使用盡可能少的概念和移動部件在一個簡單的 API 中完成上述所有任務。
可組合架構是在 Point-Free 的許多劇集中設計的,Point-Free 是探索函數式程式設計和 Swift 語言的影片系列,由 Brandon Williams 和 Stephen Celis 主持。
您可以在這裡觀看所有劇集,並從頭開始對建築進行專門的多部分遊覽。
此儲存庫附帶了大量範例,示範如何使用可組合架構解決常見和複雜的問題。查看此目錄以查看全部內容,包括:
案例研究
入門
效果
導航
高階減速器
可重複使用的組件
地點經理
運動管理器
搜尋
語音辨識
同步應用程式
井字遊戲
托多斯
語音備忘錄
尋找更實質的東西?查看 isowords 的源代碼,這是一款基於 SwiftUI 和可組合架構構建的 iOS 單字搜尋遊戲。
筆記
如需逐步互動式教程,請務必查看 Meet the Composable Architecture。
要使用可組合架構建構功能,您需要定義一些對您的網域進行建模的類型和值:
State :一種類型,描述您的功能執行其邏輯並呈現其 UI 所需的資料。
Action :表示功能中可能發生的所有操作的類型,例如使用者操作、通知、事件來源等。
減速器:描述如何將應用程式的當前狀態發展到給定操作的下一個狀態的函數。減速器還負責傳回應該運行的任何效果,例如 API 請求,這可以透過傳回Effect
值來完成。
Store :實際驅動您的功能的運行時。您將所有使用者操作傳送到store,以便store可以執行reducer和effects,您可以觀察store中的狀態變化,以便您可以更新UI。
這樣做的好處是,您將立即解鎖功能的可測試性,並且您將能夠將大型、複雜的功能分解為可以粘合在一起的較小域。
作為一個基本範例,請考慮一個顯示數字以及用於遞增和遞減數字的「+」和「−」按鈕的 UI。為了讓事情變得有趣,假設還有一個按鈕,點擊該按鈕時會發出 API 請求以獲取有關該數字的隨機事實並將其顯示在視圖中。
為了實現此功能,我們建立一個新類型來容納該功能的域和行為,並將使用@Reducer
巨集進行註解:
導入 ComposableArchitecture@Reducerstruct 功能 {}
在這裡,我們需要為功能的狀態定義一個類型,它由當前計數的整數以及表示所呈現事實的可選字串組成:
@Reducerstruct Feature { @ObservableState struct State:Equatable { var count = 0 var numberFact:字串? }}
筆記
我們已將@ObservableState
巨集應用於State
,以便利用庫中的觀察工具。
我們還需要為該功能的操作定義一個類型。有一些明顯的操作,例如點擊遞減按鈕、遞增按鈕或事實按鈕。但也有一些不太明顯的操作,例如當我們收到事實 API 請求的回應時發生的操作:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped caseincrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
然後我們實作body
屬性,它負責組成該功能的實際邏輯和行為。在裡面我們可以使用Reduce
減速器來描述如何將目前狀態改變到下一個狀態,以及需要執行什麼效果。有些動作不需要執行效果,它們可以返回.none
來表示:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: someReducer{ Reduce { 狀態,動作 in 開關動作 { case .decrementButtonTapped: state.count -= 1 回傳 .none 狀況 .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] 傳送 let (data, _) = try wait URLSession.shared.data( 來自:URL(字串:“http://numbersapi.com/(count)/trivia”)! ) wait send( .numberFactResponse(String(解碼: data, as: UTF8.self)) ) } case let .numberFactResponse(fact): state.numberFact = 事實回傳.none } } }}
最後我們定義顯示該功能的視圖。它持有StoreOf
以便它可以觀察狀態的所有更改並重新渲染,並且我們可以將所有用戶操作發送到存儲,以便狀態更改:
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Increment") { store.send(.incrementButtonTapped) } } 部分{ Button("數字事實") { store.send(.numberFactButtonTapped) } } if let fact = store.numberFact { Text(fact) } } }}
從該儲存裝置中驅動 UIKit 控制器也很簡單。您可以在viewDidLoad
中觀察儲存中的狀態變化,然後使用儲存中的資料填充 UI 元件。程式碼比 SwiftUI 版本稍長,因此我們將其折疊在這裡:
class FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, bundle: nil) } 需要 init?(coder: NSCoder) { fatalError("init(coder:) 尚未實作") } override func viewDidLoad() { super.viewDidLoad() 讓 countLabel = UILabel() 讓 decrementButton = UIButton() 讓incrementButton = UIButton() 讓factLabel = UILabel() // 省略:新增子視圖並設定約束... 觀察{[弱自我] 守護讓自己 否則{返回} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private funcincrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { prif. factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
一旦我們準備好顯示此視圖(例如在應用程式的入口點中),我們就可以建立一個商店。這可以透過指定啟動應用程式的初始狀態以及為應用程式供電的減速器來完成:
導入 ComposableArchitecture@mainstruct MyApp: App { var body: 某些場景 { WindowGroup { FeatureView( 商店:商店(初始狀態:Feature.State()){Feature()})}}}}
這足以讓螢幕上有一些東西可以玩。與以普通 SwiftUI 方式執行此操作相比,這肯定要多執行幾個步驟,但也有一些好處。它為我們提供了一種一致的方式來應用狀態突變,而不是將邏輯分散在一些可觀察物件和 UI 元件的各種操作閉包中。它也為我們提供了一種表達副作用的簡潔方式。我們可以立即測試這個邏輯,包括效果,而無需做太多額外的工作。
筆記
有關測試的更深入信息,請參閱專門的測試文章。
要進行測試,請使用TestStore
,它可以使用與Store
相同的資訊創建,但它會做額外的工作,以允許您斷言發送操作時您的功能如何演變:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
建立測試儲存後,我們可以使用它來對整個使用者步驟流進行斷言。每一步我們都需要證明狀態改變了我們的期望。例如,我們可以模擬點擊遞增和遞減按鈕的使用者流程:
// 測試點選遞增/遞減按鈕是否會更改 countawait store.send(.incrementButtonTapped) { $0.count = 1}等待 store.send(.decrementButtonTapped) { $0.計數 = 0}
此外,如果一個步驟導致執行一個效果,將資料回饋到儲存中,我們必須對此進行斷言。例如,如果我們模擬使用者點擊事實按鈕,我們期望收到包含事實的事實回應,這將導致填充numberFact
狀態:
等待 store.send(.numberFactButtonTapped)等待 store.receive(.numberFactResponse) { $0.numberFact = ???}
然而,我們如何知道什麼事實會被送回給我們呢?
目前,我們的減速器正在使用影響現實世界的效果來存取 API 伺服器,這意味著我們無法控制其行為。為了編寫此測試,我們對互聯網連接和 API 伺服器的可用性進行了調整。
最好將此依賴項傳遞給減速器,以便我們在設備上運行應用程式時可以使用即時依賴項,但使用模擬依賴項進行測試。我們可以透過向Feature
縮減器添加一個屬性來做到這一點:
@Reducerstruct Feature { let numberFact: (Int) 非同步拋出 -> String // ...}
然後我們可以在reduce
實作中使用它:
案例.numberFactButtonTapped: return .run { [count = state.count] 發送 讓事實 = 嘗試等待 self.numberFact(count) 等待發送(.numberFactResponse(fact)) }
在應用程式的入口點,我們可以提供一個與現實世界 API 伺服器實際互動的依賴項版本:
@mainstruct MyApp: App { var body: 一些場景 { WindowGroup { FeatureView( 商店:商店(初始狀態:Feature.State()){Feature( numberFact: { let (data, _) = 嘗試等待 URLSession.shared.data( 來自:URL(字串:“http://numbersapi.com/(number)”)! ) return String(解碼: 資料, as: UTF8.self) } ) } ) } }}
但在測試中,我們可以使用模擬依賴項,它立即返回確定性的、可預測的事實:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) 是一個很好的數字布倫特" }) }}
透過這一點前期工作,我們可以透過模擬使用者點擊事實按鈕來完成測試,然後接收來自依賴項的回應以呈現事實:
等待 store.send(.numberFactButtonTapped)等待 store.receive(.numberFactResponse) { $0.numberFact = "0 是一個很好的數字布倫特"}
我們還可以改進在應用程式中使用numberFact
依賴項的人體工學。隨著時間的推移,應用程式可能會發展出許多功能,其中一些功能可能還需要存取numberFact
,並且明確地將其傳遞到所有層可能會很煩人。您可以遵循一個流程來向庫「註冊」依賴項,使它們立即可供應用程式中的任何層使用。
筆記
有關依賴關係管理的更深入信息,請參閱專門的依賴關係文章。
我們可以先將數位事實功能包裝在新類型中:
struct NumberFactClient { var fetch: (Int) 非同步拋出 -> String}
然後透過將客戶端符合DependencyKey
協定來向依賴管理系統註冊該類型,該協定要求您指定在模擬器或裝置中執行應用程式時要使用的即時值:
擴展 NumberFactClient: DependencyKey { static let liveValue = Self( fetch: { let (data, _) = try wait URLSession.shared .data(from: URL(string: "http://numbersapi.com/(number)")! ) return String(解碼: data,as: UTF8.self) } )}擴充DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = newValue } }}}
完成一點前期工作後,您可以透過使用@Dependency
屬性包裝器立即開始在任何功能中使用依賴項:
@減速器 struct Feature {- let numberFact: (Int) 非同步拋出 -> String+ @Dependency(.numberFact) var numberFact ...- 嘗試等待 self.numberFact(count)+ 嘗試等待 self.numberFact.fetch(count) }
此程式碼的工作方式與之前完全相同,但您不再需要在建構該功能的減速器時明確傳遞依賴項。當在預覽、模擬器或裝置上執行應用程式時,即時依賴項將提供給減速器,並且在測試中將提供測試依賴項。
這意味著應用程式的入口點不再需要建置依賴項:
@mainstruct MyApp: App { var body: 一些場景 { WindowGroup { FeatureView( 商店:商店(初始狀態:Feature.State()){Feature()})}}}}
可以在不指定任何依賴項的情況下建立測試存儲,但您仍然可以覆蓋測試所需的任何依賴項:
let store = TestStore(initialState: Feature.State()) { Feature()} withDependencies: { $0.numberFact.fetch = { "($0) 是個很好的數字布倫特" }}// ...
這是在可組合架構中建構和測試功能的基礎知識。還有很多東西需要探索,例如組合、模組化、適應性、複雜效果等。範例目錄有很多項目可供探索,以了解更進階的用法。
版本和main
文件可在此處找到:
main
1.17.0(遷移指南)
1.16.0(遷移指南)
1.15.0(遷移指南)
1.14.0(遷移指南)
1.13.0(遷移指南)
1.12.0(遷移指南)
1.11.0(遷移指南)
1.10.0(遷移指南)
1.9.0(遷移指南)
1.8.0(遷移指南)
1.7.0(遷移指南)
1.6.0(遷移指南)
1.5.0(遷移指南)
1.4.0(遷移指南)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
當您更加熟悉該庫時,文件中的許多文章可能會對您有所幫助:
入門
依賴關係
測試
導航
共享狀態
表現
並發性
綁定
如果您想討論可組合架構或對如何使用它來解決特定問題有疑問,您可以在許多地方與其他 Point-Free 愛好者討論:
對於長篇討論,我們建議使用此儲存庫的討論標籤。
休閒聊天,我們推薦 Point-Free Community slack。
您可以透過將 ComposableArchitecture 新增為套件依賴項來將其新增至 Xcode 專案。
從“檔案”選單中,選擇“新增套件依賴項...”
在套件儲存庫 URL 文字欄位中輸入“https://github.com/pointfreeco/swift-composable-architecture”
取決於您的專案的結構:
如果您有一個需要存取該程式庫的應用程式目標,請將ComposableArchitecture直接新增至您的應用程式。
如果您想要從多個 Xcode 目標使用此程式庫,或混合 Xcode 目標和 SPM 目標,則必須建立一個依賴ComposableArchitecture的共用框架,然後在所有目標中依賴該框架。有關此範例,請查看 Tic-Tac-Toe 演示應用程序,該應用程式將許多功能拆分為模組,並使用tic-tac-toe Swift 套件以這種方式使用靜態庫。
可組合架構在建置時考慮了可擴展性,並且有許多社群支援的程式庫可用於增強您的應用程式:
可組合架構額外功能:可組合架構的配套函式庫。
TCAComposer:用於在可組合架構中產生樣板程式碼的巨集框架。
TCACoordinators:可組合架構中的協調器模式。
如果您想貢獻一個庫,請打開一個帶有連結的 PR!
本自述文件的以下翻譯由社群成員貢獻:
阿拉伯
法語
印地語
印尼
義大利語
日本人
韓國人
拋光
葡萄牙語
俄文
簡體中文
西班牙語
烏克蘭
如果您想貢獻翻譯,請打開帶有要點連結的 PR!
我們有一篇專門的文章來解答人們關於圖書館的所有最常見問題和評論。
以下人員在圖書館的早期階段提供了回饋,並幫助圖書館發展成為今天的樣子:
保羅·科爾頓、卡恩·德德奧格魯、馬特·迪普豪斯、約瑟夫·多萊扎爾、艾曼塔斯、馬修·約翰遜、喬治·凱馬卡斯、尼基塔·列昂諾夫、克里斯多福·利西奧、傑弗瑞·馬科、亞歷杭德羅·馬丁內斯、謝·米沙利、威利斯·普拉默、西蒙-皮埃爾·羅伊、賈斯汀·普萊斯、斯文·A·施密特、Kyle Sherman、Petr Šíma、Jasdev Singh、Maxim Smirnov、Ryan Stone、Daniel Hollis Tavares 以及所有 Point-Free 訂戶?
特別感謝 Chris Liscio,他幫助我們解決了許多奇怪的 SwiftUI 怪癖,並幫助完善了最終的 API。
感謝 Shai Mishali 和 JointCommunity 項目,我們從中獲取了Publishers.Create
的實現,我們在Effect
中使用它來幫助橋接委託和基於回調的 API,從而更輕鬆地與第 3 方框架進行交互。
可組合架構建立在其他函式庫(特別是 Elm 和 Redux)提出的想法基礎上。
Swift 和 iOS 社群中也有很多架構庫。其中每一個都有自己的一組優先順序和權衡,與可組合架構不同。
肋骨
環形
瑞斯威夫特
工作流程
反應器套件
接收回饋
莫比烏斯‧史威夫特
通量器
PromisedArchitectureKit
該庫是在 MIT 許可下發布的。有關詳細信息,請參閱許可證。