Warning: Test the migration thoroughly in your app. It might cause unintended data loss if you're not careful.
We have improved the stored representation of types. Some types will require migration. Previously, all Codable types were serialized to a JSON string and stored as a UserDefaults string. Defaults is now able to store more types using the appropriate native UserDefaults type.
- The following types require no changes:
Int(8/16/32/64)UInt(8/16/32/64)DoubleCGFloatFloatStringBoolDateDataURL
- Custom types (
struct,enum, etc.) must now conform toDefaults.Serializable(in addition toCodable). Array,Set, andDictionarywill need to be manually migrated withDefaults.migrate().
In v4, Defaults stored many types as a JSON string.
In v5, Defaults stores many types as native UserDefaults types.
// v4
let key = Defaults.Key<[Int]>("key", default: [0, 1])
UserDefaults.standard.string(forKey: "key")
//=> "[0, 1]"// v5
let key = Defaults.Key<[Int]>("key", default: [0, 1])
UserDefaults.standard.dictionary(forKey: "key")
//=> [0, 1]-
The compiler complains that
Defaults.Key<Value>does not conform toDefaults.Serializable. Since we replacedCodablewithDefaults.Serializable,Key<Value>will have to conform toValue: Defaults.Serializable. For this situation, please follow the guides below: -
The previous value in
UserDefaultsis not readable. (for example,Defaults[.array]returnsnil). In v5,Defaultsreads value fromUserDefaultsas a natively supported type, but sinceUserDefaultsonly contains JSON string before migration forCodabletypes,Defaultswill not be able to work with it. For this situation,Defaultsprovides theDefaults.migrate()method to automate the migration process.
We recommend doing some manual testing after migrating.
For example, let's say you are trying to migrate an array of Codable string to a native array.
- Get the previous value in
UserDefaults(usingdefaultscommand or whatever you want).
let string = "[\"a\",\"b\",\"c\"]"- Insert the above value into
UserDefaults.
UserDefaults.standard.set(string, forKey: "testKey")- Call
Defaults.migrate()and then useDefaultsto get its value.
let key = Defaults.Key<[String]>("testKey", default: [])
Defaults.migrate(key, to: .v5)
Defaults[key] //=> [a, b, c]In v4, struct had to conform to Codable to store it as a JSON string.
In v5, struct has to conform to Codable and Defaults.Serializable to store it as a JSON string.
private struct TimeZone: Codable {
var id: String
var name: String
}
extension Defaults.Keys {
static let timezone = Defaults.Key<TimeZone?>("TimeZone")
}- Make
TimeZoneconform toDefaults.Serializable.
private struct TimeZone: Codable, Defaults.Serializable {
var id: String
var name: String
}- Now
Defaults[.timezone]should be readable.
In v4, enum had to conform to Codable to store it as a JSON string.
In v5, enum has to conform to Codable and Defaults.Serializable to store it as a JSON string.
private enum Period: String, Codable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension Defaults.Keys {
static let period = Defaults.Key<Period?>("period")
}- Make
Periodconform toDefaults.Serializable.
private enum Period: String, Defaults.Serializable, Codable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}- Now
Defaults[.period]should be readable.
From Codable Array/Dictionary/Set in Defaults v4 to native Array/Dictionary/Set (with natively supported elements) in Defaults v5
In v4, Defaults stored array/dictionary as a JSON string: "[\"a\", \"b\", \"c\"]".
In v5, Defaults stores it as a native array/dictionary with natively supported elements: ["a", "b", "c"].
extension Defaults.Keys {
static let arrayString = Defaults.Key<[String]?>("arrayString")
static let setString = Defaults.Key<Set<String>?>("setString")
static let dictionaryStringInt = Defaults.Key<[String: Int]?>("dictionaryStringInt")
static let dictionaryStringIntInArray = Defaults.Key<[[String: Int]]?>("dictionaryStringIntInArray")
}- Call
Defaults.migrate(.arrayString, to: .v5),Defaults.migrate(.setString, to: .v5),Defaults.migrate(.dictionaryStringInt, to: .v5),Defaults.migrate(.dictionaryStringIntInArray, to: .v5). - Now
Defaults[.arrayString],Defaults.[.setString],Defaults[.dictionaryStringInt],Defaults[.dictionaryStringIntInArray]should be readable.
From Codable Array/Dictionary/Set in Defaults v4 to native Array/Dictionary/Set (with Codable elements) in Defaults v5
In v4, Defaults would store array/dictionary as a single JSON string: "{\"id\": \"0\", \"name\": \"Asia/Taipei\"}", "[\"10 Minutes\", \"30 Minutes\"]".
In v5, Defaults will store it as a native array/dictionary with Codable elements: {id: 0, name: "Asia/Taipei"}, ["10 Minutes", "30 Minutes"].
private struct TimeZone: Hashable, Codable {
var id: String
var name: String
}
private enum Period: String, Hashable, Codable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension Defaults.Keys {
static let arrayTimezone = Defaults.Key<[TimeZone]?>("arrayTimezone")
static let setTimezone = Defaults.Key<[TimeZone]?>("setTimezone")
static let arrayPeriod = Defaults.Key<[Period]?>("arrayPeriod")
static let setPeriod = Defaults.Key<[Period]?>("setPeriod")
static let dictionaryTimezone = Defaults.Key<[String: TimeZone]?>("dictionaryTimezone")
static let dictionaryPeriod = Defaults.Key<[String: Period]?>("dictionaryPeriod")
}- Make
TimeZoneandPeriodconform toDefaults.Serializable.
private struct TimeZone: Hashable, Codable, Defaults.Serializable {
var id: String
var name: String
}
private enum Period: String, Hashable, Codable, Defaults.Serializable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}- Call
Defaults.migrate(.arrayTimezone, to: .v5),Defaults.migrate(.setTimezone, to: .v5),Defaults.migrate(.dictionaryTimezone, to: .v5),Defaults.migrate(.arrayPeriod, to: .v5),Defaults.migrate(.setPeriod, to: .v5),Defaults.migrate(.dictionaryPeriod, to: .v5). - Now
Defaults[.arrayTimezone],Defaults[.setTimezone],Defaults[.dictionaryTimezone],Defaults[.arrayPeriod],Defaults[.setPeriod],Defaults[.dictionaryPeriod]should be readable.
In v4, Defaults will store enum as a JSON string: "10 Minutes".
In v5, Defaults can store enum as a native string: 10 Minutes.
private enum Period: String, Codable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension Defaults.Keys {
static let period = Defaults.Key<Period?>("period")
}- Create another enum called
CodablePeriodand create an extension of it. Make the extension conform toDefaults.CodableTypeand its associated typeNativeFormtoPeriod.
private enum CodablePeriod: String {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension CodablePeriod: Defaults.CodableType {
typealias NativeForm = Period
}- Remove
Codableconformance soPeriodcan be stored natively.
private enum Period: String {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}- Create an extension of
Periodthat conforms toDefaults.NativeType. ItsCodableFormshould beCodablePeriod.
extension Period: Defaults.NativeType {
typealias CodableForm = CodablePeriod
}- Call
Defaults.migrate(.period) - Now
Defaults[.period]should be readable.
You can also instead implement the toNative function in Defaults.CodableType for flexibility:
extension CodablePeriod: Defaults.CodableType {
typealias NativeForm = Period
public func toNative() -> Period {
switch self {
case .tenMinutes:
return .tenMinutes
case .halfHour:
return .halfHour
case .oneHour:
return .oneHour
}
}
}This happens when you have a struct which is stored as a Codable JSON string before, but now you want it to be stored as a native UserDefaults dictionary.
private struct TimeZone: Codable {
var id: String
var name: String
}
extension Defaults.Keys {
static let timezone = Defaults.Key<TimeZone?>("TimeZone")
static let arrayTimezone = Defaults.Key<[TimeZone]?>("arrayTimezone")
static let setTimezone = Defaults.Key<Set<TimeZone>?>("setTimezone")
static let dictionaryTimezone = Defaults.Key<[String: TimeZone]?>("setTimezone")
}- Create a
TimeZoneBridgewhich conforms toDefaults.Bridgeand itsValueisTimeZoneandSerializableis[String: String].
private struct TimeZoneBridge: Defaults.Bridge {
typealias Value = TimeZone
typealias Serializable = [String: String]
func serialize(_ value: TimeZone?) -> Serializable? {
guard let value = value else {
return nil
}
return [
"id": value.id,
"name": value.name
]
}
func deserialize(_ object: Serializable?) -> TimeZone? {
guard
let dictionary = object,
let id = dictionary["id"],
let name = dictionary["name"]
else {
return nil
}
return TimeZone(
id: id,
name: name
)
}
}- Create an extension of
TimeZonethat conforms toDefaults.NativeTypeand its static bridge isTimeZoneBridge. The compiler will complain thatTimeZonedoes not conform toDefaults.NativeType. We will resolve that later.
private struct TimeZone: Hashable {
var id: String
var name: String
}
extension TimeZone: Defaults.NativeType {
static let bridge = TimeZoneBridge()
}- Create an extension of
CodableTimeZonethat conforms toDefaults.CodableType.
private struct CodableTimeZone {
var id: String
var name: String
}
extension CodableTimeZone: Defaults.CodableType {
/// Convert from `Codable` to native type.
func toNative() -> TimeZone {
TimeZone(id: id, name: name)
}
}- Associate
TimeZone.CodableFormtoCodableTimeZone
extension TimeZone: Defaults.NativeType {
typealias CodableForm = CodableTimeZone
static let bridge = TimeZoneBridge()
}- Call
Defaults.migrate(.timezone, to: .v5),Defaults.migrate(.arrayTimezone, to: .v5),Defaults.migrate(.setTimezone, to: .v5),Defaults.migrate(.dictionaryTimezone, to: .v5). - Now
Defaults[.timezone],Defaults[.arrayTimezone],Defaults[.setTimezone],Defaults[.dictionaryTimezone]should be readable.
See DefaultsMigrationTests.swift for more example.