Sharing authentication and data between the iOS app and widget

Realm data storage and fine-grained notifications of changes in collections

Viktoras Laukevičius
Treatwell Engineering

--

tl;dr To build the mini-calendar iOS widget that mirrors our salon calendar in Treatwell Connect app we tried several approaches to share data between the app and widget. In the end we decided to use Realm and fine-grained notifications.

We wanted a widget that would show stylists or therapists an at-a-glance view of their upcoming appointments. Tapping an appointment would open more details within the Treatwell Connect app.

To make it work we had to come up with solutions to challenges like:

  • Access to the currently logged in user’s calendar
  • Sharing calendar data from the main iOS app to the widget app extension and vice versa

Sharing authentication

Making the widget capable of updating calendar events for the logged in user requires sharing of authentication. It can be done by either sharing of login credentials or access tokens / cookies.

The decision on the approach for sharing authentication between the iOS app and the Widget depends on the authentication system used and sometimes also on the minimum iOS version supported

  • Keychain. This is the most painless and secure method to share auth details between the main app and the extension. The best part is that after keychain sharing is enabled (Select Keychain sharing in the target’s Capabilities tab) on both main iOS app and the widget app extension (actually, any app extension), items can be accessed on both targets even without specification of kSecAttrAccessGroup
  • Shared Cookie Storage. Great approach to share auth (and other) cookies if supported iOS target is ≥iOS9.0
  • Manually sharing cookies (using shared UserDefaults). Saving serialised cookie data (i.e using NSKeyedArchiver) to the UserDefaults shared app group container and then loading them (i.e deserialising with NSKeyedUnarchiver) in the widget.
    To be able to save data to the shared UserDefaults container, both the main app target and the widget app extension target must be assigned to the same app group (turn on App Groups in the target’s Capabilities tab, create a group identifier with group. prefix and check it).

Fine-grained notifications driven sync

We’re using Realm as our primary data store. It saves us from needing to notify all the listeners when any of the stored data changes. We love it.

The Realm team has done a stellar job with collection fine-grained notifications. The best part is that notifications work perfectly not only in a local target file system, but in a secure application group container as well. This means that sync can be done not only to e.g. Spotlight index, but also to app extensions and of course to an Apple Watch app, as it is yet another app extension target.

iOS app events sync to widget

The Treatwell Connect calendar treatment event has a quite complex and deep structure containing different attributes about customers, stylists, treatments, offers, time granularity etc. Deep structure means that the whole schema tree of dependencies would have to be used in a new shared container Realm schema. That’s why reusing the same Realm models in a shared container is not a valid option, especially because the widget is only meant to display a read-only version of a diary showing only the most important attributes. To make this work, we introduced a LightCalendarEvent model containing only essential attributes.

A lightweight version of a calendar event of course has it’s tradeoffs — data from the main app will have to be transformed and mirrored to the shared Realm container. Luckily that’s easily done using Realm’s fine-grained notifications. On app launch a synchronisation mechanism is started which transforms calendar events to lightweight versions and synchronises them to a shared Realm container.

Widget events sync to iOS app

The LightCalendarEvent has an attribute defining whether it has to be synced to the main app. New events received by widget may not exist on the main app and this helps us better understand how to handle opening of the main app. App also expects to see more information than LightCalendarEvent has. The solution is to introduce a property of type Data that contains JSON data of the full calendar event.

On app launch the synchronisation mechanism kicks off. For each event flagged as needed to be synced to the main app, it constructs full calendar events from JSON data in LightCalendarEvent.

These mechanisms are just two code blocks, listening for notifications of updates to the Realm collection with the correct predicates. Therefore, the code for synchronisation or triggering of sync is not distributed across the whole app on each action and each HTTP response handler related to calendar events.

One more thing…

Initial synchronisation dispatch semaphore

Typically communication between the widget and app is done using deep links. The main app claims in Info.plist that it can handle custom url schemes and the widget then tries to open the URL with that scheme using NSExtensionContext . The widget then calls

func open(_ URL: URL, completionHandler: ((Bool) -> Void)?)

The URL is then passed to main app’s UIApplicationDelegate method:

func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool

The solution of opening details in the main app when a calendar event is tapped in the widget has three distinct scenarios:

  1. The calendar event is already in main app’s default Realm and can be opened — nothing needs to be done.
  2. The calendar event was received from the back-end to the widget so it does not exist in the main app’s default Realm, but the main app is running in the background — Luckily, running the sync mechanism is fast enough (again, thanks to Realm) to sync the data as soon as the widget stores the light version of it to the shared Realm container.
  3. The third and the most tricky scenario is when a calendar event is received from the back-end to the widget, it does not exist in the main app’s default Realm and main app is not running in background.

As the main app is not running and the widget will trigger the launch of the app, deep link handling will be executed before the light calendar events will be synced to the main app default Realm. This means that the deep link handler won’t be able to open details of the calendar event because the event won’t yet exist. To be able to sync items before handling of them we’ve introduced an initial sync semaphore (using DispatchSemaphore) which is freed after the initial sync of the light calendar events to the main app. This allows the widget deep link handler mechanism to show a loading indicator until the initial sync is done and then try to handle the URL of the calendar event.

I hope this will highlight some non-straightforward paths or at least confirm your thoughts about possible solutions to similar problems. Mucho apreciado for your 👏s in advance.

Viktoras builds Treatwell Connect app for iOS, integrating with every iOS feature that Apple has ever released. Follow Viktoras on Twitter

--

--