前言
SwiftUI與蘋果之前的UI框架的區(qū)別不僅僅在于如何定義視圖和其他UI組件,還在于如何在整個(gè)使用它的應(yīng)用程序中管理視圖層級(jí)的狀態(tài)。
SwiftUI沒有使用委托、數(shù)據(jù)源或任何其他在UIKit和AppKit等命令式框架中常見的狀態(tài)管理模式,而是配備了一些屬性包裝器[1],使我們能夠準(zhǔn)確地聲明我們的數(shù)據(jù)如何被我們的視圖觀察、渲染和改變。
本周,讓我們仔細(xì)看看這些屬性包裝器中的每一個(gè),它們之間的關(guān)系,以及它們?nèi)绾螛?gòu)成SwiftUI整體狀態(tài)管理系統(tǒng)的不同部分。
屬性狀態(tài)
由于SwiftUI主要是一個(gè)UI框架(盡管它也開始獲得用于定義更高層次結(jié)構(gòu)(如應(yīng)用程序和場景)的API),其聲明式設(shè)計(jì)不一定需要影響應(yīng)用程序的整個(gè)模型和數(shù)據(jù)層——而只是直接綁定到我們各種視圖的狀態(tài)。
例如,假設(shè)我們正在開發(fā)一個(gè)SignupView,使用戶能夠通過輸入用戶名和電子郵件地址在應(yīng)用程序中注冊(cè)一個(gè)新賬戶。我們將使用這兩個(gè)值形成一個(gè)用戶模型,并將其傳遞給一個(gè)閉包:
struct SignupView: View { var handler: (User) -> Void var username = “” var email = “” var body: some View { … }}
由于這三個(gè)屬性中只有兩個(gè)——username和email——實(shí)際上會(huì)被我們的視圖修改,而且這兩個(gè)狀態(tài)可以保持私有,我們將使用SwiftUI的State屬性包裝器來標(biāo)記它們——像這樣:
struct SignupView: View { var handler: (User) -> Void @State private var username = “” @State private var email = “” var body: some View { … }}
這樣做將自動(dòng)在這兩個(gè)值和我們的視圖本身之間建立一個(gè)連接——這意味著我們的視圖將在每次改變這兩個(gè)值的時(shí)候被重新渲染。在我們的主體中,我們將把這兩個(gè)屬性分別綁定到一個(gè)相應(yīng)的TextField上,以使它們可以被用戶編輯:
struct SignupView: View { var handler: (User) -> Void @State private var username = “” @State private var email = “” var body: some View { VStack { TextField(“Username”, text: $username) TextField(“Email”, text: $email) Button( action: { self.handler(User( username: self.username, email: self.email )) }, label: { Text(“Sign up”) } ) } .padding() }}
因此,State 被用來表示SwiftUI視圖的內(nèi)部狀態(tài),并在該狀態(tài)被改變時(shí)自動(dòng)使視圖更新。因此,最常見的做法是將State屬性包裝器保持為私有,這可以確保它們只在該視圖的主體內(nèi)被改變(試圖在其他地方改變它們實(shí)際上會(huì)導(dǎo)致運(yùn)行時(shí)崩潰)。
雙向綁定
看一下上面的代碼樣本,我們將每個(gè)屬性傳入其TextField 的方式是在這些屬性名稱前加上$ 。這是因?yàn)槲覀儾恢皇菍⑵胀ǖ腟tring 值傳入這些文本字段,而是與我們的State包裝的屬性本身綁定。
為了更詳細(xì)地探討這意味著什么,讓我們現(xiàn)在假設(shè)我們想創(chuàng)建一個(gè)視圖,讓我們的用戶編輯他們最初在注冊(cè)時(shí)輸入的個(gè)人資料信息。由于我們現(xiàn)在要修改外部狀態(tài)值,而不僅僅是私人狀態(tài)值,所以這次我們將username和email 屬性標(biāo)記為Bingding:
struct ProfileEditingView: View { @Binding var username: String @Binding var email: String var body: some View { VStack { TextField(“Username”, text: $username) TextField(“Email”, text: $email) } .padding() }}
最酷的是,綁定不僅僅局限于單一的內(nèi)置值,比如字符串或整數(shù),而是可以用來將任何Swift值綁定到我們的一個(gè)視圖中。例如,我們可以將用戶模型本身傳遞給ProfileEditingView ,而不是傳遞兩個(gè)單獨(dú)的username和email:
struct ProfileEditingView: View { @Binding var user: User var body: some View { VStack { TextField(“Username”, text: $user.username) TextField(“Email”, text: $user.email) } .padding() }}就像我們?cè)趯tate和
就像我們?cè)趯tate和Binding 包裝的屬性傳入各種TextField 實(shí)例時(shí)用$ 作為前綴一樣,我們?cè)趯⑷魏蜸tate 值連接到我們自己定義的Binding屬性時(shí)也可以做同樣的事情。
例如,這里有一個(gè)ProfileView 的實(shí)現(xiàn),它使用一個(gè)Stage 包裝屬性來跟蹤一個(gè)用戶模型,然后在將上述ProfileEditingView 的實(shí)例作為工作表呈現(xiàn)時(shí),將該模型傳遞一個(gè)綁定——這將自動(dòng)同步用戶對(duì)該原始State屬性值的任何改變:
struct ProfileView: View { @State private var user = User.load() @State private var isEditingViewShown = false var body: some View { VStack(alignment: .leading, spacing: 10) { Text(“Username: “) .foregroundColor(.secondary) + Text(user.username) Text(“Email: “) .foregroundColor(.secondary) + Text(user.email) Button( action: { self.isEditingViewShown = true }, label: { Text(“Edit”) } ) } .padding() .sheet(isPresented: $isEditingViewShown) { VStack { ProfileEditingView(user: self.$user) Button( action: { self.isEditingViewShown = false }, label: { Text(“Done”) } ) } } }}
請(qǐng)注意,我們也可以通過給一個(gè)State 包裝的屬性分配一個(gè)新的值來改變它——比如我們?cè)?“Done “按鈕的動(dòng)作處理程序中把isEditingViewShown 設(shè)置為false。
因此,一個(gè)Binding 標(biāo)記的屬性在給定的視圖和定義在該視圖之外的狀態(tài)屬性之間提供了一個(gè)雙向的連接,而Statr和Binding 包裝的屬性都可以通過在其屬性名前加上$來作為綁定物傳遞。
觀察對(duì)象
State和Bingding的共同點(diǎn)是,它們處理的是在SwiftUI視圖層次結(jié)構(gòu)本身中管理的值。然而,雖然建立一個(gè)將所有的狀態(tài)都保存在其各種視圖中的應(yīng)用程序是肯定可行的,但從架構(gòu)和關(guān)注點(diǎn)分離的角度來看,這通常不是一個(gè)好主意,而且很容易導(dǎo)致我們的視圖變得相當(dāng)龐大和復(fù)雜。
值得慶幸的是,SwiftUI還提供了一些機(jī)制,使我們能夠?qū)⑼獠磕P蛯?duì)象連接到我們的各種視圖。其中一個(gè)機(jī)制是ObservableObject 協(xié)議,當(dāng)它與ObservedObject屬性包裝器結(jié)合時(shí),我們可以設(shè)置與我們視圖層之外管理的引用類型的綁定。
作為一個(gè)例子,讓我們更新上面定義的ProfileView ——通過將管理User 模型的責(zé)任從視圖本身轉(zhuǎn)移到一個(gè)新的、專門的對(duì)象中?,F(xiàn)在,我們可以用許多不同的方式來描述這樣一個(gè)對(duì)象,但由于我們正在尋找創(chuàng)建一個(gè)類型來控制我們的一個(gè)模型的實(shí)例——讓我們把它變成一個(gè)符合SwiftUI的ObservableObject協(xié)議的模型控制器[2]:
class UserModelController: ObservableObject { @Published var user: User …}
Published屬性包裝器用于定義對(duì)象的哪些屬性在被修改時(shí)應(yīng)讓觀察通知被觸發(fā)。
有了上面的類型,現(xiàn)在讓我們回到ProfileView ,讓它觀察新的UserModelController 的實(shí)例,作為一個(gè)ObservedObject ,而不是用一個(gè)State 屬性包裝器來跟蹤我們的用戶模型。最重要的是,我們?nèi)匀豢梢院苋菀椎貙⑦@個(gè)模型綁定到我們的ProfileEditingView 上,就像以前一樣,因?yàn)镺bservedObject屬性包裝器也可以轉(zhuǎn)換為綁定:
struct ProfileView: View { @ObservedObject var userController: UserModelController @State private var isEditingViewShown = false var body: some View { VStack(alignment: .leading, spacing: 10) { Text(“Username: “) .foregroundColor(.secondary) + Text(userController.user.username) Text(“Email: “) .foregroundColor(.secondary) + Text(userController.user.email) Button( action: { self.isEditingViewShown = true }, label: { Text(“Edit”) } ) } .padding() .sheet(isPresented: $isEditingViewShown) { VStack { ProfileEditingView(user: self.$userController.user) Button( action: { self.isEditingViewShown = false }, label: { Text(“Done”) } ) } } }}
然而,我們的新實(shí)現(xiàn)與之前使用的基于狀態(tài)的實(shí)現(xiàn)之間的一個(gè)重要區(qū)別是,我們的UserModelController 現(xiàn)在需要作為初始化器的一部分被注入到ProfileView中。
除了 “迫使 “我們?cè)诖a庫中建立一個(gè)更明確的依賴關(guān)系圖之外,原因是一個(gè)標(biāo)有ObservedObject的屬性并不意味著對(duì)這個(gè)屬性所指向的對(duì)象有任何形式的所有權(quán)。
因此,雖然下面的內(nèi)容在技術(shù)上可能會(huì)被編譯,但最終會(huì)導(dǎo)致運(yùn)行時(shí)的問題——因?yàn)楫?dāng)我們的視圖在更新時(shí)被重新創(chuàng)建,UserModelController實(shí)例可能會(huì)被刪除(因?yàn)槲覀兊囊晥D現(xiàn)在是它的主要所有者):
struct ProfileView: View { @ObservedObject var userController = UserModelController.load() …}
重要的是要記住: SwiftUI視圖不是對(duì)正在屏幕上渲染的實(shí)際UI組件的引用,而是描述我們的UI的輕量級(jí)值——因此它們沒有像UIView實(shí)例那樣的生命周期。
為了解決上述問題,蘋果在iOS 14和macOS Big Sur中引入了一個(gè)新的屬性包裝器,名為StateObject 。標(biāo)記為StateObject 的屬性與ObservedObject的行為完全相同——此外,SwiftUI將確保存儲(chǔ)在此類屬性中的任何對(duì)象不會(huì)因?yàn)榭蚣茉谥匦落秩疽晥D時(shí)重新創(chuàng)建新實(shí)例而被意外釋放:
struct ProfileView: View { @StateObject var userController = UserModelController.load() …}
盡管從技術(shù)上來說,從現(xiàn)在開始可以只使用StateObject ——我仍然建議在觀察外部對(duì)象時(shí)使用ObservedObject ,而在處理視圖本身擁有的對(duì)象時(shí)只使用StateObject 。把StateObject和ObservedObject 看作是State和Binding的參考類型,或者SwiftUI版本的強(qiáng)和弱屬性。
觀察和修改環(huán)境變量
最后,讓我們來看看SwiftUI的環(huán)境系統(tǒng)如何被用來在兩個(gè)互不直接連接的視圖之間傳遞各種狀態(tài)。盡管在一個(gè)父視圖和它的一個(gè)子視圖之間創(chuàng)建綁定通常很容易,但在整個(gè)視圖層次結(jié)構(gòu)中傳遞某個(gè)對(duì)象或值可能相當(dāng)麻煩——而這正是環(huán)境變量旨在解決的問題類型。
有兩種主要的方法來使用SwiftUI的環(huán)境。一種是首先在想要檢索給定對(duì)象的視圖中定義一個(gè)EnvironmentObject 包裝的屬性——例如像這個(gè)ArticleView 如何檢索一個(gè)包含顏色信息的Theme對(duì)象:
struct ArticleView: View { @EnvironmentObject var theme: Theme var article: Article var body: some View { VStack(alignment: .leading) { Text(article.title) .foregroundColor(theme.titleTextColor) Text(article.body) .foregroundColor(theme.bodyTextColor) } }}
然后,我們必須確保在我們的視圖的某一個(gè)父類中提供我們的環(huán)境對(duì)象(在這種情況下是一個(gè)Theme 實(shí)例),然后SwiftUI會(huì)處理其余的事情。這是通過使用environmentalObject修飾符完成的,例如,像這樣:
struct RootView: View { @ObservedObject var theme: Theme @ObservedObject var articleLibrary: ArticleLibrary var body: some View { ArticleListView(articles: articleLibrary.articles) .environmentObject(theme) }}
請(qǐng)注意,我們不需要將上述修改器應(yīng)用于將使用我們的環(huán)境對(duì)象的確切視圖——我們可以將其應(yīng)用于我們的層次結(jié)構(gòu)中任何在其之上的視圖。
使用 SwiftUI 環(huán)境系統(tǒng)的第二種方式是定義一個(gè)自定義的EnvironmentKey ——然后它可以被用來向內(nèi)置的 EnvironmentValues 類型分配和檢索值:
struct ThemeEnvironmentKey: EnvironmentKey { static var defaultValue = Theme.default}extension EnvironmentValues { var theme: Theme { get { self[ThemeEnvironmentKey.self] } set { self[ThemeEnvironmentKey.self] = newValue } }}
有了上述內(nèi)容,我們現(xiàn)在可以使用Enviroment 屬性包裝器(而不是EnvironmentObject )來標(biāo)記我們視圖的theme屬性,并傳入我們希望檢索的環(huán)境鍵的鍵值路徑:
struct ArticleView: View { @Environment(.theme) var theme: Theme var article: Article var body: some View { VStack(alignment: .leading) { Text(article.title) .foregroundColor(theme.titleTextColor) Text(article.body) .foregroundColor(theme.bodyTextColor) } }}
上述兩種方法的一個(gè)明顯區(qū)別是,基于鍵的方法要求我們?cè)诰幾g時(shí)定義一個(gè)默認(rèn)值,而基于環(huán)境對(duì)象EnvironmentObject的方法則假設(shè)在運(yùn)行時(shí)提供這樣一個(gè)值(如果不這樣做將導(dǎo)致崩潰)。
小結(jié)
SwiftUI管理狀態(tài)的方式絕對(duì)是該框架最有趣的方面之一,它可能需要我們稍微重新思考數(shù)據(jù)在應(yīng)用中的傳遞方式——至少在涉及到將被我們的UI直接消費(fèi)和修改的數(shù)據(jù)時(shí)是這樣。
我希望這篇指南能成為一個(gè)很好的方式來概述SwiftUI的各種狀態(tài)處理機(jī)制,盡管一些更具體的API被遺漏了,這篇文章中強(qiáng)調(diào)的概念應(yīng)該涵蓋了所有基于SwiftUI的狀態(tài)處理的絕大多數(shù)用例。
感謝你的閱讀!