Mastering Live Activities in iOS: The Complete Developer’s Guide
Transform your app’s user experience with real-time updates that keep users engaged even when your app isn’t open
Picture this: Your users can track their food delivery, monitor their workout progress, or follow their favorite team’s score without ever opening your app. They glance at their Lock Screen or Dynamic Island and instantly see exactly what they need to know. This isn’t science fiction — this is Live Activities in iOS, and it’s revolutionizing how users interact with mobile applications.
Since its introduction in iOS 16.1, Live Activities has quietly become one of the most powerful tools for user engagement on the iOS platform. Yet, surprisingly few developers have fully embraced its potential. This comprehensive guide will take you from zero to Live Activities expert, with real code examples, production-ready implementations, and insights from building dozens of Live Activity experiences.
The Psychology Behind Live Activities: Why They Work
Before diving into code, let’s understand why Live Activities are so effective from a user experience perspective. Traditional push notifications create interruption-based engagement — they demand immediate attention, then disappear. Live Activities, on the other hand, create what UX researchers call “ambient awareness.”
The Ambient Computing Revolution
Live Activities tap into our natural human behavior of peripheral monitoring. Just as you might glance at a clock without actively seeking the time, users naturally scan their Lock Screen for relevant information. This creates several psychological benefits:
Reduced Cognitive Load: Users don’t need to remember to check your app-the information is always visible when they need it.
Decreased Anxiety: For time-sensitive activities like deliveries or rides, constant visibility reduces uncertainty and stress.
Increased Trust: Persistent, accurate updates build confidence in your service’s reliability.
Habit Formation: Regular, valuable glances at Live Activities can become part of users’ daily routines.
What Are Live Activities, Really?
Live Activities are far more sophisticated than simple widgets. They’re dynamic, persistent interfaces that live in two critical locations:
Lock Screen Integration
The Lock Screen presentation is your primary canvas — a rectangular area that appears above notifications but below the time. Here, you have room for rich content including:
- Multiple lines of text with hierarchical typography
- Progress indicators and status badges
- Custom color schemes and branding
- Subtle animations and transitions
Dynamic Island Mastery
For iPhone 14 Pro and later, the Dynamic Island offers three distinct presentation modes:
Compact Mode: Two small areas flanking the pill-shaped cutout
Minimal Mode: A single, tiny indicator when multiple activities compete for space
Expanded Mode: A larger area that appears when users long-press or tap
Get Gaurav Harkhani’s stories in your inbox
Join Medium for free to get updates from this writer.
Understanding when and how iOS switches between these modes is crucial for designing effective experiences.
Key Benefits for Developers and Users
For Users
- Glanceable Information: Quick access to important updates without opening the app
- Real-time Updates: Information stays current automatically
- Interactive Elements: Tap to perform actions or open specific app screens
- Persistent Visibility: No risk of missing critical updates
For Developers
- Increased Engagement: Users stay connected to your app’s content
- Reduced App Opens: Users can get information without launching your app
- Better User Experience: Seamless integration with iOS interface
- Competitive Advantage: Stand out with modern, native iOS features
Technical Requirements
Before implementing Live Activities, ensure your project meets these requirements:
// iOS Deployment Target
iOS 16.1 or later
// Required Frameworks
import ActivityKit
import WidgetKit
import SwiftUI
// Capabilities
- Push Notifications capability
- Live Activities entitlementProject Configuration
- Enable Live Activities in Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>- Add Widget Extension: Create a new Widget Extension target for your Live Activity widgets.
- Configure App Groups (if sharing data between app and widget):
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourcompany.yourapp</string>
</array>Implementation Deep Dive
Step 1: Define Your Activity Attributes
Create a struct conforming to ActivityAttributes protocol:
import ActivityKit
import Foundation
struct DeliveryActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic content that changes over time
var status: DeliveryStatus
var estimatedDeliveryTime: Date
var currentLocation: String
var driverName: String
}
// Static content that doesn't change
var orderNumber: String
var restaurantName: String
var orderItems: [String]
var deliveryAddress: String
}
enum DeliveryStatus: String, CaseIterable, Codable {
case confirmed = "Order Confirmed"
case preparing = "Preparing"
case ready = "Ready for Pickup"
case picked = "Out for Delivery"
case delivered = "Delivered"
var emoji: String {
switch self {
case .confirmed: return "✅"
case .preparing: return "👨🍳"
case .ready: return "🥡"
case .picked: return "🚗"
case .delivered: return "🎉"
}
}
}Step 2: Create the Live Activity Widget
In your Widget Extension target, create the Live Activity widget:
import ActivityKit
import WidgetKit
import SwiftUI
@main
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
// Lock screen/banner UI goes here
DeliveryLiveActivityView(
attributes: context.attributes,
state: context.state
)
} dynamicIsland: { context in
// Dynamic Island UI goes here
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
HStack {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
Text(context.attributes.orderNumber)
.font(.caption)
.fontWeight(.semibold)
}
}
DynamicIslandExpandedRegion(.trailing) {
Text(context.state.estimatedDeliveryTime, style: .timer)
.font(.caption2)
.fontWeight(.semibold)
.foregroundColor(.secondary)
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.status.rawValue)
.font(.caption)
.fontWeight(.medium)
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Text(context.attributes.restaurantName)
.font(.caption2)
Spacer()
Text("Driver: \(context.state.driverName)")
.font(.caption2)
}
.foregroundColor(.secondary)
}
} compactLeading: {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
} compactTrailing: {
Text(context.state.estimatedDeliveryTime, style: .timer)
.font(.caption2)
.fontWeight(.semibold)
} minimal: {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
}
}
}
}
struct DeliveryLiveActivityView: View {
let attributes: DeliveryActivityAttributes
let state: DeliveryActivityAttributes.ContentState
var body: some View {
VStack(spacing: 12) {
// Header
HStack {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
.font(.title2)
VStack(alignment: .leading) {
Text("Order #\(attributes.orderNumber)")
.font(.headline)
.fontWeight(.semibold)
Text(attributes.restaurantName)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text("ETA")
.font(.caption)
.foregroundColor(.secondary)
Text(state.estimatedDeliveryTime, style: .timer)
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.orange)
}
}
// Progress indicator
HStack {
ForEach(DeliveryStatus.allCases, id: \.self) { status in
HStack {
Circle()
.fill(state.status == status ? Color.orange : Color.gray.opacity(0.3))
.frame(width: 12, height: 12)
if status != DeliveryStatus.allCases.last {
Rectangle()
.fill(shouldShowProgress(current: state.status, target: status) ?
Color.orange : Color.gray.opacity(0.3))
.frame(height: 2)
}
}
}
}
// Current status
HStack {
Text("\(state.status.emoji) \(state.status.rawValue)")
.font(.subheadline)
.fontWeight(.medium)
Spacer()
if state.status == .picked {
Text("📍 \(state.currentLocation)")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.padding(16)
.background(Color(UIColor.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
private func shouldShowProgress(current: DeliveryStatus, target: DeliveryStatus) -> Bool {
let allCases = DeliveryStatus.allCases
guard let currentIndex = allCases.firstIndex(of: current),
let targetIndex = allCases.firstIndex(of: target) else {
return false
}
return currentIndex > targetIndex
}
}Step 3: Start a Live Activity
In your main app, start the Live Activity:
import ActivityKit
class DeliveryTrackingManager: ObservableObject {
@Published var currentActivity: Activity<DeliveryActivityAttributes>?
func startLiveActivity(for order: Order) {
let attributes = DeliveryActivityAttributes(
orderNumber: order.number,
restaurantName: order.restaurant,
orderItems: order.items,
deliveryAddress: order.address
)
let initialState = DeliveryActivityAttributes.ContentState(
status: .confirmed,
estimatedDeliveryTime: order.estimatedDelivery,
currentLocation: order.restaurant,
driverName: "Driver will be assigned"
)
do {
currentActivity = try Activity<DeliveryActivityAttributes>.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .token
)
} catch {
print("Error starting Live Activity: \(error.localizedDescription)")
}
}
func updateLiveActivity(status: DeliveryStatus, location: String, driver: String, eta: Date) {
guard let activity = currentActivity else { return }
let updatedState = DeliveryActivityAttributes.ContentState(
status: status,
estimatedDeliveryTime: eta,
currentLocation: location,
driverName: driver
)
Task {
await activity.update(
.init(state: updatedState, staleDate: nil)
)
}
}
func endLiveActivity() {
guard let activity = currentActivity else { return }
let finalState = DeliveryActivityAttributes.ContentState(
status: .delivered,
estimatedDeliveryTime: Date(),
currentLocation: "Delivered",
driverName: "Delivery Complete"
)
Task {
await activity.end(
.init(state: finalState, staleDate: nil),
dismissalPolicy: .after(.now.addingTimeInterval(5))
)
DispatchQueue.main.async {
self.currentActivity = nil
}
}
}
}Remote Push Updates
For server-driven updates, configure push notifications for Live Activities:
Server-side Implementation
{
"aps": {
"timestamp": 1678886400,
"event": "update",
"content-state": {
"status": "picked",
"estimatedDeliveryTime": 1678890000,
"currentLocation": "Main St & 5th Ave",
"driverName": "John Smith"
}
}
}Handle Push Token
func monitorLiveActivity() {
guard let activity = currentActivity else { return }
Task {
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
// Send token to your server
await sendPushTokenToServer(tokenString, activityId: activity.id)
}
}
}Best Practices and Design Guidelines
Content Strategy
- Keep It Relevant: Only show information users actually need to see
- Progressive Disclosure: Start with essential info, add details as needed
- Clear Hierarchy: Use typography and spacing to guide attention
- Consistent Updates: Regular updates keep users engaged
Visual Design
- Brand Consistency: Use your app’s colors and visual language
- Readable Typography: Ensure text is legible at small sizes
- Meaningful Icons: Use system icons or simple custom icons
- Appropriate Contrast: Consider both light and dark modes
Performance Optimization
- Efficient Updates: Only update when content actually changes
- Reasonable Frequency: Don’t overwhelm users with constant updates
- Proper Lifecycle: End activities when they’re no longer relevant
- Memory Management: Keep widget views lightweight
Common Use Cases and Examples
1. Food Delivery
- Order confirmation and preparation status
- Driver assignment and location tracking
- Estimated delivery time updates
2. Transportation
- Ride booking and driver arrival
- Route progress and ETA updates
- Trip completion and rating prompts
3. Sports and Entertainment
- Live game scores and key moments
- Workout progress and milestones
- Event countdowns and reminders
4. E-commerce
- Order processing and shipping updates
- Delivery tracking and notifications
- Return status and processing
5. Productivity
- Timer and pomodoro sessions
- Task deadlines and reminders
- Meeting countdowns and agenda items
Troubleshooting Common Issues
Live Activity Not Appearing
- Verify iOS version (16.1+)
- Check Live Activities are enabled in Settings
- Ensure proper entitlements and capabilities
- Validate ActivityAttributes implementation
Updates Not Working
- Check network connectivity for push updates
- Verify push token registration
- Ensure proper server payload format
- Check activity state and lifecycle
Dynamic Island Issues
- Test on supported devices (iPhone 14 Pro+)
- Verify Dynamic Island layouts
- Check content sizing and constraints
- Test different activity states
Testing and Debugging
Local Testing
#if DEBUG
extension DeliveryActivityAttributes {
static var preview: DeliveryActivityAttributes {
DeliveryActivityAttributes(
orderNumber: "12345",
restaurantName: "Demo Restaurant",
orderItems: ["Burger", "Fries", "Drink"],
deliveryAddress: "123 Main St"
)
}
}
#endifWidget Preview
struct DeliveryLiveActivityView_Previews: PreviewProvider {
static let attributes = DeliveryActivityAttributes.preview
static let contentState = DeliveryActivityAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: .now.addingTimeInterval(1800),
currentLocation: "Kitchen",
driverName: "John Doe"
)
static var previews: some View {
attributes
.previewContext(contentState, viewKind: .content)
.previewDisplayName("Live Activity")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Dynamic Island Compact")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Dynamic Island Expanded")
}
}Future Considerations and iOS 17+ Enhancements
Apple continues to enhance Live Activities with each iOS release. iOS 17 brought several improvements:
- Interactive Elements: Buttons and controls within Live Activities
- Enhanced Animations: Smoother transitions and custom animations
- Improved Performance: Better memory management and update efficiency
- Expanded Use Cases: New scenarios and interaction patterns
Conclusion
Live Activities represent a powerful tool for creating engaging, persistent user experiences on iOS. By providing real-time, glanceable information directly on the Lock Screen and Dynamic Island, they can significantly improve user engagement and satisfaction.
The key to successful Live Activity implementation lies in understanding your users’ needs, designing clear and useful information hierarchies, and maintaining consistent, timely updates. Whether you’re tracking deliveries, monitoring workouts, or following live events, Live Activities can transform how users interact with your app’s most important content.
Start small with a focused use case, iterate based on user feedback, and gradually expand to other scenarios. With thoughtful implementation and attention to Apple’s design guidelines, Live Activities can become one of your app’s most valuable features.
Remember: the best Live Activities feel like a natural extension of iOS itself, providing exactly the right information at exactly the right time. Focus on user value, maintain visual consistency, and always respect the user’s attention — your users will thank you for it.
Additional Resources:
- Apple’s ActivityKit Documentation
- WWDC 2022: Meet ActivityKit
- Human Interface Guidelines — Live Activities
Happy Coding! 🎉










