Developear
All Apps   Steps+   Twitter   Contact   RSS  



Recently, I decided to work on a few features for Steps+ that essentially require a lot of my the App's data be in an App Group.

I began the endeavor by doing a quick Google search, which led me to a solution that I ended up modifying slightly and shipping. Here is the snippet I found.

As for the code I ended up shipping to solve this problem, It ended up looking like this:

final class Migrator: NSObject {
    private let from: UserDefaults
    private let to: UserDefaults

    private var hasMigrated = false

    init(from: UserDefaults, to: UserDefaults) {
        self.from = from
        self.to = to
    }

    // Returns the proper defaults to be used by the application
    func defaults() -> UserDefaults {
        return to
    }

    func migrate() {
        // User Defaults - Old
        let userDefaults = from

        // App Groups Default - New
        let groupDefaults = to

        // Don't migrate if they are the same defaults!
        if userDefaults == groupDefaults {
            return
        }

        // Key to track if we migrated
        let didMigrateToAppGroups = "DidMigrateToAppGroups"

        if !groupDefaults.bool(forKey: didMigrateToAppGroups) {

            // Doing this loop because in practice we might want to filter things (I did), instead of a straight migration
            for key in userDefaults.dictionaryRepresentation().keys {
                groupDefaults.set(userDefaults.dictionaryRepresentation()[key], forKey: key)
            }
            groupDefaults.set(true, forKey: didMigrateToAppGroups)
        }
    }

}

As you can see, the migration code is isolated into its own class - this allows me to more easily reason about the code and write some tests for the code in isolation.

My strategy was to create a new "App Group" static let to return the new UserDefaults and use the migrator in there when that is created the first time, like so:

@objc extension UserDefaults {

    private static let migrator = Migrator(
        from: .standard,
        to: UserDefaults(suiteName: "group.steps.plus") ?? .standard)


    @objc static let appGroup: UserDefaults = {
        migrator.migrate()
        return migrator.defaults()
    }()

}

I can then just use appGroup instead of the standard in the app to access or pass around into my methods that require access to User Defaults.

I mentioned tests earlier, and having this code in it's own migrator class - while also making this more re-useable and isolated, also lets me right tests such as this one I have:

func testMigrationWorks() {
    let firstDefaults = UserDefaults.init(suiteName: "\(Date())")!

    firstDefaults.set(10093, forKey: "MYKEY")

    let secondDefaults = UserDefaults.init(suiteName: "\(#function)")!

    let migrator = Migrator(from: firstDefaults, to: secondDefaults)
    migrator.migrate()

    XCTAssert(secondDefaults.integer(forKey: "MYKEY") == 10093)
}

And thats it! It's always a good idea to search around for solutions/thoughts on problems that you are solving. But remember, if you use/modify code you find - you should understand it and give credit! Thanks for reading/checking this small post out, if you want to support this blog - check out my step tracking app, Steps+.