SwiftUI:原理

原創(chuàng):有趣知識(shí)點(diǎn)摸索型文章
創(chuàng)作不易糕再,請(qǐng)珍惜量没,之后會(huì)持續(xù)更新,不斷完善
個(gè)人比較喜歡做筆記和寫(xiě)總結(jié)突想,畢竟好記性不如爛筆頭哈哈殴蹄,這些文章記錄了我的IOS成長(zhǎng)歷程,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書(shū)不支持目錄跳轉(zhuǎn)猾担,大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容

目錄

  • 一袭灯、聲明式的界面開(kāi)發(fā)方式
  • 二、預(yù)覽
  • 三绑嘹、some關(guān)鍵詞的解釋
  • 四稽荧、ViewBuilder的解釋
  • 五、鏈?zhǔn)秸{(diào)用修改 View 的屬性原理
  • 六工腋、List的解釋
  • 七姨丈、@State的解釋
  • 八、Animating的解釋
  • 九擅腰、生命周期
  • Demo
  • 參考文獻(xiàn)

簡(jiǎn)介

SwiftUI 的最低支持的版本是iOS 13蟋恬,可能想要在實(shí)際項(xiàng)目中使用,還需要等待一兩年時(shí)間趁冈。在 view的描述表現(xiàn)力上和與 app 的結(jié)合方面歼争,SwiftUI 要?jiǎng)龠^(guò) FlutterDart的組合很多。Swift雖然開(kāi)源了渗勘,但是 Apple對(duì)它的掌控并沒(méi)有減弱沐绒。Swift 的很多特性幾乎可以說(shuō)都是為了SwiftUI量身定制的。

另外旺坠,Apple 在背后使用Combine.framework 這個(gè)響應(yīng)式編程框架來(lái)對(duì) SwiftUI.framework進(jìn)行驅(qū)動(dòng)和數(shù)據(jù)綁定乔遮,相比于現(xiàn)有的RxSwift/RxCocoa或者是ReactiveSwift 的方案來(lái)說(shuō),得到了語(yǔ)言和編譯器層級(jí)的大力支持价淌。


一申眼、聲明式的界面開(kāi)發(fā)方式

描述「UI 應(yīng)該是什么樣子」而不再是用一句句的代碼來(lái)指導(dǎo)「要怎樣構(gòu)建 UI」瞒津。比如傳統(tǒng)的 UIKit蝉衣,我們會(huì)使用這樣的代碼來(lái)添加一個(gè) Hello World 的標(biāo)簽,它負(fù)責(zé)創(chuàng)建 label巷蚪,設(shè)置文字并將其添加到 view 上病毡。

func viewDidLoad() 
{
     super.viewDidLoad()

     let label = UILabel()
     label.text = "Hello World"
     view.addSubview(label)
     // 省略了布局的代碼
 }

而相對(duì)起來(lái),使用SwiftUI我們只需要告訴SDK需要一個(gè)文字標(biāo)簽屁柏。

var body: some View 
{
     Text("Hello World")
}

接下來(lái)啦膜,框架內(nèi)部讀取這些view 的聲明有送,負(fù)責(zé)將它們以合適的方式繪制渲染。注意僧家,這些 view的聲明只是純數(shù)據(jù)結(jié)構(gòu)的描述雀摘,而不是實(shí)際顯示出來(lái)的視圖,因此這些結(jié)構(gòu)的創(chuàng)建并不會(huì)帶來(lái)太多性能損耗八拱。相對(duì)來(lái)說(shuō)阵赠,將描述性的語(yǔ)言進(jìn)行渲染繪制的部分是最慢的,這部分工作將交由框架以黑盒的方式為我們完成肌稻。

如果 View 需要根據(jù)某個(gè)狀態(tài) (state) 進(jìn)行改變清蚀,那我們將這個(gè)狀態(tài)存儲(chǔ)在變量中,并在聲明view時(shí)使用它爹谭。

