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번까지 갈줄은 몰랐는데, 상당히 도움이 많이 되었고 특히 여러 영감을 받게 되어 아주 만족스러웠다.