Nov 21, 2025

iOS Live Activities in React Native: A Complete Guide

A comprehensive step-by-step guide to building real-time Lock Screen & Dynamic Island experiences

Author

Yuvraj KumarYuvraj KumarSoftware Engineer III
iOS Live Activities in React Native: A Complete Guide

Understanding Live Activities & Dynamic Island

Live Activities change how users engage with time-sensitive information. They bring real-time updates straight to the iPhone's Lock Screen and Dynamic Island. For flight-tracking apps, this means you do not have to keep opening them to check departure gates, delays, or boarding status.

Dynamic Island Integration (iPhone 14 Pro and later) provides three distinct presentation modes:

  • Minimal: Single icon or short text (gate number)
  • Compact: Leading and trailing elements (flight status + countdown)
  • Expanded: Full, detailed view with interactive elements

In flight-tracking scenarios, users can monitor boarding countdowns, receive gate-change notifications, and access quick actions like viewing boarding passes—all without unlocking their device.

iOS flight tracking app showing live flight status and travel alerts

Key Benefits:

  • 40% higher engagement than traditional push notifications
  • Persistent visibility during critical travel moments
  • Seamless integration with iOS design language
  • Real-time updates even when the app is terminated

Project Configuration Requirements

Push Notification Setup

Live Activities depend on Apple Push Notification Service (APNs) for real-time updates. Configure push notifications first:

  1. In Xcode: Select your app target → Signing & Capabilities
  2. Add Capability: Click "+" and select "Push Notifications"
  3. Verify Entitlements: Ensure aps-environment is properly configured
Xcode configuration for iOS app signing and remote notification capabilities

Info.plist Configuration

Enable Live Activities in your main app's Info.plist:

<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>

The NSSupportsLiveActivitiesFrequentUpdates key is essential for flight tracking as it allows updates more frequently than the standard limit—critical for gate changes and boarding updates.

Widget Extension Target

Create a separate Widget Extension for Live Activities:

  1. Add Target: File → New → Target → Widget Extension
  2. Configuration: Name it FlightTrackerWidget, include Live Activity intent
  3. Build Settings: Set deployment target to iOS 16.1+, configure App Groups for data sharing
Template selection pop-up displayed in a desktop application for iOS App

Build Phases Critical Step

For React Native compatibility, verify the Build Phases configuration:

  • Navigate to Build Phases Embed App Extensions
  • Ensure "Copy only when installing" is unchecked
Xcode build settings displaying Live Activity extension embedding options

This step is crucial for Live Activities to function properly in React Native environments.

Live Activity Widget Structure

Core SwiftUI Implementation

The Widget Extension contains several key files, but focus on these essential components:

FlightActivityAttributes.swift (Data Structure example ):

struct FlightActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic properties - updated via push or app
        var status: String      // "Boarding", "Delayed", "On Time"
        var gate: String        // "A12", "B7", "TBD"
        var countdown: Date     // Departure time
    }
    
    // Fixed properties - set once during creation
    var flightNumber: String    // "AI 101"
    var route: String          // "DEL → BOM"
    var airline: String        // "Air India"
}

FlightActivityWidget.swift (UI Implementation example ):

struct FlightActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: FlightActivityAttributes.self) { context in
            // Lock Screen UI
            FlightLockScreenView(
                flight: context.attributes.flightNumber,
                route: context.attributes.route,
                status: context.state.status,
                gate: context.state.gate
            )
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.center) {
                    FlightStatusExpandedView(context: context)
                }
            } compactLeading: {
                FlightStatusIcon(status: context.state.status)
            } compactTrailing: {
                Text(context.state.gate)
                    .font(.system(.caption, design: .rounded))
                    .fontWeight(.semibold)
            } minimal: {
                FlightProgressIndicator(
                    departureTime: context.state.countdown
                )
            }
        }
    }
}

Understanding the Architecture:

  • ActivityAttributes: Defines both static data (flight details) and dynamic state (status, gate)
  • Lock Screen Layout: Primary real estate for detailed information
  • Dynamic Island Regions: Different areas serve specific purposes (leading for status, trailing for gate info)

Native Module Bridge Implementation

