前言
SwiftUI與蘋果之前的UI框架的區(qū)別不僅僅在于如何定義視圖和其他UI組件砸喻,還在于如何在整個(gè)使用它的應(yīng)用程序中管理視圖層級(jí)的狀態(tài)涡匀。
SwiftUI沒(méi)有使用委托淌实、數(shù)據(jù)源或任何其他在UIKit和AppKit等命令式框架中常見(jiàn)的狀態(tài)管理模式募逞,而是配備了一些屬性包裝器,使我們能夠準(zhǔn)確地聲明我們的數(shù)據(jù)如何被我們的視圖觀察驱显、渲染和改變诗芜。
本周瞳抓,讓我們仔細(xì)看看這些屬性包裝器中的每一個(gè),它們之間的關(guān)系伏恐,以及它們?nèi)绾螛?gòu)成SwiftUI整體狀態(tài)管理系統(tǒng)的不同部分孩哑。
屬性狀態(tài)
由于SwiftUI主要是一個(gè)UI框架(盡管它也開(kāi)始獲得用于定義更高層次結(jié)構(gòu)(如應(yīng)用程序和場(chǎng)景)的API),其聲明式設(shè)計(jì)不一定需要影響應(yīng)用程序的整個(gè)模型和數(shù)據(jù)層——而只是直接綁定到我們各種視圖的狀態(tài)翠桦。
例如横蜒,假設(shè)我們正在開(kāi)發(fā)一個(gè)SignupView
,使用戶能夠通過(guò)輸入用戶名和電子郵件地址在應(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
屬性包裝器來(lái)標(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
被用來(lái)表示SwiftUI視圖的內(nèi)部狀態(tài),并在該狀態(tài)被改變時(shí)自動(dòng)使視圖更新渠羞。因此,最常見(jiàn)的做法是將State
屬性包裝器保持為私有智哀,這可以確保它們只在該視圖的主體內(nèi)被改變(試圖在其他地方改變它們實(shí)際上會(huì)導(dǎo)致運(yùn)行時(shí)崩潰)次询。
雙向綁定
看一下上面的代碼樣本,我們將每個(gè)屬性傳入其TextField
的方式是在這些屬性名稱前加上$
瓷叫。這是因?yàn)槲覀儾恢皇菍⑵胀ǖ?code>String值傳入這些文本字段屯吊,而是與我們的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ù)虹蓄,而是可以用來(lái)將任何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è)趯?code>State和Binding
包裝的屬性傳入各種TextField
實(shí)例時(shí)用$
作為前綴一樣,我們?cè)趯⑷魏?code>State值連接到我們自己定義的Binding
屬性時(shí)也可以做同樣的事情坐儿。
例如律胀,這里有一個(gè)ProfileView
的實(shí)現(xiàn)宋光,它使用一個(gè)Stage
包裝屬性來(lái)跟蹤一個(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)注意罪佳,我們也可以通過(guò)給一個(gè)
State
包裝的屬性分配一個(gè)新的值來(lái)改變它——比如我們?cè)?"Done "按鈕的動(dòng)作處理程序中把isEditingViewShown
設(shè)置為false
。
因此娃兽,一個(gè)Binding
標(biāo)記的屬性在給定的視圖和定義在該視圖之外的狀態(tài)屬性之間提供了一個(gè)雙向的連接菇民,而Statr
和Binding
包裝的屬性都可以通過(guò)在其屬性名前加上$
來(lái)作為綁定物傳遞。
觀察對(duì)象
State
和Bingding
的共同點(diǎn)是投储,它們處理的是在SwiftUI視圖層次結(jié)構(gòu)本身中管理的值第练。然而,雖然建立一個(gè)將所有的狀態(tài)都保存在其各種視圖中的應(yīng)用程序是肯定可行的玛荞,但從架構(gòu)和關(guān)注點(diǎn)分離的角度來(lái)看娇掏,這通常不是一個(gè)好主意,而且很容易導(dǎo)致我們的視圖變得相當(dāng)龐大和復(fù)雜勋眯。
值得慶幸的是婴梧,SwiftUI還提供了一些機(jī)制,使我們能夠?qū)⑼獠磕P蛯?duì)象連接到我們的各種視圖客蹋。其中一個(gè)機(jī)制是ObservableObject
協(xié)議塞蹭,當(dāng)它與ObservedObject
屬性包裝器結(jié)合時(shí),我們可以設(shè)置與我們視圖層之外管理的引用類型的綁定讶坯。
作為一個(gè)例子番电,讓我們更新上面定義的ProfileView
——通過(guò)將管理User
模型的責(zé)任從視圖本身轉(zhuǎn)移到一個(gè)新的、專門的對(duì)象中×纠牛現(xiàn)在漱办,我們可以用許多不同的方式來(lái)描述這樣一個(gè)對(duì)象,但由于我們正在尋找創(chuàng)建一個(gè)類型來(lái)控制我們的一個(gè)模型的實(shí)例——讓我們把它變成一個(gè)符合SwiftUI的ObservableObject
協(xié)議的模型控制器:
class UserModelController: ObservableObject {
@Published var user: User
...
}
Published
屬性包裝器用于定義對(duì)象的哪些屬性在被修改時(shí)應(yīng)讓觀察通知被觸發(fā)婉烟。
有了上面的類型娩井,現(xiàn)在讓我們回到ProfileView
,讓它觀察新的UserModelController
的實(shí)例似袁,作為一個(gè)ObservedObject
洞辣,而不是用一個(gè)State
屬性包裝器來(lái)跟蹤我們的用戶模型。最重要的是叔营,我們?nèi)匀豢梢院苋菀椎貙⑦@個(gè)模型綁定到我們的ProfileEditingView
上屋彪,就像以前一樣,因?yàn)?code>ObservedObject屬性包裝器也可以轉(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庫(kù)中建立一個(gè)更明確的依賴關(guān)系圖之外婴谱,原因是一個(gè)標(biāo)有ObservedObject
的屬性并不意味著對(duì)這個(gè)屬性所指向的對(duì)象有任何形式的所有權(quán)蟹但。
因此躯泰,雖然下面的內(nèi)容在技術(shù)上可能會(huì)被編譯,但最終會(huì)導(dǎo)致運(yùn)行時(shí)的問(wèn)題——因?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í)值——因此它們沒(méi)有像
UIView
實(shí)例那樣的生命周期。
為了解決上述問(wèn)題客叉,蘋果在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ù)上來(lái)說(shuō)卵慰,從現(xiàn)在開(kāi)始可以只使用StateObject
——我仍然建議在觀察外部對(duì)象時(shí)使用ObservedObject
,而在處理視圖本身?yè)碛械膶?duì)象時(shí)只使用StateObject
佛呻。把StateObject
和ObservedObject
看作是State
和Binding
的參考類型裳朋,或者SwiftUI版本的強(qiáng)和弱屬性。
觀察和修改環(huán)境變量
最后吓著,讓我們來(lái)看看SwiftUI的環(huán)境系統(tǒng)如何被用來(lái)在兩個(gè)互不直接連接的視圖之間傳遞各種狀態(tài)鲤嫡。盡管在一個(gè)父視圖和它的一個(gè)子視圖之間創(chuàng)建綁定通常很容易,但在整個(gè)視圖層次結(jié)構(gòu)中傳遞某個(gè)對(duì)象或值可能相當(dāng)麻煩——而這正是環(huán)境變量旨在解決的問(wèn)題類型绑莺。
有兩種主要的方法來(lái)使用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ì)處理其余的事情罢荡。這是通過(guò)使用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
——然后它可以被用來(lái)向內(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
)來(lái)標(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è)很好的方式來(lái)概述SwiftUI的各種狀態(tài)處理機(jī)制,盡管一些更具體的API被遺漏了虐先,這篇文章中強(qiáng)調(diào)的概念應(yīng)該涵蓋了所有基于SwiftUI的狀態(tài)處理的絕大多數(shù)用例怨愤。
感謝你的閱讀!