포스트

WidgetKit (16)

BasketballSim 이라는 앱으로 Dynamic Island & LiveActivity를 구현하려고 한다.

해당프로젝트의 기본적인 내용은 이미 만들어진걸 사용해서 패스

Dynamic Island와 Live Activity

  • Live Activity: 잠금 화면 및 배너에 실시간 정보를 제공.
  • Dynamic Island: iPhone 14 Pro 이상에서 상단 영역에 동적으로 실시간 데이터를 표시.

Displaying live data with Live Activities, Live Activities - Human Interface Guidelines 이거 두개를 한번 읽어보는걸 추천

게임의 매커니즘

이 게임 시뮬레이터는 농구 경기를 단순화하여 특정 조건과 로직에 따라 경기 상황을 자동으로 업데이트하는 방식으로 작동한다. 주요 로직은 다음과 같다:

1. 초기 설정

  • GameSimulator 클래스는 두 팀(homeTeam, awayTeam)과 관련된 데이터를 초기화한다.
    • Team 클래스: 팀 이름과 선수 목록을 포함.
    • 점수 추적:
      • homeScoreawayScore는 각각의 점수를 저장.
    • 소유권 추적:
      • homePossession은 현재 공 소유권을 나타냄. (홈팀: true, 원정팀: false)

2. 경기 진행

  • start():
    • 2초 간격으로 runGameSimulator를 호출하는 타이머를 설정하여 경기를 시뮬레이션.
  • runGameSimulator 주요 동작:
    • progressGame()을 호출해 현재 경기 상태를 업데이트.
    • delegate.didUpdate(gameState:)를 통해 업데이트된 경기 상태를 외부로 전달.
    • 공 소유권(homePossession)을 변경하고, 총 소유권 횟수(possessionCount)를 증가.
    • 총 소유권 횟수가 120을 초과하면 경기 종료 처리(delegate.didCompleteGame()).

3. 게임 상태 업데이트

  • progressGame():
    • 현재 공 소유 팀이 무작위로 0~3점 중 하나를 획득.
    • 점수는 무작위로 정해지며, 점수에 따라 특정 행동(예: 3점슛 성공, 자유투 성공/실패 등)을 생성.
    • 점수와 소유권 변경 결과를 반영한 게임 상태를 GameState 객체로 반환.

4. 경기 종료

  • endGame():
    • 최종 점수를 계산하고 승리한 팀을 판별.
    • “게임 종료” 메시지와 함께 경기 상태를 반환.
    • 내부 데이터를 리셋(reset())하여 초기화.

5. 플레이어 행동 생성

  • createLastActionString():
    • 각 점수(0~3)에 따라 특정 행동 메시지 생성.
      • 예: 3점슛 성공 → "S. Curry drains a 3".
    • 공 소유권 변경 전, 득점 팀과 행동을 기록.

Widget 만들기

CleanShot 2024-12-11 at 18 28 11

위젯을 만들기전에 무작정 넘기지말고 이번엔 Live Activity가 필요하므로 반드시 체크하고 만들자.

그리고 반드시 iOS 버전은 16.1 이상 이어야 한다.

만약 배포하는 버전이 그 이하의 버전상태라면

1
2
3
4
5
6
7
8
struct GameWidgetBundle: WidgetBundle {
    var body: some Widget {
        GameWidget()
        if #available(iOS 16.1, *) {
            GameWidgetLiveActivity()
        }
    }
}

이렇게 특정 버전 이상 사용가능하다는 조건을 달도록 하자, 이게 없으면 그 이하버전의 유저는 에러가 발생할지도.

그리고 LiveActivity로 가서 자동으로 생성된 Struct의 이름에서 Widget을 빼주었다. (위젯과 혼동할수 있기에)

1
2
3
4
5
6
7
// before
struct GameWidgetAttributes{}
struct GameWidgetLiveActivity{}

// after
struct GameAttributes{}
struct GameLiveActivity{}

LiveActivity 알아보기

LiveActivity를 체크하고 만들게 되면 위에서 언급한대로 2개의 구조체가 만들어진다.

  1. GameWidgetAttributes
  2. GameWidgetLiveActivity

그리고 위에서 Widget을 지웠기에 아래부턴 그내용을 빼고 적도록하겠다.

GameLiveActivity

전반적인 UI부분을 담당한다.

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
struct GameLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: GameAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello \(context.state.emoji)")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)

        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI goes here.  Compose the expanded UI through
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom \(context.state.emoji)")
                    // more content
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T \(context.state.emoji)")
            } minimal: {
                Text(context.state.emoji)
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

우선 만들면 이렇게 코드가 만들어진다.

ActivityConfiguration

여기서는

CleanShot 2024-12-11 at 18 51 22

이런 UI들을 사용할 수 있는 부분이다.

1
2
3
4
5
6
7
8
9
ActivityConfiguration(for: GameAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello \(context.state.emoji)")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)

        }

친절하게 주석으로 여기에 UI를 구현하라고도 적어주었다.

BannerUI는 Dynamic Island 지원을 하지않는 기종을 대상으로 구현하는 기능이다.

Dynamic Island

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
dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI goes here.  Compose the expanded UI through
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom \(context.state.emoji)")
                    // more content
                }
                DynamicIslandExpandedRegion(.center) { // added
                    Text("Center")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T \(context.state.emoji)")
            } minimal: {
                Text(context.state.emoji)
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }

