iOS16-Live-Activities
iOS16-Live-Activities copied to clipboard
SwiftPizza App for Apple ActivityKit & WidgetKit & Dynamic Island.

iOS16 Live Activities + Dynamic Island 🏝️
SwiftPizza 🍕👨🏻🍳 App for Apple ActivityKit & WidgetKit
This is the first project example referring to the latest Apple ActivityKit beta and Dynamic Island (NEW) release.
Live Activities will help you follow an ongoing activity right from your Lock Screen, so you can track the progress of your food delivery or use the Now Playing controls without unlocking your device.
Your app’s Live Activities display on the Lock Screen and in Dynamic Island — a new design that introduces an intuitive, delightful way to experience iPhone 14 Pro and iPhone 14 Pro Max.
Preview 📱
More Videos 📼
https://twitter.com/1998design/status/1552681295607566336?s=21&t=waceX8VvaP-VCGc2KJmHpw https://twitter.com/1998design/status/1552686498276814848?s=21&t=waceX8VvaP-VCGc2KJmHpw https://twitter.com/1998design/status/1570225193095933952?s=21&t=LoYk1Llj0cLpEhG0MBFZLw
Environment 🔨
- iOS 16.1 or above
- Xcode 14.1 or above
Tutorial 🤔
Dynamic Island: https://1998design.medium.com/how-to-create-dynamic-island-widgets-on-ios-16-1-or-above-dca0a7dd1483
Live Activities: https://1998design.medium.com/how-to-create-live-activities-widget-for-ios-16-2c07889f1235
Usage
Info.plist
Add NSSupportsLiveActivities
key and set to YES
.
Import
import ActivityKit
Activity Attributes (Targeted to both App and Widget)
struct PizzaDeliveryAttributes: ActivityAttributes {
public typealias PizzaDeliveryStatus = ContentState
public struct ContentState: Codable, Hashable {
var driverName: String
// Changed from Date to ClosedRange<Date> - 16.1
var estimatedDeliveryTime: ClosedRange<Date>
}
var numberOfPizzas: Int
var totalAmount: String
}
CRUD Functions (Start / Update / Stop / Show ALL)
func startDeliveryPizza() {
let pizzaDeliveryAttributes = PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount:"$99")
// Date() changed to Date()...Date() - 16.1
let initialContentState = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
do {
let deliveryActivity = try Activity<PizzaDeliveryAttributes>.request(
attributes: pizzaDeliveryAttributes,
contentState: initialContentState,
pushType: nil)
print("Requested a pizza delivery Live Activity \(deliveryActivity.id)")
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription)")
}
}
func updateDeliveryPizza() {
Task {
// Date() changed to Date()...Date() - 16.1
let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(60 * 60))
for activity in Activity<PizzaDeliveryAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}
func stopDeliveryPizza() {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}
func showAllDeliveries() {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities {
print("Pizza delivery details: \(activity.id) -> \(activity.attributes)")
}
}
}
Widgets
import ActivityKit
import WidgetKit
import SwiftUI
@main
struct Widgets: WidgetBundle {
var body: some Widget {
PizzaDeliveryActivityWidget()
}
}
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
// attributesType changed to for - 16.1
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text("\(context.state.driverName) is on the way!").font(.headline)
HStack {
VStack {
Divider().frame(height: 6).overlay(.blue).cornerRadius(5)
}
Image(systemName: "box.truck.badge.clock.fill").foregroundColor(.blue)
VStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 6)
}
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
VStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 6)
}
Image(systemName: "house.fill").foregroundColor(.green)
}
}.padding(.trailing, 25)
Text("\(context.attributes.numberOfPizzas) 🍕").font(.title).bold()
}.padding(5)
Text("You've already paid: \(context.attributes.totalAmount) + $9.9 Delivery Fee 💸").font(.caption).foregroundColor(.secondary)
}.padding(15)
}
// NEW 16.1
dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Label("\(context.attributes.numberOfPizzas) Pizzas", systemImage: "bag")
.font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.driverName) is on his way!")
.lineLimit(1)
.font(.caption)
}
DynamicIslandExpandedRegion(.bottom) {
Button {
// Deep link into the app.
} label: {
Label("Contact driver", systemImage: "phone")
}
}
} compactLeading: {
Label {
Text("\(context.attributes.numberOfPizzas) Pizzas")
} icon: {
Image(systemName: "bag")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
} minimal: {
VStack(alignment: .center) {
Image(systemName: "timer")
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.caption2)
}
}
.keylineTint(.accentColor)
}
}
Xcode Preview (iOS 16.2 or above)
@available(iOSApplicationExtension 16.2, *)
struct PizzaDeliveryActivityWidget_Previews: PreviewProvider {
static let activityAttributes = PizzaDeliveryAttributes(numberOfPizzas: 2, totalAmount: "1000")
static let activityState = PizzaDeliveryAttributes.ContentState(driverName: "Tim", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
static var previews: some View {
activityAttributes
.previewContext(activityState, viewKind: .content)
.previewDisplayName("Notification")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Compact")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Expanded")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
}
}
Responses
Start Activity
Console: Requested a pizza delivery Live Activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150
Update Activity
Updating content state for activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150
Show ALL Activities
Console: Pizza delivery details: DA288E1B-F6F5-4BF1-AA73-E43E0CC13150 -> PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount: "$99")
How to pass image data to the widget?
Q1. Can I use Local Assets Folder?
A1. YES.
✅ Easy to implement
✅ May possible to change image (string name) when updating the event
❎ Limited options and big app size.
If you need to add more image sets, then re-upload to App Store is required (Time wasting, and not all users can get the instant update)
Q2. Can I use Network Image?
A2. YES. Load the image from the Internet, and pass the data to the widget via App Group and AppStorage (aka UserDefaults)
✅ Update in any time as the url can be changed / modify remotely.
✅ No need to store in Assets Folder and reduced app size.
❎ Unless the user re-open the app, the image cannot be updated in the background.
Q3. How about AsyncImage?
A3. NO. (Known not working)
Both cases 1 & 2 are already demoed on the sample project.
Structure
Resources 🐋
https://developer.apple.com/documentation/activitykit/displaying-live-data-on-the-lock-screen-with-live-activities
Legal 😄
Swift® and SwiftUI® are trademarks of Apple Inc.