Docs Menu
Docs Home
/ /
Atlas Device SDKs
/ /

Sync Data in the Background with SwiftUI - Swift SDK

On this page

  • Overview
  • Enable Background Modes for Your App
  • Add Background Modes Capability
  • Select Background Modes
  • Update the Info.plist
  • Schedule a Background Task
  • Create the Background Task
  • Test Your Background Task
  • Configure a Device to Run Your App
  • Set a Breakpoint
  • Run the App
  • Add or Change Data in Atlas
  • Invoke the Background Task in LLDB
  • Turn on Airplane Mode on the Device
  • Open the App

You can use a SwiftUI BackgroundTask to update a Synced realm when your app is in the background. This example demonstrates how to configure and perform background Syncing in an iOS app.

You can follow along with the example on this page using the SwiftUI Device Sync Template App. To get your own copy of the SwiftUI Device Sync Template App, check out the Device Sync SwiftUI tutorial and go through the Prerequisites and Start with the Template sections.

To enable background tasks for your app:

1

Select your app Target, go to the Signing & Capabilities tab, and click + Capability to add the capability.

Screenshot of Xcode with app Target selected, Signing & Capabilities tab open, and arrow pointing to add Capabilities.
click to enlarge

Search for "background", and select Background Modes.

2

Now you should see a Background Modes section in your Signing & Capabilities tab. Expand this section, and click the checkboxes to enable Background fetch and Background processing.

3

Go to your project's Info.plist, and add a new row for Permitted background task scheduler identifiers. If you are viewing raw keys and values, the key is BGTaskSchedulerPermittedIdentifiers. This field is an array. Add a new item to it for your background task identifier. Set the new item's value to the string you intend to use as the identifier for your background task. For example: refreshTodoRealm.

After enabling background processes for your app, you can start adding the code to the app to schedule and execute a background task. First, import BackgroundTasks in the files where you will write this code:

import SwiftUI
import RealmSwift
import BackgroundTasks

Now you can add a scheduled background task. If you're following along via the Template App, you can update your @main view:

@main
struct realmSwiftUIApp: SwiftUI.App {
@Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
ContentView(app: realmApp)
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .background: scheduleAppRefresh()
default: break
}
}
}

You can add an environment variable to store a change to the scenePhase: @Environment(\.scenePhase) private var phase.

Then, you can add the .onChange(of: phase) block that calls the scheduleAppRefresh() function when the app goes into the background.

Create the scheduleAppRefresh() function:

func scheduleAppRefresh() {
let backgroundTask = BGAppRefreshTaskRequest(identifier: "refreshTodoRealm")
backgroundTask.earliestBeginDate = .now.addingTimeInterval(10)
try? BGTaskScheduler.shared.submit(backgroundTask)
}

This schedules the work to execute the background task whose identifier you added to the Info.plist above when you enabled Background Modes. In this example, the identifier refreshTodoRealm refers to this task.

Now that you've scheduled the background task, you need to create the background task that will run to update the synced realm.

If you're following along with the Template App, you can add this backgroundTask to your @main view, after the .onChange(of: phase):

.onChange(of: phase) { newPhase in
switch newPhase {
case .background: scheduleAppRefresh()
default: break
}
}
.backgroundTask(.appRefresh("refreshTodoRealm")) {
guard let user = realmApp.currentUser else {
return
}
let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
if let foundSubscription = subs.first(named: "user_tasks") {
foundSubscription.updateQuery(toType: Item.self, where: {
$0.owner_id == user.id
})
} else {
subs.append(QuerySubscription<Item>(name: "user_tasks") {
$0.owner_id == user.id
})
}
}, rerunOnOpen: true)
await refreshSyncedRealm(config: config)
}

This background task first checks that your app has a logged-in user. If so, it sets a .flexibleSyncConfiguration with a subscription the app can use to sync the realm.

