首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >@State 研究

@State 研究

作者头像
东坡肘子
发布2022-07-28 12:51:20
发布2022-07-28 12:51:20
3.4K00
代码可运行
举报
运行总次数:0
代码可运行

@State 研究

如想获得更好的阅读体验,可以访问我的博客 www.fatbobman.com。

本文试图探讨并分析SwiftUI 中 @State的实现方式和运行特征;最后提供了一个有关扩展@State功能的思路及例程。读者需要对SwiftUI的响应式编程有基本概念。

研究的意义何在

我在去年底使用了SwiftUI写了第一个 iOS app 健康笔记,这是我第一次接触响应式编程概念。在有了些基本的认识和尝试后,深深的被这种编程的思路所打动。不过,我在使用中也发现了一些奇怪的问题。我发现在视图(View)数量达到一定程度,随着数据量的增加,整个app的响应有些开始迟钝,变得有粘滞感、不跟手。app响应出现了问题一方面肯定和我的代码效率、数据结构设计欠佳有关;不过随着继续分析,发现其中也有很大部分原因来自于SwiftUI中所使用的响应式的实现方式。不恰当的使用,可能导致响应速度会随着数据量及View量的增加而大幅下降。通过一段时间的研究和分析,我打算用两篇文章来阐述这方面的问题,并尝试提供一个现阶段的使用思路。

数据(状态)驱动

在SwiftUI中,视图是由数据(状态)驱动的。按照苹果的说法,视图是状态的函数,而不是事件的序列(The views are a function of state, not a sequence of events)。每当视图在创建或解析时,都会为该视图和与该视图中使用的状态数据之间创建一个依赖关系,每当状态的信息发生变化时,有依赖关系的视图则会马上反应出这些变化并重绘。SwiftUI中提供了诸如 @State ObservedObject EnvironmentObject等来创建应对不同类型、不同作用域的状态形式。

类型及作用域图片来自于SwiftUI for Absoloute Beginners

其中@State只能用于当前视图,并且其对应的数据类型为值类型(如果非要对应引用类型的话则必须在每次赋值时重新创建新的实例才可以)。

代码语言:javascript
代码运行次数:0
运行
复制
struct DemoView:View{  @State var name = "肘子"  var body:some View{    VStack{      Text(name)      Button("改名"){        self.name = "大肘子"      }    }  }}

通过执行上面代码,我们可以发现两个情况:

1.通过使用@State,我们可以在未使用mutating的情况下修改结构中的值2.当状态值发生变化后,视图会自动重绘以反应状态的变化。

@State如何工作的

在分析@State如何工作之前,我们需要先了解几个知识点

属性包装器

作为swift 5.1的新增功能之一,属性包装器在管理属性如何存储和定义属性的代码之间添加了一个分割层。通过该特性,可以在对值校验、持久化、编解码等多个方面获得收益。

它的实现也很简单,下面的例子定义了一个包装器用来确保它包装的值始终小于等于12。如果要求它存储一个更大的数字,它则会存储 12 这个数字。呈现值(投射值)则返回当前包装值是否为偶数

代码语言:javascript
代码运行次数:0
运行
复制
@propertyWrapperstruct TwelveOrLess {    private var number: Int    init() { self.number = 0 }    var wrappedValue: Int {        get { return number }        set { number = min(newValue, 12) }    }    var projectedValue: Bool {        self.number % 2 == 0    }}

更多的具体资料请查阅官方文档

Binding

Binding是数据的一级引用,在SwiftUI中作为数据(状态)双向绑定的桥梁,允许在不拥有数据的情况下对数据进行读写操作。我们可以绑定到多种类型,包括 State ObservedObject 等,甚至还可以绑定到另一个Binding上面。Binding本身就是一个Getter和Setter的封装。

State 的定义

代码语言:javascript
代码运行次数:0
运行
复制
@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 }}

DynamicProperty 的定义

