Defaults/migration.md

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 some 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.

  • Primitive types (Int, Double, Bool, String, etc) require no changes.
  • Custom types (struct, enum, etc.) must now conform to Defaults.Serializable (in addition to Codable).
  • Array, Set, and Dictionary will need to be manually migrated with Defaults.migrate().

In v4, Defaults stored Codable types as a JSON string.
In v5, Defaults stores many Codable types as native UserDefaults types.

// v4
let key = Defaults.Key<[String: Int]>("key", default: ["0": 0])

UserDefaults.standard.string(forKey: "key")
//=> "[\"0\": 0]"
// v5
let key = Defaults.Key<[String: Int]>("key", default: ["0": 0])

UserDefaults.standard.dictionary(forKey: "key")
//=> [0: 0]

Issues

  1. The compiler complains that Defaults.Key<Value> does not conform to Defaults.Serializable. Since we replaced Codable with Defaults.Serializable, Key<Value> will have to conform to Value: Defaults.Serializable. For this situation, please follow the guides below:

  2. The previous value in UserDefaults is not readable. (for example, Defaults[.array] returns nil). In v5, Defaults reads value from UserDefaults as a natively supported type, but since UserDefaults only contains JSON string before migration for Codable types, Defaults will not be able to work with it. For this situation, Defaults provides the Defaults.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.

  1. Get the previous value in UserDefaults (using defaults command or whatever you want).
let string = "[\"a\",\"b\",\"c\"]"
  1. Insert the above value into UserDefaults.
UserDefaults.standard.set(string, forKey: "testKey")
  1. Call Defaults.migrate() and then use Defaults 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

  1. Make TimeZone conform to Defaults.Serializable.
private struct TimeZone: Codable, Defaults.Serializable {
	var id: String
	var name: String
}
  1. 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

  1. Make Period conform to Defaults.Serializable.
private enum Period: String, Defaults.Serializable, Codable {
	case tenMinutes = "10 Minutes"
	case halfHour = "30 Minutes"
	case oneHour = "1 Hour"
}
  1. 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

  1. Call Defaults.migrate(.arrayString, to: .v5), Defaults.migrate(.setString, to: .v5), Defaults.migrate(.dictionaryStringInt, to: .v5), Defaults.migrate(.dictionaryStringIntInArray, to: .v5).
  2. 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

  1. Make TimeZone and Period conform to Defaults.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"
}
  1. 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).
  2. 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

  1. Create another enum called CodablePeriod and create an extension of it. Make the extension conform to Defaults.CodableType and its associated type NativeForm to Period.
private enum CodablePeriod: String {
	case tenMinutes = "10 Minutes"
	case halfHour = "30 Minutes"
	case oneHour = "1 Hour"
}

extension CodablePeriod: Defaults.CodableType {
	typealias NativeForm = Period
}
  1. Remove Codable conformance so Period can be stored natively.
private enum Period: String {
	case tenMinutes = "10 Minutes"
	case halfHour = "30 Minutes"
	case oneHour = "1 Hour"
}
  1. Create an extension of Period that conforms to Defaults.NativeType. Its CodableForm should be CodablePeriod.
extension Period: Defaults.NativeType {
	typealias CodableForm = CodablePeriod
}
  1. Call Defaults.migrate(.period)
  2. 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

  1. Create a TimeZoneBridge which conforms to Defaults.Bridge and its Value is TimeZone and Serializable is [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
		)
	}
}
  1. Create an extension of TimeZone that conforms to Defaults.NativeType and its static bridge is TimeZoneBridge. The compiler will complain that TimeZone does not conform to Defaults.NativeType. We will resolve that later.
private struct TimeZone: Hashable {
	var id: String
	var name: String
}

extension TimeZone: Defaults.NativeType {
	static let bridge = TimeZoneBridge()
}
  1. Create an extension of CodableTimeZone that conforms to Defaults.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)
	}
}
  1. Associate TimeZone.CodableForm to CodableTimeZone
extension TimeZone: Defaults.NativeType {
	typealias CodableForm = CodableTimeZone

	static let bridge = TimeZoneBridge()
}
  1. Call Defaults.migrate(.timezone, to: .v5), Defaults.migrate(.arrayTimezone, to: .v5), Defaults.migrate(.setTimezone, to: .v5), Defaults.migrate(.dictionaryTimezone, to: .v5).
  2. Now Defaults[.timezone], Defaults[.arrayTimezone] , Defaults[.setTimezone], Defaults[.dictionaryTimezone] should be readable.

See DefaultsMigrationTests.swift for more example.