12, Sep 2024

React Native Live Activities: A Simple Guide to Implementation

This step-by-step guide walks you through integrating iOS Live Activities into React Native applications. With iOS 17 enhancements, discover how to make your apps more interactive with real-time notifications and seamless user experiences.

React Native Live Activities Integration

ABOUT THE AUTHOR

Dmitry Boyko, Android Team Lead

Danil Kasianenko,
React Developer

Danil Kasianenko, React Developer

Danil is a skilled React Native, React, and NextJS developer with deep expertise in performance optimization and creating complex animations and visual effects, delivering high-quality solutions for web and mobile applications.

Live Activities is an essential feature of iOS that enables dynamic interactive notifications to appear on the lock screen. These elaborate notifications can update a user on certain installed apps without requiring one to unlock a phone screen.

In this guide, we will walk you through integrating Live Activities with your React Native application. By the end of this article, you'll have a fully functioning Live Activity in your app with a detailed breakdown of each step. Let’s start.

What Live Activity Is and the Requirements for Using It

Live Activity is a widget feature from Apple designed to show up-to-date information about ongoing activities directly on the lock screen and Dynamic Island. Apple provides some conditions for using iOS Live Activity. Among them are:

  • Fixed Size: Widgets for Live Activities have predefined height and width limits set by Apple’s Guidelines.
  • 8-hour Limit: Live Activities can remain active for up to 8 hours. This duration is more than enough for most activities like event tracking or order deliveries.
  • iOS 17 Enhancements: From iOS 17, Live Activities provide full support for interaction through push notifications and can now allow users to interact directly with widgets via buttons, toggles, and other interactive elements, which wasn’t possible in iOS 16.

These enhancements offer new ways for businesses and developers to create more interactive experiences for users by keeping them constantly updated with relevant information on their screens.

Where Can Live Activities Be Used?

Live Activity iOS is ideal for a variety of real-time use cases in certain types of applications. Starting from iOS 17, you can also start and stop Live Activity via push notifications, while in iOS 16, this can only be done through the app.

Live activities can be used for:

  • Flight information: Live Activities can provide real-time information about flight progress, including where you are and how much time remains until your arrival;
  • Delivery tracking: Live Activities can show where your courier is and estimate their arrival;
  • Sports updates: Sports apps can display the current score of an ongoing match along with recent events.

Where Live Activities can be used

You may want to see an instance of Live Activity running in action when learning to integrate it. For demonstration purposes, we'll take the example of our case study - an app for the Australian rugby club "Rabbitohs". This app was designed to provide real-time updates on sports matches, displaying live scores and game progress. We implemented Live Activities in it to enable users to track match scores without needing to open the app.

Live Activities and iOS Widgets Overview

For the remainder of the article, we will explore the core functionalities of Live Activities and guide you through the process of adding this feature to a React Native app. So, buckle up, and let’s go.

Before diving into the technical aspects of React Native iOS Live Activities integration, it’s important to understand the structure and the sequence of actions. Two main stages are required for implementation:

  1. The first is adding the Live Activity Widget to the React Native app;
  2. The second is writing a Native Module for interacting with Live Activity in React Native.

Adding Live Activity Widget to a React Native App

In this section, we will cover how to add a Live Activity widget to your React Native project, set up native modules to bridge React Native with ActivityKit and ensure seamless communication between JavaScript and native iOS code.

Configuring the Live Activity Widget in Xcode

Let’s begin by setting up the Live Activity widget using Xcode. For this integration, we are assuming that your React Native project is running on version 0.72.4 or above.

1.1 Push Notification Configuration

Push notifications are critical for Live Activities because they enable real-time data updates. Follow these steps to configure push notifications for your app:

  1. Open your project in Xcode;
  2. Select the Target of your app;
  3. Navigate to Signing & Capabilities;
  4. Click the "+" button and select Push Notifications.

Push Notification Configuration

This step ensures that your app can receive real-time updates via the Apple Push Notification Service (APNs).

1.2 Creating the Live Activity Widget

Next, we will create a Live Activity widget within Xcode.

In Xcode, go to File > New Target and choose Widget Extension.

New Target

Widget Extension

Next, you need to configure the widget in the window. All you need to do is specify the widget's name (it must be saved for future integration). For example, choose the name 'Live Activity'. The name is used in many parts of the code, which may cause errors if the name differs from 'Live Activity.' Such places will be highlighted further:

Configure the widget in the window

Once the widget is created, it will appear in a new folder inside your project.

New folder inside project

Ensure that the Target Membership for the Live_ActivityLiveActivity.swift file is properly set for your app.

Target Membership

1.3 Enabling Live Activities in info.plist

