SwiftUI 狀態(tài)管理系統(tǒng)指南

前言

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è)——usernameemail——實(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)值蔽介,所以這次我們將usernameemail屬性標(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ú)的usernameemail:

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è)雙向的連接菇民,而StatrBinding包裝的屬性都可以通過(guò)在其屬性名前加上$來(lái)作為綁定物傳遞。

觀察對(duì)象

StateBingding的共同點(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佛呻。把StateObjectObservedObject看作是StateBinding的參考類型裳朋,或者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ù)用例怨愤。

感謝你的閱讀!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蛹批,隨后出現(xiàn)的幾起案子撰洗,更是在濱河造成了極大的恐慌篮愉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件差导,死亡現(xiàn)場(chǎng)離奇詭異试躏,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)设褐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門颠蕴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人助析,你說(shuō)我怎么就攤上這事犀被。” “怎么了貌笨?”我有些...
    開(kāi)封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵弱判,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我锥惋,道長(zhǎng)昌腰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任膀跌,我火速辦了婚禮遭商,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捅伤。我一直安慰自己劫流,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布丛忆。 她就那樣靜靜地躺著祠汇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熄诡。 梳的紋絲不亂的頭發(fā)上可很,一...
    開(kāi)封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音凰浮,去河邊找鬼我抠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛袜茧,可吹牛的內(nèi)容都是我干的菜拓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼笛厦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纳鼎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起裳凸,我...
    開(kāi)封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤喷橙,失蹤者是張志新(化名)和其女友劉穎啥么,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贰逾,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悬荣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疙剑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氯迂。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖言缤,靈堂內(nèi)的尸體忽然破棺而出嚼蚀,到底是詐尸還是另有隱情,我是刑警寧澤管挟,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布轿曙,位于F島的核電站,受9級(jí)特大地震影響僻孝,放射性物質(zhì)發(fā)生泄漏导帝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一穿铆、第九天 我趴在偏房一處隱蔽的房頂上張望您单。 院中可真熱鬧,春花似錦荞雏、人聲如沸虐秦。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)悦陋。三九已至,卻和暖如春筑辨,著一層夾襖步出監(jiān)牢的瞬間叨恨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工挖垛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人秉颗。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓痢毒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蚕甥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哪替,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容