To control Live Activities from React Native, create a bridge using three essential files:

1. LiveActivityModule.swift (Core Logic)

import ActivityKit
import Foundation

@objc(LiveActivityModule)
class LiveActivityModule: RCTEventEmitter {
    private var currentActivity: Activity<FlightActivityAttributes>?
    private var hasListeners: Bool = false
    
    override func startObserving() {
        hasListeners = true
        startFlightActivity()
    }
    
    private func startFlightActivity() {
        let attributes = FlightActivityAttributes(
            flightNumber: "AI 101",
            route: "DEL → BOM",
            airline: "Air India"
        )
        
        let initialState = FlightActivityAttributes.ContentState(
            status: "Scheduled",
            gate: "TBD",
            countdown: Date().addingTimeInterval(7200) // 2 hours
        )
        
        do {
            currentActivity = try Activity.request(
                attributes: attributes,
                contentState: initialState,
                pushType: .token
            )
            
            monitorTokenUpdates()
        } catch {
            print("Failed to start Live Activity: \(error)")
        }
    }
    
    private func monitorTokenUpdates() {
        guard let activity = currentActivity, hasListeners else { return }
        
        Task {
            for await tokenData in activity.pushTokenUpdates {
                let token = tokenData.map { String(format: "%02x", $0) }.joined()
                self.sendEvent(withName: "onTokenUpdate", body: [
                    "token": token,
                    "activityId": activity.id
                ])
            }
        }
    }
    
    override func supportedEvents() -> [String] {
        return ["onTokenUpdate", "onActivityStart", "onActivityEnd"]
    }
    
    @objc override static func requiresMainQueueSetup() -> Bool {
        return true
    }
}

@objc(FlightActivity)
class FlightActivity: NSObject {
    @objc(updateFlight:status:gate:resolver:rejecter:)
    func updateFlight(
        activityId: String,
        status: String,
        gate: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        Task {
            let activities = Activity<FlightActivityAttributes>.activities
            guard let activity = activities.first(where: { $0.id == activityId }) else {
                reject("NOT_FOUND", "Activity not found", nil)
                return
            }
            
            let newState = FlightActivityAttributes.ContentState(
                status: status,
                gate: gate,
                countdown: activity.contentState.countdown
            )
            
            await activity.update(using: newState)
            resolve(["success": true])
        }
    }
    
    @objc(endActivity:resolver:rejecter:)
    func endActivity(
        activityId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        Task {
            let activities = Activity<FlightActivityAttributes>.activities
            await activities
                .filter { $0.id == activityId }
                .first?
                .end(dismissalPolicy: .immediate)
            resolve(["ended": true])
        }
    }
}

2. RCTFlightActivityModule.m (Objective-C Bridge)

#import "FlightActivity-Bridging-Header.h"

