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.