Skip to content

Instantly share code, notes, and snippets.

@levochkaa
Last active October 10, 2023 16:00
Show Gist options
  • Save levochkaa/58563c4df77b821e8c7fa4625cf94800 to your computer and use it in GitHub Desktop.
Save levochkaa/58563c4df77b821e8c7fa4625cf94800 to your computer and use it in GitHub Desktop.
@PublishedAppStorage
// PublishedAppStorage.swift
import Combine
import SwiftUI
@propertyWrapper
struct PublishedAppStorage<Value> {
@UserDefault private var storedValue: Value
private var publisher: Publisher?
private var objectWillChange: ObservableObjectPublisher?
// swiftlint:disable nesting
struct Publisher: Combine.Publisher {
typealias Output = Value
typealias Failure = Never
func receive<S: Subscriber>(subscriber: S) where S.Input == Value, S.Failure == Never {
subject.subscribe(subscriber)
}
fileprivate let subject: CurrentValueSubject<Value, Never>
fileprivate init(_ output: Output) {
self.subject = .init(output)
}
}
// swiftlint:enable nesting
var projectedValue: Publisher {
mutating get {
if let publisher {
return publisher
}
let publisher = Publisher(storedValue)
self.publisher = publisher
return publisher
}
}
@available(*, unavailable, message: "@Published is only available on properties of classes")
var wrappedValue: Value {
get { fatalError() }
set { fatalError() } // swiftlint:disable:this unused_setter_value
}
static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped _: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
) -> Value {
get { object[keyPath: storageKeyPath].storedValue }
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
object[keyPath: storageKeyPath].storedValue = newValue
}
}
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) {
self._storedValue = UserDefault(wrappedValue: wrappedValue, key, store: store)
}
}
// UserDefault.swift
import Foundation
@propertyWrapper
struct UserDefault<Value> {
let defaultValue: Value
let key: String
var store: UserDefaults
init(wrappedValue defaultValue: Value, _ key: String, store: UserDefaults = .standard) {
self.defaultValue = defaultValue
self.key = key
self.store = store
}
var wrappedValue: Value {
get { store.object(forKey: key) as? Value ?? defaultValue }
set { store.set(newValue, forKey: key) }
}
}
@levochkaa
Copy link
Author

levochkaa commented Oct 10, 2023

UserDefault is a property wrapper just to easily use UserDefaults with any objects.

class ExampleUserDefault {
    @UserDefault("someKey") var someValue = 0
    
    func updateValue(to newValue: Int) {
        someValue = newValue
        // UserDefaults.standard.integer(forKey: "someKey") == someValue
    }
}

PublishedAppStorage is a much more complex property wrapper, that uses UserDefault property wrapper to save properties as it does AppStorage in SwiftUI and updates corresponding views using objectWillChange of a parent ObservableObject

class ExamplePublishedAppStorageViewModel: ObservableObject {
    @PublishedAppStorage("someKey") var someValue = 0
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // Works the same as Published.Publisher
        $someValue
            .sink { someValue in
                // do something
            }
            .store(in: &cancellables)
    }
    
    func updateValue(to newValue: Int) {
        someValue = newValue
        // UserDefaults.standard.integer(forKey: "someKey") == someValue
    }
}
struct ExamplePublishedAppStorageView: View {
    @StateObject var viewModel = ExamplePublishedAppStorageViewModel()
    
    var body: some View {
        Text("\(viewModel.someValue)")
            .onTapGesture {
                viewModel.updateValue(to: viewModel.someValue + 1)
                // The view gets updated, because objectWillChange is triggered
            }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment