스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
위젯 구현하기
앱과 게임용 위젯에 애니메이션과 상호 작용성을 추가하는 방법을 살펴봅니다. 엔트리 전환 시 애니메이션을 어떻게 수정하는지 알아보고, SwiftUI Button과 Toggle을 사용해서 홈 화면과 잠금 화면에서 강력한 효과를 생성하도록 상호 작용성을 추가해 봅니다.
챕터
- 1:23 - Animations
- 7:45 - Interactivity
리소스
관련 비디오
WWDC23
- 새 위치로 위젯 가져오기
- 푸시 알림으로 실시간 현황 업데이트하기
- ActivityKit 알아보기
- App Intent 개선 사항 살펴보기
- Xcode Previews로 프로그램적인 UI 구축하기
WWDC22
-
다운로드
♪ ♪
안녕하세요, 루카입니다 SwiftUI 팀 소속 엔지니어예요 오늘은 새롭고 흥미로운 기능을 활용해 위젯을 구현하는 방법을 알아보겠습니다 위젯은 iOS와 macOS 환경에서 사랑받는 요소인데 이제 상호 작용성과 애니메이션을 통해 훨씬 더 강력해졌습니다
사용자가 상호 작용성을 통해 위젯의 데이터를 직접 조작하며 앱에서 가장 중요한 동작을 실행하는 강력한 상호 작용을 생성할 수 있습니다 애니메이션은 콘텐츠가 어떻게 바뀌었고 동작의 결과가 무엇인지 사용자가 이해할 수 있도록 도와서 위젯을 구현합니다 새로운 기능이 정말 기대되는군요 바로 시작해 봅시다 우선 애니메이션을 살펴보고 위젯을 근사하게 만드는 게 얼마나 쉬운지 보여 드릴게요 이어서 위젯에 상호 작용성을 추가하는 방법을 알아보도록 하겠습니다 애니메이션부터 시작해 보죠 하루 카페인 섭취량을 파악하고자 제 친구 닐스가 작업한 앱을 이번 세션 내내 사용할 겁니다 해당 앱에는 카페인 총량이 얼마나 되고 오늘 마신 마지막 음료가 뭔지 알려 주는 위젯이 있습니다 최신 SDK로 위젯을 재컴파일하면 위젯의 콘텐츠가 바뀔 때마다 시스템이 기본 애니메이션으로 엔트리 간 전환을 애니메이팅할 겁니다 더 그럴싸하게 보이도록 몇 가지를 수정해 볼 텐데요 Xcode로 들어가기 전에 애니메이션이 위젯에서 어떻게 작동하는지 간략히 설명해 볼게요 일반 SwiftUI 앱에서는 상태를 사용해 뷰를 바꿀 수 있습니다 그리고 상태 변경이 애니메이션을 구동하죠 withAnimation 같은 수정자를 사용해서요 하지만 위젯 작동 방식은 살짝 다릅니다 위젯에는 상태가 없어요 대신 엔트리로 구성되는 타임라인을 생성합니다 엔트리는 특정 시간에 렌더링된 각기 다른 뷰와 대응하고요 SwiftUI는 엔트리 간에 같거나 다른 부분을 판단하고 바뀐 부분을 애니메이팅합니다 위젯은 기본적으로 암시적 스프링 애니메이션과 다양한 암시적 콘텐츠 전환을 얻습니다 하지만 SwiftUI에서 즉시 제공하는 전환과 애니메이션 콘텐츠 전환 API를 모두 사용해 위젯 애니메이팅을 사용자화할 수 있죠 SwiftUI 애니메이션 프리미티브가 전부 어떻게 작동하는지 자세히 설명하진 않을 거예요 해당 내용이 궁금하시면 'SwiftUI 애니메이션 살펴보기'를 시청해 보세요 그럼 이제 Xcode를 열어서 몇 가지 수정을 통해 모닝커피에 그린 라테 아트처럼 위젯을 화려하게 꾸미고 새 Xcode Preview API를 사용해 애니메이션을 빠르게 반복하는 방법을 보여 드리겠습니다
위젯을 구성하는 뷰를 여기서 전부 확인할 수 있는데요 메인 뷰에는 두 가지 뷰가 있는 VStack이 있습니다 첫 번째 뷰는 카페인의 총량을 보여 주고 두 번째 뷰는 오늘 마지막으로 마신 음료를 보여 줍니다 containerBackground 수정자를 어떻게 사용해서 위젯의 백그라운드를 정의하는지 잘 살펴보시길 바라요 이렇게 하면 Mac과 iPad에서 지원하는 모든 새 위치에 표시할 수 있습니다 보통 위젯이 애니메이팅되는 걸 확인하려면 엔트리가 많아야 하고 화면에 나타날 때까지 기다려야 하는데 이런 방식으로 작업하면 지루하고 속도가 나질 않죠 올해 도입하게 될 새 Preview API를 사용하면 훌륭한 솔루션을 얻을 수 있어요 systemSmall에서 새로운 위젯 프리뷰를 정의하고 위젯을 정의하는 형식을 전달할 수 있습니다 이제 앞서 정의해 둔 엔트리를 사용해서 타임라인을 렌더링하는 방법을 지정하겠습니다 캔버스에서 작업을 수행해 봤더니 타임라인 프리뷰가 나오면서 모든 엔트리가 어떻게 보일지 알 수 있죠 한번 확인해 볼까요? 프리뷰를 클릭하면 엔트리가 전환될 때 위젯이 어떻게 애니메이팅될지 살펴볼 수 있습니다 정말 멋지네요 새 Preview API의 기능은 이 밖에도 아주 무궁무진합니다 이처럼 새롭고 강력한 API를 더 알아보고 싶다면 'Xcode Preview로 프로그래밍 방식 UI 구축하기' 세션을 확인해 보세요 이제 애니메이션을 수정해 보도록 합시다 우선 카페인양 텍스트부터 바꿔 볼게요 지금은 다음 값으로 넘어갈 때 크로스페이딩되는데 값이 위로 올라가는 효과를 추가하려고 합니다 뷰는 그대로 있고 텍스트 콘텐츠만 바뀌는 거죠 콘텐츠 전환을 사용해 애니메이팅하면 됩니다 카페인양으로 숫자 텍스트를 추가하도록 선택할게요 이런 콘텐츠 전환은 중요한 숫자 값이 바뀔 때 강조할 수 있도록 특별히 고안되었습니다 꽤 근사한걸요 이제 마지막 음료 표시 뷰로 넘어가 보겠습니다 새로운 음료 추가를 강조하는 전환을 추가하려고 합니다 우선 ID 수정자를 사용해서 뷰의 정체성과 뷰가 렌더링하는 특정 로그를 연결하겠습니다 이렇게 하면 로그가 바뀔 때마다 새로운 뷰가 있고 해당 뷰로 전환해야 한다고 SwiftUI에 알릴 수 있겠죠 이제 전환을 지정할게요 푸시가 적합하겠네요 모서리는 어느 쪽으로 할까요? 하단으로 할게요 다음 순서가 뭔지 아시죠? 프리뷰 캔버스로 돌아가 봅시다
하단부터 바뀌는 전환 방식이 맘에 쏙 들어요 이제 마지막 수정 사항입니다 저는 커피를 많이 마시면 약간 초조해져요 전환을 위한 애니메이션 곡선에 이를 반영하고 싶습니다 좋은 점이 있다면 일반 SwiftUI 앱처럼 애니메이션 수정자를 사용해서 지속 시간이 더 짧고 잔잔한 스프링을 선택하고 해당 애니메이션을 로그값에 바인딩할 수 있다는 겁니다 이제 애니메이션과 카페인 섭취 상태가 일치하겠죠 지금까지 작업한 내용이 상당히 만족스럽네요 그럼 상호 작용성으로 넘어가 보도록 하죠 상호 작용성을 통해 위젯에서 바로 동작을 실행할 수 있습니다 Xcode로 들어가기 전에 잠시 짬을 내서 위젯 작동 방식에 대한 아키텍처를 살펴볼게요 상호 작용성이 어떻게 작동하는지 멘탈 모델을 생성하는 데 도움이 될 겁니다 위젯을 생성할 때 위젯 확장을 정의하는데 시스템에서 검색하고 독립적인 프로세스로 실행하죠 위젯은 사실상 위젯의 모델인 일련의 엔트리를 반환하는 타임라인 공급자를 정의합니다 위젯이 보이면 시스템은 위젯 확장 프로세스를 시작하고 타임라인 공급자에 엔트리를 요청합니다 이런 엔트리는 위젯 구성의 일부인 뷰 빌더로 피드백되어 해당 엔트리에 근거해 일련의 뷰를 생성하는 데 사용됩니다 그런 다음 시스템은 이런 뷰의 표현을 생성하고 디스크에 아카이빙합니다 특정 엔트리를 표시할 때가 되면 시스템은 아카이빙된 위젯 표현을 프로세스에서 디코딩하고 렌더링합니다 잠시 숨을 고르면서 마지막 요점을 반복해 볼게요 뷰 코드는 아카이빙 중에만 실행됩니다 해당 뷰의 별도 표현은 시스템 프로세스에서 렌더링하고요 하지만 데이터가 정적이지 않다면 해당 엔트리를 업데이트해야겠죠 위젯에 표시되는 데이터를 업데이트할 때마다 앱에서 reloadTimelines 함수를 호출하면 됩니다 이렇게 하면 방금 설명한 프로세스를 반복하고 새 엔트리를 재생성하며 새로운 뷰의 사본을 디스크에서 아카이빙할 겁니다 이런 아키텍처에서 중요한 핵심은 세 가지예요 위젯이 보이면 코드는 실행되지 않습니다 타임라인 엔트리를 업데이트해 위젯 콘텐츠를 바꿀 수 있고 이는 인터랙티브 위젯도 마찬가지입니다 일반적으로 위젯 업데이트는 최선의 노력으로 수행되지만 중요한 건 상호 작용에서 시작된 새로 고침은 항상 발생한다는 겁니다 이제 다음으로 넘어가서 상호 작용성을 추가하는 방법을 알아봅시다 Button과 Toggle처럼 익숙한 컨트롤을 사용해 위젯의 일부를 상호 작용하도록 만들 수 있어 편리하죠 다만 위젯은 다른 프로세스에서 렌더링되므로 SwiftUI는 프로세스 공간에서 클로저를 실행하거나 바인딩을 변형하진 못합니다 꼭 기억해 두세요 따라서 위젯 확장으로 실행하고 시스템에서 호출하는 동작을 표현하는 방법이 필요합니다 다행히도 이런 문제에 대한 솔루션이 이미 있습니다 바로 App Intent입니다 App Intent를 사용해 앱에 대한 동작을 단축어 또는 Siri에 나타내 본 적 있으실 텐데요 동일한 Intent를 사용해 위젯 내 동작을 표현할 수 있습니다 본질적으로 App Intent는 시스템에서 실행 가능한 동작을 코드로 정의하도록 허용하는 프로토콜입니다 이번 예시에서는 To Do 앱에서 할 일 항목을 전환하도록 App Intent를 정의해 볼게요 Intent는 여러 매개변수를 입력값으로 정의합니다 Intent를 실행하는 비즈니스 로직이 있는 비동기 함수 perform도 정의하고요 App Intent는 아주 강력합니다 알아 둬야 할 내용도 훨씬 많죠 WWDC22에서 'App Intent 살펴보기'와 WWDC23에서 'App Intent의 개선 사항 살펴보기' 두 세션을 꼭 시청해 보세요 UI에서 App Intent를 바로 실행할 수 있도록 SwiftUI와 App Intent를 모두 가져오는 경우 Button과 Toggle에 새로운 이니셜라이저를 마련했습니다 컨트롤이 상호 작용 할 때 App Intent를 인수로 받고 해당 Intent를 실행하죠 App Intent를 사용하는 Button과 Toggle만 인터랙티브 위젯에서 지원한다는 점을 명심하세요 다른 컨트롤은 작동하지 않을 겁니다 이런 이니셜라이저는 앱에서도 잘 작동합니다 App Intent 로직을 위젯과 앱 사이에 공유할 수 있다는 점이 아주 유용하죠 Xcode와 커피 섭취량 추적 앱으로 돌아가 상호 작용성을 추가해 봅시다 현재는 사용자가 새로운 음료를 로깅하려면 앱을 여는 수밖에 없습니다 인터랙티브 위젯이 진가를 발휘하는 순간은 액셀러레이터로 작용해서 앱에서 가장 중요한 동작을 표시할 때입니다 제 앱에서 이런 순간은 새 음료를 로깅할 때겠죠 미리 생성해 둔 파일에 이를 추가해 봅시다 우선 App Intent와 일치하는 유형을 정의하겠습니다 새로운 음료를 로깅할 수 있도록요 사람이 읽기 가능하면서 시스템에서 사용할 수 있는 이름을 붙여 줄게요 그런 다음 스토어에 espresso를 로깅하고 빈 Intent 결과를 반환해서 perform 요구 사항을 구현합니다 여기서 한 가지 강조하고 싶은 게 있어요 perform은 비동기 함수입니다 데이터베이스를 작성하는 등 비동기 작업을 진행한다면 이 함수를 최대한 활용하세요 지금 저도 보시다시피 로그 작성 작업을 기다릴 때 해당 함수를 활용하고 있습니다 perform에서 반환하자마자 시스템이 위젯 타임라인을 곧바로 새로 고침 하면서 위젯 콘텐츠를 업데이트할 기회를 제공할 겁니다 다시 한번 말씀드리지만 perform에서 반환하기 전에 업데이트된 위젯 새로 고침에 필요한 정보가 모두 잘 있는지 꼭 확인하세요 음료는 espresso로 하드 코딩 했지만 특정한 음료를 로그에 전달하도록 설정해야겠죠 @Parameter 프로퍼티 래퍼를 사용해 저장된 프로퍼티를 추가하고 모든 매개변수를 채우는 이니셜라이저도 추가하면 됩니다 여기서 핵심은 이 프로퍼티 래퍼를 쓰는 겁니다 주석이 달리고 저장된 프로퍼티만 지속적으로 유지되며 위젯 확장에서 Intent를 수행할 때 사용할 수 있거든요 해당 Intent를 호출하는 버튼을 추가하기 전에 App Intent를 사용할 때 중요한 생태계 이점을 강조하려고 합니다 방금 정의한 App Intent는 단축어 또는 Siri에서 사용할 수 있을 겁니다 지금 잘 정의해 두면 위젯을 넘어 사용자 경험에 크게 도움이 되겠죠 이제 위젯에 버튼을 추가해 봅시다 버튼이 있는 뷰를 새로 생성해 볼 거예요 이 뷰에서는 App Intent를 취하는 버튼 이니셜라이저를 사용해 방금 정의한 걸 전달하겠습니다 위젯의 나머지 부분에 이 뷰를 추가하고 스페이서를 몇 개 넣을게요 이제 모든 준비를 마쳤습니다 빌드하고 실행해서 위젯에서 잘 작동하는지 확인해 봅시다 여기서 소소한 팁이 있다면 위젯 확장 타깃을 직접 빌드할 수 있다는 겁니다 Xcode 홈 화면에서 위젯을 바로 실행할 거고요 방금 정의한 버튼이 위젯에 생겨났습니다 버튼을 탭하면 마지막으로 마신 에스프레소를 로깅할 수 있죠 위젯에서 최상의 사용자 경험을 제공하도록 한 가지 더 바꿔 볼게요 App Intent가 작동을 멈추면 위젯이 타임라인을 새로 고침 할 겁니다 이로 인해 동작이 실행되고 UI에 변경 사항이 반영될 때까지 지연 시간이 발생할 수 있습니다 Mac의 iPhone 위젯에서는 지연 시간이 더 길어질 거예요 곧바로 적용할 수 있는 솔루션을 알려 드릴게요 예를 들어 이 위젯에서는 업데이트된 엔트리 도착 전까지 카페인 총량을 나타내는 값은 업데이트되지 않을 겁니다 invalidatableContent 수정자로 이 뷰에 주석을 달겠습니다 iPhone에서 Mac으로 위젯을 추가해 뒀어요 버튼을 탭해 보죠 카페인양을 표시하는 뷰는 업데이트가 들어올 때까지 해당 값이 무효화됨을 나타내는 시스템 효과를 보여 줍니다 Button 작동을 확인했고 지연 시간에 대한 사용자 인식을 invalidatableContent 수정자로 개선하는 방법을 알아봤습니다 이 수정자는 신중하게 사용하세요 변경 사항이 생기는 뷰에 전부 주석을 달 필요는 없어요 사용자 기대치를 적절하게 설정할 수 있도록 의미 있는 뷰에 해당 수정자를 사용해야겠죠 Toggle은 한 단계 더 나아가 상호 작용 할 때 프레젠테이션을 최적으로 업데이트합니다 위젯 확장으로 왕복 이동 할 때까지 대기할 필요가 없죠 사용자를 대신해 아카이브 시간에 두 가지 구성 모두에서 토글 스타일을 사전 렌더링해 자동으로 수행됩니다 고유한 토글 스타일을 정의하는 경우 스타일에서 구성 isOn 프로퍼티를 확인하고 모양새를 바꾸는 데 사용해야 합니다 상호 작용성과 애니메이션의 개요를 살펴봤습니다 애니메이션과 상호 작용성은 위젯의 새 지평을 여는 계기를 마련해 줍니다 모든 새 위치에 설치된 위젯을 통해서 사용자가 어디에 있든 소소하고 즐거운 상호 작용을 제공할 수 있고요 새 Xcode Preview API를 사용해 위젯의 애니메이션을 섬세하게 조정하고 앱에서 가장 중요한 동작을 위젯에 표시해서 사용자에게 언제 어디서든 강력한 상호 작용을 제공해 보시길 바라요 감사합니다 ♪ ♪
-
-
3:54 - Usage for the container background modifier
.containerBackground(for: .widget) { Color.cosmicLatte }
-
4:22 - Define a preview for the caffeine tracker widget
#Preview(as: WidgetFamily.systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
5:41 - Add a numeric text content transition
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
6:21 - Set up transition on LastDrinkView
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
7:18 - Configuring animation for the transition
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) .animation(.smooth(duration: 1.8), value: log) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
9:18 - Reload the timeline for a widget
WidgetCenter.shared.reloadTimelines(ofKind: "LocationForecast")
-
13:06 - App intent to log a caffeine drink
import AppIntents struct LogDrinkIntent: AppIntent { static var title: LocalizedStringResource = "Log a drink" static var description = IntentDescription("Log a drink and its caffeine amount.") @Parameter(title: "Drink", optionsProvider: DrinksOptionsProvider()) var drink: Drink init() {} init(drink: Drink) { self.drink = drink } func perform() async throws -> some IntentResult { await DrinksLogStore.shared.log(drink: drink) return .result() } }
-
15:10 - Create view to log a new drink
struct LogDrinkView: View { var body: some View { Button(intent: LogDrinkIntent(drink: .espresso)) { Label("Espresso", systemImage: "plus") .font(.caption) } .tint(.espresso) } }
-
16:28 - Use the invalidatable content modifier
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) .invalidatableContent() } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.