Lock Screen
(잠금화면)과 Dynamic Island
(다이나믹 아일랜드)에 실시간 정보 표시 제공 (only iPhone 용)widget extension
이 필요 (없으면 새로 생성해야 한다)WidgetKit
기능과 인터페이스를 위한 SwiftUI
를 사용ActivityKit
의 역할은 Live Activity
의 life cycle
(수명 주기) 을 담당request
, update
, end
Displaying 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).")
}
}