为计算属性添加@Published 行为

Nic*_*gen 5 swift swiftui combine property-wrapper

我正在尝试制作一个ObservableObject具有包装UserDefaults变量的属性。

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

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

jjo*_*son 8

当 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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

属性包装器

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

@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
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Gil*_*man 7

对于现有的@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
Run Code Online (Sandbox Code Playgroud)

然而,这是人为的,可能几乎没有实际用途。有关更有用的内容,请参阅下一节...


对于任何支持 KVO 的对象

UserDefaults支持 KVO。我们可以创建一个通用的解决方案,称为KeyPathObserver对支持 KVO 的对象的更改做出反应@ObjectObserver。以下示例将在 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
Run Code Online (Sandbox Code Playgroud)

请注意,我们在扩展中添加了一个额外的属性myCountSquaredUserDefaults计算派生值,但请注意原始KeyPath.

截屏


Nic*_*gen 5

更新:使用 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)
    }
  }
}

Run Code Online (Sandbox Code Playgroud)

  • 是的,这确实有效,但是如果封闭的实例不是 ObservableObject,编译器就会出现段错误。错误“静态下标 'subscript(_enendingInstance:wrapped:storage:)' 要求 'SomeType' 符合 'ObservableObject') - 使用 Xcode 11.5 进行测试 (2认同)