-
LiveActivity Dynamic island 구현하기개발/Swift 2024. 10. 16. 16:40
Widget 기본 구조
import WidgetKit import SwiftUI @available(iOS 16.1, *) @main struct LivaActivityBundle: WidgetBundle { var body: some Widget { LivaActivityLiveActivity() } }
LiveActivity 에서 확장 다이나믹 아일랜드가 16.1부터 지원이 되어서 어노테이션 추가
액티비티에 버튼 토글이 포함될경우에는 17을 써야함
아무튼 여기는 진입 시점이라 보면 됨
상태 관리
ActivityAttributes
인코딩 데이터 < 4KB 이하만 가능
@available(iOS 16.1, *) public struct LivaActivityAttributes: ActivityAttributes { // ContentState 내부에 상태 서술 - 동적 상태 public struct ContentState: Codable, Hashable { let orderStatus: OrderStatus } // ContentState 외부에 상수 서술 - 정적 상태 let ordernumber: Int let storeName: String let menuName: String let pickupTime: PickupTime }
update를 통해 바꿔줄상태와 내부적으로 사용할 정적 데이터 정의
라이브 액티비티 UI 구현 하기
기본 구조
ActivityConfiguration(for: ...) { context in ... //여기에 잠금 화면 UI 구현 } dynamicIsland: { context in ... //다이나믹 아일랜드 UI 구현 - 확장, 축소 둘다 }
context 내부 데이터 구성은 -> ActivityAttributes , ContentState, Live Activity의 고유 식별자 String
에 접근 가능하여 이거로 UI 구성 가능함
1. 잠금화면 Lock Screen 은 비교적 자유롭게 구현이 가능하다
//예시 ActivityConfiguration(for: LivaActivityAttributes.self) { context in VStack { HStack { Text("타이틀") Spacer() Text("\\(context.state.orderStatus.description)") } HStack { VStack { Text("주문번호 \\(context.attributes.ordernumber), \\(context.state.orderStatus.description)") Text(context.attributes.menuName) } Spacer() Image("imgCooked54") } } }
2. 다이나믹 아일랜드 부분 UI
//기본 구조 DynamicIsland { // Expanded UI goes here DynamicIslandExpandedRegion(DynamicIslandExpandedRegionPosition.leading) { Text("Leading") } DynamicIslandExpandedRegion(.trailing) { Text("Trailing") } DynamicIslandExpandedRegion(.bottom) { Text("Bottom \\(context.state.emoji)") } DynamicIslandExpandedRegion(.center) { } } // 타입 구조 protocol DynamicIslandExpandedContent {} struct DynamicIslandExpandedRegion : DynamicIslandExpandedContent {}
활용 예시이다 context에 접근하여 UI를 구성했다
//예시 dynamicIsland: { context in //확장형 UI DynamicIsland { DynamicIslandExpandedRegion(.leading, priority: 1.0) { VStack(alignment: .leading) { Text("Title") Spacer() Text("주문상태 \\(context.state.orderStatus.description)") Text(context.attributes.menuName) } .background(Color.red) .dynamicIsland(verticalPlacement: .belowIfTooWide) //Leading 영역이 bottom까지 먹도록 함 } DynamicIslandExpandedRegion(.trailing) { VStack(alignment: .trailing) { Text(context.state.orderStatus.description) Spacer() Group { if context.attributes.pickupTime == .immediatly { Image("imgCooked54") } else { Text(context.attributes.pickupTime.title) } } } .background(Color.blue) } //축소형 UI } compactLeading: { Text("Title") } compactTrailing: { Text(context.state.orderStatus.description) } minimal: { Text("Mini") //다른곳에서 라이브액티비티 실행하면 보여짐 (ex>티이머 시작) } .keylineTint(Color.red) } // 추가 속성 keylineTint - 테두리 contentMargins - 마진
다이나믹 아일랜드 배경색을 지정 해주려했지만 불가 하다고 함
You can’t change the background color of Live Activities in the Dynamic Island.
잠금화면 액티비티는 다크모드 라이트모드에따라 다르며 지정도 가능함
딥링크 - 반드시 한개
.widgetURL(URL(string: "딥링크 URL")) // 앱 SceneDelegate 함수 호출 func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) 여기서 딥링크 처리
참고 사항
OS 버전별 특징
iOS 16.* 버전에서는 Live Activity에서 가능한 상호작용이 딥링크 처리만 가능
iOS 17 및 iPadOS 17 이상부터는 Live Activity에 상호작용이 가능. 즉, 앱을 열지 않고 앱의 기능만 호출하는 것이 가능하나 제약 조건이 있다
Button과 Toggle만 사용할 수 있다 잠금 해제 인증을 선 요구한다. expanded View 및 잠금 화면 View에서만 상호작용 가능하다.
Live • 아이폰 '설정' 앱 > 해당 앱 > '실시간 현황' 활성화 여부(사용자가 제어 가능)
사용자 기기에서 활성화된 Live Activity 개수가 수용 가능한 최대치에 도달하면 새 Live Activity를 시작하는 것은 실패
이벤트 처리
1. 라이브 액티비티 생성
ActivityContent
let state: State - Attributes.ContentState 정의한거 넣어줌 let staleDate: Date? - outdate 시점 let relevanceScore: Double - 우선순위
Request - 앱 켜져있을때 호출
if ActivityAuthorizationInfo().areActivitiesEnabled { if #available(iOS 16.2, *) { let activityAttributes = LivaActivityAttributes(ordernumber: 3, storeName: "매장명", menuName: "아이스 아메리카노", pickupTime: .ten_minute) let state = LivaActivityAttributes.ContentState(orderStatus: .paymentChecking) let tenMinutesLater = Calendar.current.date(byAdding: .minute, value: 10, to: Date.now) let content = ActivityContent(state: state, staleDate: tenMinutesLater) do { let activity = try Activity.request(attributes: activityAttributes, content: content) } catch let error { print("LivaActivityAttributes request Error - \\(error)") } } attribute - Attributes(정적데이터) content - ActivityContent
2. 읽기 ( 현재 액티비티를 가져올수 있음)
if #available(iOS 16.2, *) { guard let currentActivity = Activity<LivaActivityAttributes>.activities.last else { return } //가장 최근 액티비티 가져옴 } // activity 안에 이런거 들어있음 final let id: String final let attributes: Attributes var content: ActivityContent<Activity<Attributes>.ContentState> { get } var activityState: ActivityState { get } var pushToken: Data? { get } // 액티비티 상태 ActivityState case active case dismissed - 지워짐 case ended - 종료( 표시는 되지만 업데이트안댐) case stale - 최신X
activityUpdates - 액티비티 업데이트되면 수신받는거
Task { for await newActivity in Activity<MyActivityAttributes>.activityUpdates { print("new activity added: \\(newActivity.id)") } } //아래 변동 사항이 생기면 위 호출됨 var contentUpdates: Activity<Attributes>.ContentUpdates { get } var activityStateUpdates: Activity<Attributes>.ActivityStateUpdates { get } var pushTokenUpdates: Activity<Attributes>.PushTokenUpdates { get } static var activityUpdates: Activity<Attributes>.ActivityUpdates { get }
3. 업데이트
if #available(iOS 16.2, *) { guard let currentActivity = Activity<LivaActivityAttributes>.activities.last else { return } //읽기 참조 let newContentState = LivaActivityAttributes.ContentState(orderStatus: .cooking) let tenMinutesLater = Calendar.current.date(byAdding: .minute, value: 10, to: Date.now) let updatedContent = ActivityContent(state: newContentState, staleDate: tenMinutesLater) //업데이트할 상태 포함 Task { await currentActivity.update(updatedContent) } } // AlertConfiguration 인자 넣을수있음 (워치용)
4. 종료 (dismissed 상태)
종료해도 사용자가 지우기 전까지는 잠금화면에 남아있음
그래서 종료 상태도 포함시켜야 함
if #available(iOS 16.2, *) { guard let currentActivity = Activity<LivaActivityAttributes>.activities.last else { return } let endContentState = LivaActivityAttributes.ContentState(orderStatus: .cooked) let endendContent = ActivityContent(state: endContentState, staleDate: nil) Task { await currentActivity.end(endendContent, dismissalPolicy: .immediate) } }
위 처럼 dismissalPolicy를 즉시로 지정해주면 락스크린에서 바로 지워지게 할수도 있음
Extension 생성 테스트
- widget extension 생성, infoPlist수정, extension targetOS 15로 수정
- 생성된 모든 기능이 16.1이상이라 모두 어노테이션 추가
@available(iOS 16.1, *)
- 호출부(액티비티 생성, 업데이트, 종료) 에서는 16.2 이상
if #available(iOS 16.2, *)
에러 발생
1. 접근 문제
앱(호출부) 에서 타입을 못불러올때 Cannot find type 'LivaActivityAttributes' in scope -> public 접근제한 수정 Undefined symbol: nominal type descriptor for LivaActivityExtension.LivaActivityAttributes -> extension 접근제한 수정 -> 위젯관련 코드 제거, preview 제거 -> extension 에서 타겟 설정 (public 한 놈들만 설정) @main 어노테이션 있는부분은 냅둬야 앱 @main 이랑 충돌 안남
2. 다크 모드 문제
다크모드 대응을 위해 colorScheme을 사용
@Environment(\\.colorScheme) var colorScheme if colorScheme == .dark { Color.black500 } else { Color(.systemGray6) .opacity(0.82) }
이렇게 배경색을 동적으로 바꿔줌 → 성공
그런데 아래 두가지 경우 실패했다.
Image 변경 → 실패
text color 변경 → 실패
f colorScheme == .dark { Image("darkImage") } else { Image("Image") } Text("sdfsfdf").foregroundColor(colorScheme == .dark ? Color.orange450 : Color.grey800)
이상하게 조건문으로 대응이 안됨, 다른 문제는 없고 그냥 start할떄 사용한 라이브액티비티의 다크모드 라이트모드 대로 결정되고
이후에 화면 모드를 바꿔도 적용이 안됨
해결방법은 Image Set 에 다크모드용 이미지를 적용
Color Set에 다크모드용 색상을 적용하는것
Assets에서 해당 이미지 클릭→ Appearance 에 Any,Dark로 바꿔주고 다크 모드용 이미지를 따로 넣어주면
코드상 조건문 쓰지 않아도 됨,
Color set도 마찬가지로 대응
'개발 > Swift' 카테고리의 다른 글
[Error] linker command failed with exit code 1 (0) 2024.11.24 LiveActivity - Push Console로 테스트 해보기 (0) 2024.10.19 Tuist활용하여 멀티 모듈 SwiftUI 프로젝트 생성하기 (0) 2024.10.15 Label Attribute 이미지 처리, 줄넘김 하기 (0) 2024.09.12 Contact Framework 사용하여 연락처 불러오기 (0) 2024.09.03