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.
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
- iOS 16.1 or above
- Xcode 14.1 or above
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
Add NSSupportsLiveActivities
key and set to YES
.
import ActivityKit
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
}
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)")
}
}
}
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)
}
}
@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")
}
}
Console: Requested a pizza delivery Live Activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150
Updating content state for activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150
Console: Pizza delivery details: DA288E1B-F6F5-4BF1-AA73-E43E0CC13150 -> PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount: "$99")
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.
Swiftยฎ and SwiftUIยฎ are trademarks of Apple Inc.