@State var name: String = "Tom"
var body: some View
{
    Text("Hello \(name)")
}

狀態(tài)發(fā)生改變時(shí)枷邪,框架重新調(diào)用聲明部分的代碼,計(jì)算出新的view 聲明诺凡,并和原來(lái)的 view 進(jìn)行比較东揣,之后框架負(fù)責(zé)對(duì)變更的部分進(jìn)行高效的重新繪制。


二绑洛、預(yù)覽

SwiftUIPreviewApple 用來(lái)對(duì)標(biāo) FlutterHot Reloading 的開(kāi)發(fā)工具救斑。Xcode 將對(duì)代碼進(jìn)行靜態(tài)分析 (得益于 SwiftSyntax 框架),找到所有遵守 PreviewProvider 協(xié)議的類(lèi)型進(jìn)行預(yù)覽渲染真屯。另外脸候,你可以為這些預(yù)覽提供合適的數(shù)據(jù),這甚至可以讓整個(gè)界面開(kāi)發(fā)流程不需要實(shí)際運(yùn)行 app 就能進(jìn)行绑蔫。

這套開(kāi)發(fā)方式帶來(lái)的效率提升相比 Hot Reloading 要更大运沦。Hot Reloading 需要你有一個(gè)大致界面和準(zhǔn)備相應(yīng)數(shù)據(jù),然后運(yùn)行 app配深,停在要開(kāi)發(fā)的界面携添,再進(jìn)行調(diào)整。如果數(shù)據(jù)狀態(tài)發(fā)生變化篓叶,你還需要restart app才能反應(yīng)烈掠。SwiftUI 的 Preview 相比起來(lái),不需要運(yùn)行app并且可以提供任何的假數(shù)據(jù)缸托,在開(kāi)發(fā)效率上更勝一籌左敌。


三、some關(guān)鍵詞的解釋

struct ContentView: View
{
    var body: some View
    {
        Text("Hello World")
    }
}

一眼看上去可能會(huì)對(duì) some 比較陌生俐镐,為了講明白這件事矫限,我們先從 View 說(shuō)起。ViewSwiftUI 的一個(gè)最核心的協(xié)議,代表了一個(gè)屏幕上元素的描述叼风。這個(gè)協(xié)議中含有一個(gè)associatedtype取董。

public protocol View : _View
{
    associatedtype Body : View
    var body: Self.Body { get }
}

這種帶有 associatedtype 的協(xié)議不能作為類(lèi)型來(lái)使用,而只能作為類(lèi)型約束使用无宿。

Error
func createView() -> View
{
    ...
}
OK
func createView<T: View>() -> T
{
   ...
}

想要 Swift 幫助自動(dòng)推斷出 View.Body 的類(lèi)型的話茵汰,我們需要明確地指出body的真正的類(lèi)型。在這里孽鸡,body 的實(shí)際類(lèi)型是 Text经窖。

struct ContentView: View
{
    var body: Text
    {
        Text("Hello World")
    }
}

當(dāng)然我們可以明確指定出 body的類(lèi)型,但是這帶來(lái)一些麻煩梭灿。每次修改body 的返回時(shí)我們都需要手動(dòng)去更改相應(yīng)的類(lèi)型画侣。新建一個(gè)View 的時(shí)候,我們都需要去考慮會(huì)是什么類(lèi)型堡妒。

其實(shí)我們只關(guān)心返回的是不是一個(gè) View配乱,而對(duì)實(shí)際上它是什么類(lèi)型并不感興趣。some View 這種寫(xiě)法使用了 Swift 的新特性 Opaque return types 皮迟。它向編譯器作出保證搬泥,每次 body 得到的一定是某一個(gè)確定的遵守 View 協(xié)議的類(lèi)型,但是請(qǐng)編譯器網(wǎng)開(kāi)一面伏尼,不要再細(xì)究具體的類(lèi)型忿檩。返回類(lèi)型確定單一這個(gè)條件十分重要,比如爆阶,下面的代碼也是無(wú)法通過(guò)的燥透。

