
Live Activities & Dynamic Island 를 지원하기 위해서는 Widget Extension 이 필요했다.
Widget extension 내 여러 widget 들과 함께 Live Activities 가 추가되는 원리.
File -> New -> Target -> iOS -> Widget Extension 통해 생성
Prodduct Name: widget 입력Include Live Activity 선택 (자동으로 Live Activity 예제코드 생성)Include Configuration Intent 선택 (위젯 편집 기능 제공)
Info.plist 파일 내 key: NSSupportsLiveActivities, value: YES (Boolean) 추가

Widget Extension 생성으로 자동으로 WidgetBundle 가 생성된다.
WidgetBundle 내 widget 과 widgetLiveActivity 가 포함되는 식.
import WidgetKit
import SwiftUI
@main
struct widgetBundle: WidgetBundle {
var body: some Widget {
// widget()
if #available(iOS 16.2, *) {
widgetLiveActivity()
}
}
}
Live Activities & Dynamic Island 에서 표시할 데이터를 정의하기 위한 구조체.
ContentState 내에 정의.ActivityAttributes 구조체 변수로 정의.현재 Live Activities 사용로직상 업데이트가 불필요하지만 본인의 경우 이런식으로 작성
import Foundation
import ActivityKit
struct TimerStopwatchAttributes: ActivityAttributes {
public typealias titiStatus = ContentState
public struct ContentState: Codable, Hashable {
var taskName: String
var timer: ClosedRange<Date>
}
var isTimer: Bool
}
Widget 과 다르게 App 에서 데이터를 넘길 수 있으므로 해당 swift 파일의 Target Membership 을 App, WidgetExtension 모두 체크해야 한다.

