Copied Realm not returning data


#1

I am having an issue accessing data in a copied Realm.

In my app I have three Save slots which store saved Realms of my app’s data. These are all stored in the application directory. I am able to read from these saved Realms fine.

My issue occurs when the user selects the saved Realm to reload.

My code deletes the default.realm file and makes new default.realm file by copying the saved Realm. The file copies fine, I can see it in Finder, I can open it in Realm Studio and see it contains the same data as the original. I can even set the Realm Configuration to use the new default.realm file without errors.

However any query returns an “Unexpectedly found nil” error.

Is there a step I am missing in the copying of the realm? Do I need to set any permissions on the file? This is driving me mad.

My functions’s code is below. My CustomFunction.migrateRealm performs the realm migration as per standard.

@IBAction func btnSlotLoad(_ sender: Any) {
    
    // get the URL to load from
    let slotNumber = (sender as AnyObject).tag!
    let slotName   =  "Game\(slotNumber).realm"
    let slotURL    = getRealmURL(realmName: slotName)
    print ("btnSlotLoad will load from Realm: ", slotURL)
    
    var realm      = try! Realm()
    var gameData    : GameData
    
    //Check Realm version
    _ = Realm.Configuration()
    // get default URL for this app's instance of Realm
    let defaultURL =  Realm.Configuration.defaultConfiguration.fileURL!
    print ("Current Game URL: ", defaultURL)
    
    try! realm.write {
        realm.deleteAll()
    }
    //Deallocate realm
    
    let realmURLs = [
        defaultURL,
        defaultURL.appendingPathExtension("lock"),
        defaultURL.appendingPathExtension("note"),
        defaultURL.appendingPathExtension("management")
    ]
    for URL in realmURLs {
        do {
            try FileManager.default.removeItem(at: URL)
        } catch {
            print("Error deleting Realm file: \(URL)")
        }
    }
    
    //Load Realm
    do {
        try FileManager.default.copyItem(at: slotURL, to: defaultURL)
    } catch {
        print("Error loading Realm, \(error)")
    }
    
    let loadedRealmConfig = CustomFunction.migrateRealm(realmName: "default.realm")
    realm = try! Realm(configuration: loadedRealmConfig)
    
    //Code is failing with fatal error here
    gameData = try! realm.objects(GameData.self).first!
    
    self.performSegue(withIdentifier: "ContinueGameFromVC", sender: self)
}

(jay) #2

Why are you deleting and creating Realms in the first place? What does the migration have to do with creating a Realm. i.e. if you totally delete a realm, there would be no reason to have a migration since there’s nothing to migrate to, all of your data would be fresh in a new Realm anyway.


#3

Hi Jay, I added the migration to ensure that any previously saved Realms that may have last been opened before any schema changes were properly migrated.
Apologies for the duplication with SO, my first post in either forum so please excuse my ignorance on inexperience. Any way I can delete this?


(jay) #4

No worries and I believe you can delete you own posts by clicking the small trash can in the lower corner of the post.

I think I would leave it in place though and add a link to your post on SO so others can follow the breadcrumbs in case they have a similar question.


#5

Afraid I am still having issues trying to copy Realms.

My use case is very straightforward. My app is a game. When a user wants to start a new game, the game data needs to be returned to the initial state.

I have observed the following:

  • I am able to delete the old Realm and all associated lock files.
  • I am able to delete all old Realm objects from memory.
  • I am able to copy over the bundled Realm to my app’s directory.

However, if I copy over the bundled Realm to the same URL I have used previously, the Realm appears empty.
If I use a different URL, the Realm displays data.

In both of the above cases, I can view the same data via Realm Studio - the only difference occurs when running the app.

This is a problem, because ideally I want to use the same URL (to default.realm) without having to worry about it. Is this a known bug? if so is there a fix?

My code is below.

