为计算 属性 添加 @Published 行为
Add @Published behaviour for computed property
我正在尝试制作一个 ObservableObject
,它具有包装 UserDefaults
变量的属性。
为了符合ObservableObject
,我需要用@Published
包裹属性。不幸的是,我无法将其应用于计算属性,因为我将其用于 UserDefaults
值。
我怎样才能让它发挥作用?我需要做什么才能实现 @Published
行为?
对于现有的@Published 属性
这是一种方法,您可以创建一个惰性 属性,returns 从您的 @Published
发布者派生的发布者:
import Combine
class AppState: ObservableObject {
@Published var count: Int = 0
lazy var countTimesTwo: AnyPublisher<Int, Never> = {
$count.map { [=10=] * 2 }.eraseToAnyPublisher()
}()
}
let appState = AppState()
appState.count += 1
appState.$count.sink { print([=10=]) }
appState.countTimesTwo.sink { print([=10=]) }
// => 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
扩展中添加了 additional 属性 myCountSquared
来计算派生值,但请注意原文KeyPath
.
当更新 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")
}
}
}
属性包装器
正如我在评论中提到的,我认为没有办法将其包装在 属性 包装器中,从而删除 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
中这样做),但至少 属性定义本身很简单。
更新:
有了EnclosingSelf下标,就可以了!
很有魅力!
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)
}
}
}
现在我们有 @AppStorage
个:
应用存储
一种 属性 包装器类型,它反映了 UserDefaults 中的值并使该用户默认值的更改视图无效。
https://developer.apple.com/documentation/swiftui/appstorage
我正在尝试制作一个 ObservableObject
,它具有包装 UserDefaults
变量的属性。
为了符合ObservableObject
,我需要用@Published
包裹属性。不幸的是,我无法将其应用于计算属性,因为我将其用于 UserDefaults
值。
我怎样才能让它发挥作用?我需要做什么才能实现 @Published
行为?
对于现有的@Published 属性
这是一种方法,您可以创建一个惰性 属性,returns 从您的 @Published
发布者派生的发布者:
import Combine
class AppState: ObservableObject {
@Published var count: Int = 0
lazy var countTimesTwo: AnyPublisher<Int, Never> = {
$count.map { [=10=] * 2 }.eraseToAnyPublisher()
}()
}
let appState = AppState()
appState.count += 1
appState.$count.sink { print([=10=]) }
appState.countTimesTwo.sink { print([=10=]) }
// => 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
扩展中添加了 additional 属性 myCountSquared
来计算派生值,但请注意原文KeyPath
.
当更新 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")
}
}
}
属性包装器
正如我在评论中提到的,我认为没有办法将其包装在 属性 包装器中,从而删除 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
中这样做),但至少 属性定义本身很简单。
更新: 有了EnclosingSelf下标,就可以了!
很有魅力!
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)
}
}
}
现在我们有 @AppStorage
个:
应用存储
一种 属性 包装器类型,它反映了 UserDefaults 中的值并使该用户默认值的更改视图无效。
https://developer.apple.com/documentation/swiftui/appstorage