揭秘SwiftUI

原文鏈接

如果你已經(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è)例子:


dog

這是一個(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è)概念,讓我向你們介紹下我的朋友們(狗狗)。


image.png

注意這兩張照片可以是不同的狗狗,就算看起來一樣。
所以什么樣的信息可以幫助我們標(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),如下圖。


image.png

因?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)容是泛型的。


image.png

這個(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,允許它們在背后各自被分配一個(gè)隱式的、穩(wěn)定的id。


image.png

事實(shí)上,這是理解之前提到的“Good Dog,Bad Dog”app的關(guān)鍵。


image.png

在上面的代碼中,我們有一個(gè)if語句,為每個(gè)條件分支定義不同的視圖。
這將導(dǎo)致視圖動(dòng)畫是fade in和fade out,因?yàn)镾wiftUI知道if語句的每個(gè)分支代表一個(gè)具有不同標(biāo)識(shí)的不同視圖。

或者,我們可以只使用一個(gè)PawView來改變它的布局和顏色。

image.png

當(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)。


image.png

現(xiàn)在讓我們看一個(gè)不同的例子,一個(gè)使用AnyView的例子。


image.png

這是我編寫的一個(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)。


image.png

image.png

這樣,現(xiàn)在很容易看到每個(gè)分支都返回一個(gè)單一的視圖,所以我們的局部變量dogView已經(jīng)不需要了,我們在每個(gè)分支中使用return語句。


image.png

正如我們前面看到的,普通的SwiftUI View代碼可以使用if語句返回不同類型的視圖。 但是如果我們嘗試從代碼中刪除返回語句和AnyViews,我們會(huì)看到一些錯(cuò)誤和警告。


image.png

這是因?yàn)镾wiftUI需要我們的助手函數(shù)提供一個(gè)單一的返回類型。
那么我們?nèi)绾伪苊膺@些錯(cuò)誤呢?回想一下,視圖的body屬性是特殊的,因?yàn)関iew協(xié)議隱式地將它封裝在ViewBuilder中。


image.png

這將屬性中的邏輯轉(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í)。


image.png

但還有一個(gè)地方可以改進(jìn)。我們的功能的頂層只是針對(duì)犬種的不同情況進(jìn)行匹配,我們可以將if語句改成switch語句。
image.png

現(xiàn)在就更容易快速理解所有不同的case了。
因?yàn)閟witch語句實(shí)際上只是條件語句的語法糖,結(jié)果視圖右邊的類型簽名保持完全相同。
我們剛剛向您展示了anyview如何從代碼中擦除了類型信息,并演示了如何通過利用ViewBuilder來消除不必要的anyview。
一般來說,我們建議盡可能避免使用AnyViews。
anyview會(huì)令代碼難以閱讀和理解。
而且因?yàn)锳nyView對(duì)編譯器隱藏靜態(tài)類型信息,它有時(shí)會(huì)隱藏編譯錯(cuò)誤和警告。

最后,請記住,在不需要的時(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允許我們在一段時(shí)間為一個(gè)穩(wěn)定的元素指定不同的值,換句話說,它允許我們在一段時(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作為例子


image.png

當(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)的持久性。


image.png

這是一個(gè)有趣的例子,我們有相同的視圖,但在兩個(gè)不同的分支。
如果你還記得,因?yàn)閟tructural identity,這兩視圖有不同的identity。
Matt已經(jīng)討論這如何影響動(dòng)畫,但是這還會(huì)影響持久化的狀態(tài)。
讓我們在實(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ù)類型。請跟我一起看一看我們這里使用的初始化式的定義。
在這個(gè)簡短的定義中有很多有趣的東西,所以讓我們試著把它們拆開。


image.png

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è)視圖。


image.png

這是一個(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à)的圖

image.png

當(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)


image.png

這個(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)系有很多種。
我們在前面看到了一些關(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è)例子中,我列出了我最喜歡的寵物。


image.png

我們在pet結(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è)索引是新元素,所以我們在末尾而不是開始處插入了一個(gè)元素。
這是因?yàn)?,與計(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è)例子:


image.png

在這個(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í)請小心,特別是在計(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)。
還是拿之前的食物罐子做例子:


image.png

作為一個(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)然,我們在這里討論的所有內(nèi)容都適用于視圖和視圖modifiers。
那么我們?nèi)绾伪苊膺@種情況呢?一種方法是將分支折疊在一起,并將條件移動(dòng)到不透明度修改器中,就像這樣:


image.png

通過刪除這個(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í),請暫停一下,并考慮您是在表示多個(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吧。


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容