【问题标题】:Add @Published behaviour for computed property为计算属性添加 @Published 行为
【发布时间】:2020-03-21 01:00:09
【问题描述】:

我正在尝试创建一个 ObservableObject,它具有包装 UserDefaults 变量的属性。

为了符合ObservableObject,我需要用@Published包装属性。不幸的是,我不能将它应用于计算属性,因为我用于 UserDefaults 值。

我怎样才能让它工作?我必须做什么才能实现@Published 行为?

【问题讨论】:

  • 也许我理解错了,但没有必要用@Published 包装每个属性。因此,您可以在您的类中并排计算已发布的属性和普通属性。如果您分享一些代码来阐明您尝试实现的目标会有所帮助。

标签: swift swiftui combine property-wrapper


【解决方案1】:

当更新 Swift 以启用嵌套属性包装器时,执行此操作的方法可能是创建一个 @UserDefault 属性包装器并将其与 @Published 组合。

同时,我认为处理这种情况的最佳方法是手动实现ObservableObject,而不是依赖@Published。像这样的:

class ViewModel: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    var name: String {
        get {
            UserDefaults.standard.string(forKey: "name") ?? ""
        }
        set {
            objectWillChange.send()
            UserDefaults.standard.set(newValue, forKey: "name")
        }
    }
}

属性包装器

正如我在 cmets 中提到的,我认为没有办法将其包装在一个删除 all 样板的属性包装器中,但这是我能想到的最好的方法:

@propertyWrapper
struct PublishedUserDefault<T> {
    private let key: String
    private let defaultValue: T

    var objectWillChange: ObservableObjectPublisher?

    init(wrappedValue value: T, key: String) {
        self.key = key
        self.defaultValue = value
    }

    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            objectWillChange?.send()
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

class ViewModel: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    @PublishedUserDefault(key: "name")
    var name: String = "John"

    init() {
        _name.objectWillChange = objectWillChange
    }
}

您仍然需要声明 objectWillChange 并以某种方式将其连接到您的属性包装器(我在 init 中这样做),但至少属性定义本身非常简单。

【讨论】:

  • 是的,我这工作得很好,但我明确地想通过使用 PorpertyWrapper 来摆脱样板文件..
  • 不幸的是,据我所知,自动触发 objectWillChange 的行为被硬编码为仅与 @Published 一起使用,因此您可以创建一个自定义属性包装器,该包装器可以保存到用户默认值并且也是发布者正如您的回答一样,但如果没有某种额外的样板文件,您将不会让它自动更新。
  • 为了实现这一目标,您会对我的回答做出哪些改变?
  • 啊,我明白了。我曾尝试在属性 init 上将 ref 传递给 objectWillChange,但由于它尚未初始化,但不幸的是它不起作用..
【解决方案2】:

对于现有的@Published 属性

这是一种方法,您可以创建一个惰性属性,该属性返回从您的@Published 发布者派生的发布者:

import Combine

class AppState: ObservableObject {
  @Published var count: Int = 0
  lazy var countTimesTwo: AnyPublisher<Int, Never> = {
    $count.map { $0 * 2 }.eraseToAnyPublisher()
  }()
}

let appState = AppState()
appState.count += 1
appState.$count.sink { print($0) }
appState.countTimesTwo.sink { print($0) }
// => 1
// => 2
appState.count += 1
// => 2
// => 4

但是,这是人为的,可能没有什么实际用途。请参阅下一节了解更有用的内容...


对于任何支持 KVO 的对象

UserDefaults 支持 KVO。我们可以创建一个名为KeyPathObserver 的通用解决方案,它可以通过单个@ObjectObserver 对支持KVO 的对象的更改做出反应。以下示例将在 Playground 中运行:

import Foundation
import UIKit
import PlaygroundSupport
import SwiftUI
import Combine

let defaults = UserDefaults.standard

extension UserDefaults {
  @objc var myCount: Int {
    return integer(forKey: "myCount")
  }

  var myCountSquared: Int {
    return myCount * myCount
  }
}

