Dynamic Island를 위한 ActivityKit_ 개념 적용

Zeto·2022년 12월 13일
0

Swift_Framework

목록 보기
2/6

앞에서 Dynamic Island를 구현해보기 위한 간략적인 개념을 학습하였다. 이번에는 5초마다 배달 주문의 상태가 변하는 형태의 간단한 구현을 진행하는 것으로, ActivityKit을 적용해보며 세부적인 사항들을 학습하여 실 구현을 해보고자 한다.

작성 코드 Github 주소

📱 Dynamic Island UI 구현하기

1) ActivityAttributes 구현

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의 변화 값들이 무엇인지 좀 더 직관적으로 보여주기 위함이다.

2) Widget 구현

이제는 본격적으로 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도 구현해줄 수 있다는 것이다.

compactminimal은 위의 이미지와 같이 디폴트 상태에서 왼쪽과 오른쪽, 분리된 공간에 대한 UI를 구성할 수 있고 아무래도 expandedView와 달리 공간의 제약이 조금 더 있기에 가독성을 위해서는 한정적인 UI 구성이 될 수 밖에 없어 보인다.

⏱ Dynamic Island 실행하기

본격적으로 다이내믹 아일랜드를 실행해보기 위해서 먼저 버튼을 생성하고 이에 맞춰 실행 로직과 상태 변화 값 등의 기본적인 요소를 구현해보기로 했다.

@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를 추가하고 필요한 부분에 대한 체크를 하게되면 이러한 문제를 해결할 수 있다.

profile
중2병도 iOS가 하고싶어

0개의 댓글