iOS Live Activities in React Native

Live Activities in iOS allow apps to display up-to-date information right in the user's lock screen and, on devices which support it, within the Dynamic Island.

I created an Expo Module called react-native-widget-extension that makes it super easy to add Widgets and Live Activities to iOS apps built with React Native and interact with them from JavaScript land.

<img src="/images/posts/live-activities.jpg" />

Please note that the widgets themselves still need to be created in Swift. My library provides the module and plugin to add the build target to the iOS app and provides the necessary function in JavaScript to interact with the Live Activity. To demonstrate how easy this is to use, I have now added an example to GitHub repo. The example uses a Live Activity provided in the OneSignal iOS Sample. Here are the steps I took to make this example work:

Create React Native/Expo app

npx create-expo-app LiveActiviesExample

Install react-native-widget-extension

npx expo install react-native-widget-extension

Add config to app.json

{ "plugins": [ [ "../app.plugin.js", { "frequentUpdates": true, "widgetsFolder": "SampleWidgetExtension" } ] ] }

Drop in widget folder

Take the [widget folder from the OneSignal iOS Sample](https://github.com/OneSignalDevelopers/onesignal-ios-sample/tree/main/SampleWidgetExtension) (or any other widget folder) and drop it into the project.

Add Module.swift and Attributes.swift

Now for the only code you have to write/adapt. Within the widgets folder you dropped in, you need to create two files, Module.swift and Attributes.swift.

The Modules.swift files contains the interface to be used from your app and will be modeled after [https://github.com/bndkt/react-native-widget-extension/blob/main/example/SampleWidgetExtension/Module.swift](https://github.com/bndkt/react-native-widget-extension/blob/main/example/SampleWidgetExtension/Module.swift). I can not provide it with the library because the code will be very specific to your widget, although I'm looking for ways to make this even easier.

import ExpoModulesCore import ActivityKit internal class MissingCurrentWindowSceneException: Exception { override var reason: String { "Cannot determine the current window scene in which to present the modal for requesting a review." } } public class ReactNativeWidgetExtensionModule: Module { public func definition() -> ModuleDefinition { Name("ReactNativeWidgetExtension") Function("areActivitiesEnabled") { () -> Bool in let logger = Logger() logger.info("areActivitiesEnabled()") if #available(iOS 16.2, *) { return ActivityAuthorizationInfo().areActivitiesEnabled } else { return false } } Function("startActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in let logger = Logger() logger.info("startActivity()") if #available(iOS 16.2, *) { let future = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! let attributes = SportsLiveActivityAttributes(timer: Date.now...future, imageLeft: "Knights", teamNameLeft: "Knights", imageRight: "Pirates", teamNameRight: "Pirates", gameName: "Western Conference Round 1") let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText) let activityContent = ActivityContent(state: contentState, staleDate: Calendar.current.date(byAdding: .minute, value: 30, to: Date())!) do { let activity = try Activity.request(attributes: attributes, content: activityContent) logger.info("Requested a Live Activity \(String(describing: activity.id)).") } catch (let error) { logger.info("Error requesting Live Activity \(error.localizedDescription).") } } } Function("updateActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in let logger = Logger() logger.info("updateActivity()") if #available(iOS 16.2, *) { let future = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText) let alertConfiguration = AlertConfiguration(title: "Score update", body: "This is the alert text", sound: .default) let updatedContent = ActivityContent(state: contentState, staleDate: nil) Task { for activity in Activity<SportsLiveActivityAttributes>.activities { await activity.update(updatedContent, alertConfiguration: alertConfiguration) logger.info("Updated the Live Activity: \(activity.id)") } } } } Function("endActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in let logger = Logger() logger.info("endActivity()") if #available(iOS 16.2, *) { let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText) let finalContent = ActivityContent(state: contentState, staleDate: nil) Task { for activity in Activity<SportsLiveActivityAttributes>.activities { await activity.end(finalContent, dismissalPolicy: .default) logger.info("Ending the Live Activity: \(activity.id)") } } } } } }

The Attributes.swift file contains the "ActivityAttributes" and can likely be extracted from whatever widget you are using. In the case of the example, it was taken out of the [SampleWidgetExtension/SportsScoreLiveActivity.swift](https://github.com/OneSignalDevelopers/onesignal-ios-sample/blob/main/SampleWidgetExtension/SportsScoreLiveActivity.swift) file.

import ActivityKit import WidgetKit import SwiftUI struct SportsLiveActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var quarter: Int var scoreLeft: Int var scoreRight: Int var bottomText: String } var timer: ClosedRange<Date> var imageLeft: String // Knight var teamNameLeft: String // Kinghts var imageRight: String // Pirates var teamNameRight: String // Pirates var gameName: String // "Western Conference Round 1" }

Add JavaScript code to start, update, and end the live activity

import * as ReactNativeWidgetExtension from "react-native-widget-extension"; if (ReactNativeWidgetExtension.areActivitiesEnabled()) { ReactNativeWidgetExtension.startActivity( quarter, scoreLeft, scoreRight, bottomText ); }

Now, build the project and experience the magic.