12 KiB
Migration guide from v4 to v5
Warning: Test the migration thoroughly in your app. It might cause unintended data loss if you're not careful.
Summary
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)
Double
CGFloat
Float
String
Bool
Date
Data
URL
- Custom types (
struct
,enum
, etc.) must now conform toDefaults.Serializable
(in addition toCodable
). Array
,Set
, andDictionary
will 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]
Issues
-
The compiler complains that
Defaults.Key<Value>
does not conform toDefaults.Serializable
. Since we replacedCodable
withDefaults.Serializable
,Key<Value>
will have to conform toValue: Defaults.Serializable
. For this situation, please follow the guides below: -
The previous value in
UserDefaults
is not readable. (for example,Defaults[.array]
returnsnil
). In v5,Defaults
reads value fromUserDefaults
as a natively supported type, but sinceUserDefaults
only contains JSON string before migration forCodable
types,Defaults
will not be able to work with it. For this situation,Defaults
provides theDefaults.migrate()
method to automate the migration process.
Testing
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
(usingdefaults
command 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 useDefaults
to get its value.
let key = Defaults.Key<[String]>("testKey", default: [])
Defaults.migrate(key, to: .v5)
Defaults[key] //=> [a, b, c]
Migrations
From Codable
struct in Defaults v4 to Codable
struct in Defaults v5
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.
Before migration
private struct TimeZone: Codable {
var id: String
var name: String
}
extension Defaults.Keys {
static let timezone = Defaults.Key<TimeZone?>("TimeZone")
}
Migration steps
- Make
TimeZone
conform toDefaults.Serializable
.
private struct TimeZone: Codable, Defaults.Serializable {
var id: String
var name: String
}
- Now
Defaults[.timezone]
should be readable.
From Codable
enum in Defaults v4 to Codable
enum in Defaults v5
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.
Before migration
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")
}
Migration steps
- Make
Period
conform 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"]
.
Before migration
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")
}
Migration steps
- 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"]
.
Before migration
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")
}
Migration steps
- Make
TimeZone
andPeriod
conform 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.
Optional migrations
From Codable
enum in Defaults v4 to RawRepresentable
enum in Defaults v5 (Optional)
In v4, Defaults
will store enum
as a JSON string: "10 Minutes"
.
In v5, Defaults
can store enum
as a native string: 10 Minutes
.
Before migration
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")
}
Migration steps
- Create another enum called
CodablePeriod
and create an extension of it. Make the extension conform toDefaults.CodableType
and its associated typeNativeForm
toPeriod
.
private enum CodablePeriod: String {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension CodablePeriod: Defaults.CodableType {
typealias NativeForm = Period
}
- Remove
Codable
conformance soPeriod
can be stored natively.
private enum Period: String {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
- Create an extension of
Period
that conforms toDefaults.NativeType
. ItsCodableForm
should 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
}
}
}
From Codable
struct in Defaults v4 to Dictionary
in Defaults v5 (Optional)
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.
Before migration
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")
}
Migration steps
- Create a
TimeZoneBridge
which conforms toDefaults.Bridge
and itsValue
isTimeZone
andSerializable
is[String: String]
.
private struct TimeZoneBridge: Defaults.Bridge {
typealias Value = TimeZone
typealias Serializable = [String: String]
func serialize(_ value: TimeZone?) -> Serializable? {
guard let value else {
return nil
}
return [
"id": value.id,
"name": value.name
]
}
func deserialize(_ object: Serializable?) -> TimeZone? {
guard
let object,
let id = object["id"],
let name = object["name"]
else {
return nil
}
return TimeZone(
id: id,
name: name
)
}
}
- Create an extension of
TimeZone
that conforms toDefaults.NativeType
and its static bridge isTimeZoneBridge
. The compiler will complain thatTimeZone
does 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
CodableTimeZone
that 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.CodableForm
toCodableTimeZone
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.