Open utterances-bot opened 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?
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.
@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
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.
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
}
}
}
UserDefaults and Property Wrappers • Andy Ibanez
https://www.andyibanez.com/posts/nsuserdefaults-property-wrappers/