var body: some View
{
    if someCondition
    {
        // 這個(gè)分支返回 Text
        return Text("Hello World")
    }
    else
    {
        // 這個(gè)分支返回 Button,和 if 分支的類(lèi)型不統(tǒng)一
        return Button(action: {}) {
            Text("Tap me")
        }
    }
}

這是一個(gè)編譯期間的特性辨图,在保證associatedtype protocol的功能的前提下班套,使用 some 可以抹消具體的類(lèi)型。


四故河、ViewBuilder的解釋

創(chuàng)建 Stack 的語(yǔ)法很有趣吱韭。

VStack(alignment: .leading)
{
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
}

一開(kāi)始看起來(lái)好像我們給出了兩個(gè) Text,似乎是構(gòu)成的是一個(gè)類(lèi)似數(shù)組形式的 [View]鱼的,但實(shí)際上并不是這么一回事理盆。這里調(diào)用了 VStack 類(lèi)型的初始化方法。

public struct VStack<Content> where Content : View
{
    init(
        alignment: HorizontalAlignment = .center,
        spacing: Length? = nil,
        @ViewBuilder content: () -> Content)
}

前面的 alignmentspacing 沒(méi)啥好說(shuō)凑阶,最后一個(gè) content 比較有意思猿规。看簽名的話晌砾,它是一個(gè)() -> Content類(lèi)型坎拐,但是我們?cè)趧?chuàng)建這個(gè)VStack 時(shí)所提供的代碼只是簡(jiǎn)單列舉了兩個(gè) Text烦磁,并沒(méi)有實(shí)際返回一個(gè)可用的 Content养匈。

這里使用了 Swift 5.1 的另一個(gè)新特性 Funtion builders哼勇。如果你實(shí)際觀察 VStack 這個(gè)初始化方法的簽名,會(huì)發(fā)現(xiàn)content前面其實(shí)有一個(gè)@ViewBuilder標(biāo)記呕乎,而 ViewBuilder則是一個(gè)由 @_functionBuilder 進(jìn)行標(biāo)記的 struct积担。

@_functionBuilder public struct ViewBuilder { /* */ }

使用 @_functionBuilder 進(jìn)行標(biāo)記的類(lèi)型 (這里的 ViewBuilder),可以被用來(lái)對(duì)其他內(nèi)容進(jìn)行標(biāo)記 (這里用 @ViewBuilder 對(duì) content 進(jìn)行標(biāo)記)猬仁。被用function builder標(biāo)記過(guò)的 ViewBuilder 標(biāo)記以后帝璧,content 這個(gè)輸入的 function 在被使用前,會(huì)按照 ViewBuilder 中合適的 buildBlock 進(jìn)行 build后再使用湿刽。如果你閱讀 ViewBuilder 的文檔的烁,會(huì)發(fā)現(xiàn)有很多接受不同個(gè)數(shù)參數(shù)的 buildBlock 方法,它們將負(fù)責(zé)把閉包中一一列舉的 Text和其他可能的 View 轉(zhuǎn)換為一個(gè) TupleView并返回诈闺。由此渴庆,content 的簽名() -> Content可以得到滿足。實(shí)際上構(gòu)建這個(gè) VStack 的代碼會(huì)被轉(zhuǎn)換為類(lèi)似下面這樣的等效偽代碼(不能實(shí)際編譯)雅镊。

VStack(alignment: .leading)
{ viewBuilder -> Content in
    let text1 = Text("Turtle Rock").font(.title)
    let text2 = Text("Joshua Tree National Park").font(.subheadline)
    return viewBuilder.buildBlock(text1, text2)
}

