스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
WidgetKit로 멋진 컴플리케이션 만들기
WidgetKit을 사용하여 시계 페이스에서 아름다운 컴플리케이션을 만드는 방법을 살펴보겠습니다. WidgetKit에서 확인할 수 있는 watchOS별 기능을 소개하고, 기존 ClockKit 컴플리케이션에서 마이그레이션하는 방법을 알려드립니다. WidgetKit에 대해 자세히 알아보려면 WWDC22의 ‘Complications and Widgets: Reloaded(컴플리케이션 및 위젯: 리로리드)'를 시청하시기 바랍니다.
리소스
- Creating accessory widgets and watch complications
- Emoji Rangers: Supporting Live Activities, interactivity, and animations
- Migrating ClockKit complications to WidgetKit
- WidgetKit
관련 비디오
Tech Talks
WWDC22
WWDC20
-
다운로드
♪ ♪ 안녕하세요 August Joki입니다 watchOS의 소프트 엔지니어고요 오늘 WidgetKit 컴플리케이션 발전을 논의해 보려고 합니다 그 전에 WidgetKit 컴플리케이션의 기본 내용을 다룬 다음 세션을 보고 오셨길 바랍니다 이건 시계 페이스 컴플리케이션과 연관되는데요 오늘 세션은 그 개념을 기반으로 진행됩니다 그리고 2020년도 WWDC에서 제가 진행한 또다른 세션에서는 컴플리케이션에서의 색조 지정과 SwiftUI 드로잉을 다룹니다 오늘은 watchOS에서만 가능한 WidgetKit의 기능과 ClockKit 컴플리케이션을 WidgetKit에 마이그레이션하는 방법을 이야기해보겠습니다 Coffee Tracker 샘플 앱에서 영감을 얻었기에 오늘 세션 동안 예시로 사용하겠습니다 하루 종일 물을 제외한 음료 섭취 횟수를 기록해 시간이 지날수록 쌓이는 체내 카페인량을 추적하는 앱입니다 watchOS에서만 나타나는 특징을 먼저 살펴 봅시다 iOS 16에서는 컴플리케이션 스타일 위젯을 iPhone 잠금 화면에 가지고 왔으며 watchOS 9에서는 WidgetKit를 시계에 가져왔습니다 워치 페이스에는 스크린 코너에 독특한 컴플리케이션을 구성해 보았는데요 이 때문에 accessoryCorner라는 특유의 WidgetKit 그룹군이 필요합니다 독특한 화면의 일부는 보조 콘텐츠로 SwiftUI 뷰에서 구체화됩니다 하지만 콘텐츠 일부로 렌더링되지는 않죠 대신 워치 페이스에 의해 렌더링됩니다
코너의 둥근 부분은 표준 SwiftUI 렌더링으로 보조 콘텐츠는 코너의 곡선 부분입니다
아니면 Infograph 페이스의 다이얼에 있습니다
accessoryInline 그룹군은 독특한 행동을 하는데요 페이스에 따라 렌더링되는 방식이 다양합니다 어떨 때는 평평하고 어떨 때는 다이얼에 맞게 곡선형이죠
WidgetKit를 사용해 앱이 업데이트되는 방식을 통해 이 기능들을 지원하는 방법에 대해 알아봅시다
iOS 16에서는 컴플리케이션 스타일 위젯 패밀리 AccessoryRectangular accessoryCircular, accessoryInline 외에도 accessoryCorner라는 4번째 그룹군을 개발했습니다
이건 하단 코너에 나타나는 지도, 심장 박동처럼 크고 둥근 콘텐츠로 나타나거나 상단 코너에 나타나는 커피 추적기나 달 형상처럼 곡선 라벨이나 게이지가 있는 작고 둥근 콘텐츠로 나타납니다
내부 보조 콘텐츠 제시 여부를 제어하기 위해 watchOS 9에는 새로운 뷰 모디파이어가 추가됐습니다
예시 앱에서 코너 컴플리케이션 빌딩을 살펴 봅시다
더 큰 원형 콘텐츠 스타일로 시작해 보죠 SF Symbol과 백그라운드를 갖춘 ZStack을 넣었습니다 SwiftUI 콘텐츠는 다른 코너 컴플리케이션의 디자인과 맞추기 위해 자동으로 원 모양으로 고정됩니다
내부 곡선 콘텐츠 추가를 위해 새로운 걸 추가했는데요 widgetLabel 뷰 모디파이어입니다 시계 페이스는 패밀리와 워치 페이스 스타일에 적합한 제어를 끌어내기 위해 모디파이어 콘텐츠를 추출합니다 원형 콘텐츠를 자동으로 축소되어 공간을 만들죠 accessoryCorner에서는 텍스트, 게이지 구체화가 progressView는 위젯 레벨 구체화가 가능합니다
AccessoryCorner는 widgetLabel을 유일하게 지원하진 않습니다 accessoryCircular 그룹군에서는 어떻게 사용될까요? Infograph 워치 페이스에선 코너 컴플리케이션 이외에도 다이얼 내부에 4개의 원형 컴플리케이션이 있습니다 중간 상단에 있는 커피 추적기 컴플리케이션은 우리가 봤던 코너 컴플리케이션과 매우 유사합니다 하지만 다이얼 안쪽 텍스트와 있죠 이 텍스트는 어떻게 추가할까요?
이 디자인을 위해 코너 컴플리케이션에 있는 widgetLabel의 게이지를 앞쪽 중간으로 옮기는 게 더 좋을 거라 판단했습니다 Infograph의 중앙 상단을 이용하기 위해서요 그냥 두면 원형 콘텐츠에 맞지 않을 긴 베젤 영역에 추가 텍스트를 넣고자 widgetLabel을 추가했고요 그런데 메인 뷰와 그 위의 텍스트 사이에 너무 정보가 많네요 원형 콘텐츠를 코너 컴플리케이션의 커피 컵 SF Symbol로 변환해 이 부분을 지워 보겠습니다 하지만 베젤이 없는 원형 컴플리케이션이 나타나는 페이스로 바꾸면 카페인 정보가 모두 사라집니다 운 좋게도 두 사례 모두에서 추가할 수 있는 API가 있네요
showsWidgetLabel이라는 Environment 프로퍼티를 뷰에 추가하여 컴플리케이션을 업데이트합니다 컴플리케이션이 위젯 라벨의 콘텐츠가 보이는 시계 페이스의 위치에 있을 때마다 이건 참이 될 겁니다
다음으로 showsWidgetLabel 값에 따라 콘텐츠를 바꿀 수 있습니다 적절한 정도의 정보를 각 스팟에 둘 수 있도록요 accessoryCircular 그룹군이 시계 페이스 나타나는 두 가지 방법을 보여드렸는데요 알고 계셔야 할 방법이 한 가지 더 있습니다 Extra Large 시계 페이스는 굉장히 큰 포맷으로 시간을 보기에 좋은 방법이었죠 하나의 큰 원형 컴플리케이션을 지원합니다 이 페이스는 accessoryCircular 그룹군을 사용하고 페이스 스타일에 맞게 콘텐츠를 자동 확대합니다 단, 이게 하나의 큰 컴플리케이션으로 디자인된다면 캔버스 크기 확대를 통해 컴플리케이션을 꽉 채우려고 하지 마세요 콘텐츠는 보통의 원형 패밀리와 동일하되, 약간 커야 합니다 말씀드렸다시피 시계 페이스에 사용되는 위젯 그룹군에는 accessoryRectangular와 accessoryInline도 있죠 직사각형 컴플리케이션 페이스는 없습니다 accessoryInline 그룹군은 이미 widgetLabel로 작동하고요 시계 페이스는 인라인 콘텐츠의 이미지와 텍스트를 추출하고 페이스 모습에 맞춰 스스로 렌더링합니다 다음은 '마이그레이션'입니다 이건 두 부분으로 나뉘는데요 WidgetKit에서 기존 ClockKit 컴플리케이션 코드를 다시 쓰고 매핑을 제공함으로써 페이스 적용 컴플리케이션으로 업그레이드하는 방법을 시스템에 알려줍니다 시스템은 새 콘텐츠에 대해 ClockKit 데이터 소스를 안 묻고 페이스 에디팅 피커에 새 컴플리케이션만 보일 겁니다
watchOS 9는 WidgetKit에 시계를 가져올 뿐 아니라 모든 페이스를 업데이트하여 컴플리케이션 패밀리 수를 아주 극적으로 줄여줍니다 12개에서 4개로요 Rectangular와 Corner 맵은 accessoryCorner와 accessoryCorner에 직접 매핑됩니다 Circular 스타일 ClockKit 그룹군 모두 이제 단일 accessoryCircular WidgetKit 그룹군이 된 거죠 accessoryInline 그룹군은 기존 utilitarianSmallFlat이나 utilitarianLarge이 사용되던 곳에 사용됩니다
utilitarianSmall이었던 곳이 accessoryCorner 그룹군으로 많이 업데이트되었죠
WidgetKit에서는 SwiftUI 뷰와 레이아웃이 ClockKit의 템플릿으로 대체됩니다 WidgetKit의 타임라인과 엔트리는 친숙합니다 실제로 ClockKit 자체에서 영감을 받은 것이거든요 컴플리케이션 데이터 소스가 정적 또는 인텐트 기반의 WidgetKit 구성에 잘 마이그레이션된다는 거죠
WidgetKit 지원 구성 유형과 일반 그룹군 지원을 더 알아보고 싶으시다면 다음 세션을 확인해 보세요 시스템이 누군가의 컴플리케이션을 자동으로 마이그레이션하도록 Clockfit에 마지막 API를 추가했는데요 이미 시계 페이스에 있는 컴플리케이션을 사용자 상호작용 없이 새로운 WidgetKit 기반으로 업그레이드하실 수 있습니다 시계에서 앱이 업데이트되면 Watch Faces가 앱의 번들에서 위젯 표시를 확인할 겁니다 뭔가 발견할 경우 ClockKit 데이터 소스를 공개해 기존 컴플리케이션에 대한 마이그레이션을 생성하겠죠 이 관점에서 더 나가면 CLKComplicationDataSource는 당신의 ClockKit 공유 페이스를 누군가 받게 될 때 마이그레이션을 요청할 때만 작동할 겁니다 시스템은 새 페이스가 공유될 때마다 요청하겠죠 마이그레이션이 일관적으로 이루어질 수 있도록 말이죠 WidgetKit 컴플리케이션을 다 만드셨다면 새 프로퍼티 widgetMigrator부터 추가하세요 새 Migrator 프로토콜을 준수하는 객체를 제공합니다 데이터 소스 자체이건 다른 유형이건 상관 없이요
CLKComplication WidgetMigrator에는 기존 CLKComplicationDescriptors의 위젯 마이그레이션 구성을 시계 페이스에 제공하는 단일 함수만이 존재합니다 새 API를 채택하는 가장 직접적인 방법은 데이터 소스를 새 프로토콜 Migrator에 준수시키는 거죠
WidgetKit 컴플리케이션이 정적 구성을 사용한다면 정적 마이그레이션 구성을 제공합니다 위젯 컴플리케이션에서 인텐트를 사용한다면 동등한 마이그레이션 구성이 되는 겁니다 인텐트 기반 마이그레이션 구성 제공 시 시계 앱과 위젯 확장에 인텐트 정의를 포함시키셔야 한다는 점 기억하세요 양쪽에서 인텐트 객체를 만들 수 있어야 하니까요 WidgetKit은 경험을 극적으로 단순화시키면서도 시계 컴플리케이션 만들기를 창의적이고 새롭게 만들죠 시청해 주셔서 감사합니다
-
-
3:06 - Large Corner
struct CornerView: View { let value: Double var body: some View { ZStack { AccessoryWidgetBackground() Image(systemName: "cup.and.saucer.fill") .font(.title.bold()) .widgetAccentable() } } }
-
3:27 - Corner with Gauge
struct CornerView: View { let value: Double var body: some View { ZStack { AccessoryWidgetBackground() Image(systemName: "cup.and.saucer.fill") .font(.title.bold()) .widgetAccentable() } .widgetLabel { Gauge(value: value, in: 0...500) { Text("MG") } currentValueLabel: { Text("\(Int(value))") } minimumValueLabel: { Text("0") } maximumValueLabel: { Text("500") } } } }
-
4:24 - Circular Gauge
struct CircularView: View { let value: Double var body: some View { Gauge(value: value, in: 0...500) { Text("MG") } currentValueLabel: { Text("\(Int(value))") } .gaugeStyle(.circular) } }
-
4:34 - Circular Gauge with Widget Label
struct CircularView: View { let value: Double var body: some View { let mg = value.inMG() Gauge(value: value, in: 0...500) { Text("MG") } currentValueLabel: { Text("\(Int(value))") } .gaugeStyle(.circular) .widgetLabel { Text("\(mg, formatter: mgFormatter) Caffeine") } } var mgFormatter: Formatter { let formatter = MeasurementFormatter() formatter.unitOptions = [.providedUnit] return formatter } } extension Double { func inMG() -> Measurement<UnitMass> { Measurement<UnitMass>(value: self, unit: .milligrams) } }
-
4:51 - Circular Stack with Widget Label
struct CircularView: View { let value: Double var body: some View { let mg = value.inMG() ZStack { AccessoryWidgetBackground() Image(systemName: "cup.and.saucer.fill") .font(.title.bold()) .widgetAccentable() } .widgetLabel { Text("\(mg, formatter: mgFormatter) Caffeine") } } var mgFormatter: Formatter { let formatter = MeasurementFormatter() formatter.unitOptions = [.providedUnit] return formatter } } extension Double { func inMG() -> Measurement<UnitMass> { Measurement<UnitMass>(value: self, unit: .milligrams) } }
-
5:12 - Circular Stack or Gauge
struct CircularView: View { let value: Double @Environment(\.showsWidgetLabel) var showsWidgetLabel var body: some View { let mg = value.inMG() if showsWidgetLabel { ZStack { AccessoryWidgetBackground() Image(systemName: "cup.and.saucer.fill") .font(.title.bold()) .widgetAccentable() } .widgetLabel { Text("\(mg, formatter: mgFormatter) Caffeine") } } else { Gauge(value: value, in: 0...500) { Text("MG") } currentValueLabel: { Text("\(Int(value))") } .gaugeStyle(.circular) } } var mgFormatter: Formatter { let formatter = MeasurementFormatter() formatter.unitOptions = [.providedUnit] return formatter } } extension Double { func inMG() -> Measurement<UnitMass> { Measurement<UnitMass>(value: self, unit: .milligrams) } }
-
9:47 - Widget Migrator
var widgetMigrator: CLKComplicationWidgetMigrator { self }
-
9:56 - Static Migration Configuration
func widgetConfiguration(from complicationDescriptor: CLKComplicationDescriptor) async -> CLKComplicationWidgetMigrationConfiguration? { CLKComplicationStaticWidgetMigrationConfiguration(kind: "CoffeeTracker", extensionBundleIdentifier: widgetBundle) }
-
10:03 - Intent Migration Configuration
func widgetConfiguration(from complicationDescriptor: CLKComplicationDescriptor) async -> CLKComplicationWidgetMigrationConfiguration? { CLKComplicationIntentWidgetMigrationConfiguration(kind: "CoffeeTracker", extensionBundleIdentifier: widgetBundle, intent: intent, localizedDisplayName: "Coffee Tracker") }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.