포스트

WidgetKit (Fin)

UI 디자인

UI를 그대로 디자인하면 가독성이 떨어지니 새로운 SwiftUI View파일을 만들어 거기에 디자인을 하도록 한다.

LiveActivityView로 만들었다.

코드는 생략

CleanShot 2024-12-11 at 20 18 31

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을 여기서 한다.

이떄

CleanShot 2024-12-11 at 20 29 11

뒤에있는 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에 추가를 해야한다는것.

CleanShot 2024-12-11 at 20 51 29

CleanShot 2024-12-11 at 20 51 18

CleanShot 2024-12-11 at 20 51 54

다시실행하면

Dec-11-2024 20-54-30

지금은 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는 지금 사용하지않아서 삭제!

이제 실행을 해보면

Dec-12-2024 01-43-38

잘 나오는걸 알 수 있다.

이후 댓글에 하나의 팁이 있어 적용을 해보려한다 바로 ` .contentTransition(.identity)` 이부분을 적용해보라는것.

이미지가 있는부분에 적용을 해보는걸 추천해서 해본다.

이미지까지 굳이 재 렌더링을 할 필요가 없기에 해당 부분을 추천했다.

Backgroundupdates

잘 되는것처럼 보이지만, 문제점이 있다.

현재의 문제점

앱이 Background 상태로 전환되면 Live Activity가 더 이상 업데이트되지 않는 문제가 발생한다. 이는 iOS의 기본적인 앱 라이프사이클 관리 정책 때문이며, 앱이 Background 상태일 때 특정 작업은 제한된다. 이를 해결하기 위해 Background Modes 설정을 추가하거나, Push Notification을 사용하여 데이터를 업데이트할 수 있다.

1. Background Modes

CleanShot 2024-12-12 at 02 06 07

이렇게 App group 추가하듯 Background modes를 추가해준다.

CleanShot 2024-12-12 at 02 08 55

난 여기에 추가를 해주었다.

주의사항

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를 배경 화면에서도 자연스럽게 연동할 수 있다.

현재상태

Dec-12-2024 02-31-01

현재 이렇게 백그라운드 이미지가 보이게 된다.

코드 수정

아래와 같이 @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

수정한 부분의 코드만 적어본다.

Dec-12-2024 02-41-16

그럼 이렇게 바뀌게 된다.

회고

위젯에대해 공부하면서 글을 작성하다보니 글이 17번까지 갈줄은 몰랐는데, 상당히 도움이 많이 되었고 특히 여러 영감을 받게 되어 아주 만족스러웠다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.