Combine之SwiftUI中的状态管理

06/26/2022 17:38 下午

大家应该都知道SwiftUI的设计理念是Data flow, 也就是View是由数据驱动的,我们把View依赖的这些数据称之为状态,因此,SwiftUI中的数据管理就是状态管理。

常见的状态管理由以下几个:

  • [AppStorage](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/appstorage)
  • [Binding](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/binding)
  • [Environment](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/environment)
  • [EnvironmentObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/environmentobject)
  • [FetchRequest](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/fetchrequest)
  • [ObservedObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/observedobject)
  • [State](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/state)
  • [StateObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/stateobject)

在开发中,他们的用法可以用下边这个图概括:

如果View依赖了这些数据,当数据改变的时候,View就会刷新。我们主要讲解ObservedObjectStateObject

ObservedObject

class MyViewModel: ObservableObject {
    @Published var name: String = "张三"
}

struct ContentView: View {
    @ObservedObject var dataModel: MyViewModel

    var body: some View {
        Text(dataModel.name)
    }
}

上边的代码是最常见的一种用法,dataModelContentView提供数据,那么@ObservedObject是怎么一回事呢?看它的定义:

@propertyWrapper @frozen public struct ObservedObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
    @dynamicMemberLookup @frozen public struct Wrapper {
        public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get }
    }

    public init(initialValue: ObjectType)
    public init(wrappedValue: ObjectType)

    public var wrappedValue: ObjectType

    public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}

通过分析上边的代码,我们发现下边几个重要信息:

  • ObjectType : ObservableObject表示它的类型必须实现ObservableObject协议,这个协议我们下边会讲到
  • projectedValue: ObservedObject<ObjectType>.Wrapper,说明我们可以用$dataModel来访问这个projectedValue,它的返回值是Wrapper类型,再看上边struct Wrapper的定义,它是一个@dynamicMemberLookup,@dynamicMemberLookup的实现原理我们后续再详细讲解,大家只需要知道,当我们想要一个Bind类型的数据是,可以这样TextField("输入文字", text: $dataModel.name)

其中,上边的重点是ObservableObject协议,我们再看看它的定义:

public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}

extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {

    /// A publisher that emits before the object has changed.
    public var objectWillChange: ObservableObjectPublisher { get }
}

ObservableObject继承自AnyObject这说明了实现该协议必须是class类型,而不能是struct类型。

该协议要求返回一个objectWillChange属性,该属性必须实现Publisher协议,上边代码中的ObservableObject扩展已经实现了该协议,它返回的类型为ObservableObjectPublisher,我们再看看它的定义:

final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

    /// Creates an observable object publisher instance.
    public init()

    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output

    final public func send()
}

可以看出ObservableObjectPublisher是一个很普通的Publisher,它是一个自定义的Publisher,对外只暴露了一个send方法,用于通知数据发生变更,这个Publisher并不会输出任何数据。

到目前为止,我们已经知道,只要实现了ObservableObject协议,就能获得一个objectWillChange,它是一个Publisher,只要调用objectWillChange.send()就可以触发View的刷新

我们先实现这个协议,代码如下:

class MyViewModel: ObservableObject {
    @Published var name: String = "张三"
    var age: Int = 20

    func click() {
        age = 30
        objectWillChange.send()
    }
}

如果我们用@Published来包装某个属性,那么当属性的值变化时,就会自动调用objectWillChange.send(),否则我们需要手动调用。

我们再看一下@Published的定义:

@propertyWrapper public struct Published<Value> {

    public init(wrappedValue: Value)

    public init(initialValue: Value)

    /// A publisher for properties marked with the `@Published` attribute.
    public struct Publisher : Publisher {

        /// The kind of values published by this publisher.
        public typealias Output = Value

        /// The kind of errors this publisher might publish.
        ///
        /// Use `Never` if this `Publisher` does not publish errors.
        public typealias Failure = Never


        public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
    }

    public var projectedValue: Published<Value>.Publisher { mutating get set }
}

大家只需要记住一点,它的projectedValue是一个Publisher,要想获取到这个projectedValue,使用$符号,因为它是一个Publisher,所有我们就可以随意使用Combine中的内容了:

$name
      .map {
          "姓名是: \($0)"
      }
      .sink(receiveValue: {
          print($0)
      })

StateObject

@StateObject@ObservedObject都是用来包装实现了ObservableObject协议的属性,唯一的区别就是该属性的生命周期的管理问题。

  • @StateObject的生命周期由View管理,只初始化一次,View销毁它就销毁
  • @ObservedObject的生命周期由我们手动管理,通常由父传给子

总结

本文并没有详细地讲解SwiftUI中的全部状态管理,只讲到了跟Combine有关系的状态,其中,最核心的是ObservableObject协议,在真实的开发中,它绝对是最常用的技术,我们自定义的View Model中,通过组合使用一系列的pipline来操作数据,当作为Source for Truth的数据变更后,View自动进行刷新。

https://kean.blog/post/swiftui-data-flow

https://stackoverflow.com/questions/59912443/how-to-implement-a-custom-property-wrapper-which-would-publish-the-changes-for-s