當(dāng)然這種基于 funtion builder 的方式是有一定限制的襟雷。比如ViewBuilder 就只實(shí)現(xiàn)了最多十個(gè)參數(shù)的 buildBlock,因此如果你在一個(gè) VStack中放超過(guò)十個(gè)View的話仁烹,編譯器就會(huì)不太高興耸弄。不過(guò)對(duì)于正常的 UI 構(gòu)建,十個(gè)參數(shù)應(yīng)該足夠了卓缰。如果還不行的話计呈,你也可以考慮直接使用 TupleView 來(lái)用多元組的方式合并 View

TupleView<(Text, Text)>
(
    (Text("Hello"), Text("Hello"))
)

除了按順序接受和構(gòu)建 ViewbuildBlock 以外征唬,ViewBuilder 還實(shí)現(xiàn)了兩個(gè)特殊的方法:buildEitherbuildIf震叮。它們分別對(duì)應(yīng) block 中的 if...else 的語(yǔ)法和 if 的語(yǔ)法。也就是說(shuō)鳍鸵,你可以在 VStack里寫(xiě)這樣的代碼苇瓣。

var someCondition: Bool

VStack(alignment: .leading)
{
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
    
    if someCondition
    {
        Text("Condition")
    }
    else
    {
        Text("Not Condition")
    }
}

其他的命令式的代碼在 VStackcontent 閉包里是不被接受的,比如下面這樣就不行偿乖。let 語(yǔ)句無(wú)法通過(guò) function builder 創(chuàng)建合適的輸出击罪。

VStack(alignment: .leading)
{
    let someCondition = model.condition

    if someCondition
    {
        Text("Condition")
    }
    else
    {
        Text("Not Condition")
    }
}

五、鏈?zhǔn)秸{(diào)用修改 View 的屬性原理

var body: some View
{
    Image("turtlerock")
        .clipShape(Circle())
        .overlay(
            Circle().stroke(Color.white, lineWidth: 4))
        .shadow(radius: 10)
}

可以試想一下贪薪,在 UIKit 中要?jiǎng)邮中纬蛇@個(gè)效果的困難程度媳禁。我大概可以保證,99%的開(kāi)發(fā)者很難在不借助文檔或者 copy paste 的前提下完成這些事情画切,但是在SwiftUI中簡(jiǎn)直信手拈來(lái)竣稽,這點(diǎn)和Flutter很像。在創(chuàng)建 View 之后,用鏈?zhǔn)秸{(diào)用的方式毫别,可以將View 轉(zhuǎn)換為一個(gè)含有變更后內(nèi)容的對(duì)象娃弓。比如復(fù)原一下上面的代碼。

let image: Image = Image("turtlerock")
let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)

image 通過(guò)一個(gè) .shadowmodifier岛宦,modified 變量的類(lèi)型將轉(zhuǎn)變?yōu)?code>_ModifiedContent<Image, _ShadowEffect>台丛。如果你查看 View 上的 shadow 的定義,它是這樣的砾肺。

extension View
{
    func shadow(
        color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
        radius: Length, x: Length = 0, y: Length = 0)
    -> Self.Modified<_ShadowEffect>
}

ModifiedView 上的一個(gè)typealias挽霉,在struct Image: View的實(shí)現(xiàn)里,我們有:

public typealias Modified<T> = _ModifiedContent<Self, T>

_ModifiedContent 是一個(gè)SwiftUI的私有類(lèi)型变汪,它存儲(chǔ)了待變更的內(nèi)容侠坎,以及用來(lái)實(shí)施變更的 Modifier

struct _ModifiedContent<Content, Modifier> 
{
    var content: Content
    var modifier: Modifier
}