@interface RCT_EXTERN_MODULE(FlightActivity, NSObject)
RCT_EXTERN_METHOD(updateFlight:(NSString)activityId 
                  status:(NSString)status 
                  gate:(NSString)gate 
                  resolver:(RCTPromiseResolveBlock)resolve 
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(endActivity:(NSString)activityId 
                  resolver:(RCTPromiseResolveBlock)resolve 
                  rejecter:(RCTPromiseRejectBlock)reject)
@end

@interface RCT_EXTERN_MODULE(LiveActivityModule, RCTEventEmitter)
RCT_EXTERN_METHOD(supportedEvents)
@end

3. FlightActivity-Bridging-Header.h

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <Foundation/Foundation.h>

Critical Setup: Add the bridging header file path in Build Settings → Swift Compiler - General → Objective-C Bridging Header.

React Native Service Integration

FlightActivityService.ts

Creating a service to manage Live Activities from JavaScript:

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

const activityEmitter = new NativeEventEmitter(NativeModules.LiveActivityModule);
const FlightActivityNative = NativeModules.FlightActivity;

interface FlightActivityData {
  flightNumber: string;
  route: string;
  status: 'Scheduled' | 'Boarding' | 'Delayed' | 'On Time' | 'Departed';
  gate: string;
  departureTime: Date;
}

class FlightActivityService {
  private currentActivityId: string | null = null;
  private tokenListener: any = null;

  async startFlightTracking(flightData: FlightActivityData): Promise<void> {
    if (Platform.OS !== 'ios') return;
    
    try {
      // Clear any existing activities
      await this.stopFlightTracking();
      
      // Listen for token updates
      this.tokenListener = activityEmitter.addListener('onTokenUpdate', (data) => {
        console.log('Activity Token:', data.token);
        this.currentActivityId = data.activityId;
        // Send token to your backend for push notifications
        this.registerTokenWithBackend(data.token, data.activityId);
      });
      
    } catch (error) {
      console.error('Failed to start flight tracking:', error);
    }
  }

  async updateFlightStatus(status: string, gate: string): Promise<void> {
    if (!this.currentActivityId || Platform.OS !== 'ios') return;
    
    try {
      await FlightActivityNative.updateFlight(this.currentActivityId, status, gate);
    } catch (error) {
      console.error('Failed to update flight status:', error);
    }
  }

  async stopFlightTracking(): Promise<void> {
    if (Platform.OS !== 'ios') return;
    
    try {
      if (this.currentActivityId) {
        await FlightActivityNative.endActivity(this.currentActivityId);
      }
      
      this.tokenListener?.remove();
      this.currentActivityId = null;
    } catch (error) {
      console.error('Failed to stop flight tracking:', error);
    }
  }

  private async registerTokenWithBackend(token: string, activityId: string): Promise<void> {
    // Send token to your backend for push notifications
    try {
      await fetch('https://your-api.com/register-activity-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          activityId,
          pushToken: token,
          userId: 'current-user-id'
        })
      });
    } catch (error) {
      console.error('Failed to register token:', error);
    }
  }
}

export const flightActivityService = new FlightActivityService();

App Integration (React based Example)

import React, { useState } from 'react';
import { View, Button, Text } from 'react-native';
import { flightActivityService } from './services/FlightActivityService';

export const FlightTrackingScreen = () => {
  const [isTracking, setIsTracking] = useState(false);

  const startTracking = async () => {
    await flightActivityService.startFlightTracking({
      flightNumber: 'AI 101',
      route: 'DEL → BOM',
      status: 'Scheduled',
      gate: 'A12',
      departureTime: new Date(Date.now() + 2 * 60 * 60 * 1000) // 2 hours from now
    });
    setIsTracking(true);
  };

  const updateStatus = async () => {
    await flightActivityService.updateFlightStatus('Boarding', 'A15');
  };

  const stopTracking = async () => {
    await flightActivityService.stopFlightTracking();
    setIsTracking(false);
  };

  return (
    <View style={{ padding: 20 }}>
      <Text>Flight AI 101: DEL → BOM</Text>
      
      <Button 
        title="Start Live Tracking" 
        onPress={startTracking}
        disabled={isTracking}
      />
      
      <Button 
        title="Update: Now Boarding at A15" 
        onPress={updateStatus}
        disabled={!isTracking}
      />
      
      <Button 
        title="Stop Tracking" 
        onPress={stopTracking}
        disabled={!isTracking}
      />
    </View>
  );
};

Push Notifications Architecture

Backend Implementation (Node.JS based example)

Live Activities can receive updates even when your app is completely terminated through APNs:

const apn = require('apn');

class FlightNotificationService {
  constructor() {
    this.apnProvider = new apn.Provider({
      token: {
        key: process.env.APPLE_PUSH_KEY,
        keyId: process.env.APPLE_KEY_ID,
        teamId: process.env.APPLE_TEAM_ID
      },
      production: process.env.NODE_ENV === 'production'
    });
  }

  async updateFlightStatus(activityToken, flightUpdate) {
    const notification = new apn.Notification();
    
    notification.topic = 'your.bundle.id.push-type.liveactivity';
    notification.pushType = 'liveactivity';
    notification.payload = {
      'aps': {
        'timestamp': Math.floor(Date.now() / 1000),
        'event': 'update',
        'content-state': {
          status: flightUpdate.status,
          gate: flightUpdate.gate,
          countdown: flightUpdate.departureTime
        },
        'alert': {
          'title': `Flight ${flightUpdate.flightNumber}`,
          'body': `Gate changed to ${flightUpdate.gate}`
        }
      }
    };

    try {
      const result = await this.apnProvider.send(notification, activityToken);
      console.log('Push notification sent:', result);
    } catch (error) {
      console.error('Push notification failed:', error);
    }
  }