func okPressed() {
    
    // get default URL for this app's instance of Realm
    print ("Realm.Configuration.defaultConfiguration: ", Realm.Configuration.defaultConfiguration.description)
    let defaultURL =  Realm.Configuration.defaultConfiguration.fileURL!.deletingLastPathComponent().appendingPathComponent("default.realm")
    // set new game's URL : NOTE this code only works if the newGameURL is different to the defaultURL
    let newGameURL =  Realm.Configuration.defaultConfiguration.fileURL!.deletingLastPathComponent().appendingPathComponent("newgame.realm")
    let bundledRealmURL = Bundle.main.url(forResource: "default", withExtension: "realm")
    
    // Perform any migrations required
    let migrationBlock : MigrationBlock = { migration, oldSchemaVersion in
        if (oldSchemaVersion < 17) {
        }
    }
    // Ensure current Realm Config is set for Realm at default URL (which we want to delete)
    let currentRealmConfig = Realm.Configuration(fileURL: defaultURL, schemaVersion: 17, migrationBlock: migrationBlock, deleteRealmIfMigrationNeeded: true)
    Realm.Configuration.defaultConfiguration = currentRealmConfig
    
    // Delete all objects in the current Realm configuration
    autoreleasepool {
        let realm = try! Realm()
        try! realm.write {
            realm.deleteAll()
        }
    }
    
    // Remove existing default.realm and related lock files
    let realmURLs = [
        defaultURL,
        defaultURL.appendingPathExtension("lock"),
        defaultURL.appendingPathExtension("note"),
        defaultURL.appendingPathExtension("management"),
    ]
    realmURLs.forEach { URL in
        guard FileManager.default.fileExists(atPath: URL.path) else { return }
        do {
            try FileManager.default.removeItem(at: URL)
        } catch {
        }
        print("Removed file: ", String(describing: URL))
    }
    
    // Copy over bundled Realm to new Game URL
    do {
        try FileManager.default.copyItem(at: bundledRealmURL!, to: newGameURL)
    } catch {
        print("Error loading Realm, \(error)")
    }
    
    // Set Realm default configuration for new game
    let newGameConfig = Realm.Configuration(fileURL: newGameURL, schemaVersion: 17, migrationBlock: migrationBlock, deleteRealmIfMigrationNeeded: true)
    Realm.Configuration.defaultConfiguration = newGameConfig
    
    // Disable file protection for this directory
    let folderPath = newGameConfig.fileURL!.deletingLastPathComponent().path
    try! FileManager.default.setAttributes([.protectionKey: FileProtectionType.none], ofItemAtPath: folderPath)
    
    do {
        let newRealm = try! Realm()
        var gameArray = newRealm.objects(GameData.self)
        print ("gameData in new Realm:", gameArray as Any)
    } catch let error as NSError {
        print("Error opening newRealm: \(error)")
    }
    
    self.performSegue(withIdentifier: "startNewGame", sender: self)
}

(jay) #6

should be

let realmURL = Realm.Configuration.defaultConfiguration.fileURL!

and then

let realmURLs = [
    realmURL,
    realmURL.appendingPathExtension("lock"),
    realmURL.appendingPathExtension("note"),
    realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
    do {
        try FileManager.default.removeItem(at: URL)
    } catch {
        // handle error
    }
}

I use the above code to delete and write to the default realm all the time.


#7

Thank Jay, afraid that hasn’t worked for me. The same result occurs - I can see the default.realm in Finder, I can open it in Realm Studio and see it contains data, but the query in Swift still returns nil.

For reference the new default configuration is