Content 遵守 View裙盾,Modifier遵守 ViewModifier 的情況下硅蹦,_ModifiedContent 也將遵守 View,這是我們能夠通過(guò) View 的各個(gè) modifier extension 進(jìn)行鏈?zhǔn)秸{(diào)用的基礎(chǔ)闷煤。

extension _ModifiedContent : _View 
    where Content : View, Modifier : ViewModifier 
{
    ...
}

shadow 的例子中童芹,SwiftUI 內(nèi)部會(huì)使用 _ShadowEffect這個(gè) ViewModifier,并把image自身和 _ShadowEffect 實(shí)例存放到_ModifiedContent 里鲤拿。不論是 image 還是 modifier假褪,都只是對(duì)未來(lái)實(shí)際視圖的描述,而不是直接對(duì)渲染進(jìn)行的操作近顷。在最終渲染前生音,ViewModifierbody(content: Self.Content) -> Self.Body將被調(diào)用,以給出最終渲染層所需要的各個(gè)屬性窒升。


六缀遍、List的解釋

a、靜態(tài)List

這里的 ListHStack 或者 VStack 之類(lèi)的容器很相似饱须,接受一個(gè)view builder并采用聲明的方式列舉了兩個(gè) LandmarkRow域醇。這種方式構(gòu)建了對(duì)應(yīng)著UITableView的靜態(tài)cell的組織方式。

var body: some View
{
    List
    {
        LandmarkRow(landmark: landmarkData[0])
        LandmarkRow(landmark: landmarkData[1])
    }
}

我們可以運(yùn)行 app蓉媳,并使用XcodeView Hierarchy 工具來(lái)觀察 UI譬挚,結(jié)果可能會(huì)讓你覺(jué)得很眼熟。實(shí)際上在屏幕上繪制的 UpdateCoalesingTableView 是一個(gè) UITableView 的子類(lèi)酪呻,而兩個(gè) ListCoreCellHost也是 UITableViewCell 的子類(lèi)减宣。對(duì)于 List 來(lái)說(shuō),SwiftUI 底層直接使用了成熟的UITableView 的一套實(shí)現(xiàn)邏輯玩荠,而并非重新進(jìn)行繪制漆腌。

不過(guò)在使用 SwiftUI 時(shí)贼邓,我們首先需要做的就是跳出 UIKit 的思維方式,不應(yīng)該去關(guān)心背后的繪制和實(shí)現(xiàn)闷尿。使用 UITableView 來(lái)表達(dá)List也許只是權(quán)宜之計(jì)塑径,也許在未來(lái)也會(huì)被另外更高效的繪制方式取代。由于SwiftUI層只是 View 描述的數(shù)據(jù)抽象悠砚,因此和FlutterWidget 一樣,背后的具體繪制方式是完全解耦合堂飞,并且可以進(jìn)行替換的灌旧。這為今后 SwiftUI更進(jìn)一步留出了足夠的可能性。


b绰筛、動(dòng)態(tài) List
List(landmarkData.identified(by: \.id))
{ landmark in
    LandmarkRow(landmark: landmark)
}

除了靜態(tài)方式以外枢泰,List 當(dāng)然也可以接受動(dòng)態(tài)方式的輸入,這時(shí)使用的初始化方法和上面靜態(tài)的情況不一樣铝噩。

public struct List<Selection, Content> where Selection : SelectionManager, Content : View
{
    public init<Data, RowContent>(
        _ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
        rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent)
    where
        Content == ForEach<Data, Button<HStack<RowContent>>>,
        Data : RandomAccessCollection,
        RowContent : View,
        Data.Element : Identifiable
        
    //...
}
Content == ForEach<Data, Button<HStack<RowContent>>>

因?yàn)檫@個(gè)函數(shù)簽名中并沒(méi)有出現(xiàn) Content衡蚂,Content 僅只 List<Selection, Content> 的類(lèi)型聲明中有定義,所以在這與其說(shuō)是一個(gè)約束骏庸,不如說(shuō)是一個(gè)用來(lái)反向確定 List 實(shí)際類(lèi)型的描述毛甲。

Data : RandomAccessCollection

這基本上等同于要求第一個(gè)輸入?yún)?shù)是 Array