代码语言:javascript
代码运行次数:0
运行
复制
public protocol DynamicProperty {    /// Called immediately before the view's body() function is    /// executed, after updating the values of any dynamic properties    /// stored in `self`.    mutating func update()}

工作原理

前面我们说过 @State 有两个作用

1.通过使用@State,我们可以在未使用mutating的情况下修改结构中的值2.当状态值发生变化后,视图会自动重绘以反应状态的变化。

让我们根据上面的知识点来分析如何才能实现以上功能。

•@State本身包含 @propertyWrapper,意味着他是一个属性包装器。•public var wrappedValue: Value { get nonmutating set } 意味着他的包装值并没有保存在本地。•它的呈现值(投射值)为Binding类型。也就是只是一个管道,对包装数据的引用•遵循 DynamicProperty 协议,该协议完成了创建数据(状态)和视图的依赖操作所需接口。现在只暴露了很少的接口,我们暂时无法完全使用它。

在了解了以上几点后,我们来尝试使用自己的代码来构建一个@State的半成品

代码语言:javascript
代码运行次数:0
运行
复制
@propertyWrapperstruct MyStates:DynamicProperty{    init(wrappedValue:String){        UserDefaults.standard.set(wrappedValue, forKey: "myString")    }        var wrappedValue:String{        nonmutating set{UserDefaults.standard.set(newValue, forKey: "myString")}        get{UserDefaults.standard.string(forKey: "myString") ?? ""}    }        var projectedValue:Binding<String>{        Binding<String>(            get:{String(self.wrappedValue)},            set:{                self.wrappedValue = $0        }        )    }        func update() {        print("重绘视图")    }}

这是一个可以用来包装String类型的State。

我们使用UserDefault将数据包装后保存到本地。读取包装数据也是从本地的UserDefault里读取的。

为了能够包装其他的类型的数据,同时也为了能够提高存储效率,进一步的可以修改成如下代码:

代码语言:javascript
代码运行次数:0
运行
复制
@propertyWrapperstruct MyState<Value>:DynamicProperty{    private var _value:Value    private var _location:AnyLocation<Value>?        init(wrappedValue:Value){        self._value = wrappedValue        self._location = AnyLocation(value: wrappedValue)    }        var wrappedValue:Value{        get{ _location?._value.pointee ?? _value}        nonmutating set{ _location?._value.pointee = newValue}    }        var projectedValue:Binding<Value>{        Binding<Value>(            get:{self.wrappedValue},            set:{self._location?._value.pointee = $0}        )    }        func update() {        print("重绘视图")    }}class AnyLocation<Value>{    let _value = UnsafeMutablePointer<Value>.allocate(capacity: 1)    init(value:Value){        self._value.pointee = value    }}

至此,我们完成了这个@MyState的半成品。

之所以说是半成品,因为尽管我们也遵循了DynamicProperty协议,但我们自己编写的这段代码并不能和视图建立依赖。我们可以和使用@State一样来使用@MyState,同样支持绑定、修改,除了视图不会自动刷新😂。

但至少我们可以大概了解@State是如何让我们在视图中修改、绑定数据的。

什么时候建立的依赖?

我目前无法找到任何关于SwiftUI建立依赖的更具体的资料或实现线索。不过我们可以通过下面两段代码来猜测编译器是如何处理数据和视图之间的依赖关联时机的。

代码语言:javascript
代码运行次数:0
运行
复制
struct MainView: View {    @State var date: String = Date().description    var body: some View {        print("mainView")        return Form {            SubView(date: $date)            Button("修改日期") {                self.date = Date().description            }        }    }}struct SubView: View {    @Binding var date: String    var body: some View {        print("subView")        return Text(date)    }}

执行这段代码,我们点击修改日期 ,我们会得到如下输出

代码语言:javascript
代码运行次数:0
运行
复制
mainViewsubViewsubView...

虽然我们在MainView中使用@State声明了date,并且在MainView中修改了date的值,但由于我们并没有在MainView中使用date的值来进行显示或者判断,所以无论我们如何修改date值,MainView都不会重绘。我推测@State同视图的依赖是在ViewBuilder解析时进行的。编译器在解析我们的body时,会判断date的数据变化是否会对当前视图造成改变。如果没有则不建立依赖关联。

我们可以用另一段代码来分析编译器对 ObservedObject 的反应。

代码语言:javascript
代码运行次数:0
运行
复制
struct MainView: View {    @ObservedObject var store = AppStore()        var body: some View {        print("mainView")        return Form {            SubView(date: $store.date)            Button("修改日期") {                self.store.date = Date().description            }        }    }}struct SubView: View {    @Binding var date: String    var body: some View {        print("subView")        return Text(date)    }}class AppStore:ObservableObject{    @Published var date:String = Date().description}

执行后输出如下:

代码语言:javascript
代码运行次数:0
运行
复制
mainViewsubViewmainViewsubView...

我们把@State换成了@ObservedObject ,同样在MainView中并没有显示store.date的值或者用其来做判断,但是只要我们改变了store里的date值,MainView便会刷新重绘。由此可以推测,SwiftUI对于ObservedObject采用了不同的依赖创建时机,只要声明,无论body里是否有需要,在ObservableObject的objectWillChange产生send后,都会进行重绘。因此ObservedObject很可能是在初始化MainView的时候建立的依赖关系。

之所以花气力来判断这个问题,因为这两种创建依赖的时机的不同会导致View更新效率的巨大差异。这个差异也正是我下一篇文章要重点探讨的地方

打造适合自己的增强型 @State

@State使用属性包装器这个特性来实现了它既定的功能,不过属性包装器还被广泛用于数据验证、副作用等众多领域,我们能否将众多功能属性集于一身?

本文我们自己通过代码打造的@State半成品并不能创建和视图的依赖,我们如何才能完成这种依赖关联的创建?

@State不仅可以被用于对属性的包装,同时State本身也是一个标准的结构体。它通过内部没有暴露的功能接口完成了同视图的依赖创建。

以下两种使用方式是等效的:

代码语言:javascript
代码运行次数:0
运行
复制
@State var name = ""self.name = "肘子"
代码语言:javascript
代码运行次数:0
运行
复制
var name = State<String>(wrappedValue:"")self.name.wrappedValue = "肘子"

因此我们可以通过将State作为包装值类型,创建新的属性包装器,来实现我们的最终目标 —— 完整功能、可任意扩展的增强型@State。

代码语言:javascript
代码运行次数:0
运行
复制
@propertyWrapperstruct MyState<Value>:DynamicProperty{    typealias Action = (Value) -> Void        private var _value:State<Value>    private var _toAction:Action?        init(wrappedValue value:Value){        self._value = State<Value>(wrappedValue: value)    }        init(wrappedValue value:Value,toAction:@escaping Action){        self._value = State<Value>(wrappedValue: value)        self._toAction = toAction    }        public var wrappedValue: Value {        get {self._value.wrappedValue}        nonmutating set {self._value.wrappedValue = newValue}    }÷        public var projectedValue: Binding<Value>{        Binding<Value>(            get: {self._value.wrappedValue},            set: {                self._value.wrappedValue = $0                self._toAction?($0)        }        )    }        public func update() {       print("视图重绘")    }        }

这段代码仅作为一个例子,可以根据自己的需求任意创建自己所需的功能。

代码语言:javascript
代码运行次数:0
运行
复制
@MyState var name = "hello"  //实现和标准@State一样的功能
代码语言:javascript
代码运行次数:0
运行
复制
@MyState<String>(  wrappedValue: "hello",   toAction: {print($0)}) var name//在每次赋值后(包括通过Binding修改)执行 toAction 定义的函数

接下来?

在响应式编程开始流行的今天,越来越多的人都在使用单一数据源(Single Souce of Truth)的构架方式进行设计和开发。如何使用@State这种作用域范围仅限于当前视图的特性?仅从命名来看,苹果给了他最本质的名称——State。State属于SwiftUI架构,ObservableObject属于Combine架构,SwiftUI明显对于State的优化要好于ObservableObject。如何在满足单一数据源的情况下最大限度享受SwiftUI的优化便利?我将在下一篇文章中进行进一步探讨。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • @State 研究
    • 研究的意义何在
    • 数据(状态)驱动
    • @State如何工作的
      • 属性包装器
      • Binding
      • State 的定义
      • DynamicProperty 的定义
      • 工作原理
      • 什么时候建立的依赖?
    • 打造适合自己的增强型 @State
    • 接下来?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档