WidgetKit (Fin)
UI 디자인
UI를 그대로 디자인하면 가독성이 떨어지니 새로운 SwiftUI View파일을 만들어 거기에 디자인을 하도록 한다.
LiveActivityView
로 만들었다.
코드는 생략
Preview는 위와 같은데 버전의 차이로 양사이드 여백이 생기는건 이후에 해결 예정
GameLiveActivity 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct GameLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: GameAttributes.self) { context in
// Lock screen/banner UI goes here
LiveActivityView()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
HStack {
Image("warriors")
.teamLogoModifier(frame: 40)
Text("105")
.font(.title)
.fontWeight(.semibold)
}
}
DynamicIslandExpandedRegion(.trailing) {
HStack {
Text("115")
.font(.title)
.fontWeight(.semibold)
Image("bulls")
.teamLogoModifier(frame: 40)
}
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Image("warriors")
.teamLogoModifier(frame: 20)
Text("S. Curry drains a 3")
}
}
DynamicIslandExpandedRegion(.center) {
Text("5:24 3Q")
}
} compactLeading: {
HStack {
Image("warriors")
.teamLogoModifier()
Text("105")
.fontWeight(.semibold)
}
} compactTrailing: {
HStack {
Text("105")
.fontWeight(.semibold)
Image("warriors")
.teamLogoModifier()
}
} minimal: {
// current winning team
Image("warriors")
.teamLogoModifier()
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
이렇게 각 케이스별로 어떻게 될지 우선은 하드코딩을 한다.
GameModel에 LiveActivity 연동 함수 구현
우선 ActivityKit을 import 해준다.
그리고 GameAttributes를 별도의 파일로 옮겨준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Foundation
import ActivityKit
struct GameAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var gameState: GameState
}
// Fixed non-changing properties about your activity go here!
var homeTeam: String
var awayTeam: String
}
왜냐 var liveActivity: Activity<GameAttributes>? = nil
이렇게 Activity에 Attributes가 들어가야해서 Widget과 App 둘다 쓰이기에 별도의 파일로 새롭게 옮겨준것.
이제 startLiveActivity
를 본격적으로 구현하는데
만들어둔 liveactivity의 initializing을 여기서 한다.
이떄
뒤에있는 PushType은 Push Notification 즉 우리가 아는 푸쉬 알림을 설정하기 위한 내용.
지금은 사용하지 않으니 패스
attributes에는 정적인 콘텐츠 content에는 동적인 콘텐츠가 들어간다.
지금은 이게 Deprecated 되어있다.
1
liveActivity = try Activity.request(attributes: attributes, contentState: currentGamestate)
관련 Docs는 여기
바뀐부분에 대한 적용은 이후 서술하는걸로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func startLiveActivity() {
let attributes = GameAttributes(homeTeam: "warrios", awayTeam: "bulls")
let currentGamestate = GameAttributes.ContentState(gameState: gameState)
do {
liveActivity = try Activity.request(attributes: attributes, contentState: currentGamestate)
} catch {
print(error.localizedDescription)
}
}
func didUpdate(gameState: GameState) {
self.gameState = gameState
Task {
await liveActivity?.update(using: .init(gameState: gameState))
}
}
func didCompleteGame() {
Task {
await liveActivity?.end(using: .init(gameState: simulator.endGame()), dismissalPolicy: .default)
}
}
여기서 using 역시 Deprecated 되었다. 이후 서술 하는걸로…
dismissalPolicy: .default
은 적지않으면 보통 default인데 이부분도 관련 Docs를 읽어보도록 하자.
그리고
GameView에서 Button에 기능을 적용
1
2
3
Button("Start Live Activity") {
model.startLiveActivity()
}
그리고 실행하면 발생하는 에러.
1
The operation couldn’t be completed. Target does not include NSSupportsLiveActivities plist key
info.plist에 추가를 해야한다는것.
다시실행하면
지금은 Dummy Data로 해두어서 값이 변하지는 않는다.
LiveActivityView에 값 연동
context를 만들어준다.
만들어주는 이유는
1
2
dynamicIsland: { context in
DynamicIsland {
여기서 context를 적용하여 View와 데이터를 연동하기 때문
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct LiveActivityView: View {
let context: ActivityViewContext<GameAttributes>
var body: some View {
ZStack {
// 생략
VStack(spacing: 12) {
HStack {
Image(context.attributes.homeTeam)
Text("\(context.state.gameState.homeScore)")
Text("\(context.state.gameState.awayScore)")
Image(context.attributes.awayTeam)
}
HStack {
Image(context.state.gameState.scoringTeamName)
Text(context.state.gameState.lastAction)
}
}
}
}
}
이런식으로 context를 통해 값을 연동해준다.
다른 코드들은 생략했다.
GameLiveActivity에 연동
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
HStack {
Image(context.attributes.homeTeam)
Text("\(context.state.gameState.homeScore)")
}
}
DynamicIslandExpandedRegion(.trailing) {
HStack {
Text("\(context.state.gameState.awayScore)")
Image(context.attributes.awayTeam)
}
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Image(context.state.gameState.scoringTeamName)
Text("\(context.state.gameState.lastAction)")
}
}
} compactLeading: {
HStack {
Image(context.attributes.homeTeam)
Text("\(context.state.gameState.homeScore)")
}
} compactTrailing: {
HStack {
Text("\(context.state.gameState.awayScore)")
Image(context.attributes.awayTeam)
}
} minimal: {
Image(context.state.gameState.winningTeamName)
}
}
이렇게 적용해준다.
center는 지금 사용하지않아서 삭제!
이제 실행을 해보면
잘 나오는걸 알 수 있다.
이후 댓글에 하나의 팁이 있어 적용을 해보려한다 바로 ` .contentTransition(.identity)` 이부분을 적용해보라는것.
이미지가 있는부분에 적용을 해보는걸 추천해서 해본다.
이미지까지 굳이 재 렌더링을 할 필요가 없기에 해당 부분을 추천했다.
Backgroundupdates
잘 되는것처럼 보이지만, 문제점이 있다.
현재의 문제점
앱이 Background 상태로 전환되면 Live Activity가 더 이상 업데이트되지 않는 문제가 발생한다. 이는 iOS의 기본적인 앱 라이프사이클 관리 정책 때문이며, 앱이 Background 상태일 때 특정 작업은 제한된다. 이를 해결하기 위해 Background Modes 설정을 추가하거나, Push Notification을 사용하여 데이터를 업데이트할 수 있다.
1. Background Modes
이렇게 App group 추가하듯 Background modes를 추가해준다.
난 여기에 추가를 해주었다.
주의사항
Background Modes를 설정할 때 반드시 앱의 실제 기능과 일치하는 항목만 활성화해야 한다. 불필요한 항목을 활성화하거나 앱의 기능과 일치하지 않는 설정을 포함하면 App Store 심사에서 Reject 될 가능성이 높다.
2. Push Notification을 사용하는 방식
1
2
liveActivity = try Activity.request(attributes: attributes, contentState: currentGamestate, pushType: .token)
liveActivity?.pushToken
Push Notification을 사용하여 Live Activity를 업데이트하는 방법도 있다. 이는 서버에서 직접 Live Activity를 업데이트하는 방식으로, 아래와 같은 장점과 단점이 있다.
장점:
- 앱이 완전히 종료된 상태에서도 업데이트 가능.
- 서버에서 중앙 집중적으로 관리할 수 있음.
단점:
- 서버 설정 및 유지 관리가 필요함.
- 푸시 알림을 수신하지 못할 경우 데이터 동기화가 실패할 가능성 있음.
Docs를 참고하자.
참고
Choosing Background Strategies for Your App 이걸 한번 읽어보고 나에게 맞는 방법을 사용하는게 제일 좋을 듯 하다.
StandBy Mode 적용하기 (iOS 17)
StandBy 모드는 디바이스가 충전 중이고 가로 방향으로 두었을 때, 정보를 효과적으로 표시하기 위한 새로운 기능이다.
이 모드를 활용하면 Live Activity를 배경 화면에서도 자연스럽게 연동할 수 있다.
현재상태
현재 이렇게 백그라운드 이미지가 보이게 된다.
코드 수정
아래와 같이 @Environment(\.isActivityFullscreen)
를 활용하여 StandBy 상태를 감지하고 UI를 조정한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Environment(\.isActivityFullscreen) var isStandby
var body: some View {
ZStack {
if !isStandby {
Image("activity-background")
.resizable()
.aspectRatio(contentMode: .fill)
.overlay {
ContainerRelativeShape()
.fill(.black.opacity(0.7).gradient)
}
.contentTransition(.identity)
}
VStack(spacing: 12) {
HStack {
Text("\(context.state.gameState.awayScore)")
.font(.system(size: 40, weight: .bold))
.foregroundStyle(isStandby ? .white : .black.opacity(0.8)) // modified
수정한 부분의 코드만 적어본다.
그럼 이렇게 바뀌게 된다.
회고
위젯에대해 공부하면서 글을 작성하다보니 글이 17번까지 갈줄은 몰랐는데, 상당히 도움이 많이 되었고 특히 여러 영감을 받게 되어 아주 만족스러웠다.