RowContent : View

對(duì)于構(gòu)建每一行的 rowContent 來(lái)說(shuō)具被,需要返回是 View 是很正常的事情玻募。注意 rowContent 其實(shí)也是被 @ViewBuilder 標(biāo)記的,因此你也可以把 LandmarkRow 的內(nèi)容展開(kāi)寫(xiě)進(jìn)去一姿。不過(guò)一般我們會(huì)更希望盡可能拆小 UI 部件七咧,而不是把東西堆在一起。

Data.Element : Identifiable

要求 Data.Element (也就是數(shù)組元素的類(lèi)型) 上存在一個(gè)可以辨別出某個(gè)實(shí)例的滿足 Hashableid叮叹。這個(gè)要求將在數(shù)據(jù)變更時(shí)快速定位到變化的數(shù)據(jù)所對(duì)應(yīng)的 cell艾栋,并進(jìn)行 UI 刷新。


c蛉顽、List : View的困惑

在下面的代碼中蝗砾,我們期望 List 的初始化方法生成的是某個(gè)類(lèi)型的 View。但是你看遍 List 的文檔携冤,都找不到 List : View 之類(lèi)的聲明遥诉。

var body: some View
{
    List
    {
        //...
    }
}

難道是因?yàn)?SwiftUI 做了什么手腳,讓本來(lái)沒(méi)有滿足 View 的類(lèi)型都可以充當(dāng)一個(gè) View 嗎噪叙?當(dāng)然不是這樣…如果你在運(yùn)行時(shí)暫定 app 并用lldb打印一下List的類(lèi)型信息矮锈,可以看到下面的信息。

(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...

SwiftUI視圖_UnaryView協(xié)議雖然是滿足 View 的睁蕾,但它被隱藏起來(lái)了苞笨,而滿足它的 List雖然是 public的债朵,但是卻可以把這個(gè)協(xié)議鏈的信息也作為內(nèi)部信息隱藏起來(lái)。這是Swift內(nèi)部框架的特權(quán)瀑凝,第三方的開(kāi)發(fā)者無(wú)法這樣在在兩個(gè)public的聲明之間插入一個(gè)私有聲明序芦。


七、@State的解釋

這里出現(xiàn)了兩個(gè)以前在 Swift 里沒(méi)有的特性:@State$showFavoritesOnly粤咪。

@State var showFavoritesOnly = true

Toggle(isOn: $showFavoritesOnly)
{
    Text("Favorites only")
}

如果你點(diǎn)到 State 的定義里面谚中,可以看到它其實(shí)是一個(gè)特殊的struct

@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible
{
    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var value: Value { get nonmutating set }

    /// Returns a binding referencing the state value.
    public var binding: Binding<Value> { get }

    /// Produces the binding referencing this state value
    public var delegateValue: Binding<Value> { get }
}

@propertyWrapper標(biāo)注和@_functionBuilder 類(lèi)似寥枝,它修飾的struct可以變成一個(gè)新的修飾符并作用在其他代碼上宪塔,來(lái)改變這些代碼默認(rèn)的行為。這里 @propertyWrapper修飾的 State被用做了 @State 修飾符囊拜,并用來(lái)修飾 View中的 showFavoritesOnly 變量某筐。

@_functionBuilder 負(fù)責(zé)按照規(guī)矩重新構(gòu)造函數(shù)的作用不同,@propertyWrapper 的修飾符最終會(huì)作用在屬性上冠跷,將屬性包裹起來(lái)南誊,以達(dá)到控制某個(gè)屬性的讀寫(xiě)行為的目的。如果將這部分代碼展開(kāi)蜜托,它實(shí)際上是這個(gè)樣子的抄囚。

// @State var showFavoritesOnly = true
var showFavoritesOnly = State(initialValue: true)
    
// Toggle(isOn: $showFavoritesOnly)
Toggle(isOn: showFavoritesOnly.binding)

// if !self.showFavoritesOnly
if !self.showFavoritesOnly.value

把變化之前的部分注釋了一下,并且在后面一行寫(xiě)上了展開(kāi)后的結(jié)果橄务〉√Γ可以看到 @State 只是聲明State struct的一種簡(jiǎn)寫(xiě)方式而已。State 里對(duì)具體要如何讀寫(xiě)屬性的規(guī)則進(jìn)行了定義仪糖。對(duì)于讀取柑司,非常簡(jiǎn)單,使用 showFavoritesOnly.value 就能拿到 State 中存儲(chǔ)的實(shí)際值锅劝。而原代碼中 $showFavoritesOnly 的寫(xiě)法也只不過(guò)是 showFavoritesOnly.binding 的簡(jiǎn)化攒驰。binding 將創(chuàng)建一個(gè) showFavoritesOnly 的引用,并將它傳遞給 Toggle故爵。再次強(qiáng)調(diào)玻粪,這個(gè) binding 是一個(gè)引用類(lèi)型,所以 Toggle 中對(duì)它的修改诬垂,會(huì)直接反應(yīng)到當(dāng)前 ViewshowFavoritesOnly 去設(shè)置它的 value劲室。而 Statevalue didSet 將觸發(fā) body 的刷新,從而完成 State -> View的綁定结窘。

SwiftUI 中還有幾個(gè)常見(jiàn)的 @ 開(kāi)頭的修飾很洋,比如 @Binding@Environment隧枫,@EnvironmentObject等喉磁,原理上和 @State 都一樣谓苟,只不過(guò)它們所對(duì)應(yīng)的 struct中定義讀寫(xiě)方式有區(qū)別。它們共同構(gòu)成了 SwiftUI數(shù)據(jù)流的最基本的單元协怒。


八涝焙、Animating的解釋

直接在 View 上使用 .animation或者使用 withAnimation { }來(lái)控制某個(gè) State,進(jìn)而觸發(fā)動(dòng)畫(huà)孕暇。

button(action: {
    self.showDetail.toggle()
}) {
    Image(systemName: "chevron.right.circle")
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil)
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring())
}

