|
| 1 | +.. _swiftui-background-sync: |
| 2 | + |
| 3 | +===================================== |
| 4 | +Sync Data in the Background - SwiftUI |
| 5 | +===================================== |
| 6 | + |
| 7 | +.. meta:: |
| 8 | + :description: Learn how to use a SwiftUI BackgroundTask to sync data in the background. |
| 9 | + :keywords: Realm, Swift SDK, code example |
| 10 | + |
| 11 | +.. facet:: |
| 12 | + :name: genre |
| 13 | + :values: tutorial |
| 14 | + |
| 15 | +.. facet:: |
| 16 | + :name: programming_language |
| 17 | + :values: swift |
| 18 | + |
| 19 | +.. contents:: On this page |
| 20 | + :local: |
| 21 | + :backlinks: none |
| 22 | + :depth: 2 |
| 23 | + :class: singlecol |
| 24 | + |
| 25 | +Overview |
| 26 | +-------- |
| 27 | + |
| 28 | +You can use a SwiftUI :apple:`BackgroundTask <documentation/SwiftUI/BackgroundTask>` |
| 29 | +to update a synced database when your app is in the background. This example |
| 30 | +demonstrates how to configure and perform background syncing in an iOS app. |
| 31 | + |
| 32 | +You can follow along with the example on this page using the SwiftUI Device |
| 33 | +Sync Template App. To get your own copy of the SwiftUI Device Sync |
| 34 | +Template App, check out the :ref:`Device Sync SwiftUI tutorial |
| 35 | +<swift-swiftui-tutorial>` and go through the :guilabel:`Prerequisites` |
| 36 | +and :guilabel:`Start with the Template` sections. |
| 37 | + |
| 38 | +Enable Background Modes for Your App |
| 39 | +------------------------------------ |
| 40 | + |
| 41 | +To enable background tasks for your app: |
| 42 | + |
| 43 | +.. procedure:: |
| 44 | + |
| 45 | + .. step:: Add Background Modes Capability |
| 46 | + |
| 47 | + Select your app Target, go to the :guilabel:`Signing & Capabilities` |
| 48 | + tab, and click :guilabel:`+ Capability` to add the capability. |
| 49 | + |
| 50 | + .. figure:: /images/xcode-select-target-add-capability.png |
| 51 | + :alt: Screenshot of Xcode with app Target selected, Signing & Capabilities tab open, and arrow pointing to add Capabilities. |
| 52 | + :lightbox: |
| 53 | + |
| 54 | + Search for "background", and select :guilabel:`Background Modes`. |
| 55 | + |
| 56 | + .. step:: Select Background Modes |
| 57 | + |
| 58 | + Now you should see a :guilabel:`Background Modes` section in your |
| 59 | + :guilabel:`Signing & Capabilities` tab. Expand this section, and |
| 60 | + click the checkboxes to enable :guilabel:`Background fetch` and |
| 61 | + :guilabel:`Background processing`. |
| 62 | + |
| 63 | + .. step:: Update the Info.plist |
| 64 | + |
| 65 | + Go to your project's :file:`Info.plist`, and add a new row for |
| 66 | + ``Permitted background task scheduler identifiers``. If you are |
| 67 | + viewing raw keys and values, the key is |
| 68 | + ``BGTaskSchedulerPermittedIdentifiers``. This field is an array. |
| 69 | + Add a new item to it for your background task identifier. Set the |
| 70 | + new item's value to the string you intend to use as the identifier |
| 71 | + for your background task. For example: ``refreshTodoRealm``. |
| 72 | + |
| 73 | +Schedule a Background Task |
| 74 | +-------------------------- |
| 75 | + |
| 76 | +After enabling background processes for your app, you can start adding the |
| 77 | +code to the app to schedule and execute a background task. First, import |
| 78 | +``BackgroundTasks`` in the files where you will write this code: |
| 79 | + |
| 80 | +.. code-block:: swift |
| 81 | + :emphasize-lines: 3 |
| 82 | + |
| 83 | + import SwiftUI |
| 84 | + import RealmSwift |
| 85 | + import BackgroundTasks |
| 86 | + |
| 87 | +Now you can add a scheduled background task. If you're following along |
| 88 | +via the Template App, you can update your ``@main`` view: |
| 89 | + |
| 90 | +.. code-block:: swift |
| 91 | + :emphasize-lines: 3, 9-14 |
| 92 | + |
| 93 | + @main |
| 94 | + struct realmSwiftUIApp: SwiftUI.App { |
| 95 | + @Environment(\.scenePhase) private var phase |
| 96 | + |
| 97 | + var body: some Scene { |
| 98 | + WindowGroup { |
| 99 | + ContentView(app: realmApp) |
| 100 | + } |
| 101 | + .onChange(of: phase) { newPhase in |
| 102 | + switch newPhase { |
| 103 | + case .background: scheduleAppRefresh() |
| 104 | + default: break |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | +You can add an environment variable to store a change to the ``scenePhase``: |
| 110 | +``@Environment(\.scenePhase) private var phase``. |
| 111 | + |
| 112 | +Then, you can add the ``.onChange(of: phase)`` block that calls the |
| 113 | +``scheduleAppRefresh()`` function when the app goes into the background. |
| 114 | + |
| 115 | +Create the ``scheduleAppRefresh()`` function: |
| 116 | + |
| 117 | +.. code-block:: swift |
| 118 | + |
| 119 | + func scheduleAppRefresh() { |
| 120 | + let backgroundTask = BGAppRefreshTaskRequest(identifier: "refreshTodoRealm") |
| 121 | + backgroundTask.earliestBeginDate = .now.addingTimeInterval(10) |
| 122 | + try? BGTaskScheduler.shared.submit(backgroundTask) |
| 123 | + } |
| 124 | + |
| 125 | +This schedules the work to execute the background task whose identifier you |
| 126 | +added to the Info.plist above when you enabled Background Modes. In this |
| 127 | +example, the identifier ``refreshTodoRealm`` refers to this task. |
| 128 | + |
| 129 | +Create the Background Task |
| 130 | +-------------------------- |
| 131 | + |
| 132 | +Now that you've scheduled the background task, you need to create the background |
| 133 | +task that will run to update the synced realm. |
| 134 | + |
| 135 | +If you're following along with the Template App, you can add this |
| 136 | +``backgroundTask`` to your ``@main`` view, after the ``.onChange(of: phase)``: |
| 137 | + |
| 138 | +.. code-block:: swift |
| 139 | + :emphasize-lines: 7-23 |
| 140 | + |
| 141 | + .onChange(of: phase) { newPhase in |
| 142 | + switch newPhase { |
| 143 | + case .background: scheduleAppRefresh() |
| 144 | + default: break |
| 145 | + } |
| 146 | + } |
| 147 | + .backgroundTask(.appRefresh("refreshTodoRealm")) { |
| 148 | + guard let user = realmApp.currentUser else { |
| 149 | + return |
| 150 | + } |
| 151 | + let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in |
| 152 | + if let foundSubscription = subs.first(named: "user_tasks") { |
| 153 | + foundSubscription.updateQuery(toType: Item.self, where: { |
| 154 | + $0.owner_id == user.id |
| 155 | + }) |
| 156 | + } else { |
| 157 | + subs.append(QuerySubscription<Item>(name: "user_tasks") { |
| 158 | + $0.owner_id == user.id |
| 159 | + }) |
| 160 | + } |
| 161 | + }, rerunOnOpen: true) |
| 162 | + await refreshSyncedRealm(config: config) |
| 163 | + } |
| 164 | + |
| 165 | +This background task first checks that your app has a logged-in user. If so, |
| 166 | +it sets a :swift-sdk:`.flexibleSyncConfiguration |
| 167 | +<Extensions/User.html#/s:So7RLMUserC10RealmSwiftE25flexibleSyncConfiguration15clientResetMode20initialSubscriptions11rerunOnOpenAC0B0V0F0VAC06ClienthI0O_yAC0E15SubscriptionSetVcSbtF>` |
| 168 | +with a :ref:`subscription <sdks-manage-sync-subscriptions>` the |
| 169 | +app can use to sync the realm. |
| 170 | + |
| 171 | +This is the same configuration used in the Template App's ``ContentView``. |
| 172 | +However, to use it here you need access to it farther up the view hierarchy. |
| 173 | +You could refactor this to a function you can call from either view that |
| 174 | +takes a :swift-sdk:`User <Typealiases.html#/s:10RealmSwift4Usera>` as a |
| 175 | +parameter and returns a :swift-sdk:`Realm.configuration |
| 176 | +<Structs/Realm/Configuration.html>`. |
| 177 | + |
| 178 | +Finally, this task awaits the result of a function that actually syncs the |
| 179 | +database. Add this function: |
| 180 | + |
| 181 | +.. code-block:: swift |
| 182 | + |
| 183 | + func refreshSyncedRealm(config: Realm.Configuration) async { |
| 184 | + do { |
| 185 | + try await Realm(configuration: config, downloadBeforeOpen: .always) |
| 186 | + } catch { |
| 187 | + print("Error opening the Synced realm: \(error.localizedDescription)") |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | +By opening this synced database and using the ``downloadBeforeOpen`` parameter |
| 192 | +to specify that you want to download updates, you load the fresh data into |
| 193 | +the database in the background. Then, when your app opens again, it already |
| 194 | +has the updated data on the device. |
| 195 | + |
| 196 | +.. important:: |
| 197 | + |
| 198 | + Do not try to write to the database directly in this background task. You |
| 199 | + may encounter threading-related issues due to the SDK's thread-confined |
| 200 | + architecture. |
| 201 | + |
| 202 | +Test Your Background Task |
| 203 | +------------------------- |
| 204 | + |
| 205 | +When you schedule a background task, you are setting the earliest time that |
| 206 | +the system could execute the task. However, the operating system factors in |
| 207 | +many other considerations that may delay the execution of the background task |
| 208 | +long after your scheduled ``earliestBeginDate``. Instead of waiting for a |
| 209 | +device to run the background task to verify it does what you intend, you can |
| 210 | +set a breakpoint and use LLDB to invoke the task. |
| 211 | + |
| 212 | +.. procedure:: |
| 213 | + |
| 214 | + .. step:: Configure a Device to Run Your App |
| 215 | + |
| 216 | + To test that your background task is updating the synced database in the |
| 217 | + background, you'll need a physical device running at minimum iOS 16. |
| 218 | + Your device must be configured to run in :apple:`Developer Mode |
| 219 | + <documentation/xcode/enabling-developer-mode-on-a-device>`. If you get an |
| 220 | + ``Untrusted Developer`` notification, go to :guilabel:`Settings`, |
| 221 | + :guilabel:`General`, and :guilabel:`VPN & Device Management`. Here, you |
| 222 | + can verify that you want to run the app you're developing. |
| 223 | + |
| 224 | + Once you can successfully run your app on your device, you can test the |
| 225 | + background task. |
| 226 | + |
| 227 | + .. step:: Set a Breakpoint |
| 228 | + |
| 229 | + Start by setting a breakpoint in your ``scheduleAppRefresh()`` function. |
| 230 | + Set the breakpoint *after* the line where you submit the task to |
| 231 | + ``BGTaskScheduler``. For this example, you might add a ``print`` line and |
| 232 | + set the breakpoint at the print line: |
| 233 | + |
| 234 | + .. code-block:: swift |
| 235 | + :emphasize-lines: 5 |
| 236 | + |
| 237 | + func scheduleAppRefresh() { |
| 238 | + let backgroundTask = BGAppRefreshTaskRequest(identifier: "refreshTodoRealm") |
| 239 | + backgroundTask.earliestBeginDate = .now.addingTimeInterval(10) |
| 240 | + try? BGTaskScheduler.shared.submit(backgroundTask) |
| 241 | + print("Successfully scheduled a background task") // Set a breakpoint here |
| 242 | + } |
| 243 | + |
| 244 | + .. step:: Run the App |
| 245 | + |
| 246 | + Now, run the app on the connected device. Create or sign into an account |
| 247 | + in the app. If you're using the SwiftUI Template App, create some Items. |
| 248 | + You should see the Items sync to the ``Item`` collection linked to your |
| 249 | + Atlas App Services app. |
| 250 | + |
| 251 | + Then, while leaving the app running in Xcode, send the app to the background |
| 252 | + on your device. You should see the console print "Successfully scheduled a |
| 253 | + background task" and then get an LLDB prompt. |
| 254 | + |
| 255 | + .. step:: Add or Change Data in Atlas |
| 256 | + |
| 257 | + While the app is in the background but still running in Xcode, Insert a new |
| 258 | + document in the relevant Atlas collection that should sync to the device. |
| 259 | + Alternately, change a value of an existing document that you created from |
| 260 | + the device. After successfully running the background task, you should |
| 261 | + see this data synced to the device from the background process. |
| 262 | + |
| 263 | + If you're using the SwiftUI Template App, you can find relevant documents |
| 264 | + in your Atlas cluster's ``Item`` collection. For more information on how |
| 265 | + to add or change documents in Atlas, see: :atlas:`MongoDB Atlas: Create, |
| 266 | + View, Update, and Delete Documents </atlas-ui/documents/>`. |
| 267 | + |
| 268 | + .. step:: Invoke the Background Task in LLDB |
| 269 | + |
| 270 | + Use this command to manually execute the background task in LLDB: |
| 271 | + |
| 272 | + .. code-block:: shell |
| 273 | + |
| 274 | + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"refreshTodoRealm"] |
| 275 | + |
| 276 | + If you have used a different identifier for your background task, replace |
| 277 | + ``refreshTodoRealm`` with your task's identifier. This causes the task to |
| 278 | + immediately begin executing. |
| 279 | + |
| 280 | + If successful, you should see something like: |
| 281 | + |
| 282 | + .. code-block:: shell |
| 283 | + |
| 284 | + 2022-11-11 15:09:10.403242-0500 App[1268:196548] Simulating launch for task with identifier refreshTodoRealm |
| 285 | + 2022-11-11 15:09:16.530201-0500 App[1268:196811] Starting simulated task |
| 286 | + |
| 287 | + After you have kicked off the task, use the :guilabel:`Continue program execution` |
| 288 | + button in the Xcode debug panel to resume running the app. |
| 289 | + |
| 290 | + .. step:: Turn on Airplane Mode on the Device |
| 291 | + |
| 292 | + After waiting for the background task to complete, but before you open the |
| 293 | + app again, turn on Airplane Mode on the device. Make sure you have turned |
| 294 | + off WiFi. This ensures that when you open the app again, it doesn't |
| 295 | + start a fresh Sync and you see only the values that are now in the database |
| 296 | + on the device. |
| 297 | + |
| 298 | + .. step:: Open the App |
| 299 | + |
| 300 | + Open the app on the device. You should see the updated data that you changed |
| 301 | + in Atlas. |
| 302 | + |
| 303 | + To verify the updates came through the background task, confirm you have |
| 304 | + successfully disabled the network. |
| 305 | + |
| 306 | + Create a new task using the app. You should see the task in the app, but |
| 307 | + it should not sync to Atlas. Alternately, you could create or change data |
| 308 | + in Atlas, but should not see it reflected on the device. |
| 309 | + |
| 310 | + This tells you that the network has successfully been disabled, |
| 311 | + and the updated data that you see came through the background task. |
0 commit comments