After setting up the widget, we need to update the info.plist file to declare that the app supports Live Activities. To do this, go to the info.plist and hover the mouse over the Informal Property List until a '+' appears:

Updating the info.plist

Next, you need to select 'Supports Live Activities' from the list:

Supports Live Activities

After that, you need to switch the value from NO to YES:

Switching from from NO to YES

This step is crucial for ensuring that the iOS system recognizes and allows the app to use Live Activities.

1.4 Build Phases Configuration

Although the last step may not be necessary, Live Activities doesn't work in this version of React Native without it.

In Xcode, navigate to Build Phases and verify the following:

  1. Go to Embed App Extensions or Embed Foundation Extensions
  2. Ensure that Copy only when installing is unchecked

Build Phases Configuration

This completes the basic setup of Live Activity.

Getting started with the code

In the Live Activities folder (the name may vary), there are 6 files:

  • Live_ActivityBundle.swift
  • Live_ActivityLiveActivity.swift
  • Live_Activity.swift
  • AppIntent.swift
  • Assets
  • Info.plist

Live_ActivityBundle.swift

import WidgetKit
import SwiftUI
@main
struct Live_ActivityBundle: WidgetBundle {
    var body: some Widget {
//        Live_Activity() need for static widget
        Live_ActivityLiveActivity() // Live activity widget
    }
}

Live_ActivityBundle.swift defines a set of widgets for iOS Live Activities.

Live_ActivityLiveActivity.swift

import ActivityKit
import WidgetKit
import SwiftUI

struct Live_ActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var emoji: String
    }

    // Fixed non-changing properties about your activity go here!
    var name: String
}

struct Live_ActivityLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: Live_ActivityAttributes self ) {
context in
            // Lock screen/banner UI goes here
             VStack {
                Text("Hello from Live Activity
 (context.state.emoji)")
             }
             .activityBackgroundTint(Color.cyan)
             .activitySystemActionForegroundColor(Color.black)

         } dynamicIsland: { context in 
             DynamicIsland {
                 // Expanded UI goes here.  Compose the expanded UI
 through
                 // various regions, like
 leading/trailing/center/bottom
                 DynamicIslandExpandedRegion(.leading) {
                     Text("Leading")
                 }
                 DynamicIslandExpandedRegion(.trailing) {
                     Text("Trailing")
                 }
                 DynamicIslandExpandedRegion(.bottom) {
                     Text("Bottom (context.state.emoji)")
                      // more content
                  }
             } compactLeading: {
                 Text("L")
              } compactTrailing: {
                 Text("T (context.state.emoji)")
             } minimal: {
                 Text(context.state.emoji)
              }
              .widgetURL(URL(string: "http://www.apple.com"))
              .keylineTint(Color.red)
        }
     }
  }

This is the main file, and there are two sections that we need to focus on:

  • The first is the Live_ActivityAttributes section (the name may vary). In this structure, two types of variables are described:
  1. Fixed (name)
  2. Dynamic (emoji)
  3. Dynamic variables are updated in real-time via push notifications or React Native.
  4. Fixed are all other variables that do not require real-time updates.
  • The second is the Live_ActivityLiveActivity structure (the name may vary). This structure is responsible for the visual representation of the widget and is divided into two parts:
  1. VStack describes the widget's display on the lock screen.
  2. DynamicIsland describes the widget's display on the dynamic island.

As an example, a standard template provided by Apple was used.

  • Live_Activity.swift : This file is needed for a static widget, not for Live Activity. It was disabled in Live_ActivityBundle.swift;

  • AppIntent.swift: This file is also needed for the static widget;

  • Assets: A place for storing resources and media files.

Writing a Native Module for interacting with Live Activity

In this step, the focus will be on the interaction with Live Activity, while the implementation of the native module can be chosen by you. First, you need to create 3 files:

  • LiveActivityModule.swift

  • RCTLiveActivityModule.m

  • nativeModules-Bridging-Header.h


Creating LiveActivityModule.swift: In Xcode, go to File, then New, and select File…

Creating LiveActivityModule.swift

After that, select Swift File and create it with the name LiveActivityModule.

Swift File


Creating RCTLiveActivityModule.m:

Next, create the RCTLiveActivityModule.m file using the same method, but instead of a Swift File, select Objective-C File.

Objective-C File.


Creating nativeModules-Bridging-Header.h:

The final step is to create the nativeModules-Bridging-Header.h file using the same method, but this time, select Header File.

Header File

A crucial step is adding nativeModules-Bridging-Header.h as the Objective-C compiler in the project.

adding nativeModules-Bridging-Header.h

To do this, go to Build Settings, search for Swift C, and for Swift Compiler - General, set the filename to nativeModules-Bridging-Header.h

LiveActivityModule.swift

import ActivityKit

import Foundation

