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) {
            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+.