With due reference:
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#user-defaults
We've started to use property wrappers for the UserDefaults, it works seamlessly with non-optional properties.
However, setting nil of an optional property crashes with:
[User Defaults] Attempt to set a non-property-list object as an
NSUserDefaults/CFPreferences value for key "someKeyThatWeSet"
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'Attempt to insert non-property
list object null for key "someKeyThatWeSet"'
The code below can be tested on Playground directly:
#propertyWrapper
struct C2AppProperty<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct C2User {
#C2AppProperty("userID", defaultValue: nil)
public static var publicUserID: String?
}
print(C2User.publicUserID)
C2User.publicUserID = "edusta"
print(C2User.publicUserID)
C2User.publicUserID = nil
print(C2User.publicUserID)
Expected:
nil
Optional<"edusta">
nil
Found:
nil
Optional<"edusta">
libc++abi.dylib: terminating with uncaught exception of type
NSException
What I've tried so far:
set {
// Comparing non-optional value of type 'T' to nil always returns false.
if newValue == nil {
UserDefaults.standard.removeObject(forKey: combinedKey)
} else {
UserDefaults.standard.set(newValue, forKey: combinedKey)
}
}
What kind of a check is needed here to catch that newValue is nil? Or an Optional<nil>?
This code works for me:
#propertyWrapper
struct UserDefault<T> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(_ key: String, defaultValue: T, userDefaults: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
}
var wrappedValue: T {
get {
guard let value = userDefaults.object(forKey: key) else {
return defaultValue
}
return value as? T ?? defaultValue
}
set {
if let value = newValue as? OptionalProtocol, value.isNil() {
userDefaults.removeObject(forKey: key)
} else {
userDefaults.set(newValue, forKey: key)
}
}
}
}
fileprivate protocol OptionalProtocol {
func isNil() -> Bool
}
extension Optional : OptionalProtocol {
func isNil() -> Bool {
return self == nil
}
}
Thanks to Michcio for providing the solution,
If we read property very frequently, the following code may have better performance. Use storage property can save value in memory, it won't be read value from disk every time.
#propertyWrapper
struct ReadOptimizeStorage<T> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(key: String, defaultValue: T, userDefaults: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
wrappedValue = userDefaults.object(forKey: key) as? T ?? defaultValue
}
var wrappedValue: T {
didSet {
if let value = wrappedValue as? OptionalProtocol, value.isNil() {
userDefaults.removeObject(forKey: key)
} else {
userDefaults.set(wrappedValue, forKey: key)
}
}
}
}
fileprivate protocol OptionalProtocol {
func isNil() -> Bool
}
extension Optional : OptionalProtocol {
func isNil() -> Bool {
return self == nil
}
}
I made a tiny spm package inspired by the ripples answers above:
https://github.com/mezhevikin/userdefault.git
import UserDefault
extension UserDefaults {
// Optional property
#UserDefault("login", .standard)
static var login: String?
// Property with default value
#UserDefault("isDarkMode", false)
static var isDarkMode: Bool
}
UserDefaults.login = "mezhevikin"
UserDefaults.isDarkMode = true
Related
Let's say I have a very common use case for a property wrapper using UserDefaults.
#propertyWrapper
struct DefaultsStorage<Value> {
private let key: String
private let storage: UserDefaults
var wrappedValue: Value? {
get {
guard let value = storage.value(forKey: key) as? Value else {
return nil
}
return value
}
nonmutating set {
storage.setValue(newValue, forKey: key)
}
}
init(key: String, storage: UserDefaults = .standard) {
self.key = key
self.storage = storage
}
}
I am now declaring an object that would hold all my values stored in UserDefaults.
struct UserDefaultsStorage {
#DefaultsStorage(key: "userName")
var userName: String?
}
Now when I want to use it somewhere, let's say in a view model, I would have something like this.
final class ViewModel {
func getUserName() -> String? {
UserDefaultsStorage().userName
}
}
Few questions arise here.
It seems that I am obliged to use .standard user defaults in this case. How to test that view model using other/mocked instance of UserDefaults?
How to test that property wrapper using other/mocked instance of UserDefaults? Do I have to create a new type that is a clean copy of the above's DefaultsStorage, pass mocked UserDefaults and test that object?
struct TestUserDefaultsStorage {
#DefaultsStorage(key: "userName", storage: UserDefaults(suiteName: #file)!)
var userName: String?
}
As #mat already mentioned in the comments, you need a protocol to mock UserDefaults dependency. Something like this will do:
protocol UserDefaultsStorage {
func value(forKey key: String) -> Any?
func setValue(_ value: Any?, forKey key: String)
}
extension UserDefaults: UserDefaultsStorage {}
Then you can change your DefaultsStorage propertyWrapper to use a UserDefaultsStorage reference instead of UserDefaults:
#propertyWrapper
struct DefaultsStorage<Value> {
private let key: String
private let storage: UserDefaultsStorage
var wrappedValue: Value? {
get {
return storage.value(forKey: key) as? Value
}
nonmutating set {
storage.setValue(newValue, forKey: key)
}
}
init(key: String, storage: UserDefaultsStorage = UserDefaults.standard) {
self.key = key
self.storage = storage
}
}
After that a mock UserDefaultsStorage might look like this:
class UserDefaultsStorageMock: UserDefaultsStorage {
var values: [String: Any]
init(values: [String: Any] = [:]) {
self.values = values
}
func value(forKey key: String) -> Any? {
return values[key]
}
func setValue(_ value: Any?, forKey key: String) {
values[key] = value
}
}
And to test DefaultsStorage, pass an instance of UserDefaultsStorageMock as its storage parameter:
import XCTest
class DefaultsStorageTests: XCTestCase {
class TestUserDefaultsStorage {
#DefaultsStorage(
key: "userName",
storage: UserDefaultsStorageMock(values: ["userName": "TestUsername"])
)
var userName: String?
}
func test_userName() {
let testUserDefaultsStorage = TestUserDefaultsStorage()
XCTAssertEqual(testUserDefaultsStorage.userName, "TestUsername")
}
}
This might not be the best solution, however, I haven't figured out a way to inject UserDefaults that use property wrappers into a ViewModel. If there is such an option, then gcharita's proposal to use another protocol would be a good one to implement.
I used the same UserDefaults in the test class as in the ViewModel. I save the original values before each test and restore them after each test.
class ViewModelTests: XCTestCase {
private lazy var userDefaults = newUserDefaults()
private var preTestsInitialValues: PreTestsInitialValues!
override func setUpWithError() throws {
savePreTestUserDefaults()
}
override func tearDownWithError() throws {
restoreUserDefaults()
}
private func newUserDefaults() -> UserDefaults.Type {
return UserDefaults.self
}
private func savePreTestUserDefaults() {
preTestsInitialValues = PreTestsInitialValues(userName: userDefaults.userName)
}
private func restoreUserDefaults() {
userDefaults.userName = preTestsInitialValues.userName
}
func testUsername() throws {
//"inject" User Defaults with the desired values
let username = "No one"
userDefaults.userName = username
let viewModel = ViewModel()
let usernameFromViewModel = viewModel.getUserName()
XCTAssertEqual(username, usernameFromViewModel)
}
}
struct PreTestsInitialValues {
let userName: String?
}
I'm creating a property wrapper for UserDefaults.
What i'm trying to achieve is:
Setting a non-nil value to property will store it in User default.
Setting nil will remove the Object from UserDefault.
But below code throws compiler error:
Initializer for conditional binding must have Optional type, not 'T'
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get { UserDefaults.standard.value(forKey: key) as? T ?? defaultValue }
set {
if let newValue = newValue {
UserDefaults.standard.setValue(newValue, forKey: key)
} else {
UserDefaults.standard.removeObject(forKey: key)
}
}
}
}
// Declaration
#UserDefault("key", defaultValue: nil)
static var newUserDefaultValue: String
Is there any way to identify T is optional? as I can remove the key from UserDefaults. If not how to achieve the desired output?
You have 2 problems in your code:
1) In your case, wrappedValue and defaultValue should be an optional:
struct UserDefault<T> {
let key: String
let defaultValue: T? // defaultValue should be optional
// defaultValue should be optional
init(_ key: String, defaultValue: T?) {
self.key = key
self.defaultValue = defaultValue
}
// wrappedValue should also be optional
var wrappedValue: T? {
get { UserDefaults.standard.value(forKey: key) as? T ?? defaultValue }
set {
if let newValue = newValue {
UserDefaults.standard.setValue(newValue, forKey: key)
} else {
UserDefaults.standard.removeObject(forKey: key)
}
}
}
}
2) If you initialize with nil defaultValue, you should specify type T for compiler:
// Type T should be expicitly specified. For example as String
let valueWithNilDefault = UserDefault<String>("key", defaultValue: nil)
// Type T will be determined during compile time as Int
let valueWithDefault = UserDefault("key", defaultValue: 15)
I want to set or retrieve value from UserDefaults that is an optional Int, meaning it is either nil or set to a value. I am confused in the setter however. In the code below, I set it to either nil or an integer. I am not sure if this is correct. Is there a better or cleaner way (assuming if at all what I am doing is correct)?
public var selectedOption:myOption? {
get {
if let opt = UserDefaults.standard.object(forKey: "myKey") as? Int {
if let val = myOption(rawValue: opt) {
return val
}
}
return nil
}
set {
if let newVal = newValue {
UserDefaults.standard.set(newVal.rawValue, forKey:"myKey")
} else {
UserDefaults.standard.set(nil, forKey:"myKey")
}
}
}
You can reduce the setter
set {
UserDefaults.standard.set(newValue?.rawValue, forKey:"myKey")
}
Due to optional chaining nil is written out if newValue is nil.
public var selectedOption:myOption? {
get {
if let opt = UserDefaults.standard.object(forKey: "myKey") as? Int {
return myOption(rawValue: opt)
}
return nil
}
set {
UserDefaults.standard.set(newValue?.rawValue, forKey: "myKey")
}
}
Or
Confirm to codable protocol and store the enum instance in UserDefaults
enum myOption: Int, Codable {
case a
case b
}
public var selectedOption1:myOption? {
get {
if let storedObject: Data = UserDefaults.standard.object(forKey: "myKey") as? Data {
return try? PropertyListDecoder().decode(myOption.self, from: storedObject)
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "myKey")
}
}
I am trying to fetch a data from UserDefault but when I am doing this I am getting error
var sharedPreference: UserDefaults = UserDefaults.init(suiteName: "user-key-value")!
func getLastLoginClientId() -> Int64? {
for (key, value) in sharedPreference.dictionaryRepresentation() {
if key == LAST_USER {
return value as! Int64
}
}
return nil
}
I am getting that my key is having some value but when returning it, it throws error.
This is how I save
func setLastLoginClientId(clientId: Int64) {
sharedPreference.set(clientId, forKey: LAST_USER)
sharedPreference.synchronize()
}
I think you could do something as simple as
func getLastLoginClientId() -> Int64? {
return sharedPreference.value(forKey: LAST_USER) as? Int64
}
Here is what I've tested
struct CustomUserDefaults {
var sharedPreference : UserDefaults = UserDefaults.init(suiteName: "user-key-value")!
let LAST_USER = "test"
func test() {
let value = Int64(20.0)
self.setLastLoginClientId(value)
let testValue = getLastLoginClientId()
print(testValue) // 20.0
}
func setLastLoginClientId(_ value: Int64) {
sharedPreference.set(value, forKey: LAST_USER)
}
func getLastLoginClientId() -> Int64? {
return sharedPreference.value(forKey: LAST_USER) as? Int64
}
}
When I try to save to NSUserDefaults by adding a setter to my class variable (in this case the id and authToken variables, the values don't seem to be saved. When I run with a breakpoint on, the getter of id and authToken always return nil even after setting them with a value.
class CurrentUser {
static let defaultInstance = CurrentUser()
func updateUser(id id: String, authToken: String) {
self.id = id
self.authToken = authToken
}
var authToken: String? {
get {
if let authToken = NSUserDefaults.standardUserDefaults().objectForKey("userAuthToken") {
return (authToken as! String)
} else {
return nil
}
}
set {
NSUserDefaults.standardUserDefaults().setObject(authToken, forKey: "userAuthToken")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
var id: String? {
get {
if let id = NSUserDefaults.standardUserDefaults().objectForKey("userId") {
return (id as! String)
} else {
return nil
}
}
set {
NSUserDefaults.standardUserDefaults().setObject(id, forKey: "userId")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
}
However, when I pull the lines out to the updateUser function (one level higher), all works as expected.
class CurrentUser {
static let defaultInstance = CurrentUser()
func updateUser(id id: String, authToken: String) {
NSUserDefaults.standardUserDefaults().setObject(id, forKey: "userId")
NSUserDefaults.standardUserDefaults().synchronize()
NSUserDefaults.standardUserDefaults().setObject(authToken, forKey: "userAuthToken")
NSUserDefaults.standardUserDefaults().synchronize()
}
var authToken: String? {
get {
if let authToken = NSUserDefaults.standardUserDefaults().objectForKey("userAuthToken") {
return (authToken as! String)
} else {
return nil
}
}
}
var id: String? {
get {
if let id = NSUserDefaults.standardUserDefaults().objectForKey("userId") {
return (id as! String)
} else {
return nil
}
}
}
}
Why would this be? What am I missing? Does the { set } run on a different thread / mode where NSUserDefaults isn't accessible?
You must use newValue inside set method, value is still nil, or use didSet and then you can use variable.
As Alex said, you must use newValue in set method. Moreover, you can refer to this link for more detail:
Store [String] in NSUserDefaults (Swift)