如果你已經(jīng)在使用SwiftUI串结,那么今天要學(xué)習(xí)的內(nèi)容非常重要,因?yàn)榻裉煲獙?duì)SwiftUI的三個(gè)核心的基本原理窺探究竟:1. Identity 2. Lifetime(生命周期) 3. Dependencies(依賴關(guān)系)亮隙,找出通用的模式绪商,學(xué)習(xí)框架驅(qū)動(dòng)的原理,并了解如何使用它們來保證APP的正確和性能。
我們已經(jīng)聽說了很多次:SwiftUI是一個(gè)聲明式的UI framework. 意思是你在一個(gè)很高的代碼層次上描述你的app想要什么碉克,然后SwiftUI能夠準(zhǔn)確地領(lǐng)會(huì)你的意思,然后實(shí)現(xiàn)你要的目標(biāo)并齐。
大部分時(shí)間漏麦,SwiftUI運(yùn)行的很好,你會(huì)感覺SwiftUI很神奇况褪。
但有時(shí)候SwiftUI也會(huì)做出你意想不到的事情撕贞,它可能無法達(dá)到你的預(yù)期,所以了解SwiftUI背后的原理测垛,變得很重要捏膨,這有助于SwiftUI呈現(xiàn)給我們想要的結(jié)果。
今天的問題是食侮,當(dāng)SwiftUI看到你的代碼時(shí)号涯,它看到了什么?答案是三件東西:identity(ID), lifetime(生命周期), and dependencies(依賴關(guān)系).
Identity
是SwiftUI在更新界面時(shí)識(shí)別相同或不同元素的方式。
LifeTime
是SwiftUI跟蹤視圖和數(shù)據(jù)的生存周期锯七。
dependencies
SwiftUI如何理解你的界面何時(shí)需要更新链快,以及為什么需要更新。
這三個(gè)概念共同決定了SwiftUI如何決定需要改變什么起胰、如何改變久又、何時(shí)改變巫延,從而產(chǎn)生你在屏幕上看到的動(dòng)態(tài)用戶界面。
今天我們將深入的探討這三個(gè)概念地消。
Identity
首先是identity, 舉個(gè)例子:假如界面上有兩張狗的圖片炉峰,除了狗的表情不一樣,兩張圖片基本一樣脉执;那如何判斷它們是兩只狗還是一只狗的兩個(gè)狀態(tài)疼阔,我們沒辦法判斷,因?yàn)槿鄙僮銐虻男畔ⅰ?br>
這個(gè)問題的核心是狗的"Identity", 這很重要半夷。
這也是SwiftUI如何理解你的app的關(guān)鍵婆廊。
我們看一個(gè)例子:
這是一個(gè)app,我們可以叫做“Good Dog巫橄,Bad Dog”淘邻,這個(gè)app幫助我們的狗狗是否有好的行為,這個(gè)app非常簡單湘换。
我們可以通過點(diǎn)擊屏幕的任何地方來切換good 和 bad兩種狀態(tài)宾舅。
所以,identity 跟我們的app有什么關(guān)系呢彩倚,我們看看兩個(gè)界面上的 狗狗的爪印的圖標(biāo)筹我,這兩個(gè)圖標(biāo)是兩個(gè)不同的視圖還是相同的視圖,只是顏色和位置不一樣帆离?這種區(qū)別實(shí)際上非常重要蔬蕊,因?yàn)樗鼤?huì)影響視圖從一種狀態(tài)轉(zhuǎn)換到另一種狀態(tài)的動(dòng)畫方式。
假如是不同的視圖哥谷,那么這兩個(gè)視圖的動(dòng)畫方式應(yīng)該是獨(dú)立的岸夯,無關(guān)的,比如漸入和漸出们妥。
那假如是相同的視圖呢囱修?這意味著視圖應(yīng)該在轉(zhuǎn)換期間滑過屏幕,然后從一個(gè)位置移動(dòng)到另一個(gè)位置王悍。
因此連接不同狀態(tài)的視圖是很重要的破镰,因?yàn)檫@是SwiftUI理解如何在它們之間轉(zhuǎn)換的方式。
這是視圖背后的identity的關(guān)鍵問題压储。
視圖共享相同的id鲜漩,用來表示相同視圖的不同狀態(tài),相反不同的視圖應(yīng)該使用不同的id集惋。
在接下去的演講中孕似,Luca和Raj將討論視圖id對(duì)應(yīng)用程序的數(shù)據(jù)和更新周期的實(shí)際影響。
現(xiàn)在讓我們看看id怎么在你的代碼中應(yīng)用刮刑。
第一:explicit identity: 數(shù)據(jù)驅(qū)動(dòng)的identifiers(標(biāo)識(shí)符)
第二:structural identity:根據(jù)數(shù)據(jù)的類型和視圖層級(jí)結(jié)構(gòu)中的位置來區(qū)分視圖
為了幫助理解上面兩個(gè)概念喉祭,讓我向你們介紹下我的朋友們(狗狗)养渴。
注意這兩張照片可以是不同的狗狗,就算看起來一樣泛烙。
所以什么樣的信息可以幫助我們標(biāo)識(shí)我們的狗狗理卑。一種方式是他們的名字。
這兩只狗狗看起來一樣蔽氨,而且如果名字一樣藐唠,我們就可以說他們是同一只狗狗。
但是如果他們有不同的名字鹉究,我們可以斷定他們是不同的狗狗宇立。
像這樣分配名字或標(biāo)識(shí)符是explicit identity的一種形式。
explicit identity 我們可以翻譯成“顯式標(biāo)識(shí)符”自赔。
顯式標(biāo)識(shí)符非常強(qiáng)大和靈活妈嘹,但需要人為的在某個(gè)地方跟蹤這些名字。
你可能已經(jīng)使用過一種顯式標(biāo)識(shí)形式是指針標(biāo)識(shí)绍妨,它已經(jīng)在整個(gè)UIKit和AppKit中使用蟋滴。
但是swiftUI并不使用指針標(biāo)識(shí),但是學(xué)習(xí)它將會(huì)幫助你更好的理解SwiftUI怎么和為什么不一樣痘绎,讓我們快速的看一看。
比如一個(gè)UIKit或AppKit視圖層級(jí)結(jié)構(gòu)肖粮,如下圖孤页。
因?yàn)閁IViews和NSViews是類,他們都有一個(gè)唯一的指針指向內(nèi)存區(qū)域涩馆,這個(gè)指針就是一個(gè)視圖的顯式標(biāo)識(shí)符行施。我們可以使用各自的指針標(biāo)識(shí)各自的視圖,而且如果兩個(gè)視圖共享相同的指針魂那,我們能斷定他們是相同的視圖蛾号。
但是SwiftUI并不使用指針,因?yàn)镾wiftUI的視圖是值類型涯雅,是結(jié)構(gòu)體而不是類鲜结。
我們知道值類型并沒有引用,所以SwiftUI不能用它來表示identity活逆,而是用另一種表示方式就是:顯式標(biāo)識(shí)符精刷。
比如,考慮下面一個(gè)搜救犬的列表蔗候。
List {
Section {
ForEach(rescueDogs, id:\.dogTagID) { rescueDog in
ProfileView(rescueDog)
}
}
Section("Status") {
ForEach(adoptedDogs, id:\.dogTagID) { rescueDog in
ProfileView(rescueDog, foundForeverHome: true)
}
}
}
這里的id參數(shù)是一種顯式標(biāo)識(shí)符怒允。
每個(gè)救援犬的狗標(biāo)簽ID用于顯式地標(biāo)識(shí)其在列表中的對(duì)應(yīng)視圖闸拿。 如果搜救犬的集合發(fā)生了變化必尼,SwiftUI可以使用這些id來了解到底發(fā)生了什么變化愕撰,并在列表中生成正確的動(dòng)畫览露。
在這種情況下,SwiftUI甚至能夠正確地在不同Section之間執(zhí)行移動(dòng)動(dòng)畫丽惶。
讓我們來看一個(gè)更高級(jí)的例子:
我們有一個(gè) ScrollViewReader炫七,通過點(diǎn)擊底部的按鈕來來跳來跳轉(zhuǎn)到視圖頂部,代碼如下蚊夫。
ScrollViewReader { proxy in
ScrollView {
HeaderView(rescueDog).id(headerID)
Text(rescueDog.backstory)
Button("Jump to Top") {
withAnimation {
proxy.scrollTo(headerID)
}
}
}
}
上面的id(_:) modifier 顯式指定一個(gè)id诉字, 我們的HeaderView在頁面頂部。
然后我們可以通過proxy的scrollTo方法來滑動(dòng)到指定視圖知纷。
這很好壤圃,不需要每個(gè)視圖都指定id,需要在需要的視圖指定id琅轧,但是沒有指定id并不意味著沒有id伍绳,因?yàn)槊恳粋€(gè)視圖都有一個(gè)id。
這就是structural identity乍桂。SwiftUI使用根據(jù)的視圖層級(jí)結(jié)構(gòu)自動(dòng)為你生成隱式id冲杀,我們無需手動(dòng)指定。
讓我介紹更多的朋友來幫助解釋這里面的意思睹酌。
我們說我們有兩只相識(shí)的狗狗权谁,但是我們不知道他們的名字,所以我們需要他們的id憋沿。
如果我們能保證它們不動(dòng)旺芽,我們就能根據(jù)它們坐的位置來識(shí)別它們,比如“狗在左邊”或“狗在右邊”辐啄。
我們用視圖相對(duì)排列位置來區(qū)分彼此采章,這就是structural identity.
SwiftUI在它的API中利用了structural identity,一個(gè)典型的例子就是在當(dāng)你在代碼中使用if語句或其他條件語句時(shí):
var body: some View {
if rescueDogs.isEmpty {
AdoptionDirectory(selection: $rescueDogs)
} else {
DogList(rescueDogs)
}
}
這個(gè)條件語句的結(jié)構(gòu)給了我們一個(gè)標(biāo)識(shí)每個(gè)視圖的清晰的方式壶辜,第一個(gè)視圖只在條件為true時(shí)顯示悯舟,而第二個(gè)視圖只在false時(shí)顯示。
那意味著我們總能分辨出哪個(gè)視圖是哪個(gè)砸民,即使它們碰巧看起來很相似抵怎。
然而,這只有在SwiftUI能夠靜態(tài)地保證這些視圖保持在它們所在的位置并且永不交換位置的情況下才有效岭参。
SwiftUI通過查看視圖層次結(jié)構(gòu)的類型結(jié)構(gòu)來實(shí)現(xiàn)這一點(diǎn)便贵。
當(dāng)SwiftUI查看視圖時(shí),它會(huì)看到它們的泛型類型——在本例中冗荸,我們的if語句被轉(zhuǎn)換成一個(gè)_ConditionalContent視圖承璃,它的true和false內(nèi)容是泛型的。
這個(gè)翻譯是由ViewBuilder支持的蚌本,它是Swift中的一種結(jié)果構(gòu)建器盔粹。
View協(xié)議隱式地將其body屬性封裝在ViewBuilder中隘梨,ViewBuilder從屬性中的邏輯語句構(gòu)建一個(gè)范型視圖。
body屬性的some View返回類型是一個(gè)表示此靜態(tài)復(fù)合類型的占位符舷嗡,將其隱藏起來轴猎,以便它不會(huì)干擾我們的代碼。
使用這種泛型類型进萄,SwiftUI可以保證true視圖始終是AdoptionDirectory捻脖,而false視圖始終是DogList,允許它們?cè)诒澈蟾髯员环峙湟粋€(gè)隱式的中鼠、穩(wěn)定的id可婶。
事實(shí)上,這是理解之前提到的“Good Dog援雇,Bad Dog”app的關(guān)鍵矛渴。
在上面的代碼中,我們有一個(gè)if語句惫搏,為每個(gè)條件分支定義不同的視圖具温。
這將導(dǎo)致視圖動(dòng)畫是fade in和fade out,因?yàn)镾wiftUI知道if語句的每個(gè)分支代表一個(gè)具有不同標(biāo)識(shí)的不同視圖筐赔。
或者铣猩,我們可以只使用一個(gè)PawView來改變它的布局和顏色。
當(dāng)它轉(zhuǎn)換到一個(gè)不同的狀態(tài)時(shí)茴丰,視圖將平滑地滑到它的下一個(gè)位置达皿。這是因?yàn)槲覀冇梦ㄒ坏膇d修改了同一個(gè)視圖。
這兩種策略都可以奏效较沪,但SwiftUI通常推薦第二種方法。
默認(rèn)情況下失仁,嘗試使用同一id尸曼,并提供流暢的轉(zhuǎn)換動(dòng)畫。
這也有助于保存視圖的生命周期和狀態(tài)萄焦,這一點(diǎn)Luca將在后面更詳細(xì)地討論控轿。
既然我們理解了structural identity,下面需要談?wù)勊乃罃常篈nyView拂封。
為了理解使用AnyView的影響茬射,讓我們看看它對(duì)視圖接口的影響。
前面我們寫了一個(gè)if語句用來切換AdoptionDirectory和DogList.
當(dāng)SwfitUI看到這個(gè)代碼冒签,它會(huì)在右邊看到泛型類型結(jié)構(gòu)在抛。
現(xiàn)在讓我們看一個(gè)不同的例子,一個(gè)使用AnyView的例子萧恕。
這是我編寫的一個(gè)幫助函數(shù)刚梭,用于獲得一個(gè)代表狗的品種的視圖肠阱。
函數(shù)中的每個(gè)條件分支都返回不同類型的視圖,所以我把它們都包裝在AnyView中朴读,因?yàn)镾wift需要整個(gè)函數(shù)返回單一類型屹徘。
但不幸的是:這也意味著SwiftUI無法看到我代碼的條件結(jié)構(gòu)。它只是將AnyView視為函數(shù)的返回類型衅金。這是因?yàn)锳nyView是所謂的“類型擦除包裝器類型”——它從其范型簽名中隱藏它包裝的視圖類型噪伊。
而且更嚴(yán)重的是,這段代碼可讀性非常差氮唯。
讓我們看看是否可以簡化這段代碼鉴吹,并讓SwiftUI更多地看到它的結(jié)構(gòu)。
首先您觉,如果 sheepNearby == true拙寡,這個(gè)分支似乎是有條件地在我們的BorderCollieView旁邊添加了一個(gè)SheepView。
我們可以通過在HStack中有條件地添加視圖而不是在視圖周圍有條件地添加HStack來簡化這一點(diǎn)琳水。
這樣肆糕,現(xiàn)在很容易看到每個(gè)分支都返回一個(gè)單一的視圖,所以我們的局部變量dogView已經(jīng)不需要了在孝,我們?cè)诿總€(gè)分支中使用return語句诚啃。
正如我們前面看到的,普通的SwiftUI View代碼可以使用if語句返回不同類型的視圖私沮。 但是如果我們嘗試從代碼中刪除返回語句和AnyViews始赎,我們會(huì)看到一些錯(cuò)誤和警告。
這是因?yàn)镾wiftUI需要我們的助手函數(shù)提供一個(gè)單一的返回類型仔燕。
那么我們?nèi)绾伪苊膺@些錯(cuò)誤呢?回想一下造垛,視圖的body屬性是特殊的,因?yàn)関iew協(xié)議隱式地將它封裝在ViewBuilder中晰搀。
這將屬性中的邏輯轉(zhuǎn)換為一個(gè)單一的五辽、通用的視圖結(jié)構(gòu)。所以我們可以手動(dòng)在助手函數(shù)上面聲明 @ViewBuilder外恕, 這樣就不會(huì)報(bào)錯(cuò)了杆逗。
現(xiàn)在代碼經(jīng)過優(yōu)化,看起來非常好鳞疲,避免使用AnyView罪郊,更加簡潔易懂。
如果我們看一下結(jié)果的類型簽名尚洽,它現(xiàn)在用一個(gè)條件內(nèi)容樹精確地復(fù)制了我們函數(shù)的條件邏輯悔橄,為SwiftUI提供了更豐富的視圖視圖和組件標(biāo)識(shí)。
但還有一個(gè)地方可以改進(jìn)。我們的功能的頂層只是針對(duì)犬種的不同情況進(jìn)行匹配橄维,我們可以將if語句改成switch語句尺铣。
現(xiàn)在就更容易快速理解所有不同的case了。
因?yàn)閟witch語句實(shí)際上只是條件語句的語法糖争舞,結(jié)果視圖右邊的類型簽名保持完全相同凛忿。
我們剛剛向您展示了anyview如何從代碼中擦除了類型信息,并演示了如何通過利用ViewBuilder來消除不必要的anyview竞川。
一般來說店溢,我們建議盡可能避免使用AnyViews。
anyview會(huì)令代碼難以閱讀和理解委乌。
而且因?yàn)锳nyView對(duì)編譯器隱藏靜態(tài)類型信息床牧,它有時(shí)會(huì)隱藏編譯錯(cuò)誤和警告。
最后遭贸,請(qǐng)記住戈咳,在不需要的時(shí)候使用AnyView會(huì)導(dǎo)致更糟糕的性能。如果可能的話壕吹,使用泛型來保留靜態(tài)類型信息著蛙,而不是在代碼中傳遞anyview。
好了耳贬,關(guān)于identity踏堡,介紹完畢。
通過顯式標(biāo)識(shí)咒劲,我們可以將視圖的標(biāo)識(shí)綁定到數(shù)據(jù)顷蟆,或者提供自定義標(biāo)識(shí)符來引用特定的視圖。
通過結(jié)構(gòu)標(biāo)識(shí)腐魂,我們了解了SwiftUI如何根據(jù)視圖層次結(jié)構(gòu)中的類型和位置來標(biāo)識(shí)視圖帐偎。
現(xiàn)在我將把事情交給Luca來討論identity是如何與視圖的生命周期和狀態(tài)聯(lián)系起來的。
LifeTime
Thanks蛔屹,Matt
現(xiàn)在我們已經(jīng)理解了SwiftUI如果標(biāo)識(shí)你的視圖削樊,讓我們繼續(xù)探索identity如何聯(lián)系視圖的生命周期和數(shù)據(jù)。
這將幫助你更好的理解SwiftUI如何工作判导。
當(dāng)我們看到一只貓嫉父,它可能小睡一會(huì)沛硅,或者生氣眼刃,但永遠(yuǎn)是那只貓,這時(shí)identity和生命周期聯(lián)系起來了摇肌。
identity允許我們?cè)谝欢螘r(shí)間為一個(gè)穩(wěn)定的元素指定不同的值擂红,換句話說,它允許我們?cè)谝欢螘r(shí)間引入連續(xù)性。
你可能想知道這是如何應(yīng)用于SwiftUI的昵骤?所以讓我們回到Matt開發(fā)的cat-friendly app:
就像貓一樣在不同的時(shí)刻有不同的狀態(tài)树碱,我們的視圖在整個(gè)生命周期同樣可以有不同的狀態(tài),每個(gè)狀態(tài)都有不同的值变秦。
Identity 在整個(gè)生命周期連接著這些不同的值成榜。
讓我們看一些代碼來闡明這一點(diǎn)。
var body: some View {
PurrDecibelView(intensity: 25)
}
這里有一個(gè)簡單的視圖顯示咕嚕聲的強(qiáng)度蹦玫,SwfitUI會(huì)創(chuàng)建一個(gè)視圖赎婚,它的強(qiáng)度值時(shí)25. 如果修改值為50,Swift UI需要再次調(diào)用此代碼:
var body: some View {
PurrDecibelView(intensity: 50)
}
這是從同一個(gè)視圖定義創(chuàng)建的兩個(gè)不同的值樱溉。SwiftUI會(huì)保留一個(gè)值的副本挣输,以便進(jìn)行比較,并知道視圖是否發(fā)生了變化福贞,但是之后這個(gè)值被銷毀了撩嚼。
這里重要的是要理解視圖值不同于視圖標(biāo)識(shí)。
view value != view identity
視圖值是短暫的挖帘,您不應(yīng)該依賴于它們的生命周期, 你能控制的是他們的identity完丽。
當(dāng)一個(gè)視圖第一次創(chuàng)建并顯示出來,SwiftUI給它分配一個(gè)identity肠套,前面已經(jīng)討論過舰涌。
隨著時(shí)間的推移,在更新的驅(qū)動(dòng)下你稚,視圖的新值被創(chuàng)建瓷耙。
但在SwiftUI看來,這些是相同的視圖刁赖。一旦視圖的identity發(fā)生改變或視圖被刪除搁痛,它的生命周期就結(jié)束了。
每當(dāng)我們說到視圖的生命周期宇弛,我們指的是與該視圖相關(guān)的身份(identity)的持續(xù)時(shí)間鸡典。
能夠?qū)⒁晥D的identity與其生命周期聯(lián)系起來是理解SwiftUI如何持久化狀態(tài)的基礎(chǔ)。
我們舉State和StateObject作為例子
當(dāng)SwiftUI正在看你的視圖和看一個(gè)State或一個(gè)StateObject枪芒,它知道它需要在整個(gè)視圖的生存期持久化這段數(shù)據(jù)彻况。
換句話說,State和StateObject是與視圖identity相關(guān)聯(lián)的持久存儲(chǔ)舅踪。
在視圖identity的開始纽甘,當(dāng)它第一次被創(chuàng)建時(shí),SwiftUI將使用它們的初始值為State和StateObject分配內(nèi)存空間抽碌。
我們關(guān)注下title的狀態(tài)悍赢。
在視圖的整個(gè)生命周期中,SwiftUI將在視圖發(fā)生變化或視圖體被重新評(píng)估時(shí)持久化此存儲(chǔ)。
讓我們看一個(gè)具體的例子左权,說明identity的變化如何影響狀態(tài)的持久性皮胡。
這是一個(gè)有趣的例子,我們有相同的視圖赏迟,但在兩個(gè)不同的分支屡贺。
如果你還記得,因?yàn)閟tructural identity锌杀,這兩視圖有不同的identity烹笔。
Matt已經(jīng)討論這如何影響動(dòng)畫,但是這還會(huì)影響持久化的狀態(tài)抛丽。
讓我們?cè)趯?shí)踐中看看:
當(dāng)?shù)谝淮螆?zhí)行body并進(jìn)入true分支時(shí)谤职,SwiftUI將用初始值為狀態(tài)分配內(nèi)存。
在這個(gè)視圖的整個(gè)生命周期中亿鲜,SwiftUI將保持狀態(tài)允蜈,因?yàn)樗鼤?huì)因各種操作而發(fā)生改變,如果dayTime變成false蒿柳,將進(jìn)入另一個(gè)分支饶套,SwiftUI知道這是另一個(gè)有著不同identity的視圖,它會(huì)為這個(gè)false視圖開辟一個(gè)新的內(nèi)存空間垒探,之前的ture view將會(huì)被釋放妓蛮,但是如果我們又回到true分支,那將是一個(gè)新的視圖圾叼,所以SwiftUI創(chuàng)建新的存儲(chǔ)空間蛤克,再從狀態(tài)的初始值開始,老的視圖會(huì)被釋放夷蚊。
所以結(jié)論就是构挤,只要identity發(fā)生改變,狀態(tài)就會(huì)被替換惕鼓。
讓我在這里暫停一下筋现,確保您理解了這一點(diǎn):您的狀態(tài)的持久性與您的視圖的生命周期相關(guān)聯(lián)。
這是一個(gè)非常強(qiáng)大的概念因?yàn)槲覀兛梢郧逦膮^(qū)分什么是視圖的本質(zhì)----它的狀態(tài)——并將其與它的identity聯(lián)系起來箱歧。
其他的都可以從它推導(dǎo)出來矾飞。
您的數(shù)據(jù)是如此重要,以至于SwiftUI擁有一組數(shù)據(jù)驅(qū)動(dòng)結(jié)構(gòu)呀邢,這些結(jié)構(gòu)將您的數(shù)據(jù)的identity用作視圖的顯式標(biāo)識(shí)形式洒沦。
這方面的典型例子是ForEach。
讓我們看看你能初始化一個(gè)ForEach的所有不同的方式驼鹅。
這將幫助我們更好地理解這種類型
下面foreach代碼微谓,簡單遍歷range
ForEach(0..<5) { offset in
Text("\(offset)")
}
SwiftUI會(huì)把offset作為每個(gè)視圖的identity,view的生命周期也是穩(wěn)定的输钩。
事實(shí)上豺型,在動(dòng)態(tài)range中使用這個(gè)初始化式是錯(cuò)誤的:
ForEach(0..<sheeps) { offset in
Text("\(offset)")
}
它會(huì)有一個(gè)警告,讓我們讓事情更有趣买乃,引入動(dòng)態(tài)數(shù)據(jù)集合姻氨。
struct RescueCat {
var tagID: UUID
}
ForEach(rescueCats, id: \.tagID) { rescueCat in
ProfileView(rescueCat)
}
這個(gè)初始化器接受一個(gè)集合和一個(gè)指向作為標(biāo)識(shí)符的屬性的keypath。 這個(gè)屬性必須是可哈希的剪验,因?yàn)镾wiftUI將使用它的值作為集合中元素生成的視圖的一個(gè)標(biāo)識(shí)(identity)肴焊。
稍后,Raj會(huì)給你展示一些例子功戚,說明選擇一個(gè)穩(wěn)定的identity如何影響你的應(yīng)用程序的性能和正確性娶眷。
為數(shù)據(jù)提供穩(wěn)定標(biāo)識(shí)的非常重要,而且標(biāo)準(zhǔn)庫定義了Identifiable協(xié)議來支持這種功能啸臀。
SwiftUI充分利用了該協(xié)議届宠。允許您省略keypath,并將數(shù)據(jù)元素類型遵循該協(xié)議:
struct RescueCat: Identifiable {
var tagID: UUID
var id: UUID { tagID }
}
ForEach(rescueCats) { rescueCat in
ProfileView(rescueCat)
}
我真正喜歡Swift的一點(diǎn)是乘粒,我們可以利用它的類型系統(tǒng)來精確約束我們的參數(shù)類型豌注。請(qǐng)跟我一起看一看我們這里使用的初始化式的定義。
在這個(gè)簡短的定義中有很多有趣的東西灯萍,所以讓我們?cè)囍阉鼈儾痖_轧铁。
ForEach 需要兩個(gè)主要參數(shù), 一個(gè)集合旦棉,這里的范型類型是Data齿风,以及從集合的每個(gè)元素生成視圖的方法。這里約束了Data的Element 必須遵循Identifiable绑洛,并且ID 的類型和 Data.Element.ID相同聂宾。
這里給你一個(gè)直觀的印象就是,F(xiàn)orEach在數(shù)據(jù)集合和視圖集合之間定義了一個(gè)關(guān)系表诊笤。
但是事實(shí)上系谐,最有趣的是我們約束了元素的類型必須是Identifiable. Identifiable協(xié)議的目的是允許你的類型提供一個(gè)穩(wěn)定的identity,以便SwiftUI可以在它的生命周期跟蹤你的數(shù)據(jù)讨跟。事實(shí)上纪他,這與我們之前討論的identity和生命周期的概念非常相似。
接受Identifiable類型和視圖構(gòu)建器的SwiftUI視圖是數(shù)據(jù)驅(qū)動(dòng)組件晾匠。
這些視圖使用您提供的數(shù)據(jù)的identity來限定與之關(guān)聯(lián)的視圖的生命周期茶袒。
選擇一個(gè)好的標(biāo)識(shí)符來控制視圖和數(shù)據(jù)生命周期 。
讓我們來總結(jié)下:
視圖的值是短暫的凉馆,您不應(yīng)該依賴于它們的生命周期薪寓。
但他們的identity并非如此亡资,而正是這種identity賦予了他們隨時(shí)間的延續(xù)性。
您可以控制視圖的identity向叉,并且可以使用identity清楚地限定狀態(tài)的生存期锥腻。
最后,SwiftUI充分利用了數(shù)據(jù)驅(qū)動(dòng)組件的Identifiable協(xié)議母谎,因此為數(shù)據(jù)選擇一個(gè)穩(wěn)定的標(biāo)識(shí)符非常重要瘦黑。
現(xiàn)在按照慣例,我要把話筒交給Raj奇唤。
Dependencies
Raj Ramamurthy:謝謝你幸斥,Luca!到目前為止,我們已經(jīng)解釋了什么是Identity以及它如何與視圖的生命周期相關(guān)聯(lián)咬扇。
接下來甲葬,我將深入研究SwiftUI如何更新UI。
我們的目標(biāo)是為您提供一個(gè)更好的思維模型來構(gòu)建SwiftUI代碼懈贺。
我還會(huì)在最后舉幾個(gè)例子來概括所有內(nèi)容演顾。
為了拋出對(duì)依賴關(guān)系的討論,讓我們來看一個(gè)視圖隅居。
這是一個(gè)簡單的視圖钠至,它顯示一個(gè)獎(jiǎng)勵(lì)狗狗的按鈕,
首先胎源,讓我們看看頂部棉钧,那有兩個(gè)屬性:一個(gè)是dog,一個(gè)是treat涕蚤,視圖依賴于這兩屬性宪卿。依賴項(xiàng)只是視圖的一個(gè)輸入,當(dāng)一個(gè)依賴項(xiàng)改變時(shí)万栅,
視圖需要生成一個(gè)新的body佑钾。
body是為視圖構(gòu)建層次結(jié)構(gòu)的地方。
深入到這個(gè)視圖的層次結(jié)構(gòu)中烦粒,我們有一個(gè)帶有操作的按鈕休溶。
動(dòng)作會(huì)觸發(fā)視圖依賴項(xiàng)發(fā)生變化。讓我們把代碼換成一個(gè)等價(jià)的圖
當(dāng)我們點(diǎn)擊按鈕扰她,它發(fā)出一個(gè)動(dòng)作:獎(jiǎng)勵(lì)狗狗兽掰,我們的狗狗一眨眼就把食物吞了下去??
這就導(dǎo)致了狗的變化——也許他想要另一只。
因?yàn)橐蕾囮P(guān)系改變了徒役,DogView產(chǎn)生了一個(gè)新的body孽尽。
讓我們簡化下這個(gè)圖。
關(guān)注視圖層次結(jié)構(gòu)忧勿,注意我們的視圖是如何形成樹狀結(jié)構(gòu)的杉女。
如果我們加上狗瞻讽,把依賴關(guān)系放在最上面,它看起來還是像棵樹熏挎。
然而速勇,DogView并不是唯一一個(gè)有依賴的視圖。
在SwiftUI中婆瓜,每個(gè)視圖都可以有自己的一組依賴項(xiàng)。
到目前為止贡羔,它看起來還是一棵樹廉白。
例如,其中一個(gè)后代可能也依賴于狗乖寒。這也可能發(fā)生在我們的其他依賴關(guān)系中猴蹂。
最后實(shí)際上是一張圖,我們稱之為“依賴圖”(dependency graph)
這個(gè)結(jié)構(gòu)很重要,因?yàn)樗试SSwiftUI只有效地更新那些需要新body的視圖
比如楣嘁,位于底部的依賴項(xiàng)磅轻。
如果我們檢查這個(gè)依賴關(guān)系,它有兩個(gè)依賴視圖,如這個(gè)依賴項(xiàng)發(fā)生變化逐虚,只有這兩個(gè)視圖會(huì)失效聋溜, SwiftUI將調(diào)用每個(gè)視圖的body,為每個(gè)視圖生成一個(gè)新的body值叭爱。 SwiftUI將實(shí)例化每個(gè)失效視圖的主體的值撮躁。這可能會(huì)導(dǎo)致更多依賴項(xiàng)的更改,但并不總是如此!因?yàn)橐晥D是值類型买雾,SwiftUI可以有效地比較它們把曼,只更新視圖的右邊子集。 這是Luca前面討論的另一種方式漓穿。
視圖的值是短暫的嗤军。結(jié)構(gòu)值只是用于比較,但是視圖本身有更長的生命周期晃危。這就是我們?nèi)绾伪苊鉃橹行牡囊晥D生成新的物體叙赚。
identity是依賴關(guān)系圖的骨架。
正如Matt所說僚饭,每個(gè)視圖都有identity纠俭,無論是顯式指定的還是結(jié)構(gòu)指定的。
SwiftUI通過這個(gè)identity將更改路由到正確的視圖,并有效地更新UI
依賴關(guān)系有很多種浪慌。
我們?cè)谇懊婵吹搅艘恍╆P(guān)于treat屬性和dog綁定的例子冤荆,但是你也可以通過使用Environment、State或任何可觀察對(duì)象屬性包裝器來形成依賴關(guān)系权纤。
接下來钓简,我想談?wù)勅绾卧谀囊晥D中改進(jìn)identity的使用乌妒。
這將有助于SwiftUI更好地理解你的代碼。
正如盧卡所說外邓,一個(gè)視圖的生命周期就是其identity的持續(xù)時(shí)間撤蚊,這意味著identity的穩(wěn)定性至關(guān)重要。 不穩(wěn)定的identity會(huì)導(dǎo)致較短的視圖生存期损话。
擁有一個(gè)穩(wěn)定的identity也有助于提高性能侦啸,因?yàn)镾wiftUI不需要持續(xù)創(chuàng)建新的視圖,也不需要通過更新圖(graph)來回折騰丧枪。
正如您前面看到的光涂,SwiftUI使用生命周期來管理持久存儲(chǔ),因此穩(wěn)定identity對(duì)于避免狀態(tài)丟失也很重要拧烦。
讓我們看一個(gè)代碼示例來解釋identity穩(wěn)定性的重要性忘闻。
在這個(gè)例子中,我列出了我最喜歡的寵物恋博。
我們?cè)趐et結(jié)構(gòu)體上有一個(gè)identity齐佳。
但實(shí)際上有一個(gè)bug;每次我得到一只新寵物,屏幕上的一切都在閃爍!讓我們暫停一下债沮,看看這段代碼炼吴。
你能找出bug在哪里嗎?錯(cuò)誤就在這里,在我們的Identifiable 協(xié)議實(shí)現(xiàn)中疫衩。
問題在于這里的identity不是穩(wěn)定的缺厉,只要數(shù)據(jù)一變,我們會(huì)得到一個(gè)新的identity隧土。
那怎么改正呢提针,使用pets數(shù)組的下標(biāo)作為identity?這也同樣有問題曹傀。
如果用下標(biāo)辐脖, 視圖現(xiàn)在通過各自寵物在集合中的位置來標(biāo)識(shí)。
如果我決定我有一個(gè)新的第一個(gè)最喜歡的寵物皆愉,所有其他寵物將改變他們的identity嗜价,這可能會(huì)導(dǎo)致一個(gè)糟糕的bug。
在這個(gè)例子中幕庐,按鈕在索引0處插入了一個(gè)新元素久锥,但是因?yàn)樽詈笠粋€(gè)索引是新元素,所以我們?cè)谀┪捕皇情_始處插入了一個(gè)元素异剥。
這是因?yàn)樯桑c計(jì)算形隨機(jī)identity一樣,索引不是穩(wěn)定形式的identity冤寿。
在本例中歹苦,我們需要使用穩(wěn)定identity青伤,比如來自數(shù)據(jù)庫的identity,或者來自寵物的穩(wěn)定屬性的identity殴瘦。任何持久性標(biāo)識(shí)符都是很好的選擇狠角。
現(xiàn)在我們的動(dòng)畫看起來很棒!但是穩(wěn)定性并不是一個(gè)好的identity的唯一要求,另一個(gè)要求就是唯一性蚪腋。
每個(gè)identity應(yīng)該映射到單個(gè)視圖丰歌。這確保了動(dòng)畫看起來很棒,性能流暢屉凯,層次結(jié)構(gòu)的依賴性以最有效的形式反映出來立帖。
讓我們看另一個(gè)例子:
在這個(gè)例子中,我有一個(gè)我的寵物最喜歡的食物的視圖神得。 每一種食物都有一個(gè)名字厘惦、一個(gè)表情符號(hào)和一個(gè)有效期偷仿。
我選擇用名字來區(qū)分每一種食物哩簿。
在這一點(diǎn)上——我相信你能猜到——我們這里也有一個(gè)bug:如果存在相同名字的食物怎么辦。
所以酝静,我們應(yīng)該使用序列號(hào)或其他唯一的ID节榜。
這確保了所有正確的數(shù)據(jù)都顯示在我們的視圖中,它還將確保更好的動(dòng)畫和更好的性能别智。
當(dāng)SwiftUI需要一個(gè)identity時(shí)宗苍,它就需要您的特別注意!使用隨機(jī)identity時(shí)請(qǐng)小心,特別是在計(jì)算屬性中薄榛。
通常讳窟,您希望所有identity都是穩(wěn)定的。
identity不應(yīng)該隨時(shí)間而改變;新identity表示具有新生命期的新項(xiàng)敞恋。
另外丽啡,identity需要是唯一的!多個(gè)視圖不能共享一個(gè)identity硬猫。
SwiftUI依賴于穩(wěn)定性和唯一性讓你的應(yīng)用程序運(yùn)行順暢补箍、無bug。
既然我們已經(jīng)討論了顯性id(explicit identity)啸蜜,我想繼續(xù)講結(jié)構(gòu)id(structural identity)坑雅。
還是拿之前的食物罐子做例子:
作為一個(gè)負(fù)責(zé)任的寵物愛好者,我只給我的寵物喂最好的衬横、未過期的食物??裹粤。
為了幫助我判斷什么時(shí)候食物壞了,我添加了一個(gè)新的modifier蜂林,可以在食物過期時(shí)選擇性地調(diào)暗食物單元格蛹尝。
讓我們看看這個(gè)modifier后豫,可以看到我根據(jù)比較當(dāng)前時(shí)間來決定什么時(shí)候調(diào)暗這個(gè)視圖。
這個(gè)代碼看著挺好突那,但有一個(gè)微妙的問題挫酿。
如果條件發(fā)生變化,我們的食物過期了愕难,我們得到一個(gè)新的identity因?yàn)檫@里是一個(gè)分支早龟。
正如Matt所討論的,分支是structural identity的一種形式猫缭。
這意味著我們有兩個(gè)內(nèi)容副本葱弟,而不是一個(gè)可選的修改副本。
注意猜丹,這里的分支是在修飾符中芝加。
為了清晰起見,我把修飾符和它的使用放在同一張幻燈片上射窒,但在您的項(xiàng)目中藏杖,您可能會(huì)在不知情的情況下跨文件擁有這樣的分支!當(dāng)然,我們?cè)谶@里討論的所有內(nèi)容都適用于視圖和視圖modifiers脉顿。
那么我們?nèi)绾伪苊膺@種情況呢?一種方法是將分支折疊在一起蝌麸,并將條件移動(dòng)到不透明度修改器中,就像這樣:
通過刪除這個(gè)分支艾疟,我們已經(jīng)正確地將這個(gè)視圖描述為具有單一identity来吩。
此外,將條件移動(dòng)到不透明度修改器中有助于提高性能蔽莱,因?yàn)槲覀円呀?jīng)嚴(yán)格限定了依賴代碼的范圍弟疆。
現(xiàn)在,當(dāng)條件改變時(shí)盗冷,只有不透明度需要改變怠苔。
這里的技巧是,當(dāng)條件為真時(shí)正塌,不透明度為1嘀略,就像這樣。
分支很好乓诽,他們的存在是有原因的帜羊,但沒有必要地使用時(shí),他們會(huì)導(dǎo)致很差的性能鸠天,令人驚訝的動(dòng)畫讼育,以及狀態(tài)丟失。
當(dāng)您引入一個(gè)分支時(shí),請(qǐng)暫停一下奶段,并考慮您是在表示多個(gè)視圖還是同一個(gè)視圖的兩個(gè)狀態(tài)饥瓷,如果是一個(gè)視圖的兩個(gè)狀態(tài),盡量避免用分支痹籍。
總結(jié)一下:我們向您展示了identity是神奇性能的一個(gè)秘密之一邻奠,我們討論了顯示和結(jié)構(gòu)性identity贺氓,以及如何利用他們改善您的app暑竟。
從identity中唉侄,我們可以獲得視圖的生命周期,它控制視圖關(guān)聯(lián)的存儲(chǔ)线定、轉(zhuǎn)換等娜谊。
我們還解釋了SwiftUI使用identity和生命周期來形成依賴關(guān)系,這些依賴關(guān)系由一個(gè)圖表示斤讥,可以有效地更新UI纱皆。
在揭秘SwiftUI的同時(shí),我們也為你提供了一些避免bug和提高應(yīng)用性能的技巧芭商。
既然您已經(jīng)學(xué)會(huì)了這些技巧派草,那么就review下您的代碼,看看它們是否能幫助您蓉坎。
謝謝你們澳眷,繼續(xù)構(gòu)建牛逼的app吧胡嘿。