SwiftUI數據流之State&Binding
本文字數:4569字
預計閱讀時間:14分鐘
在SwiftUI中,以單一數據源(single source of truth)為核心,構建了數據驅動狀態更新的機制。其中引入了多種新的屬性包裝器(property wrapper),用來進行狀態管理。本篇主要介紹@State和@Binding,將從簡單的使用入手,通過一系列具體的代碼實例展示它們的使用場景,并進步一探索State的內部實現原理。
環境
MacOS 10.15.5
Xcode 12.0 beta
@State
A property wrapper type that can read and write a value managed by SwiftUI.
@State是一個屬性包裝器(property wrapper),被設計用來針對值類型進行狀態管理;用于在Struct中mutable值類型
struct User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
struct ContentView: View {
@State private var user = User() //1
var body: some View {
VStack {
Text("Your name is \(user.firstName) \(user.lastName).") //2
TextField("First name", text: $user.firstName) //3
TextField("Last name", text: $user.lastName)
}
}
}
對于 @State 修飾的屬性的訪問,只能發生在 body 或者 body 所調用的方法中。你不能在外部改變 @State 的值,只能@State初始化時,設置初始化值,如注釋1處所示,它的所有相關操作和狀態改變都應該是和當前 View 生命周期保持一致。
在引用包裝為@State的屬性是,如果是讀寫都有,引用屬性需要$開頭(注釋3處),如果只讀直接使用變量名即可(注釋2處)
State針對具體View的內部變量進行管理,不應該從外部被允許訪問,所以應該標記為private(注釋1處)
但是,如果把struct User
替換為class User
將會無效,為什么呢?
@State檢測的是值類型
值類型僅有獨立的擁有者,而class類型可以多個指向一個;對于兩個SwiftUI View而言,即使發送給他們兩個相同的struct對象,事實上他們每個View都得到了一份獨立的struct的拷貝,所以其中一個View的struct值發生變化,對另一個沒有影響;反之,如果是class則會互相影響;
當User是一個結構體時,每次我們修改這個結構體的屬性時,Swift實際上是在創建一個新的結構體實例。@State能夠發現這個變化,并自動重新加載我們的視圖。現在如果改為class,我們有了一個類,這種行為就不再發生,Swift可以直接修改值。
還記得我們如何使用mutating關鍵字來修改結構方法的屬性嗎?
struct User {
var name:String
mutating func changeName(name:String) {
self.name = name
}
}
這是因為如果我們創建了作為變量的結構體屬性,但結構體本身是常量,我們不能更改屬性;當屬性發生變化時,Swift需要能夠銷毀并重新創建整個結構體,而這對于常量結構體是不可能的。類不需要mutating關鍵字,因為即使類實例被標記為常量,Swift仍然可以修改變量屬性。
如果User是一個類,屬性本身就不會改變,所以@State不會注意到任何東西,也無法重新加載視圖。即使類內的某個屬性值發生變化,但@State不監聽這些,所以視圖不會被重新加載。
如果想要改變這種情況,使得class類被監聽到變化,就不能使用@State,需要使用@ObservedObject或@StateObject
@Binding
A property wrapper type that can read and write a value owned by a source of truth.
@Binding的作用是在保存狀態的屬性和更改數據的視圖之間創建雙向連接,將當前屬性連接到存儲在別處的單一數據源(single source of truth),而不是直接存儲數據。將存儲在別處的值語意的屬性轉換為引用語義,在使用時需要在變量名加$符號。
通常使用場景是把當前View中的@State值類型傳遞給其子View,如果直接傳遞@State值類型,將會把值類型復制一份copy,那么如果子View中對值類型的某個屬性進行修改,父View不會得到變化,所以需要把@State轉成@Binding傳遞。
@Binding 修飾屬性無需有初始化值,Binding可以配合@State或ObservableObject對象中的值屬性一起使用,注意不是@ObservedObject屬性包裝器
struct Product:Identifiable {
var isFavorited:Bool
var title:String
var id: String
}
struct FilterView: View {
@Binding var showFavorited: Bool //3
var body: some View {
Toggle(isOn: $showFavorited) { //4
Text("Change filter")
}
}
}
struct ProductsView: View {
let products: [Product] = [
Product(isFavorited: true, title: "ggggg",id: "1"),
Product(isFavorited: false, title: "3333",id: "2")]
@State private var showFavorited: Bool = false //1
var body: some View {
List {
FilterView(showFavorited: $showFavorited) //2
ForEach(products) { product in
if !self.showFavorited || product.isFavorited {
Text(product.title)
}
}
}
}
}
這個例子展示了一個有過濾開關的列表,為了簡化內容說明核心問題,只有兩行內容,父視圖是ProductsView,其中嵌套著子視圖FilterView和列表元素,為了能夠使得FilterView中對showFavorited的修改能夠傳遞回父視圖:
注釋1,showFavorited使用@State修飾
注釋2,在body中通過$showFavorited獲得showFavorited對應的Binding傳遞給子視圖FilterView
注釋3,子視圖FilterView中定義了
@Binding var showFavorited: Bool
引用傳入參數注釋4,當切換開關后,由于@Binding機制的作用,會修改外層的單一數據源(single source of truth),所以列表中展示的內容會不斷根據條件進行過濾
可變和不可變
首先來使用下面示例探討一個問題
struct StateMutableView: View {
@State private var flag = false
private var anotherFlag = false
mutating func changeAnotherFlag(_ value: Bool) {
self.anotherFlag = value
}
var body: some View {
Button(action: {
//1 ok
self.flag = true
//2 Cannot assign to property: 'self' is immutable
self.anotherFlag = true
//3 Cannot use mutating member on immutable value: 'self' is immutable
changeAnotherFlag(true)
}) {
Text("Test")
}
}
}
flag是標記為State的變量,anotherFlag是沒有使用屬性包裝器的普通變量,同時增加了一個mutating的方法changeAnotherFlag
被設計修改anotherFlag;
在body中通過幾種方式對兩個變量進行修改,注釋1-3處,分別標記了修改結果和提示錯誤,顯然flag可以被修改,而anotherFlag不可以,這是為什么?
這里涉及兩個問題:
為什么可以修改flag?
為什么不可以修改anotherFlag?
先來看第二個問題
為什么不可以修改anotherFlag
計算屬性getter
示例5
struct SimpleStruct {
var anotherFlag: Bool {
_anotherFlag = true
// ^~~~~~~~~~~~
// error: cannot assign to property: 'self' is immutable
return _anotherFlag
}
private var _anotherFlag = false
}
_anotherFlag存儲屬性,anotherFlag計算屬性 在getter屬性中,self默認是nonmutating,是不能被修改的,所以報錯
但是,可以有例外,如果getter被特殊標記為mutating,就可以被修改
struct SimpleStruct {
var anotherFlag: Bool {
mutating get {
_anotherFlag = true
return _anotherFlag
}
}
private var _anotherFlag = false
}
并且還需要使用SimpleStruct時,聲明實例為var
var s0 = SimpleStruct()
_ = s0.anotherFlag // ok, and modifies s0
let s1 = SimpleStruct()
_ = s1.anotherFlag
// ^~ error: cannot use mutating getter on immutable value: 's1' is a 'let' constant
既然可以通過添加mutating,使得計算屬性get中可以修改self,那么SwiftUI中前面示例的body屬性可否添加呢?
查看View協議的定義
public protocol View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
body是set,不能被改為mutating,所以如果你改為這樣下面
struct SimpleView: View {
// ^ error: type 'SimpleView' does not conform to protocol 'View'
var body: some View {
mutating get { Text("Hello") }
}
}
會報錯,提示沒有遵守View協議
小結:不可以修改anotherFlag的原因:body計算屬性的的getter不可以被修改mutating
為什么可以修改flag
由于SwiftUI設計之初就是希望構建的View樹保持不變,這樣才能高效的渲染UI,跟蹤變化,當標記為@State的變量發生變化時,變量本身由于在Struct中不能發生變化,所以通過State為例的property wrapper本質是修改當前struct之外的變量
我們看一下State的定義
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
/// Initialize with the provided initial value.
public init(wrappedValue value: Value)
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var wrappedValue: Value { get nonmutating set }
/// Produces the binding referencing this state value
public var projectedValue: Binding<Value> { get }
}
wrappedValue就是被標記為nonmutating set,直接使用state對象是用的wrappedValue,$符號使用的projectedValue
nonmutating有什么含義?
計算屬性setter
在setter屬性中,self默認是mutating,可以被修改;我們不能給一個不可變的量賦值,可以通過聲明setter nonmutating使屬性可賦值,這個nonmutating關鍵字向編譯器表明,這個賦值過程不會修改這個struct本身,而是修改其他變量。
struct SimpleStruct {
var anotherFlag: Bool {
mutating get {
_anotherFlag = true
return _anotherFlag
}
}
private var _anotherFlag: Bool {
get {
return UserDefaults.standard.bool(forKey: "storage")
}
nonmutating set {
UserDefaults.standard.setValue(newValue, forKey: "storage")
}
}
}
let s0 = SimpleStruct()
var s1 = s0
_ = s1.anotherFlag // 同時影響s0和s1,他們內部的_anotherFlag都發生了變化
這個例子當中_anotherFlag修改了UserDefaults的值,會同時對s0和s1都產生影響,相當于起到了引用類型的作用,在實際編程中這當然是一個不好的范例,容易產生問題
小結:可以修改flag的原因,添加了property wrapper的屬性,變量本身并沒有變化,而是修改了由SwiftUI維護的當前struct之外的變量
@State內部實現
為了進一步深入分析,我
為了分析變量狀態,在16行,User結構體init方法;39行,ContentView的init方法結束;47行,按鈕點擊執行函數部分,都加入了斷點
由于@State針對值類型,為了打印出struct的地址,增加了address函數
dump系統函數,能夠打印出變量內部結構
運行界面如上圖所示,本文輸入框可以修改name,Count+1按鈕使得count計數加1
打開斷點,從頭開始執行代碼,首先執行到16行斷點處,User初始化,此時self是User結構體本身
? User
\- name : ""
\- count : 0
繼續執行到ContentView的初始化方法最后一行,此時self是ContentView,打印一下
? ContentView
? _user : State<User>
? _value : User
\- name : ""
\- count : 0
\- _location : nil
出現了一個新的_user
變量,類型是State<User>
,這個變量內部屬性_value
類型是User
;這意味著,加了@State屬性包裝器的user實例變量,由本身的User
類型轉變為一個新的State<User>
類型,這個轉變完成的新類型實例_user
由SwiftUI負責生成和管理,它的內部包裹著真實的User實例,另外_location
也需要值得注意,它目前是nil;
如果你注意到35行代碼user = User(name: "TT", count: 100)
發現它并不會改變內部_user
,如果想要修改,只能采用下面方式,通過State提供的第二個初始化方法
_user = State(wrappedValue: User(name: "TT", count: 100))
與此同時,檢查當前console的log輸出
User init
ContentView init
140732783334216
? SwiftUI.State<DemoState.User>
? _value: DemoState.User
- name: ""
- count: 0
- _location: nil
按照預期的執行順序,User init執行,ContentView init執行,然后打印出了當前結構體的地址和_user
內部結構
下一步,由于body執行完畢,頁面渲染完整,現在點擊Count+1按鈕,斷點停在47行
? ContentView
? _user : State<User>
? _value : User
- name : ""
- count : 0
? _location : Optional<AnyLocation<User>>
? some : <StoredLocation<User>: 0x600003c26a80>
_location
不再是nil
140732783330824
? SwiftUI.State<DemoState.User>
? _value: DemoState.User
- name: ""
- count: 0
? _location: Optional(SwiftUI.StoredLocation<DemoState.User>)
? some: SwiftUI.StoredLocation<DemoState.User> #0
注意user的地址發生了變化,開始時創建的user被銷毀又重新創建了,這是因為@State 修飾的屬性的它的所有相關操作和狀態改變都應該是和當前視圖生命周期保持一致,當視圖沒有被初始化完成時,無法完成狀態屬性和視圖之間的綁定關系;_location
不在是nil,其中保存了眾多標記視圖唯一性的信息,這里沒有全部展示出來;
再點擊一次Count+1按鈕,count值變為2,user的地址將持續保持不變,生命周期與視圖保持一致。
通過前面的分析,已經明確內部_user
變量的存在,下面進一步分析State內部實現中wrappedValue和projectedValue的關系
(lldb) p _user
(State<DemoState.User>) $R6 = {
_value = (name = "", count = 2)
_location = 0x0000600003c26a80 {
SwiftUI.AnyLocationBase = {}
}
}
(lldb) p _user.wrappedValue
(DemoState.User) $R8 = (name = "", count = 2)
(lldb) p _user.projectedValue
(Binding<DemoState.User>) $R10 = {
transaction = {
plist = {
elements = nil
}
}
location = 0x0000600003c26a80 {
SwiftUI.AnyLocationBase = {}
}
_value = (name = "", count = 2)
}
SwiftUI把@State var user = User()
轉換成三個屬性
private var _user: State<User> = State(initialValue: User())
private var $user: Binding<User> { return _user.projectedValue }
private var user: User {
get { return _user.wrappedValue }
nonmutating set { _user.wrappedValue = newValue }
}
為什么$user是只讀的?測試一下會發現修改失敗
(lldb) expr $user = User(name:"",count:100)
error: <EXPR>:3:1: error: cannot assign to property: '$user' is immutable
$user = User(name:"",count:100)
^~~~~
error: <EXPR>:3:9: error: cannot assign value of type 'User' to type 'Binding<User>'
$user = User(name:"",count:100)
^~~~~~~~~~~~~~~~~~~~~~~
(lldb) expr $user.name = "Tim"
error: <EXPR>:3:7: error: cannot assign to property: '$user' is immutable
$user.name = "Tim"
~~~~~ ^
error: <EXPR>:3:14: error: cannot assign value of type 'String' to type 'Binding<String>'
$user.name = "Tim"
^~~~~
說明projectedValue只讀屬性
通過上面分析可以畫出一張State內部實現屬性的關系
_user:State<User>
_value:User
_name:String
_count:Int
_wrappedValue:User
get { _value }
set { _value = newValue }
_projectedValue:User
get { _value }
我們進一步可以大致寫出State的部分可能實現邏輯
@propertyWrapper struct State<T> {
var _value:T
init(wrappedValue: T) {
_value = wrappedValue
}
var wrappedValue: T {
nonmutating set { _value = newValue }
get { _value.value }
}
var projectedValue: T { _value }
}
總結
@State屬性包裝器針對值類型進行狀態管理,用于在Struct中mutable值類型,它的所有相關操作和狀態改變和當前 View 生命周期保持一致
Binding將存儲在別處的值語意的屬性轉換為引用語義,在使用時需要在變量名加$符號
添加了property wrapper的屬性,變量本身并沒有變化,而是修改了由SwiftUI維護的當前struct之外的變量
參考
https://developer.apple.com/documentation/swiftui/state
https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject
https://kateinoigakukun.hatenablog.com/entry/2019/03/22/184356
<THE END>
本期贈書
《程序員面試金典(第6版)》蓋爾 ? 拉克曼 ? 麥克道爾 著 劉博楠,趙鵬飛,李琳驍,漆犇譯本書全面而詳盡地介紹了程序員應當如何應對面試,才能在面試中脫穎而出。內容主要涉及面試流程解析,面試官的幕后決策及可能提出的問題,面試前的準備工作,對面試結果的處理,以及出自微軟、蘋果、谷歌等多家知名公司的189道編程面試題及詳細解決方案。第6版修訂了上一版中一些題目的解法,為各章新增了介紹性內容,加入了更多的算法策略,并增添了對所有題目的提示信息。
參與方式
文末留言板留言,點贊前5名各獲贈書一本
獲獎公布
公布時間及位置:10月1日頭條推送文末
特別提醒:兌獎截止至10月8日,請參與讀者及時兌獎
上期獲獎名單公布
恭喜“阿策”、“狼喜歡夏天”、“Howie”、“@luxi...”、“smile...”!以上讀者請添加小編微信:sohu-tech20兌獎~
加入搜狐技術作者天團
千元稿費等你來!
戳這里!?
也許你還想看
(▼點擊文章標題或封面查看)
【文末有驚喜!】DLNA技術初探
探秘 App Clips
【周年福利Round2】都0202年了,您還不會Elasticsearch?
【周年福利Round1】一文看破Swift枚舉本質
iOS 隱形水印之 LSB 實現
智能推薦
Stream數據流
Stream基礎操作 Java中為了簡化集合的數據處理問題,提供了數據流接口:stream(),是一個重度使用lambda表達式的API。。常用接口:count()元素個數,distinct()消除重復數據,collect()收集處理后的數據,通常是在處理器處理后并且最后使用,filter()數據過濾。 count() 1.count()元素個數, 2.distinct() 程序結果:阿珍愛上了阿...
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
猜你喜歡
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...
統計學習方法 - 樸素貝葉斯
引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...
styled-components —— React 中的 CSS 最佳實踐
https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...