AndyIbanez / andyibanez-com

Static website.
1 stars 0 forks source link

posts/nsuserdefaults-property-wrappers/ #7

Open utterances-bot opened 4 years ago

utterances-bot commented 4 years ago

UserDefaults and Property Wrappers • Andy Ibanez

https://www.andyibanez.com/posts/nsuserdefaults-property-wrappers/

briancordanyoung commented 4 years ago

Apart from the thoughtful post that links to you – I have another question and maybe tradeoff. I ask in the spirit of understanding property wrappers (not picking apart a great post):

I see you never show an example of using a different UserDefaults instance, only showing the default standard:

@UserDefault(key: .lockOnExit, defaultValue: true) var maxAttempts

Not:

@UserDefault(userDefaults: UserDefaults(suiteName: "group.com.myname.apps"), key: .lockOnExit, defaultValue: true) var maxAttempts

That isn't all that useful. What I want to do is pass in a UserDefaults instance:

class ImagesViewController: UIViewController {
    var sharedDefaults: UserDefaults
    init(sharedDefaults: UserDefaults) {
        self.sharedDefaults = sharedDefaults
    }
    @UserDefault(userDefaults: sharedDefaults, key: .lockOnExit, defaultValue: true) var maxAttempts
}

But, this isn't possible. Understandably, Swift throws the error:

Cannot use instance member 'sharedDefaults' within property initializer; property initializers run before 'self' is available

The only way to inject something to assign a different UserDefaults instance would seem to be through static properties (essentially a singleton). I suppose a solution would be to pass in the app group string instead of a UserDefaults instance, then instantiate it anew. But that's not the same thing and not something that can be injected in to the class that hosts the wrapped properties.

I do know that property wrapper types can be used as instances themselves, outside of being a wrapper:

    private let _maxAttempts: UserDefaults
    init(sharedDefaults: UserDefaults) {
         _maxAttempts  = UserDefault(userDefaults: sharedDefaults, key: .lockOnExit, defaultValue: true)
    }

    var maxAttempts: Bool {
        get { return _maxAttempts.wrappedValue }
        set { _maxAttempts.wrappedValue = newValue }
    }

But, that's not really the ergonomics I think we all get excited about with property wrappers.

Am I missing something?

AndyIbanez commented 4 years ago

The reason I used the second variation was because, when I was toying with the idea to see how viable it would be to use it in a real app, I wanted to be able to inject as much content as possible. You are right that the user defaults would have to come from someplace else. Probably as part of an environment, or some sort of "manager"? Honestly it's not an idea I have explored too deeply after publishing this. In a real app I never ended up using this directly. What I did was to create an Observable Settings object that can be instantiated with an implementation of a protocol and I think it overall worked much better than this.

That said if you do play more with this and find an interesting implementation for the problem, I'd be interested to hear about it.

eito commented 3 years ago

@briancordanyoung did you ever find a better solution for the case you had? I'm running into the same situation right now and I really don't want to have to rely on a singleton for the particular instance of UserDefaults because in the requirements for our app the user can log out and in as someone else and the instance of UserDefaults would need to change

briancordanyoung commented 3 years ago

did you ever find a better solution for the case you had?

@eito Sadly. no. I just had another use for a property wrapper last night – which I abandoned for this same reason.

eito commented 3 years ago

In my case (I've changed some names since I can't share the source publicly), I only have one object declaring variables with these property wrappers directly so I made the init private and utilized a static. It's a bit ugly but it's private to the implementation. All the callers need to know about is Settings.makeWithDefaults(specificUserDefaults). Since that is the only way to create an instance of Settings though, it's not a big deal.


extension UserDefaults {
  static var currentUserDefaults: UserDefaults = .standard
} 

class Settings: ObservableObject {

  static func makeWithDefaults(_ defaults: UserDefaults) -> Setting {
    UserDefaults.currentUserDefaults = defaults
    return Settings()
  } 

 @UserDefaultsBackedSetting(key: .isThingEnabled, defaultValue: true)
 public var isThingEnabled: Bool {
   willSet {
     objectWillChange.send()
   }
 }
}

@propertyWrapper
public class UserDefaultsBackedSetting<T: Codable> {

    public enum Key: String {
        case isThingEnabled
    }

    // MARK: - Properties

    private let defaults = UserDefaults.currentUserDefaults
    private let key: Key
    private let defaultValue: T

    @Published var value: T

    public var projectedValue: AnyPublisher<T, Never> {
        return $value
            .eraseToAnyPublisher()
    }

    public var wrappedValue: T {

        get {
            value
        }
        set {

            value = newValue

            guard let data = try? JSONEncoder().encode(newValue) else {
                return
            }

            defaults.set(data, forKey: key.rawValue)
        }
    }

    // MARK: - Initializers

    public init(key: Key, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue

        if let data = defaults.object(forKey: key.rawValue) as? Data {
            do {
                let value = try JSONDecoder().decode(T.self, from: data)
                self.value = value
            } catch {
                self.value = defaultValue
            }
        } else {
            self.value = defaultValue
        }
    }
}