Small knowledge, big challenge! This paper is participating in theEssentials for programmers”Creative activities.

This paper also participates inProject DigginTo win the creative gift package and challenge the creative incentive money.

preface

In daily development, we always use UserDefault to save some lightweight data.

As for UserDefault, you may often use it, but you just don’t know what type it is. How do you store the data?

Today I would like to tell you in detail about my experience.

UserDefault Path for saving data

First let’s look at the path of UserDefault under the emulator:

Because in general, we do not see the sandbox folder of App in the real machine, so the path of the real machine, I print the address:

/var/mobile/Containers/Data/Application/961391DA-4E5B-44E8-A38A-DE7A5F5CBC15/Library/Preferences/com.lostsakura.www.RxSt udy.plistCopy the code

summary

  • Remember this conclusion: UserDefault saves the data in the App sandbox under the /Library/Preferences/ folder

  • This file is a PList file

  • The file name is the name of the Bundle identifier configuration setting, as you can see from my project configuration screenshot:

  • Path printing method we can refer to the following code block:

    guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
        return
    }
    
    let path = NSHomeDirectory() + "/Library" + "/Preferences" + "/\(bundleIdentifier)" + ".plist"
    print(path)
    Copy the code

So if it’s a.plist file, what can it store

Since it’s a.plist file, I think anyone with a bit of iOS development experience knows what it can save — save objects.

A detailed explanation can also be found in the UserDefault source code comment:

Key-Value Store: NSUserDefaults stores Property List objects (NSString, NSData, NSNumber, NSDate, NSArray, and NSDictionary) identified by NSString keys, similar to an NSMutableDictionary.

You may be wondering why data types that start with NS are rarely used in Swift anymore, but there are two reasons for this comment:

  • UserDefault is for Swift and NSUserDefault is for OC, which is just a class with a different name

  • When UserDefault is used in Swift, it will be automatically converted by the system internally, so that users do not have too much awareness of the conversion

Swift OC
String NSString
Data NSData
Int/Double/Float/Bool NSNumber
Date NSDate
Array NSArray
Dictionary NSDictionary

Just remember this one dot, UserDefault is actually holding data is an object, is an object, is an object.

API calls for UserDefault

Let’s first copy several apis of Swift and OC with the same function for comparison:

