Добавить поведение @Published для вычисляемого свойства
Я пытаюсь сделать ObservableObject
который имеет свойства, которые обертывают UserDefaults
переменная.
Чтобы соответствовать ObservableObject
, Мне нужно обернуть свойства @Published
. К сожалению, я не могу применить это к вычисляемым свойствам, поскольку использую дляUserDefaults
ценности.
Как я мог заставить это работать? Что мне нужно сделать, чтобы достичь@Published
поведение?
3 ответа
Обновлено: с индексом 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)
}
}
}
Когда 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")
}
}
}
Обертка свойства
Как я уже упоминал в комментариях, я не думаю, что есть способ обернуть это в оболочку свойств, которая удаляет все шаблоны, но это лучшее, что я могу придумать:
@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
), но по крайней мере само определение свойства довольно простое.
Для существующего ресурса @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
Однако это надумано и, вероятно, имеет мало практического применения. В следующем разделе вы найдете более полезные сведения...
Для любого объекта, поддерживающего КВО
UserDefaults
поддерживает КВО. Мы можем создать обобщаемое решение под названиемKeyPathObserver
который реагирует на изменения объекта, поддерживающего KVO, с помощью одного @ObjectObserver
. Следующий пример будет запущен на игровой площадке:
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
обратите внимание, что мы добавили дополнительное свойствоmyCountSquared
к UserDefaults
расширение для вычисления производного значения, но соблюдайте исходное KeyPath
.
Теперь у нас есть
@AppStorage
за это:
Хранилище приложений
Тип оболочки свойства, который отражает значение из UserDefaults и делает недействительным представление об изменении значения по умолчанию для этого пользователя.
https://developer.apple.com/documentation/swiftui/appstorage