Categories
iOS Swift UIKit

Storing struct in UserDefaults

Structs can’t be directly stored in UserDefaults because UserDefaults does not know how to serialize it. As UserDefaults is backed by plist files, struct needs to be converted representation supported by it. The core idea boils down to the question, how to convert struct into Dictionary.

Converting struct to Dictionary

The most straight-forward way would be to manually create dictionary by adding all the properties one by one. Depending on the struct, it might get pretty long and also requires maintenance when changing the struct. Therefore, let’s take a look on a way of converting struct into JSON representation instead. This can be achieved by conforming to Encodable and using JSONEncoder and JSONSerialization. In the same way, we can convert Dictionary back to struct with JSONSerialization, JSONDecoder and conforming to Decodable. When conforming struct to Encodable and Decodable, Swift compiler will take care of generating default implementations for methods in those protocols. JSONEncoder and JSONDecoder use those methods for converting struct to Data and back. It should be noted that we could just store data in user defaults. But as data is not human-readable, let’s convert it to Dictionary instead.

struct Context: Codable {
let duration: TimeInterval
let view: String
}
struct SearchInfo: Codable {
let query: String
let numberOfMatches: Int
let context: Context
}
let searchInfos = [SearchInfo(query: "query1", numberOfMatches: 1, context: Context(duration: 1.0, view: "view1")),
SearchInfo(query: "query2", numberOfMatches: 2, context: Context(duration: 2.0, view: "view2"))]
// Converting to dictionary
extension SearchInfo {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
// Converting back to struct
extension SearchInfo {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(SearchInfo.self, from: data) else { return nil }
self = info
}
}
let defaults = UserDefaults()
// [Struct] -> [Dictionary]
let searchInfoDictionaries = searchInfos.map({ $0.dictionaryRepresentation })
// [Dictionary] to UserDefaults
defaults.set(searchInfoDictionaries, forKey: "Searches")
// [Dictionary] from UserDefaults
let dictionariesFromUserDefaults = defaults.array(forKey: "Searches")! as! [[String: Any]]
// [Dictionary] -> [Struct]
let convertedSearchInfos = dictionariesFromUserDefaults.compactMap({ SearchInfo(dictionary: $0) })

Adding DictionaryConvertible and DictionaryDecodable

This implementation can be made a bit more usable by using protocols and protocol extensions for providing default implementations. This makes it extremely easy to adopt this to any objects.

protocol DictionaryConvertible {
var dictionaryRepresentation: [String: Any] { get }
}
protocol DictionaryDecodable {
init?(dictionary: [String: Any])
}
typealias DictionaryRepresentable = DictionaryConvertible & DictionaryDecodable
extension DictionaryConvertible where Self: Encodable {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
extension DictionaryDecodable where Self: Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = info
}
}
struct AutocompleteResult: Codable, DictionaryRepresentable {
let text: String
let suggestions: [String]
}

Summary

Using Swift’s Codable together with JSONEncoder, JSONDecoder and JSONSerialization we can skip writing code for converting data into different types and instead, providing a concise implementation for turning structs into dictionaries. We only talked about structs but this approach can be applied to classes as well.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Playground

StoringStructInUserDefaults (Xcode 10.2.1, Swift 5.0)

2 replies on “Storing struct in UserDefaults”

You really don’t need the dictionary conversion step. You can simply encode to a Data object i.e JSONEncoder().encode(self) and then you can store Data (NSData) inside UserDefaults, which is a lot more flexible as you don’t have to limit yourself to [String: Any] JSON representations. 😁👍👍

Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s