How can I avoid syncing on every commit?

Thanks you @Ian_Ward and @Jason_Flax for the great webinar today! I asked a question about transactions and costs, but I don’t think I got the context around the question right, so I’ll ask it here instead.

My app is a workout log app and I am using Realm locally, but want to move over to MongoDbRealm. Since I only use local realms, I have never had to worry about committing to edits to the database, but as I move to synced realms, each transaction causes sync and has a cost attached to it and because of this I will have to restructure the app to be better suited for sync.

Currently, each keystroke is saved locally. For a single user, this could easily be thousands of writes during a workout session and each would trigger a sync. From the user perspective, the only time you want to sync the data is when done with your session. Doing commits every keystroke, like I do today will both cause performance issues with sync and be expensive. Pausing sync won’t help as each transaction is still counted. Is there any other way?

I am thinking about doing something along these lines:

Creating a workout:
1: The user creates a workout and adds this to a local realm
2: The user edits the local realm
3: When the user is done, this is copied to the synced realm (1 commit right?)

When editing an existing workout
1: The user copies a synced workout to the local realm.
2: The user edits the local realm
3: When the user is done, the workout is copied to the synced realm.

Would this approach work? Do you have any other recommendations for this type of app, where you don’t need realtime sync and do loads of writes?

1 Like

Hey @Simon_Persson, thanks for joining the group! Really appreciated your question during the session as well.

It sounds like workouts effectively exist in a “draft” state before you want them synced. I can only give so much advice without seeing your data structures, but, I’ll give it a go based on your suggested approach (pseudocode incoming):

protocol Workout: Object {
    var name: String { get set }
    var reps: Int { get set }
    var sets: Int { get set }
    var weight: Double { get set }
}

class WorkoutDraft: Object, ObjectKeyIdentifiable, Workout {
    dynamic var name: String = ""
    @objc dynamic var reps: Int = 0
    @objc dynamic var sets: Int = 0
    @objc dynamic var weight: Double = 0
}

@objcMembers class SavedWorkout: Object, ObjectKeyIdentifiable, Workout {
    dynamic var name: String = ""
    dynamic var reps: Int = 0
    dynamic var sets: Int = 0
    dynamic var weight: Double = 0

    convenience init(fromDraft draft: WorkoutDraft) {
        self.init()
        self.name = draft.name
        self.reps = draft.reps
        self.sets = draft.sets
        self.weight = draft.weight
    }
}

struct WorkoutFormView<WorkoutType>: View where WorkoutType: Workout & ObjectKeyIdentifiable {
    @ObservedRealmObject var workout: WorkoutType
    @Environment(\.presentationMode) var presentation

    var body: some View {
        Form {
            Stepper(value: $workout.reps, label: { Text("reps: \(workout.reps)") })
            Stepper(value: $workout.sets, label: { Text("sets: \(workout.sets)") })
            Stepper(value: $workout.weight, step: 0.5, label: { Text("weight: \(workout.weight)") })
        }.navigationBarItems(trailing: Button("Save", action: {
            // TODO, this should already be thawed, will be worked out before release
            if let workout = workout.thaw() as? WorkoutDraft, let realm = workout.realm?.thaw() {
                try? syncedRealm.write {
                    syncedRealm.add(SavedWorkout(fromDraft: workout))
                }
                try? realm.write {
                    realm.delete(workout)
                }
                presentation.wrappedValue.dismiss()
            }
        }))
    }
}

struct MainView: View {
    @StateRealmObject var workouts: Results<SavedWorkout>
    @StateRealmObject var unsyncedWorkouts: Results<WorkoutDraft>
    var body: some View {
        NavigationView {
            List {
                ForEach(unsyncedWorkouts) { workout in
                    NavigationLink(workout.name, destination: WorkoutFormView(workout: workout))
                }
                ForEach(workouts) { workout in
                    NavigationLink(workout.name, destination: WorkoutFormView(workout: workout))
                }
            }.navigationTitle("workouts")
            .navigationBarItems(trailing: Button("add") {
                $unsyncedWorkouts.append(WorkoutDraft())
            })
        }
    }
}

@main
struct App: SwiftUI.App {
    var view: some View {
        MainView(workouts: syncedRealm.objects(SavedWorkout.self),
                 unsyncedWorkouts: localRealm.objects(WorkoutDraft.self))
    }

    var body: some Scene {
        WindowGroup { view }
    }
}

This is one solution of many. The “optimal” solution here largely depends on the details of the use case as well as the structure of how you respond to user intent, however, the general idea of consider local data as draft data is common enough.

Let me know if this gives you a decent idea of how to move forward.

1 Like

Thanks @Jason_Flax!

That helps! The objects here are a lot more complex and contains nested structures, but the approach makes sense. It also makes the transition from local to synced a bit easier I think. I had not thought about using separate datatypes for drafts vs synced sharing a common interface, but it sounds like a good idea.

For the case of updating, I assume that using realm.add(workout, update: .modified) would do the trick?

I am also thinking it might be a good idea to have the workout be self contained. I.e. correspond to one document in MongoDb Atlas, i.e. make sure that all “lists of sets” in the synced workout are of embedded object type. I have run into some problems migrating my local classes to this structure https://github.com/realm/realm-cocoa/issues/7060. But if I use separate objects for synced vs local realms and share an interface, I might be able to bypass this migration altogether and keep the current “Workout” as a local draft and then simply add a “Synced Workout” that contains a structure better adapted for sync (no floats, object ids, partitions, and embedded objects in lists).

Thanks again for the additional comments today @Jason_Flax and @Ian_Ward.

An additional question about local draft objects. I wasn’t aware of that you couldn’t copy local realm objects to synced objects (inconsistent histories?). My assumption here was that I would be able to copy the synced object to a local realm and use it as a draft object, then copy it back when done editing, but I guess I can’t do this?

Again, thanks for the great work on the new platform :slight_smile:

I wasn’t aware of that you couldn’t copy local realm objects to synced objects (inconsistent histories?)

You can copy local realm objects to to synced realms - the question on the meetup was regarding converting a local realm to sync realm - which you need to manually copy the objects over (akin to your draft use case when done).

1 Like

Thanks for the clarification! :slight_smile: I have had to wait for a recent bug fix in the cocoa sdk (converting lists to embedded objects for local realms), but now it sounds like I am all set. Thank you for your help!

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.