對(duì)于只需要對(duì)單個(gè) View 做動(dòng)畫(huà)的時(shí)候仑撞,animation(_:)要更方便一些,它和其他各類(lèi) modifier 并沒(méi)有太大不同妖滔,返回的是一個(gè)包裝了對(duì)象View 和對(duì)應(yīng)的動(dòng)畫(huà)類(lèi)型的新的 View隧哮。animation(_:)接受的參數(shù) Animation并不是直接定義 View 上的動(dòng)畫(huà)的數(shù)值內(nèi)容的,它是描述的是動(dòng)畫(huà)所使用的時(shí)間曲線铛楣、動(dòng)畫(huà)的延遲等這些和 View 無(wú)關(guān)的東西近迁。具體和 View 有關(guān)的艺普,想要進(jìn)行動(dòng)畫(huà)的數(shù)值方面的變更簸州,由其他的諸如 rotationEffectscaleEffect 這樣的 modifier來(lái)描述。

要注意歧譬,SwiftUImodifier 是有順序的岸浑。在我們調(diào)用 animation(_:)時(shí),SwiftUI做的事情等效于是把之前的所有 modifier 檢查一遍瑰步,然后找出所有滿足 Animatable 協(xié)議的view上的數(shù)值變化矢洲,比如角度、位置缩焦、尺寸等读虏,然后將這些變化打個(gè)包,創(chuàng)建一個(gè)事物(Transaction)并提交給底層渲染去做動(dòng)畫(huà)袁滥。在上面的代碼中盖桥,.rotationEffect后的 .animation(nil)rotation的動(dòng)畫(huà)提交,因?yàn)橹付?code>nil所以這里沒(méi)有實(shí)際的動(dòng)畫(huà)题翻。在最后揩徊,.rotationEffect已經(jīng)被處理了,所以末行的.animation(.spring()) 提交的只有.scaleEffect嵌赠。