@objc(LiveActivitiModule)

class LiveActivitiModule: RCTEventEmitter {

	// Property to hold the live activity instance

	private var liveActivity: Activity<Live_ActivityAttributes>?

	// Boolean to track if listeners are active

	private var hasListeners: Bool = false

	// Add other necessary properties

	// Starts observing for events, sets up the live activity

	override func startObserving() {

		hasListeners = true

		do {

			// Define the attributes and initial content state of the live activity

			let attributes = Live_ActivityAttributes(name: "Hello")

			let contentState = Live_ActivityAttributes.ContentState(emoji: "😎")

			// Request to start a new live activity

			liveActivity = try Activity.request(attributes: attributes, contentState: contentState, pushType: .token)

			// Send the initial event to JavaScript

			sendEvent()

		} catch (let error) {

			print("Error requesting Y Live Activity (error.localizedDescription).")

		}

	}

	// Stops observing for events

	override func stopObserving() {

		hasListeners = false

	}

	// Defines the events that can be sent to JavaScript

	open override func supportedEvents() -> [String] {

		["onReady", "onPending", "onFailure", "SomeEvent"] // etc.

	}

	// Specifies that the module requires the main thread

	@objc override public static func requiresMainQueueSetup() -> Bool {

		return true

	}

	// Sends events to JavaScript if there are listeners

	func sendEvent() {

		if hasListeners {

			Task {

				do {

				guard let liveActivity = liveActivity else {

				// Handle the case where liveActivity is nil, or exit the task.

					throw NSError(domain: "Error liveActivity is nil" , code: 555)

				}

				for try await data in liveActivity.pushTokenUpdates {

					let myToken = data.map { String(format: "%02x", $0) }.joined()

					print(myToken)

					self.sendEvent(withName: "SomeEvent", body: ["token": myToken, "id": liveActivity.id])

					}

				} catch {

					print("Error obtaining token: (error.localizedDescription)")

				}

			}

		}

	}

}

// Another class to manage specific live activity actions

@objc(LiveActivity)

class LiveActivity: NSObject {

	// Ends a specific live activity given its ID

	@objc(endActivity:withResolver:withRejecter:)

	func endActivity(id: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			Task {

				await Activity<Live_ActivityAttributes>.activities.filter {$0.id == id}.first?.end(dismissalPolicy: .immediate)

			}

		} else {

			reject("Not available","", NSError())

		}

	}

	// Lists all currently active Live Activities

	@objc(listAllActivities:withRejecter:)

	func listAllActivities(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			var activities = Activity<Live_ActivityAttributes>.activities

			activities.sort { $0.id > $1.id }

			return resolve(activities.map{["id": $0.id ]})

		} else {

			reject("Not available", "", NSError())

		}

	}

	// Updates a specific live activity given its ID and new emoji state

	@objc(updateActivity:emoji:withResolver:withRejecter:)

	func updateActivity(id: String, emoji: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			Task {

				do {

					let updatedStatus = try Live_ActivityAttributes

					.ContentState(emoji: emoji)

					let activities = Activity<Live_ActivityAttributes>.activities

					let activity = activities.filter {$0.id == id}.first

					await activity?.update(using: updatedStatus)

			}catch {

				print("Error: (error.localizedDescription)")

			}

		}

		} else {

			reject("Not available", "", NSError())

		}

	}

}

LiveActivityModule.swift - This file contains all the logic for the interaction between Live Activity and React Native. The file is divided into two classes:

  • The first class is LiveActivityModule;
  • The second class is LiveActivity.

Both use ActivityKit and Foundation imports:

import ActivityKit

import Foundation
  • ActivityKit is used for interacting with Live Activities;
  • Foundation provides fundamental classes and protocols.

LiveActivityModule Class

The LiveActivityModule class extends RCTEventEmitter, allowing it to send events to React Native.

Changes

private var liveActivity: Activity<Live_ActivityAttributes>? // Live_ActivityAttributes may not match yours
private var hasListeners: Bool = false

Methods

// Starts observing for events, sets up the live activity
override func startObserving()

// Stops observing for events
override func stopObserving()

// Defines the events that can be sent to JavaScript
open override func supportedEvents()

// Specifies that the module requires the main thread
@objc override public static func requiresMainQueueSetup()

// Sends events to JavaScript if there are listeners
func sendEvent()

The LiveActivityModule class acts as a bridge between the JavaScript code in the React Native app and the native iOS functionality. To interact with this class on the React Native side, you need to call a method named addListener.

myModuleEvt.addListener("SomeEvent", (token) => {
	console.log("token ", token);
});

This works similarly to how events are handled in the DOM using addEventListener.

my_element.addEventListener("click", function (e) {
  console.log(this.className); // logs the className of my_element
});

