Realm Swift Tutorial failing offline-first usage

I’ve been a fan and using Realm (locally) since 2014 in multiple apps. Even discussed sync with the original team back in the day. And like @Richard_Krueger , am excited to usher in a better way of building my iOS apps. Especially after having written three (3) sync engines, and really don’t want to write another one.

After finally dedicating some time to deep-dive into your new Realm-Sync platform, when I run through your realm swift tutorial, every time I disable data connectivity for the app, it either crashes or freezes.

Which in your messaging “should just work”.

What am I missing to validate offline-first support?

Can you give us a bit more information on what that means. Have you added a breakpoint and stepped through the code to see what specifically is crashing?

Can you also detail that as well so we understand the use case?

I downloaded the app you linked, unzipped it, did a pod install and then ran the app and it’s working correctly for me.

Certainly Jay.

I’ve updated my github comment with a video demonstrating my offline testing process.

Excellent. I am duplicating that issue as well. (Note that Sync is Beta as this time)

If the app is offline to start with, the error is being correctly trapped when clicking the Sign In button. Here’s the textual error.localizedDescription

Login failed: Error Domain=realm::app::JSONError Code=2 “[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: ‘T’” UserInfo={realm::app::JSONError=malformed json, NSLocalizedDescription=[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: ‘T’}

However, It’s not clear what path to take in these situations. The data is still on disk and should be accessible due to Realm being and offline first database but the docs are a bit thin on this topic.

Perhaps one of the Realm’ers could provide some insight.

I’ve sent up my app sandbox as well, which includes all of the Realm files.

The error that you’re getting when trying to log in while offline suggests that it’s actually getting a HTTP response from something, but that might also just be some error-handling gone wrong somewhere. We generally try to validate that the exact error reported is sensible, but this might be a case where we’re merely validating that an error is reported and the exact one is wrong…

Once the user has logged in once, we cache that locally and you can skip the login step on subsequent launches, which makes it possible to launch the app and access the data while offline. This does require not using Realm.asyncOpen() to open the Realm, though, as that waits for the latest data to be downloaded from the server before calling the callback. Instead you have to open the Realm synchronously using the normal initializer, which will open the data which is already present on the device and sync in the background. If you may also want to wait for the download to complete only the first time you open the Realm, which can be done by checking if it’s already present with Realm.fileExists(for: config) and opening the Realm in the appropriate way based on the result of that.

The crash at the end of the video is because we’re reporting that an asyncOpen() was cancelled due to the user being logged out; if you expose the option to log out users you’ll need to handle that error.

Once the user has logged in once, we cache that locally and you can skip the login step on subsequent launches, which makes it possible to launch the app and access the data while offline.

So you essentially preserve the session. On disk or inside Keychain?

This does require not using Realm.asyncOpen() to open the Realm

:thinking: peculiar, as everywhere I’ve read states that you should use Realm.asyncOpen(...)

Am I missing some docs or misread them somewhere?

you may also want to wait for the download to complete

Correct. A progress spinner would be useful. How do I check on the realm sync status and its progress?

The user session information is stored in a metadata Realm that’s encrypted using a key we automatically generate and store in the keychain.

When to use and not use Realm.asyncOpen() is definitely something we’ve had inconsistent messaging on (and it’s gotta be the most common thing that people get tripped up by which suggests it was a bad choice of API too). Not using it for the first open used to make the server do a bunch of extra work so we tried to get everyone to err on the side of using it, but after the first open it just depends on whether displaying no data is better than displaying stale data (which is sometimes but usually not the case).

Realm.asyncOpen() returns an AsyncOpenTask which you can call addProgressNotification() on to get progress notifications for the open. After opening a Realm you can call realm.syncSession.addProgressNotification() to get notifications about either the current set of sync work being done or an open-ended notification whenever data is synchronized in the future.

1 Like

The above is really good info. I know I saw it somewhere but is there list of error codes in result on .failure when logging in?

At the moment, here’s a snippet of how we’re handling connecting when the app is offline. It’s just a rough-in at the moment but appears to work.

@objc func signIn() {
    setLoading(true);
    app.login(credentials: Credentials.emailPassword(email: email!, password: password!)) { [weak self](result) in
        DispatchQueue.main.async {
            self!.setLoading(false);
            switch result {
            case .failure(let error):
                self.handleLocalConnection() //need to handle per error type
            case .success(let user):
                print("connected and logging in")
                //code as it was before
            }
        }
    }
}

func handleLocalConnection() {
    if let user = app.currentUser {
        var configuration = user.configuration(partitionValue: "user=\(user.id)")
        configuration.objectTypes = [User.self, Project.self]
        let realm = try! Realm(configuration: configuration)
        self.navigationController!.pushViewController(ProjectsViewController(userRealm: realm), animated: true);
    } else {
        print("user was nil")
    }
}

Thanks guys,

Confusing barriers like these really hurt the brand and adoption rate, especially given that most people (myself included) compare it to Firebase ramp-up. I want my team to adopt and use Realm Sync, without too many debates.

I think a better tutorial and docs update are needed to help smooth this learning process.

Thanks for the code @Jay , I’ll give it a try and continue to tinker with examples until I structure a working template.

fyi: for others, here’s the doc info on Realm.asyncOpen

Thanks for the feedback. As Thomas mentioned the advice on when to use asyncOpen() has been inconsistent. We’ll update the tutorials soon.

2 Likes

@Thomas_Goyne - can you confirm what the behaviour is supposed to be when calling Realm.asyncOpen() while the network is down (i.e. offline) ?

It will wait for the download to complete, which will obviously never happen. If you set a timeout then it’ll eventually time out.

So, essentially, throughout your code as you call Realm.asyncOpen(), you need to wrap it with checking for offline status, so that you can switch to calling Realm().

Realm.asyncOpen() is the only call that does the syncing correct?
Or if I’m offline, open with Realm(), and then the app goes online, does a background sync automatically happen? or do have to call with Realm.asyncOpen() again after detecting being online?

Is there a state flow diagram on this somewhere to see what happens when during the different online/offline modes?

If you do not specifically want to wait for synchronization to happen before opening the Realm there is no reason to ever call Realm.asyncOpen(). While a Realm is open synchronization happens in the background regardless of how you opened it. If the device goes offline and then back online while a Realm is open, the synchronization will automatically reconnect and resume.

Using Realm.asyncOpen() is not a requirement of Realm Sync and if it’s not what your program wants you never have to call it.

Reading the fine print… I guess you could customise how asyncOpen() works such that if the network is not available it will default to open the local realm after it times out. It would be useful if the timeout was associated with the network rather than completion of the sync - it is also unclear whether asyncOpen() will wait until all pending downloads are completed or just the first one.

const realmConfig = {
        sync: {
            user: app.currentUser,
            partitionValue: partitionKey,
            // The behavior to use when this is the first time opening a realm.
            newRealmFileBehavior: {
               type: "downloadBeforeOpen",
              timeout: 2,
              timeOutBehaviour: "openLocalRealm"
           },
           // The behavior to use when a realm file already exists locally,
           // i.e. you have previously opened the realm.
           existingRealmFileBehavior: {
              type: "openImmediately"
           },
        },
    };

    const target_realm = new Realm(realmConfig);

Using Realm.asyncOpen() is not a requirement of Realm Sync and if it’s not what your program wants you never have to call it.

I thought you had to call Realm.asyncOpen() at least the very first time an app is launched to sync down any contents.

@Duncan_Groenewald , that’s neat! I didn’t know about those sync configuration parameters. Thank you.

You only need to call asyncOpen() if for some reason you want the app to download data before it becomes available to the user. For example of your database contains some static shared data that the app needs in order to function then you may need to sync this before the app can be used.

If your app has any sort of shared data then you often want to use asyncOpen() and show a loading bar when the user first logs in rather than going straight to the main UI with nothing present and then updating the UI as the data becomes available, but that isn’t a requirement.

I agree with that UX and it makes sense.

So while Realm.asyncOpen() will immediately trigger a sync, I wonder how long before a background sync initiates when simply opening with Realm()?
Can you force triggering a sync operation on an opened Realm at any time?