Migrating to NSPersistentContainer

Update - 2017/07/12

if let oldUrl = oldUrl {
        let description = NSPersistentStoreDescription(url: oldUrl)
        persistentContainer.persistentStoreDescriptions = [description]
    }
    persistentContainer.loadPersistentStores { (description, error) in
        if let oldUrl = self.oldUrl {
        do {
            let psc = persistentContainer.persistentStoreCoordinator
            let store = psc.persistentStores[0]
            try psc.migratePersistentStore(store, to: url, options: nil, withType: NSSQLiteStoreType)                                                   self.deleteDatabase(url: oldUrl)
            self.cleanDb()
        } catch {
            block(description, error)
            return
        }
    }

Small update - instead of replacing, I use migratePersistentStore instead. This seems to work a little better with the current setup. So I have an oldUrl which is returned if the old store exists. If it's not nil I create the database against this and then migrate it to the correct location under Application Support. The old database still needs to be deleted in a separate step (assuming you no longer need it).


iOS 10 brought some welcome changes to Core Data and I need to migrate an existing database into this. Here’s how I did it.

First off, my existing DB is in the Documents folder. That’s not really good practice and NSPersistentContainer will create one in Application Support. That seems more sensible so I decided to move my Database. The default SQLite backed Core Data store includes SHM and WAL files (Write-Ahead Logging). To ensure you don’t lose data you need to move all three files at the same time. Fortunately, NSPersistentStoreCoordinator has built-in support for moving a database around.

First, I set up the NSPersistentStoreContainer:

persistentContainer = NSPersistentContainer(name: "GameModel")
persistentContainer.loadPersistentStores { (description, error) in
    if let error = error {
        fatalError("Could not create CoreData store: \(error)")
    }
    print(description)
}

This gives me something to work against. NSPersistentStoreContainer will create a NSPersistentStoreCoordinator with an sqlite DB at ApplicationSupport/GameModel.sqlite upon calling loadPersistentStores(completionHandler:). I want to replace that with the old Documents/Core_Data.sqlite. Fortunately, there’s a replace function right there.

let psc = persistentContainer.persistentStoreCoordinator
guard let storeUrl = psc.persistentStores.first?.url else {
    return
}
do {
    try psc.replacePersistentStore(at: storeUrl,
                                   destinationOptions: nil,
                                   withPersistentStoreFrom: oldUrl,
                                   sourceOptions: nil,
                                   ofType: NSSQLiteStoreType)
    persistentContainer.loadPersistentStores(completionHandler: { (description, error) in
        if let error = error {
            fatalError("Could not create CoreData store: \(error)")
        }
        print(description)
    })
} catch {
    print("Could not replace store: \(error)")
}

The newly created GameModel sqlite DB is replaced with the contents of the existing database by the replace call. I then need to call loadPersistentStores again to set up the NSPersistentContainer against the updated DB. Once it’s successful, I can delete the old DB files. There might be a better way, but this worked for me:

private func deleteOld(url: URL) {
        let parent = url.deletingLastPathComponent()
        let name = url.lastPathComponent
        do {
            try FileManager.default.contentsOfDirectory(at: parent, includingPropertiesForKeys: nil, options: [])
                .filter {
                    $0.lastPathComponent.hasPrefix(name)
                }
                .forEach {
                    try FileManager.default.removeItem(at: $0)
                }
        } catch {
            print("Failed to clear old DB: \(error)")
        }
    }

Where the passed URL is the old store URL.

Nonoku - SpriteKit Shader

A quick one because I was having trouble with this:

let node = SKShapeNode(rectOf: CGSize(width: 100, height: 100), cornerRadius: 5)
        let dashedShader = SKShader(source: "void main() {" +
            "float diff = u_path_length / 5.0;" +
            "float stripe = u_path_length / diff;" +
            "float distance = v_path_distance;" +
            "int h = int(mod(distance / stripe, 2.0));" +
            "gl_FragColor = vec4(h, h, h, h);" +
            "}")

        node.strokeShader = dashedShader

        node.fillColor = SKColor.clear
        node.lineWidth = 2

I've not done much with shaders before so when it didn't work I was short on tools to debug it. In the end it was a bunch of little things I had to solve, from converting the original version to one that would work against the iOS GL ES version, to making sure I converted to int before setting the colour.

Unity - Generating Builds

Adding the below code to Assets/Editor will add an entry to the menu bar that allows me to generate two builds automatically. Since they're also placed in a folder with the day's date, I also get build histories for the work done. For GameJams this is ideal as I'll usually only get a small amount of time on an evening to work on these so I'm not doing much more (and I could always use a more precise timestamp if needed). This is basic but so easy to automate and a nice little time saver that I thought I'd share.

using UnityEditor;
using System.Diagnostics;
using System;

public class JamBuilds 
{
    [MenuItem("Build/Jam Builds")]
    public static void BuildGame () {
        // Get filename.
        string path = EditorUtility.SaveFolderPanel("Choose Location of Built Game", "", "");
        string[] levels = new string[] {"Assets/Scenes/Test.unity", "Assets/Scenes/Finale.unity"};
        string projectName = PlayerSettings.productName;

        string date = DateTime.Now.ToString("/yyyy-MM-dd");

        // Build player.
        BuildPipeline.BuildPlayer(levels, path + date + "/" + projectName + ".exe", BuildTarget.StandaloneWindows, BuildOptions.None);

        // Build player.
        BuildPipeline.BuildPlayer(levels, path + date + "/WebPlayer", BuildTarget.WebPlayer, BuildOptions.None);

    }
}