After calling addListener, the method startObserving is triggered on the native side via the bridge.

func startObserving()

// Starts observing for events, sets up the live activity
  override func startObserving() {
    hasListeners = true
    do {
      // Define the attributes and initial content state of the live
activity
      let attributes = Live_ActivityAttributes(name: "Hello")
      let contentState = Live_ActivityAttributes.ContentState(emoji:
"😎")
      // Request to start a new live activity
      liveActivity = try Activity.request(attributes: attributes,
 contentState: contentState, pushType: .token)
       // Send the initial event to JavaScript
       sendEvent()
      } catch (let error) {
       print("Error requesting Y Live Activity
 (error.localizedDescription).")
     }
   }

hasListeners = true:

  • It sets the value of hasListeners to true, which means that events are currently being listened to;

  • Defines attributes and the initial state of the Live Activity.

    // Define the attributes and initial content state of the live activity

    let attributes = Live_ActivityAttributes(name: "Hello")

    let contentState = Live_ActivityAttributes.ContentState(emoji: "😎")

These are the fields declared in the Live_ActivityLiveActivity.swift file.

struct Live_ActivityAttributes: ActivityAttributes {

	public struct ContentState: Codable, Hashable {

	// Dynamic stateful properties about your activity go here!

		var emoji: String

	}
	// Fixed non-changing properties about your activity go here!

	var name: String

}

After that, a request is made to start the Live Activity.

// Request to start a new live activity

liveActivity = try Activity.request(attributes: attributes, contentState: contentState, pushType: .token)

The result is stored in the liveActivity variable.

At the end, the sendEvent() method is called.

// Send the initial event to JavaScript
sendEvent()

func sendEvent()

func sendEvent() {

		if hasListeners {

			Task {

				do {

				guard let liveActivity = liveActivity else {

				// Handle the case where liveActivity is nil, or exit the task.

					throw NSError(domain: "Error liveActivity is nil" , code: 555)

				}

				for try await data in liveActivity.pushTokenUpdates {

					let myToken = data.map { String(format: "%02x", $0) }.joined()

					print(myToken)

					self.sendEvent(withName: "SomeEvent", body: ["token": myToken, "id": liveActivity.id])

					}

				} catch {

					print("Error obtaining token: (error.localizedDescription)")

				}

			}

		}

	}

It first checks if hasListeners is set to true. Then, it checks whether the liveActivity variable is defined.

Token Retrieval

for try await data in liveActivity.pushTokenUpdates {
    let myToken = data.map { String(format: "%02x", $0) }.joined()
    print(myToken)
    self.sendEvent(withName: "SomeEvent", body: ["token": myToken,
"id": liveActivity.id])
}

Asynchronous token updates waiting:

for try await data in liveActivity.pushTokenUpdates
  • This line launches a loop that asynchronously waits for new token updates for the live activity;
  • liveActivity.pushTokenUpdates is an asynchronous sequence that returns data (tokens) in the form of byte arrays.

Converting bytes to hexadecimal format:

let myToken = data.map { String(format: "%02x", $0) }.joined()

data.map { String(format: "%02x", $0) }.joined() converts each byte into a hexadecimal string and joins them into a single string. This is necessary to obtain the token in a readable format.

Sending an event to JavaScript code:

  • self.sendEvent(withName: "SomeEvent", body: ["token": myToken, "id": liveActivity.id]) sends an event with the name "SomeEvent" and the data token and id to the JavaScript code;
  • myToken is the string of the token that was previously converted;
  • liveActivity.id is the identifier of the live activity, which will be needed later.

Logic and Necessity of the Function

  • Monitoring Live Activity token updates:

Reason: In iOS apps that use ActivityKit, the token can be updated during the lifecycle of the live activity. This can happen for various reasons, such as changes in the activity state, sending new messages, and more.

Logic: Since the token can be updated, we need to monitor these changes asynchronously so that the React Native app is always aware of the latest data.

  • Sending updated token and Live Activity ID to JavaScript code:

Reason: Multiple Live Activities can be running simultaneously in the app. To manage each one, it's necessary to know not only the new token but also the corresponding activity ID.

Logic: Each time the token is updated, we send it along with the activity ID to the JavaScript code. This allows the app to correctly identify which activity should be updated or stopped and use the appropriate token for further operations.

func stopObserving()

// Stops observing for events

override func stopObserving() {

	hasListeners = false

}

In this method, hasListeners = false is set so that sendEvent no longer sends information.

func supportedEvents()

// Defines the events that can be sent to JavaScript
open override func supportedEvents() -> [String] {
    return ["onReady", "onPending", "onFailure", "SomeEvent"]
}
  • open means that this method can be overridden in subclasses;
  • override indicates that this method overrides a method from the base class RCTEventEmitter.

Returning an array of strings:

return ["onReady", "onPending", "onFailure", "SomeEvent"]

This method returns an array of strings, each representing an event name that the native module can send to the JavaScript code.

The following events are supported in this case:

Three points (onReady, onPending, onFailure) are not implemented:

  • "onReady": Sent when the activity is ready to operate;

  • "onPending": Sent when the activity is pending;

  • "onFailure": Sent in case of failure or error;

  • "SomeEvent": This event can be sent when tokens are updated or for other specific events, as defined in the sendEvent() method.

func requiresMainQueueSetup()

@objc override public static func requiresMainQueueSetup() -> Bool {
    return true
}
  • @objc Annotation: @objc makes the method accessible to the Objective-C environment, which is necessary for integration with React Native, as React Native uses Objective-C under the hood for communication with native modules.

-Method Override:

  1. override means that this method overrides a method from the base class.
  2. public static means that the method is static (it can be called on the class rather than an instance) and public (accessible outside the module)
  • Determining the need for the main thread:

     return true
    

Returning true indicates that the initialization of the module must be performed on the main thread.

Why is this necessary?

React Native calls the requiresMainQueueSetup() method during module initialization to determine whether the module needs to be initialized on the main thread. This is important for modules that interact with UIKit or other APIs that must run on the main thread.

Example of use: Suppose the native LiveActivityModule uses some functions that must be executed on the main thread (e.g., updating the user interface). In this case, it's important for the module to be initialized on the main thread to avoid issues with multithreading.

class LiveActivity: NSObject

// Another class to manage specific live activity actions

@objc(LiveActivity)

class LiveActivity: NSObject {

	// Specifies that the module requires the main thread

	@objc static func requiresMainQueueSetup() -> Bool {

		return true

	}

	// Ends a specific live activity given its ID

	@objc(endActivity:withResolver:withRejecter:)

	func endActivity(id: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			Task {

				await Activity<Live_ActivityAttributes>.activities.filter {$0.id == id}.first?.end(dismissalPolicy: .immediate)

			}

		} else {

			reject("Not available","", NSError())

		}

	}

	// Lists all currently active Live Activities

	@objc(listAllActivities:withRejecter:)

	func listAllActivities(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			var activities = Activity<Live_ActivityAttributes>.activities

			activities.sort { $0.id > $1.id }

			return resolve(activities.map{["id": $0.id ]})

		} else {

			reject("Not available", "", NSError())

		}

	}

	// Updates a specific live activity given its ID and new emoji state

	@objc(updateActivity:emoji:withResolver:withRejecter:)

	func updateActivity(id: String, emoji: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			Task {

				do {

					let updatedStatus = try Live_ActivityAttributes

					.ContentState(emoji: emoji)

					let activities = Activity<Live_ActivityAttributes>.activities

					let activity = activities.filter {$0.id == id}.first

					await activity?.update(using: updatedStatus)

			}catch {

				print("Error: (error.localizedDescription)")

			}

		}

		} else {

			reject("Not available", "", NSError())

		}

	}

}

