앞에서 Dynamic Island를 구현해보기 위한 간략적인 개념을 학습하였다. 이번에는 5초마다 배달 주문의 상태가 변하는 형태의 간단한 구현을 진행하는 것으로, ActivityKit을 적용해보며 세부적인 사항들을 학습하여 실 구현을 해보고자 한다.
Dynamic Island를 구현하는 데에 있어 Model
과 유사한 ActivityAttributes
.
struct DynamicIslandWidgetAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
}
// Fixed non-changing properties about your activity go here!
}
해당 Struct를 어떻게 구현해야될 지는 친절하게도 주석으로 알려주고 있다. associatedType
으로 구현된 ContentState
는 변화하는 상태에 대한 데이터 값을 할당해줄 수 있는 변수를 선언해주면 된다. 이와 함께 아래에서는 변화 요소가 없는 값들을 구현.
여기서는 배달 상태와 함께 적절한 이미지를 변화하는 값으로 설정하고, 간략한 배달 앱 이름을 상수로 지정하였다.
struct DynamicIslandWidgetAttributes: ActivityAttributes {
public typealias OrderState = ContentState
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var nowState: String
var stateImage: String
}
// Fixed non-changing properties about your activity go here!
let title: String
}
typealias
를 지정한 이유로는 ContentState
보다 해당 ActivityAttributes
의 변화 값들이 무엇인지 좀 더 직관적으로 보여주기 위함이다.
이제는 본격적으로 Dynamic Island의 UI를 구현해주어야 한다.
ActivityConfiguration(for: DynamicIslandWidgetAttributes.self) { context in
// Lock screen/banner UI goes here
...
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
}
DynamicIslandExpandedRegion(.trailing) {
}
DynamicIslandExpandedRegion(.bottom) {
}
} compactLeading: {
} compactTrailing: {
} minimal: {
}
}
ActivityConfiguration
의 구성을 보면 크게 두 개의 클로저 부분으로 나뉘는데 해당 부분들에도 친절하게 주석이 달려있어 각 클로저에 어떤 UI를 구성해야할 지 알기 쉽다. 상단의 클로저는 잠금 화면에서의 UI 구성을, 아래의 dynamicIsland
클로저 구문에는 본격적인 다이내믹 아일랜드의 UI를 구성하면 된다.
다만 여기서 상단의 클로저와 달리 dynamicIsland
클로저 구문에는 또 각각의 클로저 구문들이 존재한다. DynamicIslandExpandedRegion
의 경우에는 우리가 다이내믹 아일랜드를 길게 눌렀을 때 나타나는 커다란 영역에서의 UI를 구현할 수 있다.
expandedView
에서 각각 차지하는 위치이며, Bottom의 경우 어느정도는 높이 값이 늘어나지만 한계는 있다. 그리고 기본적으로 생성된 로직에서는 leading/trailing/bottom 뿐이지만 상기의 이미지를 보면 center가 존재한다. 즉, 원할 경우에는 .center
로 중앙 부분에 대한 UI도 구현해줄 수 있다는 것이다.
compact
와 minimal
은 위의 이미지와 같이 디폴트 상태에서 왼쪽과 오른쪽, 분리된 공간에 대한 UI를 구성할 수 있고 아무래도 expandedView
와 달리 공간의 제약이 조금 더 있기에 가독성을 위해서는 한정적인 UI 구성이 될 수 밖에 없어 보인다.
본격적으로 다이내믹 아일랜드를 실행해보기 위해서 먼저 버튼을 생성하고 이에 맞춰 실행 로직과 상태 변화 값 등의 기본적인 요소를 구현해보기로 했다.
@State var orderState: OrderState = .ordered
@State var timer: Timer? = nil
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Button("Start DynamicIsland") {
startDynamicIsland()
self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { _ in
updateDynamicIsland()
})
}
}
.padding()
}
상태 변화에 대한 값을 보관하는 변수와 시간에 따라 상태를 변화시키기 위한 Timer
를 각각 구현하였으며, 버튼을 클릭하였을 때 다이내믹 아일랜드를 실행시킬 함수와 Timer
에 맞춰 값을 변화시켜줄 함수를 추가적으로 작성하였다.
func startDynamicIsland() {
let widgetAttributes = DynamicIslandWidgetAttributes(title: "집밥 스위프트 선생")
let orderStateAttributes = DynamicIslandWidgetAttributes.OrderState(nowState: orderState.orderString, stateImage: orderState.orderImageString)
do {
let orderActivity = try Activity<DynamicIslandWidgetAttributes>.request(attributes: widgetAttributes, contentState: orderStateAttributes, pushType: nil)
print("Requested ordering Live Activity \(orderActivity.id)")
} catch(let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription)")
}
}
먼저 Attributes
는 두 가지를 생성하는데 앞서 얘기했듯이 변화가 없는 상수 데이터와 ContentState
로 대변되는 변수 데이터를 각각 생성해주어야 한다. 이유는 아래의 do...catch
구문의 Activity<T>.request
함수를 보면 알 수 있다. request
자체가 인자 값으로 상수와 변수 데이터에 해당하는 Attributes
인스턴스를 요구하고 있다.
이렇게 요구하는 인자 값을 넘겨주게 되면 다이내믹 아일랜드의 Activity에 대한 요청이 호출되어 실행이 되게 된다. 하지만 실행만 시키는 것도 중요하지만 전달한 변수 데이터가 변화하고 이에 대한 다이내믹 아일랜드의 UI 변화를 확인하는 것도 중요하니 업데이트에 대한 로직도 필요하다.
func updateDynamicIsland() {
switch orderState {
case .ordered:
self.orderState = .making
case .making:
self.orderState = .delivering
case .delivering:
self.orderState = .delivered
case .delivered:
self.timer?.invalidate()
self.timer = nil
}
Task {
let newOrderStateAttributes = DynamicIslandWidgetAttributes.OrderState(nowState: orderState.orderString, stateImage: orderState.orderImageString)
for activity in Activity<DynamicIslandWidgetAttributes>.activities {
await activity.update(using: newOrderStateAttributes)
print("Requested a pizza delivery Live Activity \(orderState.orderString)")
}
}
}
ContentState
값을 변경해서 Activity
를 업데이트하기 위해서는 변화된 데이터를 새로 할당한 Attributes
를 생성하고 활성화된 Activity
를 찾아 업데이트 시켜주면 된다.
다이내믹 아일랜드를 학습하던 중, 궁금하던 몇 가지를 찾아보게 되었고 이에 대한 내용을 간략하게나마 적어놓고자 한다.
다이내믹 아일랜드를 사용하는 여러 서드파티 어플들이 존재할 경우, 다이내믹 아일랜드에 올려놓을 서드파티 어플을 정하는 것은 OS에서 결정한다.
이 부분은 확실히 OS가 아닌 개발자 측에서 제어가 가능하다면 누구나 다 자기 어플을 위로 올려놓으려고할 테고 그러다보면 사이드이펙트나 여러 문제들이 발생할 가능성이 높으니 OS에서 제어하는 것이 맞다고 생각된다. 다만 어떤 게 최상단으로 올라가고 미니멀로 내려가는지 등에 대한 좀 더 명확한 힌트가 있으면 좋지 않을까.
좌우 스와이프 제스처로 다이내믹 아일랜드 숨기기, 표시하기, 미니멀 표시 형태로 변경할 수 있다
사실 아이폰을 쓸 때 하단 좌우 스와이프 기능이 매우 편리하게 느껴졌는데, 혹시 다이내믹 아일랜드도 이처럼 여러 서드파티 앱들이 백그라운드에 있는 경우 좌우 스와이프로 다이내믹 아일랜드에 올라와 있는 어플을 변경할 수 있을까라는 생각이 들었다.
하지만 이런 기능은 Nope! 스와이프 제스처에 이런 기능이 있으면 좋을 텐데 아쉬울 따름이다.
Background Modes를 통해 백그라운드 상에서도 타이머가 작동되도록 할 수 있다
해당 사항은 다이내믹 아일랜드와 직접적이 연관이 없긴하나 연습 코드를 짜던 중, 백그라운드 상에서 타이머가 동작하지 않아 원하는 데이터 변경이 적용되지 않는 문제를 발견하면서 찾게된 것이다. Signing & Capabilities
탭에서 Background Modes
를 추가하고 필요한 부분에 대한 체크를 하게되면 이러한 문제를 해결할 수 있다.