This is the same configuration used in the Template App's ContentView. However, to use it here you need access to it farther up the view hierarchy. You could refactor this to a function you can call from either view that takes a User as a parameter and returns a Realm.configuration.

Finally, this task awaits the result of a function that actually syncs the realm. Add this function:

func refreshSyncedRealm(config: Realm.Configuration) async {
do {
try await Realm(configuration: config, downloadBeforeOpen: .always)
} catch {
print("Error opening the Synced realm: \(error.localizedDescription)")
}
}

By opening this synced realm and using the downloadBeforeOpen parameter to specify that you want to download updates, you load the fresh data into the realm in the background. Then, when your app opens again, it already has the updated data on the device.

Important

Do not try to write to the realm directly in this background task. You may encounter threading-related issues due to Realm's thread-confined architecture.

When you schedule a background task, you are setting the earliest time that the system could execute the task. However, the operating system factors in many other considerations that may delay the execution of the background task long after your scheduled earliestBeginDate. Instead of waiting for a device to run the background task to verify it does what you intend, you can set a breakpoint and use LLDB to invoke the task.

1

To test that your background task is updating the synced realm in the background, you'll need a physical device running at minimum iOS 16. Your device must be configured to run in Developer Mode. If you get an Untrusted Developer notification, go to Settings, General, and VPN & Device Management. Here, you can verify that you want to run the app you're developing.

Once you can successfully run your app on your device, you can test the background task.

2

Start by setting a breakpoint in your scheduleAppRefresh() function. Set the breakpoint after the line where you submit the task to BGTaskScheduler. For this example, you might add a print line and set the breakpoint at the print line:

func scheduleAppRefresh() {
let backgroundTask = BGAppRefreshTaskRequest(identifier: "refreshTodoRealm")
backgroundTask.earliestBeginDate = .now.addingTimeInterval(10)
try? BGTaskScheduler.shared.submit(backgroundTask)
print("Successfully scheduled a background task") // Set a breakpoint here
}
3

Now, run the app on the connected device. Create or sign into an account in the app. If you're using the SwiftUI Template App, create some Items. You should see the Items sync to the Item collection linked to your Atlas App Services app.

Then, while leaving the app running in Xcode, send the app to the background on your device. You should see the console print "Successfully scheduled a background task" and then get an LLDB prompt.

4

While the app is in the background but still running in Xcode, Insert a new document in the relevant Atlas collection that should sync to the device. Alternately, change a value of an existing document that you created from the device. After successfully running the background task, you should see this data synced to the device from the background process.

If you're using the SwiftUI Template App, you can find relevant documents in your Atlas cluster's Item collection. For more information on how to add or change documents in Atlas, see: MongoDB Atlas: Create, View, Update, and Delete Documents.

5

Use this command to manually execute the background task in LLDB:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"refreshTodoRealm"]

If you have used a different identifier for your background task, replace refreshTodoRealm with your task's identifier. This causes the task to immediately begin executing.

If successful, you should see something like:

2022-11-11 15:09:10.403242-0500 App[1268:196548] Simulating launch for task with identifier refreshTodoRealm
2022-11-11 15:09:16.530201-0500 App[1268:196811] Starting simulated task

After you have kicked off the task, use the Continue program execution button in the Xcode debug panel to resume running the app.

6

After waiting for the background task to complete, but before you open the app again, turn on Airplane Mode on the device. Make sure you have turned off WiFi. This ensures that when you open the app again, it doesn't start a fresh Sync and you see only the values that are now in the realm on the device.

7

Open the app on the device. You should see the updated data that you changed in Atlas.

To verify the updates came through the background task, confirm you have successfully disabled the network.

Create a new task using the app. You should see the task in the app, but it should not sync to Atlas. Alternately, you could create or change data in Atlas, but should not see it reflected on the device.

This tells you that the network has successfully been disabled, and the updated data that you see came through the background task.

Back

Handle Sync Errors

Next

Use Realm with SwiftUI Previews