This class manages specific actions related to Live Activities in the iOS app. It implements three main functions:

  1. Ending the activity
  2. Listing all active Live Activities
  3. Updating the activity

The LiveActivity class inherits from NSObject. In Swift, NSObject is the base class for most iOS classes that interact with Objective-C. It provides a fundamental level of functionality that allows classes to interact with the Objective-C environment. This is important for creating native modules in React Native, which uses Objective-C to bridge between JavaScript and native code.

Methods:

// Specifies that the module requires the main thread
func requiresMainQueueSetup()

// Ends a specific live activity given its ID
func endActivity()

// Lists all currently active Live Activities
func listAllActivities()

// Updates a specific live activity given its ID and new emoji state
func updateActivity()

func requiresMainQueueSetup()

// Specifies that the module requires the main thread

@objc static func requiresMainQueueSetup() -> Bool {

	return true

}

The static method requiresMainQueueSetup, which returns true, indicating that this module must be initialized on the main thread. This is important because this class is connected to the LiveActivityModule class, which is initialized on the main thread, so the LiveActivity class must also run on the main thread for proper synchronization.

func endActivity()

// Ends a specific live activity given its ID

	@objc(endActivity:withResolver:withRejecter:)

	func endActivity(id: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

		if #available(iOS 16.1, *) {

			Task {

				await Activity<Live_ActivityAttributes>.activities.filter {$0.id == id}.first?.end(dismissalPolicy: .immediate)

			}

		} else {

			reject("Not available","", NSError())

		}

	}

