
Lock Screen(잠금화면)과 Dynamic Island(다이나믹 아일랜드)에 실시간 정보 표시 제공 (only iPhone 용)widget extension 이 필요 (없으면 새로 생성해야 한다)WidgetKit 기능과 인터페이스를 위한 SwiftUI를 사용ActivityKit 의 역할은 Live Activity 의 life cycle(수명 주기) 을 담당request, update, endDisplaying live data with Live Activities
compact presentation: 단일 Live Activity 가 표시되는 모드Leading side, Trailing side 로 표시되는 모드
circular minimal presentation: 두가지 Live Activity 를 표시하는 모드Minimal, 우측에 떨어진 형태로 Minimal 한 원형형태로 표시되는 모드
expanded presentation: 길게 눌러서 확장된 형태로 표시되는 모드

Displaying live data with Live Activities
8시간동안 활성화될 수 있다.최대 12시간동안 표시되는 셈)image assets 가 필요하다.
Network사용이나 location 업데이트를 할 수 없다.ActivityKit 프레임워크를 사용하거나 ActivityKit push notifications(APN) 알림수신을 통해 업데이트할 수 있다.widget extension 내 여러 widget 을 넣을 수 있다.
기능별로 (location information 이 필요한 위젯끼리, 필요없는 위젯끼리) widget extension 을 여러개 만들 수 있다.
Widget Extension 클릭Include Live Activity 선택 (자동으로 Live Activity 를 Widget 내 추가하는 기본 코드 생성)Include Configuration Intent 선택 (위젯을 꾹눌러 사용자 설정기능을 제공할지 여부)StaticConfiguration: 사용자가 구성할 수 있는 속성이 없는 위젯인 경우 (뉴스 위젯)IntentConfiguration: 사용자가 구성할 수 있는 속성이 있는 위젯인 경우
kind: 위젯 식별 문자열provider: TimelineProvider 를 준수하는 WidgetKit 을 주기적으로 부르는 객체, 주기적으로 TimelineEntry 를 content 로 반환한다.intent: 사용자가 구성할 수 있는 속성을 정의content: closure 를 통해 provider 의 TimelineEntry 를 받아 위젯을 표시하는 SwiftUI View 부분configurationDisplayName: 위젯 표시명description: 위젯 설명내용supportedFamilies: 위젯 크기설정StaticConfiguration 설정된 단일 위젯 GameStatusWidget 예시코드
@main
struct GameStatusWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "com.mygame.game-status",
provider: GameStatusProvider(),
) { entry in
GameStatusView(entry.gameStatus)
}
.configurationDisplayName("Game Status")
.description("Shows an overview of your game status")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
@main 부분이 widget extension 의 시작점을 나타낸다.Widget -> WidgetBundle 을 통해 시작점을 설정해야 한다.widget gallery(위젯선택창) 에 표시되려면 앱을 실행한적이 있어야 표시된다.TinelineEntry 들로 구성된 timeline 을 생성합니다. TimelineEntry 는 widget 을 업데이트하기 위한 date 값과 status 값을 지닙니다.gameStatus 를 지닌 GameStatusEntry 예시코드
struct GameStatusEntry: TimelineEntry {
var date: Date
var gameStatus: String
}
widget gallery(위젯선책창)에 위젯을 서버응답이 지연되기 전에 빠르게 표시하기 위한 용도로 provider 에서 preview snapshot 을 제공해야 한다. (isPreview 값이 True 인 경우 getSnapshot 함수를 통해 반환)TimelineProvider 의 preview snapshot 제공하는 예시코드
struct GameStatusProvider: TimelineProvider {
var hasFetchedGameStatus: Bool
var gameStatusFromServer: String
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
let date = Date()
let entry: GameStatusEntry
if context.isPreview && !hasFetchedGameStatus {
entry = GameStatusEntry(date: date, gameStatus: "—")
} else {
entry = GameStatusEntry(date: date, gameStatus: gameStatusFromServer)
}
completion(entry)
}
getTimeline 함수를 호출합니다. (provider 에게 정기적인 timeline 을 요청)timeline 은 tineline entries 와 WidgetKit 에서 후속 timeline을 요청할 때 reload 하는 policy 로 구성됩니다.server 로부터 status 를 받는 timeline 과 15분 간격 reload 하는 예시코드
struct GameStatusProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> Void) {
// Create a timeline entry for "now."
let date = Date()
let entry = GameStatusEntry(
date: date,
gameStatus: gameStatusFromServer
)
// Create a date that's 15 minutes in the future.
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!
// Create the timeline with the entry and a reload policy with the date
// for the next update.
let timeline = Timeline(
entries:[entry],
policy: .after(nextUpdateDate)
)
// Call the completion to pass the timeline to WidgetKit.
completion(timeline)
}
}
redacted 함수를 통해 백그라운드에서 데이터를 로드중일 때 placeholder 를 표시할 수 있습니다.unredacted 함수를 사용합니다.struct GameStatusView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var gameStatus: GameStatus
var selectedCharacter: CharacterDetail
@ViewBuilder
var body: some View {
switch family {
case .systemSmall: GameTurnSummary(gameStatus)
case .systemMedium: GameStatusWithLastTurnResult(gameStatus)
case .systemLarge: GameStatusWithStatistics(gameStatus)
case .systemExtraLarge: GameStatusWithStatisticsExtraLarge(gameStatus)
case .accessoryCircular: HealthLevelCircular(selectedCharacter)
case .accessoryRectangular: HealthLevelRectangular(selectedCharacter)
case .accessoryInline: HealthLevelInline(selectedCharacter)
default: GameDetailsNotAvailable()
}
}
}
accessoryCircular, accessoryRectangular, accessoryInline 의 경우 홈화면의 위젯 보다 훨씬 작은 위젯입니다.@ViewBuilder 로 선언합니다.IntentConfiguration 이 설정된 위젯인 경우 IntentTimelineProvider 를 준수하는 provider 를 사용합니다.read-only 정보만 표시합니다.widgetURL 을 설정하여 위젯을 터치했을 경우 앱 내 특정창으로 이동할 수 있습니다.onOpenURL, application(_:open:options:), application(_:open:) 으로 수신할 수 있습니다.widgetURL 설정된 widget SwiftUI 예시코드
@ViewBuilder
var body: some View {
ZStack {
AvatarView(entry.character)
.widgetURL(entry.character.url)
.foregroundColor(.white)
}
.background(Color.gameBackground)
}
WidgetBundle 구조체를 사용하여 Widget 의 body 내 그룹화할 수 있습니다.widgetBundle 예시 코드
@main
struct GameWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
GameStatusWidget()
CharacterDetailWidget()
LeaderboardWidget()
}
}
@Environment(\.widgetFamily) var family
Group {
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryInline))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .systemMedium))
.previewDisplayName("\(family)")
}
widget extension 내 추가하는 식widget extension 을 생성 및 추가Info.plist 파일 내 key: NSSupportsLiveActivities, value: YES (Boolean) 추가ActivityAttributes 구조체를 정의ActivityAttributes 를 통해 ActivityConfiguration 을 생성Live Activities 의 configure, start, update, end 관련 코드를 작성widget extension 을 추가 후 Live Activity 를 표시하기 위한 데이터인 ActivityAttributes 를 작성해야 한다.정적인 데이터의 경우 변수로 추가동적인 데이터의 경우 ContentState 구조체 내 추가import Foundation
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
public typealias PizzaDeliveryStatus = ContentState
public struct ContentState: Codable, Hashable {
var driverName: String
var deliveryTimer: ClosedRange<Date>
}
var numberOfPizzas: Int
var totalAmount: String
var orderNumber: String
}
WidgetKit으로 Widget 을 반환하는 코드 내 ActivityConfiguration 을 추가합니다.Include Live Activities 를 선택하면 자동으로 widget 과 Live Activity 가 포함된 widget bundle 을 생성합니다.import SwiftUI
import WidgetKit
@main
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the presentation that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
// ...
} dynamicIsland: { context in
// Create the presentations that appear in the Dynamic Island.
// ...
}
}
}
WidgetBundle 이 있는 경우 Live Activity 를 추가합니다.@main
struct PizzaDeliveryWidgets: WidgetBundle {
var body: some Widget {
FavoritePizzaWidget()
if #available(iOS 16.1, *) {
PizzaDeliveryLiveActivity()
}
}
}
Widget extension 내에서 SwiftUI 를 사용하여 Live Activity UI를 작성합니다.widget extension 코드
@main
struct PizzaDeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the presentation that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the presentations that appear in the Dynamic Island.
// ...
}
}
}
Lock Screen 용 Live Activity UI 코드
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<PizzaDeliveryAttributes>
var body: some View {
VStack {
Spacer()
Text("\(context.state.driverName) is on their way with your pizza!")
Spacer()
HStack {
Spacer()
Label {
Text("\(context.attributes.numberOfPizzas) Pizzas")
} icon: {
Image(systemName: "bag")
.foregroundColor(.indigo)
}
.font(.title2)
Spacer()
Label {
Text(timerInterval: context.state.deliveryTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 50)
.monospacedDigit()
} icon: {
Image(systemName: "timer")
.foregroundColor(.indigo)
}
.font(.title2)
Spacer()
}
Spacer()
}
.activitySystemActionForegroundColor(.indigo)
.activityBackgroundTint(.cyan)
}
}
ActivityAttributes 구조체를 지닌 ActivityContent 객체를 통해 설정할 수 있다.staleDate 는 옵셔널 설정이지만, 설정하면 오래된 정보인지를 표시할 수 있다.relevanceScore 값을 통해 잠금화면과 다이나믹아일랜드에 어떤 Activity 를 표시할 지 우선순위를 정할 수 있다. (기본적으로 먼저 시작된 Activity 를 표시, 큰값을 우선적으로 표시: 100 ~ 50)activity: Activity 프로퍼티를 지니고 있어야 종료 및 업데이트를 반영할 수 있다. ActivityContent 객체 설정 예시코드 (30분 후 업데이트)
if ActivityAuthorizationInfo().areActivitiesEnabled {
var future = Calendar.current.date(byAdding: .minute, value: (Int(minutes) ?? 0), to: Date())!
future = Calendar.current.date(byAdding: .second, value: (Int(seconds) ?? 0), to: future)!
let date = Date.now...future
let initialContentState = PizzaDeliveryAttributes.ContentState(driverName: "Bill James", deliveryTimer:date)
let activityAttributes = PizzaDeliveryAttributes(numberOfPizzas: 3, totalAmount: "$42.00", orderNumber: "12345")
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 30, to: Date())!)
// Start the Live Activity.
do {
deliveryActivity = try Activity.request(attributes: activityAttributes, content: activityContent)
print("Requested a pizza delivery Live Activity \(String(describing: deliveryActivity?.id)).")
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription).")
}
}