  async startRemoteActivity(userToken, flightData) {
    // Push-to-Start capability (iOS 17.2+)
    const notification = new apn.Notification();
    
    notification.topic = 'your.bundle.id.push-type.liveactivity';
    notification.payload = {
      'aps': {
        'timestamp': Math.floor(Date.now() / 1000),
        'event': 'start',
        'attributes-type': 'FlightActivityAttributes',
        'attributes': {
          flightNumber: flightData.flightNumber,
          route: flightData.route,
          airline: flightData.airline
        },
        'content-state': {
          status: 'Scheduled',
          gate: 'TBD',
          countdown: flightData.departureTime
        }
      }
    };

    await this.apnProvider.send(notification, userToken);
  }
}

Push Notification Flow:

  1. App starts Live Activity and receives a unique push token
  2. Token sent to the backend and stored with the activity ID
  3. Backend monitors flight data changes
  4. Real-time updates are sent via APNs to a specific activity
  5. iOS automatically updates Live Activity UI
Backend to APNs to Device Live Activities flow
Backend to APNs to Device Live Activities flow

Key Flow Benefits

  • Updates work even when the app is completely closed
  • Each Live Activity has a unique push token
  • No polling required - real push notifications
  • Automatic UI refresh by the iOS system
  • Battery efficient - no background processing
  • Multiple activities can run simultaneously

Example APNs Payload structure :

{
  "aps": {
    "timestamp": 1672531200,
    "event": "update",
    "content-state": {
      "status": "Boarding",
      "gate": "A15",
      "countdown": 1672534800
    },
    "alert": {
      "title": "Flight AI 101",
      "body": "Now boarding at Gate A15"
    }
  }
}

AppIntents Implementation

Adding interactive buttons to your Live Activity:

import AppIntents

struct ViewBoardingPassIntent: AppIntent {
    static var title: LocalizedStringResource = "View Boarding Pass"
    
    @Parameter(title: "Flight Number")
    var flightNumber: String
    
    func perform() async throws -> some IntentResult {
        let url = URL(string: "flighttracker://boarding-pass/\(flightNumber)")!
        await UIApplication.shared.open(url)
        return .result()
    }
}

struct BookGroundTransportIntent: AppIntent {
    static var title: LocalizedStringResource = "Book Cab"
    
    func perform() async throws -> some IntentResult {
        let url = URL(string: "flighttracker://book-transport")!
        await UIApplication.shared.open(url)
        return .result()
    }
}

Integration in Live Activity UI