This method terminates a specific live activity using its ID. Parameters:

  • id: The identifier of the activity to be terminated;
  • resolve: The promise resolve block, which is called if the operation is successful;
  • reject: The promise reject block, called if the operation fails.

On the React Native side, this would look like calling an async function.

import { NativeModules } from 'react-native';
const { LiveActivity } = NativeModules;
const endActivity = async (id) => {
  try {
    await LiveActivity.endActivity(id);
  } catch (error) {
    console.error('Failed to end activity:', error);
  }
};

func listAllActivities()

// Lists all currently active Live Activities

@objc(listAllActivities:withRejecter:)

func listAllActivities(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

	if #available(iOS 16.1, *) {

		var activities = Activity<Live_ActivityAttributes>.activities

		activities.sort { $0.id > $1.id }

		return resolve(activities.map{["id": $0.id ]})

	} else {

		reject("Not available", "", NSError())

	}

}

listAllActivities Method is responsible for retrieving a list of all current Live Activities in the iOS app. It implements an asynchronous call that returns an array of activities or triggers an error if the functionality is not supported.

func updateActivity()

/ Updates a specific live activity given its ID and new emoji state

@objc(updateActivity:emoji:withResolver:withRejecter:)

func updateActivity(id: String, emoji: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {

	if #available(iOS 16.1, *) {

		Task {

			do {

				let updatedStatus = try Live_ActivityAttributes

				.ContentState(emoji: emoji)

				let activities = Activity<Live_ActivityAttributes>.activities

				let activity = activities.filter {$0.id == id}.first

				await activity?.update(using: updatedStatus)

			} catch {

				print("Error: (error.localizedDescription)")

			}

		}

	} else {

		reject("Not available", "", NSError())

	}

	}

}

This method is crucial as it can serve as an alternative to updating through push notifications. It allows using Live Activity without third-party services and updating all information directly from the app, ensuring more integrated and controlled management of activities.

  • Alternative to push notifications;
  • Direct update from the app;
  • Reduced dependency on third-party services.

updateActivity Method

This method allows updating a specific live activity by changing its state to a new emoji. It implements an asynchronous call that updates the activity or triggers an error if the functionality is not supported. Parameters:

  • id: String: The ID of the activity to be updated;
  • emoji: String: The new state of the activity as an emoji;
  • resolve: RCTPromiseResolveBlock: The block called upon successful method execution, passing the result to JavaScript;
  • reject: RCTPromiseRejectBlock: The block called upon an error, passing the error to JavaScript.

nativeModules-Bridging-Header.h

// Import the React Native bridge module header
#import <React/RCTBridgeModule.h>

// Import the React Native event emitter header
#import <React/RCTEventEmitter.h>

// Import the Foundation framework header
#import <Foundation/Foundation.h>

There are several variations for importing modules in Objective-C:

  • #import <File.h>: This is used for system header files. It looks for the file in the standard system directories;

  • #import "File.h": This is used for header files in your own application. It first searches for the file in the current project's paths or user-defined paths, and then in the same directories as File.

Libraries installed via CocoaPods or similar tools usually use the <...> format to include header files since these files are placed in known directories accessible to the compiler. In this case, #import <React/RCTBridgeModule.h> is used.

  • #import <React/RCTBridgeModule.h>: This line imports the header file for the React Native bridge module. The bridge module allows the creation of native modules that can be accessed from JavaScript. This is necessary for creating custom native functionality in your React Native app;

  • #import <React/RCTEventEmitter.h>: This line imports the header file for the React Native event emitter. The event emitter is used to send events from the native code to JavaScript, enabling communication between the native and JavaScript parts of your app. This is especially useful for sending updates or notifications from the native code to React Native components;

  • #import <Foundation/Foundation.h>: This line imports the header file for the Foundation framework. Foundation provides basic classes and functions for working with objects, string management, dates and times, data collection, and more. This framework is the foundation of most Objective-C and Swift applications, and its import is necessary for working with many basic functions.

This file must be added as the Swift Compiler - General in Build Settings. This step was described earlier when creating the nativeModules-Bridging-Header.h file.

RCTLiveActivityModule.m

#import "nativeModules-Bridging-Header.h"

// Define the LiveActivity module for React Native
@interface RCT_EXTERN_MODULE(LiveActivity, NSObject)

