7

I'm using realm.io in a swift app. This is the first time I've had to run a migration since I have an app in production. I changed one of the models and added a couple of extra fields to it.

I followed the example in the documentation and then referenced the github repo's example when that didn't work. I assumed that it was probably more complex then what the example in the documentation was letting on.

Here's what I have in my appdelegate.swift file:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        print ("i'm in here")
        // Override point for customization after application launch.

        // Inside your application(application:didFinishLaunchingWithOptions:)

        window = UIWindow(frame: UIScreen.mainScreen().bounds)
        window?.rootViewController = UIViewController()
        window?.makeKeyAndVisible()

        // copy over old data files for migration
        let defaultPath = Realm.Configuration.defaultConfiguration.path!
        let defaultParentPath = (defaultPath as NSString).stringByDeletingLastPathComponent

        if let v0Path = bundlePath("default-v0.realm") {
            do {
                try NSFileManager.defaultManager().removeItemAtPath(defaultPath)
                try NSFileManager.defaultManager().copyItemAtPath(v0Path, toPath: defaultPath)
            } catch {}
        }

        let config = Realm.Configuration(
            // Set the new schema version. This must be greater than the previously used
            // version (if you've never set a schema version before, the version is 0).
            schemaVersion: 1,

            // Set the block which will be called automatically when opening a Realm with
            // a schema version lower than the one set above
            migrationBlock: { migration, oldSchemaVersion in
                // We haven’t migrated anything yet, so oldSchemaVersion == 0
                if (oldSchemaVersion < 1) {
                    // Nothing to do!
                    // Realm will automatically detect new properties and removed properties
                    // And will update the schema on disk automatically
                }
        })

        // define a migration block
        // you can define this inline, but we will reuse this to migrate realm files from multiple versions
        // to the most current version of our data model
        let migrationBlock: MigrationBlock = { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {

            }

            print("Migration complete.")
        }

        Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 1, migrationBlock: migrationBlock)

        // Tell Realm to use this new configuration object for the default Realm
        Realm.Configuration.defaultConfiguration = config

        // Now that we've told Realm how to handle the schema change, opening the file
        // will automatically perform the migration
        _ = try! Realm()


        return true
    }

The print never runs which I find strange. Am I screwing something up? I'm assuming I must be.

Here's what the documentation said to do, I'm not sure if they were leaving something off or not:

// Inside your application(application:didFinishLaunchingWithOptions:)

let config = Realm.Configuration(
  // Set the new schema version. This must be greater than the previously used
  // version (if you've never set a schema version before, the version is 0).
  schemaVersion: 1,

  // Set the block which will be called automatically when opening a Realm with
  // a schema version lower than the one set above
  migrationBlock: { migration, oldSchemaVersion in
    // We haven’t migrated anything yet, so oldSchemaVersion == 0
    if (oldSchemaVersion < 1) {
      // Nothing to do!
      // Realm will automatically detect new properties and removed properties
      // And will update the schema on disk automatically
    }
  })

// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config

// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let realm = try! Realm()

Any ideas what I'm doing wrong?

Jason Shultz
  • 940
  • 1
  • 19
  • 36
  • Hi, I have a similar issue (http://stackoverflow.com/questions/43612789/realm-swift-retrieve-user-object-not-returned-when-launching-app). Looks like I have the same code but it doesn't work for me. I can't get the user object to know if he is logged in when the app is launching. Any idea how to fix this? – Marie Dm Apr 25 '17 at 14:41
  • 1
    i honestly don't know. i stopped doing iOS development about 6 months ago and switched to progressive web apps. :( – Jason Shultz Apr 25 '17 at 18:52

5 Answers5

7

This ended up being the solution. I cannot say that I came up with it on my own because I didn't. I got some excellent help from a wonderful engineer named Claire at realm.

Here's what needed to be done:

class RoomsViewController: UIViewController, UITableViewDelegate {

    var activeRoom = -1
    var room: Room? = nil
    var array = [Room]()

    lazy var realm:Realm = {
        return try! Realm()
    }()


    var notificationToken: NotificationToken?

    @IBOutlet weak var roomsTable: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        array = Array(realm.objects(Room.self))

        setupUI()
        // Set realm notification block
        notificationToken = realm.addNotificationBlock { [unowned self] note, realm in
            // TODO: you are going to need to update array
            self.roomsTable.reloadData()
        }
    }

This is first view controller that get's loaded and it was immediately querying the realm database to build the array. With Claire's suggestion, Realm was lazy loaded (or tried) and the code to build the array was moved into the viewDidLoad method whereas before it was called at the top.

This allowed the realm to migration to run ahead. As you might imagine, I honestly assumed that the view never got loaded until after the application function loaded in the AppDelegate. However, I guess I was wrong.

So, this will solve it. If you ever happen to run into the same problem, give this a shot.

UPDATE:

Here's how the appDelegate function looks now:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        // Inside your application(application:didFinishLaunchingWithOptions:)

        let config = Realm.Configuration(
            // Set the new schema version. This must be greater than the previously used
            // version (if you've never set a schema version before, the version is 0).
            schemaVersion: 1,

            // Set the block which will be called automatically when opening a Realm with
            // a schema version lower than the one set above
            migrationBlock: { migration, oldSchemaVersion in
                // We haven’t migrated anything yet, so oldSchemaVersion == 0
                if (oldSchemaVersion < 1) {
                    migration.enumerate(Inventory.className()) { oldObject, newObject in
                        // No-op.
                        // dynamic properties are defaulting the new column to true
                        // but the migration block is still needed
                    }
                    migration.enumerate(Profile.className()) { oldObject, newObject in
                        // No-op.
                        // dynamic properties are defaulting the new column to true
                        // but the migration block is still needed
                    }
                    migration.enumerate(Room.className()) { oldObject, newObject in
                        // No-op.
                        // dynamic properties are defaulting the new column to true
                        // but the migration block is still needed
                    }
                    migration.enumerate(Box.className()) { oldObject, newObject in
                        // No-op.
                        // dynamic properties are defaulting the new column to true
                        // but the migration block is still needed
                    }
                }
        })

        // Tell Realm to use this new configuration object for the default Realm
        Realm.Configuration.defaultConfiguration = config

        // Now that we've told Realm how to handle the schema change, opening the file
        // will automatically perform the migration
        do {
            _ = try Realm()
        } catch let _ as NSError {
            // print error
        }


        return true
    }
