5

I want a variable to be an EnvironmentObject and I also want it to be persisted, so that it's the same every time that I relaunch my app.

To achieve that, I have already created the following propertyWrapper:

import Foundation

@propertyWrapper
struct UserDefault<T: Codable> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            if let encodedValue = UserDefaults.standard.object(forKey: key) as? Data {
                let decoder = JSONDecoder()
                let decodedValue = try! decoder.decode(T.self, from: encodedValue)
                return decodedValue
            } else {
                return defaultValue
            }
        } set {
            let encoder = JSONEncoder()
            let encodedValue = try! encoder.encode(newValue)
            UserDefaults.standard.set(encodedValue, forKey: key)
        }
    }
}

But already having a property wrapper means that I can't use the @Published property wrapper from Combine. (Using two property wrappers on one variable doesn't sound like a good idea, and I haven't found a way to get that working.)

I solved that problem by making a custom objectWillChange let constant and calling its .send(input:) method in willSet for every variable.

So this is my DataStore class:

import SwiftUI
import Combine

final class DataStore: ObservableObject {
    let objectWillChange = PassthroughSubject<DataStore, Never>()

    @UserDefault(key: "the text", defaultValue: "Hello world!")
    var text: String {
        willSet {
            objectWillChange.send(self)
        }
    }
}

And this is my View:

struct StartView : View {
    @EnvironmentObject var dataStore: DataStore
    var body: some View {
        VStack {
            TextField("Enter text", text: $dataStore.text)
            Button("Reset text", action: {
                self.dataStore.text = "Hello World!"
            })
        }
    }
}

But somehow I really believe that there should be a more beautiful way than making a custom objectWillChange. Is there a way to make a single property wrapper that covers both the persisting and the "publishing"? Or should I do something completely different, to reach my goal?

Thanks!

2 Answers 2

3

Based on the guessed implementation of Genetec Tech (https://medium.com/genetec-tech/property-wrappers-in-swift-5-1-the-missing-published-implementation-1a466ebcf660) you could combine the two property wrappers into one @PublishedUserDefault.

Example:

This is the normal propertyWrapper for the UserDefaults

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

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

This viewModel also updates, when UserDefaults.set(_ ,forKey:) is called.

final class UserSettings: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    @UserDefault(key: "text", defaultValue: "")
    var text: String

    private var notificationSubscription: AnyCancellable?

    init() {
        notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
            self.objectWillChange.send()
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0
private var cancellables = [String:AnyCancellable]()

extension Published {
    init(wrappedValue defaultValue: Value, key: String) {
        let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
        self.init(initialValue: value)
        cancellables[key] = projectedValue.sink { val in
            UserDefaults.standard.set(val, forKey: key)
        }
    }
}

final class DataStore: ObservableObject {
    @Published(key: "theText")
    var text = "Hello world!"
}

Sample: https://youtu.be/TXdAg_YvBNE

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.