// Define the endActivity method, which ends a live activity given its ID
RCT_EXTERN_METHOD(endActivity:(NSString)id withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

// Define the listAllActivities method, which lists all currently active Live Activities
RCT_EXTERN_METHOD(listAllActivities:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

// Define the updateActivity method, which updates a live activity given its ID and new emoji state
RCT_EXTERN_METHOD(updateActivity:(NSString)id emoji:(NSString)emoji withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

@end

// Define the LiveActivitiModule for React Native, which extends RCTEventEmitter
@interface RCT_EXTERN_MODULE(LiveActivitiModule, RCTEventEmitter)

// Define the supportedEvents method, which lists the events that can be sent to JavaScript
RCT_EXTERN_METHOD(supportedEvents)

@end

Header file imports:

// Import the bridging header
#import "nativeModules-Bridging-Header.h"

This file is imported using "File" because it is located in our directory. All the necessary libraries for interaction between native code and React Native are imported in nativeModules-Bridging-Header.h.

Declaration of LiveActivity module:

// Define the LiveActivity module for React Native
@interface RCT_EXTERN_MODULE(LiveActivity, NSObject)

// Define the endActivity method, which ends a live activity given its ID
RCT_EXTERN_METHOD(endActivity:(NSString)id withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

// Define the listAllActivities method, which lists all currently active Live Activities
RCT_EXTERN_METHOD(listAllActivities:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

// Define the updateActivity method, which updates a live activity given its ID and new emoji state
RCT_EXTERN_METHOD(updateActivity:(NSString)id emoji:(NSString)emoji withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

@end

Declaration of LiveActivityModule:

// Define the LiveActivitiModule for React Native, which extends RCTEventEmitter
@interface RCT_EXTERN_MODULE(LiveActivitiModule, RCTEventEmitter)

// Define the supportedEvents method, which lists the events that can be sent to JavaScript
RCT_EXTERN_METHOD(supportedEvents)

@end
  • RCT_EXTERN_MODULE: Declares a native module that inherits from NSObject;
  • RCT_EXTERN_METHOD: Declares a method.

For RCT_EXTERN_MODULE, the module name and what the module depends of must be specified. For LiveActivity, it's NSObject. For LiveActivityModule, it's RCTEventEmitter. For RCT_EXTERN_METHOD, the method name and function parameters must be specified.

React Native Development

The Rabbitohs app, in which we integrated Live Activity, was created using react-native-cli. In the test project, the same approach will be used. Taking into account the recent React Conf 2024, where the React Native team stated that all new projects should be started using the React Native Framework, represented by Expo, there is an option to try using Expo for creating the project.

The project creation step was skipped.

Creating service live-activity.ts

We need to create the service live-activity.ts in the folder src => services.

live-activity.ts

import { NativeEventEmitter, NativeModules, Platform } from
"react-native";
const myModuleEvt = new 
NativeEventEmitter(NativeModules.LiveActivitiModule);
const LiveActivity = NativeModules.LiveActivity
  ? NativeModules.LiveActivity
  : new Proxy(
      {},
      {
        get() {
          throw new Error("hmm");
         },
       }
    );
const clearAllActivities = async () => {
  if (!(Platform.OS === "ios")) return;
  myModuleEvt.removeAllListeners("SomeEvent");
  try {
    const activities = await LiveActivity.listAllActivities();
    if (!activities.length) return;
     activities.forEach(async (act: { id: string }) => {
       if (act.id) await LiveActivity.endActivity(act.id);
     });
   } catch (error) {
     console.log({ error });
     return;
    }
  };

  const startLiveActivities = async (
    setActivityTokenId: React.Dispatch<React.SetStateAction<string |
  null>>
  ) => {
    try {
      await clearAllActivities();
    myModuleEvt.addListener("SomeEvent", (token) => {
      console.log("token ", token);
      if (token.id) {
        setActivityTokenId(token.id);
       }
     });
  } catch (error) {
   console.error(error);
  }
};

const endLiveActivities = async () => {
  try {
    await clearAllActivities();
  } catch (error) {
     console.error(error);
   }
};

const updateLiveActivity = async (activityTokenId: string | null) =>
{
  try {
    if (!activityTokenId) return;
    await LiveActivity.updateActivity(activityTokenId, "🤩");
  } catch (error) {
    console.log(error);
   }
 };

 export const LiveActivityService = {
   startLiveActivities,
   endLiveActivities,
   updateLiveActivity,
    clearAllActivities,
  };

This code provides a JavaScript API for managing live activity.

import { NativeEventEmitter, NativeModules, Platform } from "react-native";

Import the necessary modules from react-native:

  • NativeEventEmitter for creating events that can be listened to in JavaScript
  • NativeModules for accessing native modules
  • A platform for determining the platform on which the app is running

Creating NativeEventEmitter:

const myModuleEvt = new NativeEventEmitter(NativeModules.LiveActivitiModule);

It creates a new instance of NativeEventEmitter, which will listen for events from the native LiveActivityModule.

const LiveActivity = NativeModules.LiveActivity
  ? NativeModules.LiveActivity
  : new Proxy(
      {},
       {
         get() {
           throw new Error("hmm");
        },
       }
      );

It checks if LiveActivity exists in NativeModules. If it exists, it uses it. If not, it creates a proxy that throws an error when accessing a non-existent method or property.

Description of functions for LiveActivityService:

  • startLiveActivities: A function to start Live Activities. It clears all current activities and adds a listener for the "SomeEvent" event, which sets the ID of the new activity;
  • updateLiveActivity: A function to update a specific live activity by its ID. It allows changing the activity's state without using third-party services;
  • clearAllActivities: A function to clear all Live Activities. It removes all listeners for the "SomeEvent" event, retrieves all current activities, and ends each one by its ID.

clearAllActivities

const clearAllActivities = async () => {
  if (!(Platform.OS === "ios")) return;
  myModuleEvt.removeAllListeners("SomeEvent");

  try {
      const activities = await LiveActivity.listAllActivities();
      if (!activities.length) return;
       activities.forEach(async (act: { id: string }) => {
         if (act.id) await LiveActivity.endActivity(act.id);
       });
     } catch (error) {
       console.error(error);
       return;
      }
     };
  1. It checks if the platform is iOS;
  2. It removes all listeners for the "SomeEvent" event;
  3. It retrieves all current activities;
  4. It ends each activity by its ID.

__startLiveActivities

  const startLiveActivities = async (
    setActivityTokenId: React.Dispatch<React.SetStateAction<string |
  null>>
  ) => {
    if (!(Platform.OS === "ios")) return;
    try {
      await clearAllActivities();
       myModuleEvt.addListener("SomeEvent", (token) => {
        console.log("token ", token);
         if (token.id) {
           setActivityTokenId(token.id);
         }
       });
     } catch (error) {
       console.error(error);
     }
   };
  • It checks if the platform is iOS;
  • First, it clears all activities;
  • It adds a listener for the "SomeEvent" event, which sets the activity ID using setActivityTokenId.

It is important here to listen for the token, which can be updated over the lifetime of the live activity. NativeEventEmitter is used for this, and when the token is updated, it replaces it with a new one.

updateLiveActivity

const updateLiveActivity = async (activityTokenId: string | null) =>
{
  if (!(Platform.OS === "ios")) return;
  try {
    if (!activityTokenId) return;
     await LiveActivity.updateActivity(activityTokenId, "🤩");
   } catch (error) {
    console.error(error);
   }
};
  • It checks if the platform is iOS;
  • It updates the activity if a valid activityTokenId is passed.

Export of LiveActivityService

export const LiveActivityService = {
  startLiveActivities,
  endLiveActivities,
  updateLiveActivity,
  clearAllActivities,
};

Integration into the App.tsx component

App.tsx

import { useState } from "react";
import {
  Button,
  SafeAreaView,
  StatusBar,
  StyleProp,
  useColorScheme,
  ViewStyle,
} from "react-native";
import { Colors } from "react-native/Libraries/NewAppScreen";

import { LiveActivityService } from "./src/services/live-activity";

function App(): JSX.Element {
  const isDarkMode = useColorScheme() === "dark";
  const [activityTokenId, setActivityTokenId] = useState<string |
 null>(null);

   const backgroundStyle: StyleProp<ViewStyle> = {
     backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
     justifyContent: "center",
      alignItems: "center",
        flex: 1,
      width: "100%",
     };

    return (
      <SafeAreaView style={backgroundStyle}>
        <StatusBar
           barStyle={isDarkMode ? "light-content" : "dark-content"}
           backgroundColor={backgroundStyle.backgroundColor}
          />
         <Button
           title="Activate live activity"
            onPress={() =>
             LiveActivityService.startLiveActivities(setActivityTokenId)
          }
        />
        <Button
          title="Update live activity"
           onPress={() =>
    LiveActivityService.updateLiveActivity(activityTokenId)}
          />
          <Button

    title="Deactivate live activity"
         onPress={LiveActivityService.endLiveActivities}
       />
    </SafeAreaView>
   );
 }

 export default App;

The App uses LiveActivityService to manage live activity. It includes three buttons for starting, updating, and deactivating live activity.

Conclusion

Live Activities React Native integration can greatly enhance the user experience by providing real-time updates directly on the lock screen. If you need assistance with this or any other software task, contact seasoned professionals at Requestum.

SHARE:

SHARE:

Contact us

Our contacts

We are committed to ensure quality in detail and provide meaningful impact for customers’ business and audience.

    UA Sales Office:

    sales@requestum.com

    Offices:

    Latvia

    1000, Maskavas Iela 44, Riga


    Ukraine

    61000, 7/9 Svobody street, Kharkiv


    Switzerland

    6313, Seminarstrasse, 5, Menzingen


    Follow us:

Requested Service Optionals:

WebMobileAIUI/UXOther

Your Budget: $ 20k