Widget Extension 생성으로 자동으로 widgetLiveActivity 가 생성된다.
ActivityConfiguration 클로저를 통해 context 로 위에서 구현한 ActivityAttributes 구조체가 전달된다.
LiveActivities(잠금화면 실시간 현황)의 경우 해당 클로저 안에 context 를 받아 표시하는 SwiftUI View 를 정의 하면 된다.
struct widgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerStopwatchAttributes.self) { context in
// Presentation on Lock Screen and as a banner on the Home Screen
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the presentations that appear in the Dynamic Island.
// ...
}
}
}
context 를 통해 ActivityAttributes 구조체를 받아 view 에 표시한다.
isLuminanceReduced 변수를 통해 iPhone 14Pro 사용자의 Always On Display 상태의 농도가 옅은 view 를 저의 할 수 있다.
View 표시에 필요한 Assets 의 경우 WidgetExtension 추가로 생성된 Assets 내 추가되어야 한다.
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<TimerStopwatchAttributes>
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
if isLuminanceReduced {
VStack(spacing: -3) {
Spacer(minLength: 14)
Text("\(context.state.taskName)")
.foregroundColor(.white.opacity(0.5))
Text(timerInterval: context.state.timer, countsDown: context.attributes.isTimer)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.system(size: 44, weight: .semibold))
.foregroundColor(color(context: context).opacity(0.5))
Spacer()
}
.background(.black.opacity(0.6))
} else {
VStack(spacing: -3) {
Spacer(minLength: 14)
Text("\(context.state.taskName)")
.foregroundColor(.white)
Text(timerInterval: context.state.timer, countsDown: context.attributes.isTimer)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.system(size: 44, weight: .semibold))
.foregroundColor(color(context: context))
Spacer()
}
.background(.black.opacity(0.6))
}
}
func color(context: ActivityViewContext<TimerStopwatchAttributes>) -> Color {
if let color = UserDefaults.colorForKey(key: context.attributes.isTimer ? .timerBackground : .stopwatchBackground) {
return Color(color)
} else {
return Color(UIColor(named: context.attributes.isTimer ? "Background" : "Background2")!)
}
}
}
| Always On Display | Dark Mode | Light Mode |
|---|---|---|
![]() | ![]() | ![]() |
위에서 본 widgetLiveActivity 내 dynamicIsland 클로저 내 정의하면 된다.
Dynamic Island 의 전체구조는 아래와 같다.
expanded 상태의 View 는 DynamicIsland 클로저 내에 영역별로 정의leading, trailing, conter, bottom 영역별로 정의compact 상태의 View 는 compactLeading, compactTrailing 클로저 내에 영역별로 정의minimal 상태의 view 는 minimal 클로저 내에 정의struct widgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerStopwatchAttributes.self) { context in
// Presentation on Lock Screen and as a banner on the Home Screen
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the presentations that appear in the Dynamic Island.
DynamicIsland {
// Create the expanded presentation.
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.center) {
Text("Center")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom")
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T")
} minimal: {
Text("M")
}
}
}
}
| expanded | compact | minimal |
|---|---|---|
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
최종 구현한 코드
struct widgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerStopwatchAttributes.self) { context in
// Presentation on Lock Screen and as a banner on the Home Screen
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the presentations that appear in the Dynamic Island.
DynamicIsland {
// Create the expanded presentation.
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.taskName)")
.lineLimit(1)
}
DynamicIslandExpandedRegion(.bottom) {
Text(timerInterval: context.state.timer, countsDown: context.attributes.isTimer)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.system(size: 44, weight: .semibold))
.foregroundColor(color(context: context))
}
} compactLeading: {
Image("titiIcon")
.renderingMode(.template)
.colorMultiply(color(context: context))
} compactTrailing: {
Text(timerInterval: context.state.timer, countsDown: context.attributes.isTimer)
.monospacedDigit()
.frame(width: 50)
.font(.system(size: 12.7, weight: .semibold))
.foregroundColor(color(context: context))
} minimal: {
Image("titiIcon")
.renderingMode(.template)
.colorMultiply(color(context: context))
}
.contentMargins(.all, 8, for: .expanded)
}
}
func color(context: ActivityViewContext<TimerStopwatchAttributes>) -> Color {
if let color = UserDefaults.colorForKey(key: context.attributes.isTimer ? .timerBackground : .stopwatchBackground) {
return Color(color)
} else {
return Color(UIColor(named: context.attributes.isTimer ? "Background" : "Background2")!)
}
}
}
| expanded | compact | minimal |
|---|---|---|
![]() | ![]() | ![]() |
본인의 경우 App 내에서 특정상황에 표시하도록 구현.
import ActivityKit 한다.if #available(iOS 16.2, *) 를 통해 사용가능한 버전인지 확인.ActivityAuthorizationInfo().areActivitiesEnabled 를 통해 활성화 가능한 상태인지 확인.ContentState, ActivityAttributes, ActivityContent 세가지가 필요.Activity.request 함수를 통해 표시된다.private func startLiveActivity() {
if #available(iOS 16.2, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled {
let future = Calendar.current.date(byAdding: .second, value: self.times.timer, to: Date())!
let date = Date.now...future
let initialContentState = TimerStopwatchAttributes.ContentState(taskName: self.taskName, timer: date)
let activityAttributes = TimerStopwatchAttributes(isTimer: true)
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 30, to: Date())!)
do {
let activity = try Activity.request(attributes: activityAttributes, content: activityContent)
print("Requested Lockscreen Live Activity(Timer) \(String(describing: activity.id)).")
} catch (let error) {
print("Error requesting Lockscreen Live Activity(Timer) \(error.localizedDescription).")
}
}
}
}
※ ActivityKit Push Notifications 방법을 통해 표시하는 방법도 존재.
본인의 경우 App 내에서 특정상황에 종료와 함께 제거하도록 구현.
ActivityContent 생성.activity.end 함수를 통해 종료된다.dismissalPolicy 옵션을 .immediate 설정하면 된다.private func endLiveActivity() async {
if #available(iOS 16.2, *) {
let finalStatus = TimerStopwatchAttributes.titiStatus(taskName: self.taskName, timer: Date.now...Date.now)
let finalContent = ActivityContent(state: finalStatus, staleDate: nil)
for activity in Activity<TimerStopwatchAttributes>.activities {
await activity.end(finalContent, dismissalPolicy: .immediate)
print("Ending the Live Activity(Timer): \(activity.id)")
}
}
}
ActivityKit
Creating a Widget Extension
Displaying live data with Live Activities
Live Activities
Github 전체 반영 코드
본 내용 PR 링크
한글번역 정리 블로그
velog: ActivityKit
참고 프로젝트
Github: Tradinza
Widget 관련
Keeping a Widget Up To Date
잘 보고 배웁니다 감사합니다.