withAnimation { } 閉包內(nèi)部塑荒,我們一般會(huì)觸發(fā)某個(gè) State 的變化,并讓View.body進(jìn)行重新計(jì)算.

Button(action: {
    withAnimation
    {
        self.showDetail.toggle()
    }
}) {
  //...
}

如果需要姜挺,你也可以為它指定一個(gè)具體的 Animation齿税。這個(gè)方法相當(dāng)于把一個(gè)animation設(shè)置到 View 數(shù)值變化的 Transaction 上,并提交給底層渲染去做動(dòng)畫(huà)炊豪。從原理上來(lái)說(shuō)偎窘,withAnimation 是統(tǒng)一控制單個(gè)的 Transaction乌助,而針對(duì)不同 Viewanimation(_:)調(diào)用則可能對(duì)應(yīng)多個(gè)不同的 Transaction

withAnimation(.basic())
{
    self.showDetail.toggle()
}

九陌知、生命周期

UIKit 開(kāi)發(fā)時(shí)他托,我們經(jīng)常會(huì)接觸一些像是 viewDidLoadviewWillAppear這樣的生命周期的方法仆葡,并在里面進(jìn)行一些配置赏参。SwiftUI里也有一部分這類(lèi)生命周期的方法,比如.onAppear.onDisappear沿盅,它們也被統(tǒng)一在了 modifier 這面大旗下把篓。

但是相對(duì)于UIKit來(lái)說(shuō),SwiftUI中能使用的生命周期方法比較少腰涧,而且相對(duì)要通用一些韧掩。本身在生命周期中做操作這種方式就和聲明式的編程理念有些相悖。個(gè)人比較期待ViewCombine能再深度結(jié)合一些窖铡,把依賴(lài)生命周期的操作也用綁定的方式搞定疗锐。

相比于.onAppear.onDisappear,更通用的事件響應(yīng)是.onReceive(_:perform:)费彼,它定義了一個(gè)可以響應(yīng)目標(biāo) Publisher的任意的 View滑臊,一旦訂閱的 Publisher發(fā)出新的事件時(shí),onReceive就將被調(diào)用箍铲。


Demo

Demo在我的Github上雇卷,歡迎下載。
SwiftUIDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載颠猴,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者关划。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市翘瓮,隨后出現(xiàn)的幾起案子贮折,更是在濱河造成了極大的恐慌,老刑警劉巖春畔,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脱货,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡律姨,警方通過(guò)查閱死者的電腦和手機(jī)振峻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)择份,“玉大人扣孟,你說(shuō)我怎么就攤上這事∪俑希” “怎么了凤价?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵鸽斟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我利诺,道長(zhǎng)富蓄,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任慢逾,我火速辦了婚禮立倍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘侣滩。我一直安慰自己口注,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布君珠。 她就那樣靜靜地躺著寝志,像睡著了一般。 火紅的嫁衣襯著肌膚如雪策添。 梳的紋絲不亂的頭發(fā)上材部,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音舰攒,去河邊找鬼败富。 笑死悔醋,一個(gè)胖子當(dāng)著我的面吹牛摩窃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芬骄,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼猾愿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了账阻?” 一聲冷哼從身側(cè)響起蒂秘,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淘太,沒(méi)想到半個(gè)月后姻僧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒲牧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年撇贺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冰抢。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡松嘶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挎扰,到底是詐尸還是另有隱情翠订,我是刑警寧澤巢音,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站尽超,受9級(jí)特大地震影響官撼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜似谁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一歧寺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棘脐,春花似錦斜筐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至屈梁,卻和暖如春嗤练,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背在讶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工煞抬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人构哺。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓革答,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親曙强。 傳聞我的和親對(duì)象是個(gè)殘疾皇子残拐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345