Dynamic Island 부분을 클릭해서 확장을 하여 보여주는 UI의 구성을 여기에 구현한다.

코드를 초기에만들면 center는 빠져있어서 추가를 해준다.

각 위치 별 구성은 다음과 같다.

CleanShot 2024-12-11 at 19 06 26

CleanShot 2024-12-11 at 19 07 07

Compact

그 이후의 코드 부분이며 다이나믹 아일랜드를 확장하지 않았을때의 UI구성으로 생각하면 죄겠다.

1
2
3
4
5
6
7
compactLeading: {
    Text("L")
} compactTrailing: {
    Text("T \(context.state.emoji)")
} minimal: {
    Text(context.state.emoji)
}

CleanShot 2024-12-11 at 19 10 43

CleanShot 2024-12-11 at 19 10 41

자세한견 초반부에 언급한 Displaying live data with Live Activities 에 대한 Docs를 읽어보자.

혹시몰라 여기도 다시 링크를 달아둔다.

GameAttributes

1
2
3
4
5
6
7
8
9
struct GameAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var emoji: String
    }

    // Fixed non-changing properties about your activity go here!
    var name: String
}

Docs

주석으로 설명을 잘 해주었는데,

The ActivityAttributes protocol describes the content that appears in your Live Activity. Its inner type ContentState represents the dynamic content of the Live Activity.

ActivityAttributes 프로토콜은 Live Activity에 표시되는 콘텐츠를 묘사한다. 내부 유형의 ContentState는 라이브 활동의 동적 콘텐츠를 나타낸다.

즉, ActivityAttributes 는 Live Activity에서 사용되는 데이터를 정의하는 프로토콜로, Live Activity와 Dynamic Island의 콘텐츠를 관리한다.

  • 정적 콘텐츠
    • Live Activity에서 고정적으로 유지되는 데이터.
    • 즉, 변경되지 않는 데이터를 ActivityAttributes 내에서 정의.
  • 동적 콘텐츠
    • 실시간으로 변하는 데이터를 ContentState에서 정의.

예시: PizzaDelivery from Docs

1
2
3
4
5
6
7
8
9
10
11
12
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
}

구성 요소 설명

  1. 정적 콘텐츠 (ActivityAttributes):
    • numberOfPizzas: 주문한 피자 수량.
    • totalAmount: 총 주문 금액.
    • orderNumber: 주문 번호.
  2. 동적 콘텐츠 (ContentState):
    • driverName: 배달 기사의 이름.
    • deliveryTimer: 배달 예상 시간.

정적 콘텐츠 vs 동적 콘텐츠

구분정적 콘텐츠 (ActivityAttributes)동적 콘텐츠 (ContentState)
역할변경되지 않는 데이터실시간으로 업데이트되는 데이터
예시경기 이름, 팀 이름, 주문 번호현재 점수, 배달 기사 이름, 남은 시간
라이프사이클Live Activity가 생성될 때 고정Live Activity의 실행 중에 계속 변경

Live Activity 구현하기.

GameAttributes 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
struct GameAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var homeScore: Int
        var awayScore: Int
        var scoringTeamName: String
        var lastAction: String
    }

    // Fixed non-changing properties about your activity go here!
    var homeTeam: String
    var awayTeam: String
}
  1. 정적 콘텐츠
    • 각 팀명
  2. 동적 콘텐츠
    • 각 팀 점수
    • 득점한 선수의 이름
    • 마지막 선수의 행동

이렇게 설정을 하는데, 위의 동적인 코드들은 이미

1
2
3
4
5
6
7
8
9
10
struct GameState {
    let homeScore: Int
    let awayScore: Int
    let scoringTeamName: String
    let lastAction: String

    var winningTeamName: String {
        homeScore > awayScore ? "warriors" : "bulls"
    }
}

GameState에 있으므로

1
2
3
4
public struct ContentState: Codable, Hashable {
    // Dynamic stateful properties about your activity go here!
    var gameState: GameState
}

이렇게 간소화를 한다. 이때 에러가 발생하는데 (순서대로 2개가 발생)

첫번째는 GameState를 못찾는데 target을 추가해주면 된다.

CleanShot 2024-12-11 at 19 41 53

두번째는 GameState가 Codable, Hashable 프로토콜을 따르지 않아서 에러가 발생하므로 GameState가 두 프로토콜을 준수하도록 추가해주자.

1
2
3
struct GameState: Codable, Hashable {
    // 생략.
}

내용이 길어질것같아 다음글에서 계속…

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