Jason Shultz
  • 940
  • 1
  • 19
  • 36
  • It still sounds dodgy that your original code didn't produce the print. I can understand that it wasn't working because you were instantiating the view controller (which presumably used Realm) before doing the migration, but you should still have seen the print. I have similar logic in my code and it works fine. The only difference is I don't have the "copy over old data files for migration" code. What is that supposed to be? Could it be causing problems? – Michael Jan 15 '16 at 05:42
  • 1
    @Michael I updated the answer with the function code. To the best of my understanding, the view controller launches faster than the appdelegate didFinishLaunchingWithOptions code so it doesn't get a chance to run. I tested it out by putting a `print("hello didFinishLaunchingWithOptions)` at the top of the block and it does appear now. – Jason Shultz Jan 16 '16 at 14:42
  • I had the same issue. I assumed anything in `didFinishLaunchingWithOptions` was being performed first, but of course the initial view controller of my app was being initialised first, which was creating a view model that accessed the Realm database. Setting the view model initialisation to lazy fixed the issue, thanks. – danfordham Mar 20 '20 at 14:48
2

This works as intended, throwing exceptions when the downgrading (if working with branches where the versions differ) etc. Some sample migrations are shown too.

    Realm.Configuration.defaultConfiguration = Realm.Configuration(
        // Update this number for each change to the schema.
        schemaVersion: 4,
        migrationBlock: { migration, oldSchemaVersion in
            if oldSchemaVersion < 2 {
                migration.enumerate(SomeObject.className()) { oldObject, newObject in
                    newObject!["newColumn"] = Enum.Unknown.rawValue
                }
            }
            if oldSchemaVersion < 3 {
                migration.enumerate(SomeOtherObject.className()) { oldObject, newObject in
                    newObject!["defaultCreationDate"] = NSDate(timeIntervalSince1970: 0)
                }
            }
            if oldSchemaVersion < 4 {
                migration.enumerate(SomeObject.className()) { oldObject, newObject in
                    // No-op.
                    // dynamic properties are defaulting the new column to true
                    // but the migration block is still needed
                }
            }
    })

Obviously that won't compile as is with the SomeObject etc., but you get the idea :)

krider2010
  • 129
  • 1
  • 4
1

It looks like you flat-out copied the code out of the Realm Swift 'Migration' example and pasted it verbatim. A lot of that code is to actually 'set-up' a new demo migration each time the sample app is run, and not actually necessary for a normal Realm migration.

There are two components to a Realm migration:

  1. Bump the schema version number in the Configuration object. Realm files start at version 0, and everytime you want to do a new migration, increase it by 1.

  2. Supply a migration block that will be executed multiple times for each increment of your Realm schema version. While the block itself is necessary, its purpose is to let you copy/transform any data in an older Realm file. If you're simply adding new fields, you can just leave the block empty.

If your UIViewController is using Realm at all in its implementation, it's best to place your migration code before the UIWindow code to ensure the migrations have already occurred before the UIViewController starts using it (Otherwise you'll get an exception).

Since all you're doing is adding a few fields to it, this is all the code you need:

let migrationBlock: MigrationBlock = { migration, oldSchemaVersion in
    //Leave the block empty
}
Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 1, migrationBlock: migrationBlock)

window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = UIViewController()
window?.makeKeyAndVisible()
TiM
  • 15,812
  • 4
  • 51
  • 79
  • Does it still go in the `func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)` function? I'm still getting the same error that the migration needs to run? – Jason Shultz Jan 14 '16 at 14:06
  • Yes! It does. Oh dear. Are you using storyboards at all? If you have Realm logic in a view controller being created via a storyboard, that can happen before `func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)` is called. – TiM Jan 15 '16 at 02:54
1

I found this to work best in a View Controller:

let migrationBlock: MigrationBlock = { migration, oldSchemaVersion in
    //Leave the block empty
}

lazy var realm:Realm = {
    Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 1, migrationBlock: migrationBlock)
    return try! Realm()
}()

In other words, move the configuration assignment into the block so it doesn't get executed too late.

Deirdre
  • 11
  • 1
  • 2
0

Based on my previous answer here

Alternatively, If you want migration to be ran before everything, you can override the AppDelegate's init() like this:

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    override init() {
        RealmMigration()       // You code goes here
        super.init()
    }
...
}

Few points to remember:

  1. You might want to check, what's allowed/can be done here, i.e. before app delegate is initialized
  2. Also cleaner way would be, subclass the AppDelegate, and add new delegate method that you call from init, say applicationWillInitialize and if needed applicationDidInitialize
ImShrey
  • 380
  • 4
  • 12