newGameConfig: Realm.Configuration {
fileURL = file:///Users/richard/Library/Developer/CoreSimulator/Devices/E87C0BB6-CDB5-45D1-8F96-E812AFCAA95B/data/Containers/Data/Application/76C32AFC-E47E-41FB-AE74-2BD30BA734D0/Documents/default.realm;
inMemoryIdentifier = (null);
encryptionKey = (null);
readOnly = 0;
schemaVersion = 17;
migrationBlock = <NSMallocBlock: 0x600003dee100>;
deleteRealmIfMigrationNeeded = 1;
shouldCompactOnLaunch = (null);
dynamic = 0;
customSchema = (null);


(jay) #8

Well. Not sure what the issue could be as the code I suggested above has worked for a couple of years.

I just now fired up an existing project that uses that code (copy/paste), wrote some objects to realm, queried those objects, printed them to console, clicked a button that ran the above code to delete the project files and then there was nothing in the project. Created some new objects… rinse, repeat. No issues.

Is this a local, Query or Full Sync realm?

As a big picture test, I just crafted totally new iOS project. Added three buttons to the UI, Write, Read and Delete All Files and wired them up to the view controller. I added a single Realm PersonClass to the project.

If I write a couple of person objects, then read them it prints to console. If I use the Delete All Files which runs the above code and then read realm, it outputs nothing. If I then write and read it outputs new people to console.

Because you are having a different experience, that would indicate you doing something different in your code so perhaps it’s how you connect to realm or maybe how your handling URL’s. Note that I was using the default realm throughout my testing so that probably indicates an issue with how the realm url’s are being handled.

One thing I did notice is this

let loadedRealmConfig = CustomFunction.migrateRealm(realmName: "default.realm")try FileManager.default.copyItem(at: slotURL, to: defaultURL)

Perhaps the CustomFunction is not working correctly; migration issue or path issue.


#9

I don’t have a custom function in the code now. All that ever did was set the Realm configuration to migrate the Realm. I only used a custom function to do that so that I could reuse the migration block code, I removed it but the issue persists - see my revised code above.

Do you have any suggestions? Is there a standard block of code available that covers this?


(jay) #10

I would suggest doing what I did; craft a brand new clean project, add button for write, read and delete files and implement that code. Should only take about 15 minutes but then you’ll have a working project you can compare to your not working project to determine the difference.

Other than that, unless you provide code that crates a duplicatable and minimal example, there’s not a lot we can offer up.

One thing that’s confusing (to me) is that you say you want to use the default realm

because ideally I want to use the same URL (to default.realm) without having to worry about it.

But your code doesn’t always use the default realm. For me, I would think you would want to create a new realm for each game so the player can return to the game in the future, so the files would be named

player_0_game_0
player_0_game_1
etc

Or even have the player name the new games, like ‘got to level 2’ or ‘got a new item’

See Using Multiple Realms in the documentation for some hints on doing that.

That being said, it’s also unclear why you need multiple Realms. It doesn’t sound like you’re storing an enormous amount of data so as a thought, why not just store the data in a single Realm using something like a Realm GameClass Object that would store the user, what level their on and maybe their preferences?

That alleviates this issue, simplifies the code, makes it more maintainable. AND if you decide to move to Realm Cloud so the user can access the game on multiple devices then it’s a snap - no more file management.


#11

But your code doesn’t always use the default realm.

Each instance of the game just uses a single realm, the default.realm. Other realms are only ever used to store saved games (one realm per save).

That being said, it’s also unclear why you need multiple Realms.

The app does not use multiple Realms at any single point. The only time I use different realms is when the user wishes to save the game to a saved game slot, in order to store and then play another instance of the game (such as a different scenario). This is exactly the scenario you describe with the player_0_game_0 example. For the reasons I have given, I do not want to worry about managing numerous Realm files beyond the default realm for the in progress game, and 3 other Realms (Game1.realm, Game2.realm and Game3.realm) for the saved games.

It doesn’t sound like you’re storing an enormous amount of data

Potentially each game is storing a large amount of data, spread across many objects. In a single realm.

I would suggest doing what I did; craft a brand new clean project, add button for write, read and delete files and implement that code. Should only take about 15 minutes but then you’ll have a working project you can compare to your not working project to determine the difference.

I have done that (it took rather longer than 15 minutes), and the exact same issue persists - you can try this out at https://github.com/Shefto/Realm-Issue-Debug

The behaviour I am seeing now suggests that the issue is that somewhere, somehow, Realm is caching the default realm URL for the app in memory, which is for some reason stopping the app from reading from that Realm.
The copy of the new default.realm file is working fine, and can be seen to be successful in Finder.
The new default.realm file opens fine in Realm Studio, and data can be viewed & updated in it as normal.
Using any other URL resolves the issue.
Closing and re-opening the app causes the app to successfully start reading from the new default.realm file without any changes being made.


(jay) #12

That project doesn’t have anything Realm related in it. Perhaps the wrong files were uploaded?


#13

The default realm is in the project. The link is below.

https://github.com/Shefto/Realm-Issue-Debug/blob/master/Realm%20Issue%20Debug/default.realm

I’d appreciate if you can try building and running the application and see if you get the same behaviour.


(jay) #14

Unfortunately, I am not seeing anything related to Realm in those links. Here’s a screen shot from the first link. The project builds and runs but there is no code to do anything with Realm in it. In fact, no other code other than the default app code.

and the second link is an empty Github

Here’s what the project folder looks like after its downloaded. There is a Default.realm filebut no other reference
to it.

Test%203


#15

Apologies, the project is uploaded now - should be all there but let me know if there are any issues.


(jay) #16

App built and ran correctly. Not sure what the buttons do or in what sequence they are supposed to be pressed but Get Data From default worked, and then Start new Game worked.

When I clicked Get Data again, it crashed here

gameData.GameStarted = realm.objects(GameData.self).first!.GameStarted

which is dangerous as explicitly unwrapped optionals can be nil and crash the app, as in this case.

Along with that, the function that’s crashing is

getDataFromDefaultRealm(_ sender: Any) {

and while it’s supposed to get data from the default realm (assuming by the title). The default realm is not specified so it will try to get realm data from whatever the last config was. If those file were deleted then it would crash. Probably a good idea to specify the default realm within that func.

I think this is all unrelated to the question but just an observation.


#17

I created the project to show you the error I’m getting, appreciate the points raised but the aim was to get the project built as quickly as necessary to show the error. Noted your points, thanks.

The app has 4 buttons on. Clicking each in order replicates the steps in my full app.

When newRealmName is set to anything other than “default.realm”, upon clicking the buttons in order the realm is successfully queried. In that case I see the following text in the box:

However when newRealmName is set to “default.realm”, the query fails and the screen displays the following:

This only happens when newRealmName is set to “default.realm”.

When you say “app built and ran correctly”, can you run the app as described with the newRealmName set to “default.realm” and confirm what you observe once all 4 buttons are clicked?


(jay) #18

I see the same result as you do. Here’s the issue: Realm remembers it’s ‘stuff’

In a nutshell, once Realm connects to a data source, it will continue to use that data source as long as the objects are not released, even if the actual file is deleted.

That’s what you are experiencing - even when you deleted and replaced the file, it was not accessing it - it continued to pull the ‘old’ data.

The way around that is to encapsulate the Realm calls into an autorelease pool so that those objects can be released when the Realm is deleted.

Here’s an example:

I took all of the code in your app delegate and wrapped an autoreleasepool around it.

Then I created a function that adds another game data object to the default.realm file.

func addAnObject() {
        autoreleasepool {
            let realm = try! Realm()
            let testData = GameData()
            testData.Scenario = "This is my scenario"
            testData.Id = 1
            try! realm.write {
                realm.add(testData)
            }
        }
}

At this point, if you run the addAnObject code, your file will have two objects - the default one from the bundle and then one just added.

GameData {
	Id = 0;
	GameDate = 1980-01-01 00:00:00 +0000;
	Scenario = This Realm has data!!!;
	GameStarted = 0;
}
GameData {
	Id = 1;
	GameDate = (null);
	Scenario = This is my scenario;
	GameStarted = 0;
}

Then a function that delete’s the old realm, and copies the bundled realm to it’s place.

     func createDefaultRealm() {
        let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
        let defaultParentURL = defaultURL.deletingLastPathComponent()

        if let bundledRealmURL = self.bundleURL("default") {
            do {
                try FileManager.default.removeItem(at: defaultURL)
                try FileManager.default.copyItem(at: bundledRealmURL, to: defaultURL)
            } catch let error as NSError {
                print(error.localizedDescription)
                return
            }
        }

        let migrationBlock : MigrationBlock = { migration, oldSchemaVersion in
        }

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

        print("Your default realm objects: \(try! Realm().objects(GameData.self))")
    }

    func bundleURL(_ name: String) -> URL? {
        return Bundle.main.url(forResource: name, withExtension: "realm")
    }

When the createDefaultRealm code is run, it results in a clean file with the default bundled data in it

[0] GameData {
	Id = 0;
	GameDate = 1980-01-01 00:00:00 +0000;
	Scenario = This Realm has data!!!;
	GameStarted = 0;
}

So after I wrote that code it still wasn’t working… then I noticed this at the top of the viewController

class ViewController: UIViewController {
    var realm = try! Realm()

Ah! That var is outside the autoreleasepool so Realm has a connection to the old data. As soon as I commented that out and added let realm = try! Realm() inside the autoreleasepool’s, everything worked.

Hope that explanation was clear… if not, let me know.


#19

Thank you. I added your function, wrapped my appdelegate.didFinishLaunchingWithOptions function in an autoreleaspool and moved the var realm = try! Realm() inside an autoreleasepool. First time out it didn’t give me what I wanted, the app-created data still appeared but the default “seeded” data I wanted to return to did not. I then updated my function to use the autoreleasepool and have managed to get the default realm to be read from successfully. I’ve committed my updated project now.

I now understand that Realm retains a strong reference to the fileURL used in the creation of any Realm until the Realm is released from memory. I haven’t seen this mentioned anywhere in the Realm documentation. Is there a reason why Realm does not provide a method to completely remove a Realm object from memory and free up the fileURL for reuse?

I still want to provide the ability to save 2-3 games for my player to be able to return to, and saving the entire game Realm to a different URL is by far the simplest way of accomplishing this. One possible alternative is to add a timestamp in the filename of the new game’s Realm. This would mean having to handle the clean-up of these additional .realm files, but might be simpler and safer than using autoreleasepools for this.


(jay) #20

It is a bit of a mystery why there is no ‘Realm.disconnect’ function as it seems there are many use cases where this would be beneficial.

I would still suggest for this use case, deleting, recreating of multiple files/Realms is really not needed. It seems game data could be stored in just a couple of realms or even just one. There’s doesn’t seem to be a benefit to dividing it up amongst one realm per game.

A single GameObject table to store high level data and then additional tables to store details seems like it would work. That would be much faster, require less resources and if you decide to move to something cloud based where the user can pick up a different device and continue to play the same game would make that scenario easier.

However, we don’t know the full scope of the app so perhaps there’s another reason to store multiple realms. Glad I could help!