// Add to your Lock Screen view
VStack {
    // Flight information display
    FlightInfoView(context: context)
    
    HStack(spacing: 12) {
        Button(intent: ViewBoardingPassIntent(flightNumber: context.attributes.flightNumber)) {
            HStack {
                Image(systemName: "airplane")
                Text("Boarding Pass")
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(20)
        }
        
        Button(intent: BookGroundTransportIntent()) {
            HStack {
                Image(systemName: "car.fill")
                Text("Book Cab")
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            .background(Color.green)
            .foregroundColor(.white)
            .cornerRadius(20)
        }
    }
}
import { Linking } from 'react-native';

const setupDeepLinkHandling = () => {
  const handleDeepLink = (url: string) => {
    if (url.includes('boarding-pass')) {
      const flightNumber = url.split('/').pop();
      navigation.navigate('BoardingPass', { flightNumber });
    } else if (url.includes('book-transport')) {
      navigation.navigate('TransportBooking');
    }
  };

  Linking.getInitialURL().then(url => {
    if (url) handleDeepLink(url);
  });

  const subscription = Linking.addEventListener('url', ({ url }) => {
    handleDeepLink(url);
  });

  return () => subscription?.remove();
};

Push-to-Start Implementation (iOS 17.2+)

Push-to-Start allows your backend to initiate Live Activities without user interaction:

Backend Implementation

async function startFlightTrackingRemotely(userId, flightData) {
  const userDeviceToken = await getUserDeviceToken(userId);
  
  const notification = new apn.Notification();
  notification.topic = 'your.bundle.id.push-type.liveactivity';
  notification.payload = {
    'aps': {
      'timestamp': Math.floor(Date.now() / 1000),
      'event': 'start',
      'attributes-type': 'FlightActivityAttributes',
      'attributes': {
        flightNumber: flightData.flightNumber,
        route: flightData.route,
        airline: flightData.airline
      },
      'content-state': {
        status: 'Check-in Open',
        gate: 'TBD',
        countdown: flightData.departureTime
      }
    }
  };
  
  await apnProvider.send(notification, userDeviceToken);
}

// Trigger scenarios
const triggerScenarios = {
  // 24 hours before departure
  preFlightReminder: (flightData) => {
    setTimeout(() => {
      startFlightTrackingRemotely(flightData.userId, flightData);
    }, calculateTimeUntil24HoursBefore(flightData.departureTime));
  },
  
  // After booking confirmation
  postBookingActivation: (bookingData) => {
    startFlightTrackingRemotely(bookingData.userId, bookingData.flightData);
  },
  
  // Gate assignment notification
  gateAssignment: (flightData) => {
    startFlightTrackingRemotely(flightData.userId, {
      ...flightData,
      gate: flightData.assignedGate
    });
  }
};

Strategic Use Cases:

  • Auto-activate tracking 24 hours before departure
  • Begin monitoring immediately after successful booking
  • Start the activity when the gate is assigned
  • Initiate during the mobile check-in process

Business Impact & Implementation Strategy

Engagement Metrics

Live Activities create measurable business value through enhanced user engagement and new revenue streams:

User Engagement:

  • 40% higher interaction rates vs traditional push notifications
  • 60% reduction in app abandonment during flight delays
  • 50% higher session duration when Live Activities are active

Conversion Impact:

  • 30% increase in ancillary service bookings (seat upgrades, meals)
  • 45% higher click-through rates on cross-sell offers
  • 20% reduction in customer support queries

Conclusion

iOS Live Activities represent a significant evolution in mobile user experience, particularly for time-sensitive applications like flight tracking. The technical implementation requires coordination between native iOS development and React Native, but the user engagement and business benefits justify the complexity.

Key Success Factors:

  1. User-Centric Design: Focus on displaying only essential information that users need at critical moments
  2. Technical Reliability: Robust error handling and graceful degradation for network issues
  3. Performance Optimization: Minimize battery impact through intelligent update strategies
  4. Business Integration: Leverage Live Activities for revenue generation, not just user experience

The investment in Live Activities technology positions your application at the forefront of mobile user experience while creating new opportunities for user engagement and revenue generation.


Note - 

This guide reflects best practices as of September 2025 for iOS 17.5+ and React Native 0.74+. Also, I suggest always referring to Apple's latest documentation for current APIs and requirements.

Subscribe to Our Newsletter

RELATED ARTICLES

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.
Why Everything Your AI Builds Looks the Same
Why Everything Your AI Builds Looks the Same
This blog explores why AI-generated interfaces often look alike and explains how design systems, product context, and reusable engineering practices help teams build distinctive, scalable
How We Built the Missing Bridge from Code to Figma
Technology

Jul 10, 2026

How We Built the Missing Bridge from Code to Figma
This blog explores how AI-generated React apps get turned into fully editable, designer-ready Figma files by reading React Fiber instead of the DOM.
Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
Technology

Jun 27, 2026

Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
A practical breakdown of building resilient AWS-to-on-premises connectivity with WireGuard HA, active-standby failover, and deep packet-forwarding observability.
We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
Technology

Jun 19, 2026

We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
A practical guide to building a 114-second multi-cloud disaster recovery failover between AWS and Azure — what we built, what broke, and what we learned.
Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
Technology

Jun 12, 2026

Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
This blog explains how organizations can balance speed, scalability, and operational flexibility as they grow from startup to enterprise scale.

The Right Conversation Can Save You Six Months.

Whether you’re navigating AI adoption, modernizing legacy systems, or scaling a product - we start by listening. No pitch deck. No template. A real conversation.