Voltra: Building iOS Live Activities with React - No Swift Required
Executive Summary
Voltra represents a groundbreaking shift in React Native development, enabling developers to ship custom iOS Live Activities, Widgets, and Dynamic Island implementations using only React—no Swift, Xcode, or additional JavaScript runtime required. This revolutionary npm package eliminates the traditional barrier that has kept React Native developers from accessing some of iOS's most engaging user-facing features.
For years, implementing Live Activities in React Native apps required a complex dance between Swift and JavaScript: creating Widget Extensions in Xcode, building native bridges for communication, managing separate codebases in different languages, and maintaining synchronization between iOS native code and React Native logic. This complexity meant that many React Native teams either skipped these features entirely or dedicated significant iOS-specific development resources to implement them.
Voltra changes this paradigm completely. With features like hot reload for Live Activity development, server push capabilities for remote updates, AI coding agent-friendly APIs, and full compatibility with both Expo and vanilla React Native, it brings iOS's most distinctive interactive features within reach of every React developer. The package's developer experience mirrors what React developers expect: declare your UI components in JSX, handle state updates through familiar React patterns, and see changes instantly through hot reload—all without leaving the JavaScript ecosystem.
This comprehensive guide explores how Voltra works, why it matters for React Native development, and how to leverage it to build engaging Live Activities that keep users connected to your app even when it's in the background.
Understanding iOS Live Activities and Dynamic Island
The iOS Live Experience
iOS Live Activities, introduced in iOS 16.1, transformed how users interact with ongoing events and real-time information. Instead of repeatedly opening apps to check status updates, users can see live information directly on their Lock Screen and—on iPhone 14 Pro and newer models—in the Dynamic Island.
Lock Screen Live Activities: When your app has an ongoing event (a food delivery en route, a sports game in progress, a ride-sharing trip, a workout session), a Live Activity appears as a persistent banner on the Lock Screen. Unlike push notifications that disappear, Live Activities stay present and update in real-time, providing continuous visibility.
Dynamic Island Integration: On iPhone 14 Pro and later, Live Activities can also appear in the Dynamic Island—the interactive area around the front camera that expands and contracts to show contextual information. This creates an incredibly engaging experience where users can glance at critical information without interrupting their current task.
Widgets: iOS widgets allow users to place your app's information directly on their Home Screen. Unlike Live Activities which are temporary (lasting up to 8 hours), widgets provide persistent access to content and actions.
The Traditional Implementation Challenge
Before Voltra, implementing Live Activities in a React Native app involved several complex steps:
1. Create a Widget Extension in Xcode:
// WidgetExtension/LiveActivityWidget.swift
import ActivityKit
import WidgetKit
import SwiftUI
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock screen UI
HStack {
VStack(alignment: .leading) {
Text("Delivery Status")
Text(context.state.status)
.font(.headline)
}
Spacer()
ProgressView(value: context.state.progress)
}
.padding()
} dynamicIsland: { context in
// Dynamic Island UI
DynamicIsland {
// Expanded UI
} compactLeading: {
Image(systemName: "box.truck")
} compactTrailing: {
Text("\(Int(context.state.progress * 100))%")
} minimal: {
Image(systemName: "box.truck")
}
}
}
}
2. Build a Native Module Bridge:
// NativeBridge/LiveActivityModule.swift
import React
import ActivityKit
@objc(LiveActivityModule)
class LiveActivityModule: NSObject {
@objc
func startActivity(_ attributes: NSDictionary,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) {
// Complex bridging logic between React Native and ActivityKit
// Type conversion, error handling, token management...
}
}
3. Create TypeScript Definitions:
// types/LiveActivity.ts
interface NativeLiveActivityModule {
startActivity(attributes: any): Promise
updateActivity(id: string, state: any): Promise
endActivity(id: string): Promise
}
export const LiveActivity = NativeModules.LiveActivityModule as NativeLiveActivityModule
4. Implement React Native Interface:
// components/DeliveryTracker.tsx
import { LiveActivity } from '../modules/LiveActivity'
export function DeliveryTracker() {
const startLiveActivity = async () => {
try {
const activityId = await LiveActivity.startActivity({
orderId: order.id,
restaurantName: order.restaurant
})
// Store activity ID for updates...
} catch (error) {
// Handle errors...
}
}
// More complex logic...
}
This approach requires:
- •Proficiency in Swift and SwiftUI
- •Understanding of ActivityKit APIs
- •Knowledge of React Native native module architecture
- •Xcode for development and building
- •Careful type synchronization between Swift and TypeScript
- •Complex error handling across the language boundary
For a typical delivery tracking Live Activity, you might write 200+ lines of Swift code and 100+ lines of TypeScript bridging logic. Every UI change requires modifying Swift code, rebuilding the native module, and restarting the entire React Native app to test—no hot reload, no instant feedback.
How Voltra Transforms the Experience
Pure React Native Development
Voltra's core innovation is enabling Live Activities to be defined entirely in React Native using JSX, with the framework automatically handling the translation to native ActivityKit and SwiftUI under the hood:
// With Voltra - no Swift required
import { LiveActivity } from 'voltra'
function DeliveryLiveActivity({ orderId, restaurant, status, progress }) {
return (
Delivery Status
{status}
{restaurant}
}
compactTrailing={{Math.round(progress * 100)}% }
minimal={ }
>
{status}
)
}
// Start the Live Activity
function DeliveryTracker() {
const startDelivery = async () => {
await LiveActivity.start(DeliveryLiveActivity, {
orderId: order.id,
restaurant: order.restaurant,
status: "Preparing your order",
progress: 0.1
})
}
return (
)
}
That's it. No Swift code. No Xcode projects. No native bridges. Pure React Native with the same component model, styling patterns, and development workflow you already know.
Hot Reload for Instant Iteration
One of Voltra's most developer-friendly features is hot reload support for Live Activities. Traditional development required rebuilding the entire iOS app and manually triggering the Live Activity to see UI changes. With Voltra:
// Change this...
{status}
// To this...
{status.toUpperCase()}
// Save the file, and the Live Activity updates instantly on your device/simulator
// No rebuild, no restart, no manual triggering
The hot reload capability extends to:
- •UI layout and styling changes
- •Component logic modifications
- •State update handling
- •Dynamic Island configurations
- •Color scheme and theme adjustments
This tight feedback loop accelerates development from hours to minutes, enabling rapid design iteration and immediate validation of user experience changes.
Server Push for Remote Updates
Voltra includes built-in support for server-driven Live Activity updates, enabling you to push changes from your backend without requiring the app to be running:
// Backend - Node.js/Express
import { VoltraServer } from 'voltra/server'
const voltra = new VoltraServer({
apnKey: process.env.APN_KEY,
teamId: process.env.APPLE_TEAM_ID,
keyId: process.env.APPLE_KEY_ID
})
// Push update to a Live Activity
await voltra.update({
activityId: 'delivery-12345',
data: {
status: "Out for delivery",
progress: 0.6,
estimatedArrival: "2:45 PM"
}
})
// The Live Activity updates on the user's device
// even if your app is completely closed
This server push capability is essential for use cases where the backend has authoritative state:
- •Delivery tracking (driver location, ETA updates)
- •Ride-sharing (driver arrival, route changes)
- •Sports scores (real-time score updates)
- •Flight status (gate changes, delays)
- •Order status (kitchen progress, delivery assignment)
AI Coding Agent Friendly
Voltra's API design is intentionally straightforward and well-documented, making it easy for AI coding assistants to generate correct implementations:
// AI can easily generate this pattern
import { LiveActivity } from 'voltra'
// 1. Define the component
const MyLiveActivity = ({ title, value, progress }) => (
{/* UI definition */}
)
// 2. Start it
await LiveActivity.start(MyLiveActivity, { ...props })
// 3. Update it
await LiveActivity.update(activityId, { ...newProps })
// 4. End it
await LiveActivity.end(activityId)
This simplicity means you can describe your Live Activity requirements in natural language to an AI assistant, and it can generate working code without needing deep iOS expertise.
Expo and React Native Compatibility
Voltra works seamlessly with both Expo managed workflows and vanilla React Native projects:
Expo Setup:
npx expo install voltra
Add plugin to app.json
{
"expo": {
"plugins": ["voltra"]
}
}
Build and run
npx expo prebuild
npx expo run:ios
Vanilla React Native:
npm install voltra
cd ios && pod install
npx react-native run-ios
Key Features and Capabilities
Comprehensive Live Activity Components
Voltra provides React components for every aspect of Live Activities:
Root Configuration:
{/* Children components */}
Lock Screen Presentation:
{/* Your custom UI using standard React Native components */}
Order #{orderId}
{status}
Dynamic Island Regions:
}
compactTrailing={
{progress}%
}
// Minimal state (smallest representation)
minimal={
}
// Expanded state (user taps Island)
expanded={
Order Progress
{status}
ETA: {estimatedTime}
}
/>
Widget Support
Beyond Live Activities, Voltra enables React Native-based widgets:
import { Widget } from 'voltra'
const StatsWidget = ({ steps, calories, distance }) => (
Today's Activity
{steps.toLocaleString()} steps
{calories} calories
{distance} miles
)
// Register widget
Widget.register('fitness-stats', StatsWidget)
Users can then add your widget to their Home Screen, and it updates according to your specified interval or through push updates.
State Management Integration
Voltra integrates cleanly with popular state management libraries:
With Redux:
import { useSelector } from 'react-redux'
import { LiveActivity } from 'voltra'
function DeliveryMonitor() {
const deliveryState = useSelector(state => state.delivery)
useEffect(() => {
if (deliveryState.activeDelivery) {
LiveActivity.update(deliveryState.activityId, {
status: deliveryState.status,
progress: deliveryState.progress,
estimatedArrival: deliveryState.eta
})
}
}, [deliveryState])
// ...
}
With Zustand:
import create from 'zustand'
const useDeliveryStore = create((set) => ({
activityId: null,
status: 'idle',
startDelivery: async (orderData) => {
const activityId = await LiveActivity.start(DeliveryActivity, {
orderId: orderData.id,
restaurant: orderData.restaurant,
status: "Order placed",
progress: 0
})
set({ activityId, status: 'active' })
},
updateDelivery: async (updates) => {
const { activityId } = useDeliveryStore.getState()
await LiveActivity.update(activityId, updates)
set({ status: updates.status })
}
}))
With React Query:
import { useQuery } from '@tanstack/react-query'
function useDeliveryTracking(orderId) {
const { data: deliveryStatus } = useQuery({
queryKey: ['delivery', orderId],
queryFn: () => fetchDeliveryStatus(orderId),
refetchInterval: 10000, // Poll every 10 seconds
})
useEffect(() => {
if (deliveryStatus && activityId) {
LiveActivity.update(activityId, {
status: deliveryStatus.status,
progress: deliveryStatus.progress,
location: deliveryStatus.driverLocation
})
}
}, [deliveryStatus])
return deliveryStatus
}
Lifecycle Management
Voltra provides fine-grained control over Live Activity lifecycle:
import { LiveActivity } from 'voltra'
// Start a Live Activity
const activityId = await LiveActivity.start(MyActivity, {
initialProps: { /* ... */ }
})
// Update the Live Activity
await LiveActivity.update(activityId, {
newProps: { /* ... */ }
})
// Check if Live Activity is active
const isActive = await LiveActivity.isActive(activityId)
// Get current Live Activity state
const currentState = await LiveActivity.getState(activityId)
// End the Live Activity
await LiveActivity.end(activityId, {
dismissalPolicy: 'immediate' // or 'after-delay' (4 seconds)
})
// Listen to user interactions
LiveActivity.onTap(activityId, (region) => {
console.log(User tapped: ${region}
) // 'lockScreen', 'dynamicIsland', 'compactLeading', etc.
// Handle interaction - e.g., deep link to app
Linking.openURL('myapp://delivery/12345')
})
Push Notification Integration
Voltra seamlessly integrates with Apple Push Notification service for remote updates:
Registering for Push Updates:
import { LiveActivity, PushNotifications } from 'voltra'
// Enable push updates for Live Activities
const pushToken = await PushNotifications.registerForLiveActivities()
// Send the push token to your backend
await fetch('https://api.myapp.com/register-push', {
method: 'POST',
body: JSON.stringify({
userId: user.id,
pushToken: pushToken
})
})
// Start a Live Activity with push support
const activityId = await LiveActivity.start(DeliveryActivity, {
orderId: order.id,
enablePushUpdates: true
})
// Backend can now push updates via APNs
Backend Push Implementation:
// server/push-updates.ts
import apn from 'apn'
import { VoltraServer } from 'voltra/server'
const voltra = new VoltraServer({
apnProvider: new apn.Provider({
token: {
key: process.env.APN_KEY_PATH,
keyId: process.env.APN_KEY_ID,
teamId: process.env.APPLE_TEAM_ID
},
production: process.env.NODE_ENV === 'production'
})
})
// Push update when delivery status changes
async function updateDeliveryStatus(orderId: string, status: DeliveryStatus) {
const delivery = await db.deliveries.findOne({ orderId })
await voltra.pushUpdate({
pushToken: delivery.livePushToken,
activityId: delivery.liveActivityId,
data: {
status: status.message,
progress: status.progressPercentage / 100,
estimatedArrival: status.eta,
driverLocation: {
lat: status.driverLat,
lng: status.driverLng
}
}
})
}
// Call from your delivery tracking logic
await updateDeliveryStatus('ORD-12345', {
message: "Driver is 5 minutes away",
progressPercentage: 85,
eta: "2:45 PM",
driverLat: 37.7749,
driverLng: -122.4194
})
Getting Started with Voltra
Installation and Setup
For Expo Projects:
Install Voltra
npx expo install voltra
Add Voltra plugin to app configuration
app.json
{
"expo": {
"name": "My App",
"plugins": [
[
"voltra",
{
"activityTypes": ["delivery", "workout", "timer"],
"widgetFamilies": ["systemSmall", "systemMedium"],
"enablePushUpdates": true
}
]
]
}
}
Prebuild to generate native iOS project
npx expo prebuild
Run on iOS
npx expo run:ios
For React Native CLI Projects:
Install package
npm install voltra
Link native dependencies
cd ios && pod install && cd ..
Configure Info.plist with Live Activity support
ios/YourApp/Info.plist
NSSupportsLiveActivities
Run on iOS
npx react-native run-ios
Configuration
voltra.config.js:
module.exports = {
// Define your Live Activity types
activities: {
delivery: {
name: 'Delivery Tracking',
description: 'Track your order in real-time',
defaultProps: {
status: 'Processing',
progress: 0
}
},
workout: {
name: 'Workout Session',
description: 'Live workout metrics',
defaultProps: {
duration: 0,
calories: 0,
heartRate: 0
}
}
},
// Widget definitions
widgets: {
stats: {
families: ['systemSmall', 'systemMedium'],
updateInterval: 900, // 15 minutes
description: 'Daily stats at a glance'
}
},
// Push notification configuration
push: {
enabled: true,
teamId: process.env.APPLE_TEAM_ID,
keyId: process.env.APPLE_KEY_ID
}
}
Creating Your First Live Activity
Step 1: Define the Activity Component:
// components/DeliveryActivity.tsx import React from 'react' import { View, Text, Image, StyleSheet } from 'react-native' import { LiveActivity } from 'voltra'
}]} />interface DeliveryActivityProps { orderId: string restaurant: string status: string progress: number estimatedTime: string }
export const DeliveryActivity: React.FC
= ({ orderId, restaurant, status, progress, estimatedTime }) => { return ( {/* Lock Screen UI */} {restaurant} Order #{orderId}
${progress * 100}% {Math.round(progress * 100)}%
{status} Estimated arrival: {estimatedTime} {/* Dynamic Island UI */}
} compactTrailing={ {Math.round(progress * 100)}% } minimal={} > {/* Expanded Dynamic Island Content */} {restaurant} {status}
${progress * 100}% }]} />
{estimatedTime} ) }const styles = StyleSheet.create({ lockScreenContainer: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 12, }, header: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, icon: { width: 40, height: 40, marginRight: 12, }, headerText: { flex: 1, }, restaurant: { fontSize: 16, fontWeight: 'bold', color: '#000000', }, orderId: { fontSize: 12, color: '#666666', }, progressSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, progressBar: { flex: 1, height: 8, backgroundColor: '#E0E0E0', borderRadius: 4, overflow: 'hidden', marginRight: 8, }, progressFill: { height: '100%', backgroundColor: '#4CAF50', }, progressText: { fontSize: 14, fontWeight: '600', color: '#000000', }, status: { fontSize: 14, fontWeight: '500', color: '#000000', marginBottom: 4, }, eta: { fontSize: 12, color: '#666666', }, // Dynamic Island styles compactIcon: { width: 20, height: 20, }, compactText: { fontSize: 12, color: '#FFFFFF', }, minimalIcon: { width: 16, height: 16, }, expandedIsland: { padding: 16, }, expandedTitle: { fontSize: 14, fontWeight: 'bold', color: '#FFFFFF', marginBottom: 4, }, expandedStatus: { fontSize: 12, color: '#CCCCCC', marginBottom: 8, }, expandedProgress: { height: 4, backgroundColor: 'rgba(255, 255, 255, 0.3)', borderRadius: 2, overflow: 'hidden', marginBottom: 8, }, expandedEta: { fontSize: 11, color: '#CCCCCC', }, })
Step 2: Start the Live Activity:
// screens/OrderConfirmation.tsx
import React, { useState } from 'react'
import { View, Button } from 'react-native'
import { LiveActivity } from 'voltra'
import { DeliveryActivity } from '../components/DeliveryActivity'
export const OrderConfirmation = ({ order }) => {
const [activityId, setActivityId] = useState(null)
const startTracking = async () => {
try {
const id = await LiveActivity.start(DeliveryActivity, {
orderId: order.id,
restaurant: order.restaurant.name,
status: "Order confirmed",
progress: 0.1,
estimatedTime: order.estimatedDeliveryTime
})
setActivityId(id)
// Store activity ID for later updates
await AsyncStorage.setItem(delivery_activity_${order.id}
, id)
} catch (error) {
console.error('Failed to start Live Activity:', error)
}
}
return (
)
}
Step 3: Update the Live Activity:
// services/deliveryTracking.ts
import { LiveActivity } from 'voltra'
import AsyncStorage from '@react-native-async-storage/async-storage'
export class DeliveryTracker {
static async updateStatus(orderId: string, status: DeliveryStatus) {
const activityId = await AsyncStorage.getItem(delivery_activity_${orderId}
)
if (!activityId) {
console.warn('No active Live Activity found for order:', orderId)
return
}
try {
await LiveActivity.update(activityId, {
status: status.message,
progress: status.progressPercentage / 100,
estimatedTime: status.estimatedArrival
})
} catch (error) {
console.error('Failed to update Live Activity:', error)
}
}
static async endDelivery(orderId: string) {
const activityId = await AsyncStorage.getItem(delivery_activity_${orderId}
)
if (activityId) {
await LiveActivity.end(activityId, {
dismissalPolicy: 'after-delay' // Show final state for 4 seconds
})
await AsyncStorage.removeItem(delivery_activity_${orderId}
)
}
}
}
// Usage in your order status polling/websocket handler
function handleDeliveryUpdate(update: DeliveryUpdate) {
DeliveryTracker.updateStatus(update.orderId, {
message: update.statusMessage,
progressPercentage: update.progress,
estimatedArrival: update.eta
})
}
Advanced Use Cases
Fitness Tracking Live Activity
Build a workout session tracker with real-time metrics:
// components/WorkoutActivity.tsx
import { LiveActivity } from 'voltra'
import { View, Text, StyleSheet } from 'react-native'
interface WorkoutActivityProps {
workoutType: string
duration: number // seconds
calories: number
heartRate: number
distance: number // miles
}
export const WorkoutActivity: React.FC = ({
workoutType,
duration,
calories,
heartRate,
distance
}) => {
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return ${mins}:${secs.toString().padStart(2, '0')}
}
return (
{workoutType}
{formatDuration(duration)}
Duration
{calories}
Calories
{heartRate}
BPM
{distance.toFixed(2)}
Miles
🏃}
compactTrailing={{formatDuration(duration)} }
minimal={🏃 }
>
{workoutType}
{formatDuration(duration)}
{calories} cal
{heartRate} BPM
{distance.toFixed(2)} mi
)
}
// Hook for managing workout Live Activity
function useWorkoutLiveActivity() {
const [activityId, setActivityId] = useState(null)
const [metrics, setMetrics] = useState({
duration: 0,
calories: 0,
heartRate: 0,
distance: 0
})
const startWorkout = async (type: string) => {
const id = await LiveActivity.start(WorkoutActivity, {
workoutType: type,
duration: 0,
calories: 0,
heartRate: 0,
distance: 0
})
setActivityId(id)
}
const updateMetrics = async (newMetrics: Partial) => {
if (!activityId) return
const updated = { ...metrics, ...newMetrics }
setMetrics(updated)
await LiveActivity.update(activityId, {
duration: updated.duration,
calories: updated.calories,
heartRate: updated.heartRate,
distance: updated.distance
})
}
const endWorkout = async () => {
if (activityId) {
await LiveActivity.end(activityId)
setActivityId(null)
}
}
return { startWorkout, updateMetrics, endWorkout, metrics }
}
Timer/Countdown Live Activity
Create a visual countdown timer:
// components/TimerActivity.tsx
export const TimerActivity = ({ title, endTime, totalDuration }) => {
const remaining = Math.max(0, endTime - Date.now())
const progress = 1 - (remaining / totalDuration)
const formatTime = (ms: number) => {
const totalSeconds = Math.floor(ms / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return ${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}
}
return ${minutes}:${seconds.toString().padStart(2, '0')}
}
return (
{title}
{formatTime(remaining)}
⏱}
compactTrailing={{formatTime(remaining)} }
minimal={⏱ }
>
{title}
{formatTime(remaining)}
)
}
// Auto-updating timer implementation
function useTimerActivity(durationMs: number, title: string) {
const [activityId, setActivityId] = useState(null)
const startTimer = async () => {
const endTime = Date.now() + durationMs
const id = await LiveActivity.start(TimerActivity, {
title,
endTime,
totalDuration: durationMs
})
setActivityId(id)
// Update every second
const interval = setInterval(async () => {
const remaining = endTime - Date.now()
if (remaining <= 0) {
clearInterval(interval)
await LiveActivity.end(id)
setActivityId(null)
} else {
await LiveActivity.update(id, {
endTime,
totalDuration: durationMs
})
}
}, 1000)
return () => clearInterval(interval)
}
return { startTimer, activityId }
}
Sports Score Live Activity
Real-time sports game tracking:
// components/GameActivity.tsx
interface GameActivityProps {
sport: string
homeTeam: string
awayTeam: string
homeScore: number
awayScore: number
quarter: string
timeRemaining: string
possession?: 'home' | 'away' | null
}
export const GameActivity: React.FC = ({
sport,
homeTeam,
awayTeam,
homeScore,
awayScore,
quarter,
timeRemaining,
possession
}) => {
return (
{awayTeam}
{awayScore}
{homeTeam}
{homeScore}
{quarter}
{timeRemaining}
{awayScore}}
compactTrailing={{homeScore} }
minimal={🏈 }
>
{awayTeam} {awayScore}
{homeTeam} {homeScore}
{quarter} • {timeRemaining}
)
}
// WebSocket integration for real-time updates
function useGameTracking(gameId: string) {
const [activityId, setActivityId] = useState(null)
useEffect(() => {
const ws = new WebSocket(wss://api.sports.com/games/${gameId}
)
ws.onmessage = async (event) => {
const update = JSON.parse(event.data)
if (!activityId && update.type === 'game_start') {
const id = await LiveActivity.start(GameActivity, {
sport: update.sport,
homeTeam: update.homeTeam,
awayTeam: update.awayTeam,
homeScore: 0,
awayScore: 0,
quarter: update.quarter,
timeRemaining: update.time
})
setActivityId(id)
} else if (activityId && update.type === 'score_update') {
await LiveActivity.update(activityId, {
homeScore: update.homeScore,
awayScore: update.awayScore,
quarter: update.quarter,
timeRemaining: update.time,
possession: update.possession
})
} else if (activityId && update.type === 'game_end') {
await LiveActivity.end(activityId)
setActivityId(null)
}
}
return () => ws.close()
}, [gameId, activityId])
return { activityId }
}
Best Practices
Design Guidelines
Keep Lock Screen UI Concise: Live Activities on the Lock Screen compete for attention with other content. Follow these principles:
- •Hierarchy: Most important information should be largest and most prominent
- •Contrast: Ensure text is readable against various wallpapers
- •Brevity: Use short, scannable text (e.g., "Arriving in 5 min" vs "Your delivery will arrive in approximately 5 minutes")
- •Visual Balance: Don't overcrowd the space; white space improves readability
Dynamic Island Considerations: The Dynamic Island has unique size constraints:
- •Compact State: Very limited space; show only essential info
- •Expanded State: Users must tap to see; put detailed/secondary info here
- •Smooth Transitions: Design for graceful transitions between states
Performance Optimization
Minimize Update Frequency:
// Bad: Update every second
setInterval(() => {
LiveActivity.update(id, { time: Date.now() })
}, 1000)
// Good: Update only when meaningful change occurs
let lastUpdate = 0
function updateIfSignificantChange(newValue: number) {
if (Math.abs(newValue - lastUpdate) > THRESHOLD) {
LiveActivity.update(id, { value: newValue })
lastUpdate = newValue
}
}
Batch Updates:
// Bad: Multiple separate updates
await LiveActivity.update(id, { status: newStatus })
await LiveActivity.update(id, { progress: newProgress })
await LiveActivity.update(id, { eta: newEta })
// Good: Single batched update
await LiveActivity.update(id, {
status: newStatus,
progress: newProgress,
eta: newEta
})
Debounce Rapid Changes:
import { debounce } from 'lodash'
const debouncedUpdate = debounce(async (id: string, props: any) => {
await LiveActivity.update(id, props)
}, 500) // Wait 500ms after last change before updating
// Usage
function onLocationChange(location: Location) {
debouncedUpdate(activityId, {
driverLocation: location,
estimatedArrival: calculateETA(location)
})
}
Error Handling
Graceful Degradation:
async function startDeliveryTracking(order: Order) {
try {
// Check if Live Activities are supported
const supported = await LiveActivity.isSupported()
if (!supported) {
console.log('Live Activities not supported on this device')
// Fall back to push notifications
scheduleDeliveryNotifications(order)
return
}
const activityId = await LiveActivity.start(DeliveryActivity, {
orderId: order.id,
restaurant: order.restaurant,
status: "Preparing",
progress: 0.1
})
return activityId
} catch (error) {
console.error('Failed to start Live Activity:', error)
// Fallback to traditional approach
scheduleDeliveryNotifications(order)
}
}
Handle Lifecycle Edge Cases:
// Users can dismiss Live Activities manually
// Handle this gracefully by checking before updates
async function updateDelivery(orderId: string, status: DeliveryStatus) {
const activityId = await getActivityId(orderId)
if (!activityId) {
return // No active Live Activity
}
try {
const isActive = await LiveActivity.isActive(activityId)
if (!isActive) {
console.log('Live Activity was dismissed by user')
await removeActivityId(orderId)
return
}
await LiveActivity.update(activityId, {
status: status.message,
progress: status.progress
})
} catch (error) {
if (error.code === 'ACTIVITY_NOT_FOUND') {
await removeActivityId(orderId)
} else {
throw error
}
}
}
Testing Strategies
Simulator Testing:
// Use environment variables to enable mock data in simulator
const USE_MOCK_UPDATES = __DEV__ && !Platform.OS === 'ios'
function startMockDeliveryUpdates(activityId: string) {
if (!USE_MOCK_UPDATES) return
let progress = 0.1
const mockStatuses = [
"Preparing your order",
"Order ready for pickup",
"Driver on the way",
"Driver is nearby",
"Delivery complete"
]
const interval = setInterval(async () => {
progress += 0.2
await LiveActivity.update(activityId, {
status: mockStatuses[Math.floor(progress * 4)],
progress: Math.min(progress, 1),
estimatedTime: ${Math.max(0, 20 - progress * 20)} min
})
if (progress >= 1) {
clearInterval(interval)
setTimeout(() => {
LiveActivity.end(activityId)
}, 5000)
}
}, 5000) // Update every 5 seconds for testing
}
Device Testing: Live Activities only work on physical devices (iOS 16.1+) or iOS 16.1+ simulators:
Test on simulator
npx expo run:ios --device "iPhone 14 Pro" // Has Dynamic Island
Test on physical device
npx expo run:ios --device
Comparison with Traditional Approaches
Development Time
Traditional Approach:
- •Learning Swift/SwiftUI: 1-2 weeks for React developers
- •Building Widget Extension: 2-3 days
- •Creating Native Bridge: 1-2 days
- •Integration and Testing: 1-2 days
- •Total: 2-3 weeks
With Voltra:
- •Reading documentation: 2-3 hours
- •First Live Activity implementation: 4-6 hours
- •Integration and Testing: 2-4 hours
- •Total: 1-2 days
Code Maintenance
Traditional:
- •Two codebases (Swift + TypeScript)
- •Type synchronization between languages
- •Separate build processes
- •Platform-specific debugging
Voltra:
- •Single JavaScript/TypeScript codebase
- •Automatic type safety
- •Unified build process
- •Standard React Native debugging tools
Team Requirements
Traditional:
- •Requires iOS developer with Swift knowledge
- •React Native developer for integration
- •Coordination between native and RN teams
Voltra:
- •Any React Native developer can implement
- •No specialized iOS knowledge required
- •Unified team workflow
Conclusion
Voltra represents a transformative breakthrough for React Native developers, democratizing access to iOS Live Activities, Widgets, and Dynamic Island features that were previously locked behind the complexity of Swift development and native bridging. By enabling these powerful user engagement features to be built entirely in React Native, Voltra eliminates weeks of development time, reduces team specialization requirements, and brings the joy of hot reload and component-driven development to iOS platform features.
The implications extend beyond developer convenience. By lowering the barrier to implementing Live Activities, Voltra enables more apps to leverage these engaging features, ultimately creating better user experiences. A delivery app team that previously skipped Live Activities due to complexity can now add real-time tracking with a weekend of work. A fitness app can provide lock screen workout metrics without hiring an iOS specialist. A sports app can deliver live score updates in the Dynamic Island with standard React components.
As Voltra continues to mature and gain adoption in the React Native ecosystem, we can expect to see more innovative uses of Live Activities, driving competition and raising the bar for mobile app experiences. The package's AI-friendly API design, comprehensive documentation, and alignment with familiar React patterns position it to become an essential tool in the React Native developer's toolkit.
For teams building iOS apps with React Native, Voltra offers an opportunity to significantly enhance user engagement without the traditional investment in platform-specific development. The future of cross-platform mobile development includes seamless access to platform-specific features, and Voltra is leading the way.
Additional Resources
- •Official Documentation: (Package is in preview/early release; check npm for latest docs)
- •GitHub Repository: (Check npm package for repository link)
- •React Native Live Activities Guide: https://www.reactnative.university/blog/live-activities-unleashed
- •Apple Developer - ActivityKit: https://developer.apple.com/documentation/activitykit
- •Apple Developer - WidgetKit: https://developer.apple.com/documentation/widgetkit
- •Expo Live Activities Plugin: https://docs.expo.dev/versions/latest/sdk/live-activities/
- •React Native Documentation: https://reactnative.dev
- •iOS Human Interface Guidelines - Live Activities: https://developer.apple.com/design/human-interface-guidelines/live-activities
Note: Voltra is a newly announced package. For the most up-to-date installation instructions, API documentation, and examples, refer to the official npm package page and repository. As the package is in active development, some APIs and features described here may evolve.