class KeyPathObserver<T: NSObject, V>: ObservableObject {
  @Published var value: V
  private var cancel = Set<AnyCancellable>()

  init(_ keyPath: KeyPath<T, V>, on object: T) {
    value = object[keyPath: keyPath]
    object.publisher(for: keyPath)
      .assign(to: \.value, on: self)
      .store(in: &cancel)
  }
}

struct ContentView: View {
  @ObservedObject var defaultsObserver = KeyPathObserver(\.myCount, on: defaults)

  var body: some View {
    VStack {
      Text("myCount: \(defaults.myCount)")
      Text("myCountSquared: \(defaults.myCountSquared)")
      Button(action: {
        defaults.set(defaults.myCount + 1, forKey: "myCount")
      }) {
        Text("Increment")
      }
    }
  }
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController

请注意,我们在UserDefaults 扩展中添加了一个附加属性myCountSquared 以计算派生值,但请注意原始KeyPath

【讨论】:

  • 我不太明白如何在 PropertyWrapper 中使用它。将 WrappedValue 设为 AnyPublisher? 类型
  • @NicolasDegen 我在我的答案中专门添加了关于UserDefaults,这更有用吗?
  • 看起来很有趣!我不得不说我更喜欢我提出的解决方案,即使它还不能完美运行。我仍然希望我可以让它作为一个简单的 PropertyWrapper 工作:)
【解决方案3】:

更新: 有了 EnclosureSelf 下标,就可以做到!

像魅力一样工作!

import Combine
import Foundation

class LocalSettings: ObservableObject {
  static var shared = LocalSettings()

  @Setting(key: "TabSelection")
  var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
  private let key: String
  private let defaultValue: T

  init(wrappedValue value: T, key: String) {
    self.key = key
    self.defaultValue = value
  }

  var wrappedValue: T {
    get {
      UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }

  public static subscript<EnclosingSelf: ObservableObject>(
    _enclosingInstance object: EnclosingSelf,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
    storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
  ) -> T {
    get {
      return object[keyPath: storageKeyPath].wrappedValue
    }
    set {
      (object.objectWillChange as? ObservableObjectPublisher)?.send()
      UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
    }
  }
}

【讨论】:

  • 是的,这确实有效,但如果封闭实例不是 ObservableObject,编译器会出现段错误。错误“静态下标 'subscript(_enclosureInstance:wrapped:storage:)' 要求 'SomeType' 符合 'ObservableObject') - 使用 Xcode 11.5 测试
  • 是的,我也注意到了。不知道如何解决这个问题。 OpenCombine 做了一些我还没有想到的@available 技巧..
【解决方案4】:

现在我们有@AppStorage

应用存储

一种属性包装器类型,它反映来自 UserDefaults 的值,并使对该用户默认值中的值更改的视图无效。

https://developer.apple.com/documentation/swiftui/appstorage

【讨论】:

  • 当然可以,但最好将它放在ObservableObject
  • @NicolasDegen 这没有意义,ObservableObject 只能在类上使用,所以如果你已经有一个类,那么你不需要属性包装器。存在这些 View 结构属性包装器的原因是使结构表现得像一个类的实例。每次创建 View 结构时,属性包装的属性都具有与上次创建结构时相同的值。无论如何,作为一个类的实例,它始终保持其属性值。
  • 当然,不需要。但就像@Published 一样,它也很方便:)
  • @Published 在设置使用它的模型对象时将 objectWillChange 发布者发送到视图,当视图已经可以使用 @AppStorage 来执行此操作时,为什么要为用户默认执行此操作本身。如果必须,那么您可以将 objectWillChange 发布者重新定义为 UserDefaults KVO 发布者,然后您甚至不需要任何类型的 @Published 属性,它将是全部自动的。
  • 只是为了可读性,将所有属性收集在一个 ObservableObject 中。我也试图为一些自定义包装器实现它,问题类似..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-31
  • 2015-10-05
  • 2020-01-31
  • 1970-01-01
  • 1970-01-01
  • 2018-07-29
  • 2020-12-05
相关资源
最近更新 更多