Swift OC
func object(forKey defaultName: String) -> Any? – (nullable id)objectForKey:(NSString *)defaultName;
func set(_ value: Any? , forKey defaultName: String) – (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
func integer(forKey defaultName: String) -> Int – (NSInteger)integerForKey:(NSString *)defaultName;
func set(_ value: Int, forKey defaultName: String) – (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
More of the same API can be viewed in the source code

We can see that in Swift and OC, the apis are basically the same and correspond one to one.

SetObject and objectForKey are the most basic methods in UserDefault, which use key-value pairs to save and read data. The other set and GET methods are a cast wrapper around this method.

Calls in Swift

Let’s try it out with Swift setObject and objectForKey, but notice the API I’m calling.

And then you’ll notice that the getAge that comes back will have a warning saying getAge is an Any, right? Type:

Let’s look at the print:

Optional(10)
Copy the code

But if we really want to determine the type of getAge, we have to unpack it a little bit.

guard let getAge = UserDefaults.standard.object(forKey: "age") as? Int else {
    return
}

print(getAge)

10
Copy the code

Func set(_ value: Int, forKey defaultName: String) and func INTEGER (forKey defaultName: String) -> Int API values.

Let’s change the API and try again:

let age = 10

UserDefaults.standard.set(age, forKey: "age")

let getAge = UserDefaults.standard.integer(forKey: "age")

print(getAge)
Copy the code

. In fact you will find that I am UserDefaults standard. Set (age, forKey: “age”) the API doesn’t do change, or I changed the API or function of the same name, changed still look not to come out:

. But I’ll UserDefaults. Standard value (forKey: “age”) changed to UserDefaults. The standard. The integer (forKey: “age”), the effect is different.

Integer (forKey:) this method is equivalent to explicitly stating that the value read is of type Int and is not optional.

Let’s try, instead of saving the age, to read the key “age” and see what is returned. Well, to verify this, I’m going to delete my App and run it again:

As I expected, the value is not saved by key-value, but is directly retrieved by key, since integer(forKey:) returns an Int and is not optional, so the default value is 0.

Imagine what would be returned if instead of using integer(forKey:), object(forKey:) was used? Looking at the API list above, I think you get the answer.

Calls in OC

I personally find OC to be a bit of a hassle, so let’s move on:

NSNumber *age = @10;

[NSUserDefaults.standardUserDefaults setObject:age forKey:@"age"];

NSNumber *getAge = [NSUserDefaults.standardUserDefaults objectForKey:@"age"];

NSLog(@"%@",getAge);

10
Copy the code

Because the OC API explicitly states that setObject (nullable id)value is an id class, it must be a class, so we can’t use NSInteger. Instead, we use NSNumber.

What you need to remember is that in OC NSInteger is not a class at all, it’s a typedef long NSInteger, whereas in Swift Int is a structure.

We can also change the API at OC to try it out:

NSInteger age = 10; / / / using setInteger, so the age can be defined as NSInteger [NSUserDefaults. StandardUserDefaults setInteger: age forKey: @ "age"]. NSInteger getAge = [NSUserDefaults.standardUserDefaults integerForKey:@"age"]; NSLog(@"%ld",getAge);Copy the code

Then everything will be all right.

summary

Use UserDefault in Swift and OC, and setObject and objectForKey should be used sparselybecause Any will be saved and read. Okay? | nullable id type, type is not clear, but also is nil, very unreliable.

It is recommended to specify the type of the data and use the set corresponding to XXX (forKey:) method. This will have explicit values and XXX (forKey:) will have default values even without set.

Also note that setObject and objectForKey are the foundation apis for all other concrete types of apis, as we can see from their source implementation:

open func set(_ value: Int, forKey defaultName: String) {

    set(NSNumber(value: value), forKey: defaultName)

}
Copy the code
open func integer(forKey defaultName: String) -> Int {
    guard let aVal = object(forKey: defaultName) else {
        return 0
    }

    if let bVal = aVal as? Int {
        return bVal
    }

    if let bVal = aVal as? String {
        return NSString(string: bVal).integerValue
    }

    return 0
}
Copy the code

Configure initial values for UserDefaults

We said that if we don’t write to age, the default value is 0.

let getAge = UserDefaults.standard.integer(forKey: "age")

print(getAge)

0
Copy the code

Sometimes, however, we want the default value of age to be not 0, but a value that we initialized. How do we do that?

This API gives you some possibilities:

registerDefaults: adds the registrationDictionary to the last item in every search list. This means that after NSUserDefaults has looked for a value in every other valid location, it will look in registered defaults, making them useful as a “fallback” value. Registered defaults are never stored between runs of an application, and are visible only to the application that registers them.

Default values from Defaults Configuration Files will automatically be registered.

open func register(defaults registrationDictionary: [String : Any])
Copy the code
let userDefaults = UserDefaults.standard
userDefaults.register(
    defaults: [
        "enabledSound": true,
        "enabledVibration": true
    ]
)

userDefaults.bool(forKey: "enabledSound") // true
Copy the code

Note that the value of register is effectively written, if and only if key is nil.

Let’s look at the following code:

let userDefaults = UserDefaults.standard

userDefaults.set(false, forKey: "enabledSound") // false

userDefaults.register(
    defaults: [
        "enabledSound": true, 
        "enabledVibration": true
    ]
)

userDefaults.bool(forKey: "enabledSound") // false
Copy the code

I set keyenabledSound to false with userdefaults. set, and then register keyenabledSound to true with userdefaults. register. It turns out that I still get false with userdefaults.bool.

The register method is not in effect.

If you are interested, check out this article.

UserDefault is used in conjunction with Codable to compile categories

Although the comments in UserDefault indicate that it can only hold basic data types: NSString, NSData, NSNumber, NSDate, NSArray, and NSDictionar, but sometimes I just want to save a Model data what do I do?

I want this UserDefault+Codable for you to use:

Extension UserDefaults {/// follow the Codable protocol set method /// /// -parameters: /// -object: // -key: // -encoder: // Serializer public func setCodableObject<T: Codable>(_ object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) { let data = try? Encoder. Encode (object) set(data, forKey: key)} /// follow Codable get method /// /// - Parameters: /// type: Public func getCodableObject<T: Codable>(_ type: r) public func getCodableObject<T: Codable>(_ type: r) T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? { guard let data = value(forKey: key) as? Data else { return nil } return try? decoder.decode(type.self, from: data) } }Copy the code

In fact, the idea of this classification is very simple:

Graph TD Model --> Data --> UserDefaults calls Data set and get

It’s also very simple to use:

struct Person {
    let name: String
    let age: Int
}

let person = Person(name: "season", age: 10)

UserDefaults.standard.setCodableObject(person, forKey: "person")

let getPerson = UserDefaults.standard.getCodableObject(Person.self, with: "person")
Copy the code

OC cannot use the Codable protocol, but there are alternatives to convert Model to NSData. Consider using the NSCoding protocol.

UserDefault reads and writes like a Dictionary

We all know that dict types can be read and written using dict[“key”] forms, such as the following:

var dict = ["age": 10]

print(dict["age"])

dict["age"] = 20

print(dict["age"])
Copy the code

UserDefault can also be read and written like a Dictionary by adding the subscript extension to UserDefault.

The classification code is as follows:

Extension UserDefaults {/// Any? public subscript(key: String) -> Any? {get {return object(forKey: key)} set {set(newValue, forKey: key)}} String) -> Int {get {return INTEGER (forKey: key)} set {set(newValue, forKey: key)}} // more specific types of extensions can be written by yourselfCopy the code

Note that subscript functions cannot have the same name, so when I save and read Int, the function becomessubscript(int key: String).

Subscript (key: String); subscript(int key: String); subscript(key: String)

UserDefaults.standard["name"] = "season"

print(UserDefaults.standard["name"])

UserDefaults.standard[int: "age"] = 10

print(UserDefaults.standard[int: "age"])

Optional(season)

10
Copy the code

If you want more types of reads and writes to support this writing, then you can write the corresponding type extension.

Try not to use hard coding

As you can see, when UserDefault reads or writes, we’re always dealing with keys, and keys are strings, hard-coded.

Consider using UserDefault for reading and writing data that is often used. I recommend that you normalize this key and create a separate file to normalize it. For example, if I want to save the user name in UserDefault, I’ll write it like this:

Let kUsername = "kUsername"Copy the code

This will do when called:

let name = "season"

UserDefaults.standard.set(name, forKey: kUsername)
Copy the code

I don’t have to write the Synchronize method

The common UserDefault method calls Synchronize after set, as in this example:

UserDefaults.standard.set(10, forKey: "age")

UserDefaults.standard.synchronize()
Copy the code

Especially old projects, or OC and moving from OC to Swift.

Synchronize () is used for synchronization security, but can now be left out, with the official source code commented as follows:

-synchronize is deprecated and will be marked with the API_DEPRECATED macro in a future release.

In the spirit that writing less code is a bit of code, let’s not write it.

Reference documentation

swift-corelibs-foundation/Sources/Foundation/UserDefaults.swift

Setting default values for NSUserDefaults

conclusion

This paper began to explore the data saving path of UserDefault, and found that the saved data type of UserDefault was plist file, and then knew the data type saved by UserDefault. By comparing the API calls of Swift and OC, Describes the precautions and methods for configuring default values.

In addition, UserDefault has been extended to achieve greater data read and write capability and lexicographical read and write invocation.

The Synchronize method also notes that it doesn’t have to be written specifically.

Finally, I would like to emphasize that UserDefault is suitable for storing light data. Please use it according to your discretion. If you read and write too large files in UserDefault, the performance and experience of the App will be affected.

UserDefault is a tool commonly used by iOS developers. I think I have tried my best to share it. Please feel free to like and leave comments if you like.

We’ll see you next time.