스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI의 새로운 기능
SwiftUI를 사용하여 모든 Apple 플랫폼을 위한 멋진 앱을 제작하는 방법을 알아보고, iPadOS의 탭 및 문서에 적용된 참신한 디자인과 느낌을 살펴보세요. 새로운 윈도우 생성 API로 윈도우 관리를 개선하고, visionOS 앱에서 몰입형 공간 및 볼륨을 더 심도 깊게 제어할 수 있습니다. 다양한 차트를 만들거나 텍스트를 맞춤화하고 레이아웃을 지정하는 등의 작업을 하는 데 도움이 되는 흥미로운 개선 사항도 살펴봅니다.
챕터
- 0:00 - Introduction
- 0:51 - Fresh apps
- 1:04 - Fresh apps: TabView
- 2:22 - Fresh apps: Presentation sizing
- 2:39 - Fresh apps: Zoom transition
- 3:02 - Fresh apps: Custom controls
- 3:38 - Fresh apps: Vectorized and function plots
- 4:10 - Fresh apps: TableColumnForEach
- 4:25 - Fresh apps: MeshGradient
- 4:51 - Fresh apps: Document launch experience
- 5:33 - Fresh apps: SF Symbols 6
- 6:37 - Harnessing the platform
- 6:52 - Harnessing the platform: Windowing
- 8:28 - Harnessing the platform: Input methods
- 10:45 - Harnessing the platform: Widgets and Live Activities
- 12:25 - Intermezzo
- 12:55 - Framework foundations
- 13:09 - Framework foundations: Custom containers
- 13:48 - Framework foundations: Ease of use
- 16:18 - Framework foundations: Scrolling enhancements
- 17:18 - Framework foundations: Swift 6 language mode
- 18:01 - Framework foundations: Improved interoperability
- 19:18 - Crafting experiences
- 19:43 - Crafting experiences: Volumes
- 20:27 - Crafting experiences: Immersive spaces
- 21:27 - Crafting experiences: TextRenderer
- 22:12 - Next steps
리소스
관련 비디오
WWDC24
- 문서 실행 경험 향상하기
- 볼륨 및 몰입형 공간 자세히 알아보기
- 시스템 전반에서 앱의 제어 기능 확장하기
- Apple Pencil 최대한 활용하기
- Apple Watch로 실시간 현황 가져오기
- iPadOS에서 탭 및 사이드바 경험 향상하기
- SF Symbols 6의 새로운 기능
- Swift 6으로 앱을 마이그레이션하기
- Swift Charts: 벡터화된 플롯과 함수 플롯
- SwiftUI 컨테이너 쉽게 이해하기
- SwiftUI로 맞춤형 시각 효과 제작하기
- SwiftUI로 macOS 윈도우 다듬기
- SwiftUI에서 윈도우 처리하기
- SwiftUI의 손쉬운 사용 관련 업데이트
- UI 애니메이션 및 전환 효과 향상하기
- visionOS에서 맞춤형 호버 효과 제작하기
-
다운로드
안녕하세요 시청해 주셔서 감사합니다 저는 Sommer입니다 저는 Sam입니다 저희는 SwiftUI 팀의 엔지니어입니다 SwiftUI의 새로운 기능을 소개하게 되어 기쁩니다 Sam과 저는 노래방을 좋아해서 팀원들과 함께 즐길 수 있는 노래방 파티를 계획하기 위한 앱을 개발해 왔습니다 이 앱은 SwiftUI의 여러 가지 개선 사항을 활용하고 있으며 이러한 개선 사항을 여러분에게 공유해 드리려고 합니다 먼저 앱에 새롭고 신선한 느낌을 주는 수많은 멋진 기능부터 살펴보겠습니다 앱이 모든 플랫폼에서 자연스럽게 느껴지도록 앱을 다듬는 도구를 사용할 수 있고 프레임워크의 기본 구성 요소가 광범위하게 개선되었습니다 몰입감 넘치는 경험을 제작할 수 있는 새로운 도구 모음도 있습니다 후! 다룰 내용이 많으니 바로 시작하겠습니다 SwiftUI를 사용하면 새로운 탭 보기 아름다운 메시 그라디언트 멋진 제어기를 구현할 수 있어 앱에 새로운 느낌을 줄 수 있습니다 Sam과 저는 노래방 이벤트 플래너 앱을 제작했습니다 주로 사이드바로 작동하는 앱인데 iOS 18.0에서는 사이드바가 훨씬 더 유연해졌습니다 버튼을 탭하기만 하면 기본 보기가 탭 막대 모양으로 변경되어 UI가 더 아름답게 표시되죠 이제 탭 막대가 콘텐츠 위에 떠 있는 것처럼 표시됩니다 사용자는 취향에 맞게 모양을 완전히 맞춤화할 수도 있습니다 항목을 재배치하거나 자주 사용하지 않는 옵션을 숨길 수 있죠 새로운 탭 보기를 사용하도록 기본 보기를 다시 작성하는 것은 정말 간단했습니다 이제 SwiftUI에서 TabView에 타입 안정성 구문이 적용되어 빌드할 때 일반적인 오류를 잡아내기가 더 쉬워졌습니다 콘텐츠가 늘어남에 따라 손쉽게 탭 보기를 더 유연하게 만들 수 있습니다 새로운 .sidebarAdaptable 탭 보기 스타일을 적용하기만 하면 노래방 플래너를 탭 막대와 사이드바 보기 간에 전환할 수 있죠
사이드바는 여기 노래 목록처럼 콘텐츠가 많을 때 근사해 보입니다 탭을 재배치하거나 제거하는 등의 동작을 맞춤화할 수도 있는데 이는 프로그래밍 방식으로 완전히 제어할 수 있습니다 새로워진 사이드바는 tvOS에서도 멋지게 표현됩니다 macOS에서는 탭 보기의 스타일을 사이드바 또는 도구 막대의 구분 제어기로 표시할 수 있습니다 이제 시트를 표시하는 크기 조절이 플랫폼 전체에서 통일되고 간소화되었습니다 .presentationSizing 한정자를 사용해 .form이나 .page에 꼭 맞는 크기의 시트를 생성하거나 맞춤형으로 크기를 조절할 수도 있습니다 SwiftUI는 새로운 확대/축소 탐색 전환도 지원하는데 저는 이 기능을 사용해 파티 정보가 멋지게 확대되어 보이도록 만들었습니다
새로운 TabView에 대한 자세한 내용은 ‘iPadOS에서 탭 및 사이드바 경험 향상하기’에서 확인해 보세요 새로운 애니메이션에 대해 더 자세히 알아보려면 ‘UI 애니메이션 및 전환 효과 향상하기’를 시청하세요
이제 제어 센터나 잠금 화면의 버튼, 토글 등 크기 조절이 가능한 맞춤형 제어기를 만들 수 있고 동작 버튼으로 활성화할 수도 있습니다 제어기는 새로운 종류의 위젯으로 앱 인텐트로 쉽게 빌드할 수 있습니다 단 몇 줄의 코드로 WWDC 노래방 파티를 즉석에서 시작할 수 있는 ControlWidgetButton을 만들 수 있습니다 새롭고 강력한 제어기 API를 사용해 구성 가능한 버튼과 토글을 맞춤화하는 방법에 대한 자세한 내용은 ‘시스템 전반에서 앱의 제어 범위 확장하기’에서 확인해 보세요 Sam과 저는 노래방 파티 참석자 수를 늘리기 위해 부단히 노력하고 있습니다 목표 차트에 지수 함수를 사용하는 게 좋겠군요
Swift 차트의 함수 플롯 작업을 통해 멋진 그래프를 쉽게 그릴 수 있습니다 이 LinePlot처럼 말이죠
실제 참석자 수 그래프도 그려 볼게요 아이코, 아직 멀었네요
Swift Charts의 개선 사항에 대해 더 자세히 알아보려면 ‘Swift Charts: 벡터화된 플롯과 함수 플롯’ 세션을 시청하세요 데이터 보기를 좋아하는 저는 파티에서 참석자들이 노래를 얼마나 많이 부르는지 살펴보는 것도 좋아합니다 TableColumnForEach를 사용하면 표 열의 수를 동적으로 표시할 수 있습니다 Sam과 제가 파티를 얼마나 많이 열든 상관없이 말이죠 참석자 수를 늘리기 위해 멋진 파티 초대장을 보내야겠습니다 다채로운 메시 그라디언트에 대한 수준 높은 지원이 SwiftUI에 추가되었습니다 색상 그리드에서 지점들 사이에 중간값을 채워 아름다운 격자를 만들 수 있습니다
파티만큼이나 멋진 노래방 초대장을 만들 수 있는 완벽한 방법인 것 같네요
노래방 플래너 앱을 멋지게 꾸몄으니 이제 Sam과 저는 가사 표시를 약간 바꿔 재미를 더하려고 합니다 그래서 좋아하는 노래에 맞춰 단어를 편집할 수 있게 문서 기반 앱도 제작했습니다 저희 앱만의 개성을 표현하고 특징을 강조하기 위해 새로운 Document Launch Scene 유형을 사용해 시작 화면을 만들었습니다 커다란 볼드체로 제목을 표시하고 배경을 맞춤화하고 재미있는 액세서리 보기를 추가해서 실행 경험을 산뜻하게 만들었습니다 맞춤형 문서 아이콘 템플릿 같은 문서 기반 앱으로 할 수 있는 모든 작업에 대해 자세히 알아보려면 ‘문서 실행 경험 향상하기’를 시청하세요 이제 음표에 기호 효과를 더해 시작 화면에 마무리 손질을 해 보겠습니다 우와, 음표들이 흔들흔들 움직이네요
이제 앱의 SF Symbols에 3가지 새로운 애니메이션 프리셋을 채택할 수 있습니다 ‘흔들흔들’ 효과는 기호를 원하는 방향이나 각도로 흔들리게 만들어 관심을 끕니다 ‘심호흡’ 효과는 기호를 매끄럽게 확대하거나 축소해 활동이 진행 중임을 나타냅니다 ‘회전’ 효과를 적용하면 기호의 일부가 지정된 앵커 포인트를 중심으로 회전합니다
또한 기존 프리셋 중 일부가 새로운 기능으로 향상되었습니다 예를 들어 이제 기본 Replace 애니메이션에 새로운 MagicReplace 동작이 기본으로 적용됩니다 MagicReplace를 사용하면 기호에 배지와 슬래시 애니메이션이 매끄럽게 적용되죠
이런 기능들은 SF Symbols 6의 몇 가지 개선 사항에 불과합니다 자세히 알아보려면 ‘SF Symbols 6의 새로운 기능’을 시청하세요
SwiftUI가 크게 향상되어 Apple의 어느 플랫폼에서든 앱이 편안하고 자연스럽게 느껴집니다, 향상된 윈도우 기능, 강화된 입력 제어기 한눈에 파악할 수 있는 많은 콘텐츠를 통해 앱이 어느 플랫폼에서 실행되든 그 이점을 십분 활용할 수 있습니다 macOS에서는 윈도우의 스타일과 동작을 맞춤화할 수 있습니다 macOS에서 제가 만든 가사 편집기 앱을 열면 한 줄 미리보기가 표시되는 윈도우가 나타납니다 저는 새로운 일반 윈도우 스타일을 사용해 기본 윈도우 크롬을 제거했습니다 그리고 플로팅 윈도우 레벨을 지정해 다른 윈도우 위에 표시되도록 했죠 defaultWindowPlacement API를 사용해 화면 상단에 윈도우를 배치해서 가사의 나머지 부분이 가려지지 않도록 했고 디스플레이의 크기와 콘텐츠의 크기를 고려해 윈도우를 완벽한 지점에 배치했습니다 또한 미리보기의 콘텐츠 보기에 WindowDragGesture를 추가해서 윈도우를 드래그해 화면상에서 위치를 조정할 수 있도록 했죠
유틸리티 윈도우처럼 새로운 장면 유형도 있습니다 윈도우의 스타일과 동작을 맞춤화하는 방법에 대해 자세히 알아보려면 ‘SwiftUI로 macOS 윈도우 다듬기’를 시청하세요 멀티 윈도우 가사 편집기 앱은 visionOS와도 훌륭하게 연동됩니다 최근에 제 노래방 친구인 Andrew가 Botanist 앱에서 이러한 새로운 푸시 윈도우 동작을 사용해 가장 중요한 콘텐츠에 초점을 맞추는 방법을 알려주었습니다 pushWindow를 사용해 윈도우를 열고 원래 윈도우를 숨길 수 있습니다 저는 가사 편집기에도 똑같이 하고 싶었습니다 그래서 pushWindow 환경 작업을 사용해 가사 미리보기에 초점을 맞추려고 합니다 자세히 알아보려면 ‘SwiftUI에서 윈도우 처리하기’ 비디오를 시청하세요 SwiftUI는 각 플랫폼에서 제공하는 고유한 입력 방법을 활용할 수 있는 여러 새로운 도구를 제공합니다 visionOS에서는 사용자가 보기를 쳐다보거나 보기 근처에 손가락을 대거나 보기 위로 포인터를 이동할 때 보기가 반응하게 만들 수 있습니다 물론 사용자의 개인정보는 보호된 상태로 말이죠
새로운 클로저 기반의 hoverEffect 한정자 내에서 보기가 활성 상태와 비활성 상태 간에 전환할 때 보기의 모습을 제어할 수 있습니다 다양한 효과를 조정하고 효과의 타이밍을 제어하고 손쉬운 기능 설정에 반응하는 방법을 알아보려면 ‘visionOS에서 맞춤형 호버 효과 제작하기’를 확인해 보세요 iPadOS, macOS visionOS 앱에서는 키보드도 훌륭하게 지원됩니다
macOS의 주 메뉴에 미리보기 윈도우를 여는 항목이 있습니다
저는 새로운 modifierKeyAlternate 한정자를 추가해서 option 키를 눌러 전체 화면에서 미리볼 수 있는 보조 항목이 표시되도록 했습니다
낮은 수준의 제어를 위해 어떤 보기든 보조 키 누르기 상태 변화에 반응할 수 있습니다 저는 onModifierKeysChanged를 사용하기 위해 가사 편집기를 업데이트했습니다 option 키를 누르고 있으면 통통 튀는 공이 착지하는 위치를 나타내는 추가 정렬 안내선이 나타나고 이를 조정할 수 있습니다 포인터 상호작용은 여러 기기에서 작동하는 또 다른 중요한 입력 형식입니다 pointerStyle API를 사용하면 시스템 포인터의 모양과 표시 여부를 맞춤화할 수 있습니다 저는 가사의 크기를 조절할 수 있도록 만들었기 때문에 각 크기 조절 앵커에 적절한 frameResize 포인터 스타일을 적용하려고 합니다 SwiftUI는 iPadOS 17.5의 새로운 기능으로 두 번 탭과 스퀴즈 같은 Apple Pencil 및 Apple Pencil Pro의 기능을 지원합니다
.onPencilSqueeze를 사용하면 제스처에서 정보를 수집하고 어떤 동작을 선호하는지 확인할 수 있습니다 이 경우에는 Pencil의 호버 위치 아래에 가사 낙서 팔레트를 표시할 겁니다 재미있는 그리기로 가사에 마크업을 할 수 있게 말이죠 새로운 Apple Pencil API에 대한 모든 정보를 알아보려면 ‘Apple Pencil 최대한 활용하기’를 확인해 보세요 위젯은 한눈에 볼 수 있는 정보와 앱과의 주요 상호작용을 제공합니다 이제 실시간 현황이 watchOS에도 제공되므로 사용자가 별도의 동작을 하지 않아도 iOS 기반 실시간 현황이 Apple Watch에 자동으로 표시됩니다
Sam과 저는 이미 실시간 현황을 사용하고 있어서 이동 중에 노래 가사를 검토할 수 있습니다 Apple Watch에 자동으로 실시간 현황이 표시되죠 .supplementalActivityFamily를 사용해 watchOS용 콘텐츠를 다듬어 훨씬 멋지게 보이도록 할 수 있습니다 한 번에 더 많은 가사를 표시할 수 있죠 정말 멋지죠!
노래를 부르는 사람들이 이중 탭을 사용해 현재 재생 중인 가사로 이동할 수 있도록 .handGestureShortcut 한정자를 적용할 수 있습니다
또한 다음 번 노래방 이벤트 일정을 언제나 확인할 수 있도록 노래방 앱에 위젯을 추가했습니다 노래방 시간에 카운트다운을 표시하는 새로운 참조 날짜 형식 스타일을 사용했죠
이제 텍스트에 실제 시간과 날짜 표시를 위한 추가적인 형식을 사용할 수 있는데 위젯과 실시간 현황에서 멋지게 작동합니다
이러한 형식에는 날짜 참조 날짜 오프셋, 타이머가 있습니다 각 형식은 그 구성요소까지 세밀하게 맞춤화할 수 있고 컨테이너의 크기에 맞출 수도 있습니다 위젯도 이제 더 스마트해졌습니다 관련 맥락을 명시하면 시스템이 스마트 스택 같은 곳에 맥락을 더 스마트하게 나타낼 수 있습니다 이렇게 하면 지정된 시간에 또는 노래를 부르는 사람이 지정된 노래방 장소에 가까워질 때 카운트다운 위젯이 자동으로 표시될 수 있습니다 Sommer 님, 가사 편집기는 어떻게 진행되고 있나요? 제 다음 독창곡인 ‘Cupertino Dreamin’의 가사를 생각해 내고 있는데요 잊어버리기 전에 적어둬야겠습니다 잘 진행되고 있습니다! 가사 편집기를 사용해 ‘Smells Like Scene Spirit’의 가사 일부를 적어보았는데요 훌륭합니다 멋지네요, 선곡 목록 끝부분에 끼워 넣어볼 수 있겠네요 그러고 보니 선곡 목록이요! WWDC 노래방 파티의 선곡 목록이 곧 준비되겠죠? 물론입니다, Sommer 님 노래 주제는 모두 SwiftUI의 뛰어난 새 프레임워크 기초에 관한 것입니다 SwiftUI에 갖가지 새로운 API가 추가되었는데 SwiftUI의 핵심 구성요소에 대한 개선 사항 기초 API를 사용하는 새로운 방법 개선 사항의 사용 편의성 등 프레임워크 작업을 그 어느 때보다 쉽게 수행할 수 있게 해줍니다
이제 나만의 맞춤형 컨테이너 보기를 생성할 수 있습니다 ForEach, subviewOf에 새로운 API를 사용하면 자체 카드 보기에 각 하위 보기를 감싸고 있는 이 예에서처럼 특정 보기의 하위 보기를 반복할 수 있습니다 이 API를 사용해 정적 콘텐츠 지원 섹션과 동적 콘텐츠 지원 섹션을 혼합하고 컨테이너별 한정자를 추가하는 등 목록 및 선택기 같은 SwiftUI의 내장 컨테이너와 동일한 기능을 가진 맞춤형 컨테이너를 만들 수 있습니다 맞춤형 컨테이너와 그 기반이 되는 SwiftUI 기본 사항에 대해 자세히 알아보려면 ‘SwiftUI 컨테이너 쉽게 이해하기’를 확인해 보세요 개선 사항을 사용하기가 쉬워서 그 어느 때보다 SwiftUI 작업을 쉽게 할 수 있습니다 이제는 EnvironmentKey에 완전히 부합하는 코드를 작성하거나 환경 값에 대한 확장 프로그램을 추가하지 않고도 새로운 입력 매크로를 사용해 간단한 속성만 작성하면 됩니다 무엇보다 좋은 점은 입력 매크로가 환경 값에만 국한되지 않는다는 점입니다 FocusValues Transaction, 새로운 ContainerValues에도 입력 매크로를 사용할 수 있습니다 이제 SwiftUI의 내장된 손쉬운 사용 레이블에 추가 정보를 첨부할 수 있습니다 프레임워크에서 제공하는 레이블을 무효화하지 않고도 제어기에 손쉬운 사용 정보를 더 추가할 수 있다는 뜻이죠 ‘SwiftUI의 손쉬운 사용 관련 업데이트’를 확인하여 조건부 한정자 지원 앱 인텐트 기반 accessibilityActions 등 SwiftUI의 놀랍고도 새로운 손쉬운 사용 기능에 대해 알아보세요
Xcode 미리보기에는 새로운 동적 연결 아키텍처가 있어서 프로젝트를 다시 빌드할 필요 없이 미리보기와 빌드 및 실행 간에 전환할 수 있으므로 반복 속도가 향상됩니다
이제 미리보기도 더 쉽게 설정할 수 있습니다 Previewable 매크로로 미리보기에서 바로 State를 사용할 수 있어 보기에 미리보기 콘텐츠를 감싸는 표준 방식을 사용하지 않아도 됩니다 이제 새로운 방법으로 텍스트 작업을 하고 선택 항목을 관리할 수 있습니다 SwiftUI가 텍스트 편집 제어기 내에서 텍스트 선택 항목에 프로그래밍 방식으로 액세스하고 텍스트 선택 항목을 제어할 수 있습니다 선택 항목 바인딩의 콘텐츠가 가사 필드의 선택된 텍스트와 일치하도록 업데이트됩니다
이제 선택된 범위와 같은 선택 항목의 속성을 읽을 수 있습니다 이 기능을 사용해 인스펙터에서 선택한 단어들에 대해 제안된 운율을 보여 줄 수 있습니다
.searchFocused를 사용하면 프로그래밍 방식으로 검색 필드의 초점 상태를 제어할 수 있습니다 검색 필드에 초점이 맞춰져 있는지 확인하고 프로그래밍 방식으로 검색 필드 간에 초점을 이동할 수 있다는 뜻이죠
이제 어느 텍스트 필드에든 텍스트 제안을 추가할 수 있습니다 이 기능을 사용해 문장을 마무리할 수 있는 제안을 보여드리겠습니다 제안은 드롭다운 메뉴로 표시되고 옵션을 선택하면 텍스트 필드가 선택한 마무리 문구로 업데이트됩니다 SwiftUI에는 새로운 그래픽 기능도 있습니다 이제 색상을 아름답게 혼합할 수 있습니다 색상에 대한 새로운 혼합 한정자를 사용해 해당 색상을 다른 색상과 지정된 양만큼 혼합합니다 또한 셰이더를 처음 사용하기 전에 미리 컴파일할 수 있는 기능으로 사용자 설정 셰이더 기능을 확장했습니다 그래서 느린 셰이더 컴파일로 인한 프레임 드롭을 방지할 수 있습니다 스크롤 보기를 세밀하게 제어할 수 있는 새로운 API가 많이 있습니다 이제 .onScrollGeometryChange를 사용해 ScrollView의 상태와 더 심도 있게 통합할 수 있습니다 이를 사용하면 콘텐츠 오프셋 콘텐츠 크기 등의 변화에 효율적으로 대응할 수 있습니다 이 ‘Back to invitation’ 버튼처럼 말이죠 이 버튼은 스크롤 보기의 콘텐츠 상단을 지나도록 스크롤하면 나타납니다
이제 스크롤로 인해 보기의 표시 여부가 변경되면 이를 감지할 수 있습니다 화면 안팎으로 콘텐츠가 이동할 때 멋진 경험을 생성할 수 있죠 자동으로 재생되는 이 비디오처럼 말입니다
스크롤 보기를 프로그래밍 방식으로 더 세밀하게 제어할 수 있을 뿐만 아니라 상단 가장자리처럼 프로그래밍 방식으로 스크롤할 수 있는 스크롤 위치도 더 많이 있습니다
지정된 축을 따라 튀어 오르기 기능 끄기, 프로그래밍 방식으로 스크롤 중지, 콘텐츠 정렬 세밀하게 제어하기 등 완벽한 스크롤 경험을 위해 정확하고 세심하게 돌려서 제어할 수 있는 다양한 종류의 추가적인 노브가 있습니다 새로운 Swift 6 언어 모드는 컴파일 시 데이터 레이스 안정성을 갖추었습니다 SwiftUI의 API가 개선되어 앱에 새로운 언어 모드를 더 쉽게 채택할 수 있습니다 SwiftUI의 보기는 항상 주 행위자에 대해 평가되었고 이제 보기 프로토콜은 이를 반영하기 위해 주 행위자 주석으로 표시됩니다 이는 View와 일치하는 모든 유형이 기본적으로 주 행위자로 명확히 분리된다는 것을 의미합니다 따라서 View를 명시적으로 주 행위자로 표시했다면 이제 동작을 변경하지 않고 해당 주석을 제거할 수 있습니다 새로운 Swift 6 언어 모드가 활성화되어 있어서 언제든지 준비가 되면 이를 활용할 수 있습니다 컴파일 타임 확인에 대해 자세히 알아보려면 ‘Swift 6으로 앱을 마이그레이션하기’를 시청하세요 SwiftUI는 완전히 새로운 앱 제작뿐만 아니라 UIKit 및 AppKit으로 작성된 기존 앱에 새로운 기능을 제작하기 위해 설계되었습니다 뛰어난 상호운용성은 이러한 프레임워크에서 아주 중요하죠 우리는 제스처와 애니메이션의 통합을 크게 개선했습니다 이제 원하는 내장 또는 사용자 설정 UIGestureRecognizer를 가져와 SwiftUI 보기 계층에 사용할 수 있습니다 UIKit으로 직접 지원되지 않는 SwiftUI 보기에서도 작동합니다 상호운용성 개선 사항은 반대로도 작동합니다 이제 UIKit과 AppKit에서 SwiftUI 애니메이션의 강력한 기능을 활용할 수 있습니다 SwiftUI가 UIView와 NSAnimationContext에 대한 새로운 애니메이션 함수를 정의하므로 진행 중인 SwiftUI 애니메이션을 사용해 UIKit과 AppKit 변경 사항에 애니메이션을 적용할 수 있습니다 제스처로 동작하는 애니메이션에서도 벨로시티가 자동으로 유지됩니다 SwiftUI 보기에서처럼 말이죠 UI와 NSViewRepresentable 컨텍스트가 SwiftUI에서 시작된 애니메이션을 UIKit과 AppKit으로 가져오는 새로운 API를 제공하여 애니메이션이 프레임워크 경계에서도 완벽하게 동기화되어 실행됩니다 프레임워크 전반에서 애니메이션 사용에 대한 자세한 내용은 ‘UI 애니메이션 및 전환 효과 향상하기’에서 확인해 보세요 근본적인 개선 사항들이 정말 훌륭해 보입니다 이제 노래방 스킬을 연습하는 일만 남았네요 그러고 나면 Sommer 님과 함께 파티를 열 수 있겠죠 경험을 빌드하는 SwiftUI의 새로운 도구를 사용하면 훌륭한 연습 앱을 제작하고 이벤트를 성공적으로 준비할 수 있습니다 볼륨, 몰입감 넘치는 공간 새로운 텍스트 효과 작업을 위한 새로운 API를 사용해 노래방 경험을 한층 더 생생하게 만들 수 있습니다 아름다운 보컬 사운드를 위해 연습하려고 visionOS용 연습 앱을 제작했는데 볼륨 형태로 마이크를 갖추었습니다 visionOS 2에서는 볼륨이 베이스플레이트와 함께 표시될 수 있습니다 베이스플레이트는 볼륨의 경계를 인식할 수 있게 해주고 새로운 크기 조절 핸들을 비롯한 윈도우 제어기로 안내해 줍니다 우리 마이크에는 이미 마이크 스탠드 베이스가 있어서 시스템에서 제공하는 베이스플레이트가 없는 마이크 형태가 좋을 것 같습니다 .volumeBaseplateVisibility 한정자를 사용해 시스템에서 제공하는 베이스플레이트를 비활성화하겠습니다 멋지죠!
또한 새로운 .onVolumeViewpointChange 한정자를 사용해 항상 마이크가 노래하는 사람을 향하도록 회전하게 했습니다 제가 볼륨의 다른 쪽으로 움직일 때마다 이 한정자가 호출되어 볼륨이 표시되는 방식의 변화에 반응할 수 있죠 마이크는 준비되었으니 이제 노래 부를 공간이 필요할 것 같습니다, 분위기 있는 노래방 라운지보다 노래하기에 더 좋은 곳은 없죠 몰입감 넘치는 아름다운 공간을 이미 완성했으니 이제 허용되는 몰입감 수준을 제어할 수 있습니다 초기 몰입도 50%와 최소 40%를 선택해 노래를 부르는 사람들이 노래방 라운지에 친숙해지도록 할 수 있습니다
분위기를 띄우기 위해 이제 점진적인 몰입 공간 주변의 패스스루 비디오에 효과를 적용할 수 있습니다 preferred-surroundings-effect를 사용해 비디오 패스스루를 어둡게 하거나 노래방 경험을 특별하게 만들 수 있습니다 colorMultiply를 사용해 멋진 무드 조명을 연출할 수 있죠
와, 환상적이네요
오너먼트를 부착하거나 지원되는 관점을 지정하는 새로운 방법 등 볼륨과 몰입감 넘치는 공간의 개선 사항에 대한 자세한 내용은 ‘볼륨 및 몰입형 공간 자세히 알아보기’에서 확인해 보세요
무대는 준비되었지만 파티를 시작하기 전에 따라 부를 수 있도록 가사를 준비해야겠죠?
이제 사용자 설정 렌더링 효과와 상호작용 동작으로 SwiftUI 텍스트 보기를 확장할 수 있습니다 이를 사용해 노래방 가사에 단어 하이라이트 효과를 만들 수 있습니다 노래방 렌더러가 원본 그림 뒤에 텍스트의 사본을 만들면 텍스트가 흐릿해지고 텍스트에 색조가 적용되어 빛나는 보라색으로 보이죠 이 하이라이트를 특정 단어에만 적용하고 마무리 손질을 추가하면 정말 놀라운 효과를 만들어 낼 수 있습니다 이 노래방 단어 하이라이트처럼요 이런 멋진 텍스트 효과를 만들어 내는 방법을 모두 알아보려면 ‘SwiftUI로 맞춤형 시각 효과 제작하기’를 시청하세요 좋습니다, Sommer님 이제 파티 준비가 모두 끝난 것 같네요 정말 순조롭게 진행됐습니다! SwiftUI의 이러한 변경 사항 덕분에 Sommer 님과 저는 앱을 최대한 활용할 수 있었습니다 여러분도 앱을 최대한 활용하실 수 있길 바랍니다 사이드바 기반 iPad 앱이나 tvOS 앱이 있다면 새로운 탭 보기 API를 활용해 유연성을 더하고 문서 기반 앱에서는 새로운 문서 실행 경험을 향상해 보세요 macOS와 visionOS 앱에 새로운 윈도우 기능 및 입력 기능을 추가해 보세요 watchOS에서는 경험을 세밀하게 조정해 실시간 현황을 최대한 활용해 보세요 또한 visionOS에서 볼륨과 몰입감 넘치는 공간에 새로운 기능을 활용해 보세요 SwiftUI 개발자가 되기에 좋은 한 해가 될 것 같습니다 노래방 가수가 되기에도요 좋아요 Sam 님, 준비됐나요? 저야 언제든 준비되어 있죠, Sommer 님 Siri야, 파티를 시작해!
-
-
1:38 - TabView
import SwiftUI struct KaraokeTabView: View { @State var customization = TabViewCustomization() var body: some View { TabView { Tab("Parties", image: "party.popper") { PartiesView(parties: Party.all) } .customizationID("karaoke.tab.parties") Tab("Planning", image: "pencil.and.list.clipboard") { PlanningView() } .customizationID("karaoke.tab.planning") Tab("Attendance", image: "person.3") { AttendanceView() } .customizationID("karaoke.tab.attendance") Tab("Song List", image: "music.note.list") { SongListView() } .customizationID("karaoke.tab.songlist") } .tabViewStyle(.sidebarAdaptable) .tabViewCustomization($customization) } } struct PartiesView: View { var parties: [Party] var body: some View { Text("PartiesView") } } struct PlanningView: View { var body: some View { Text("PlanningView") } } struct AttendanceView: View { var body: some View { Text("AttendanceView") } } struct SongListView: View { var body: some View { Text("SongListView") } } struct Party { static var all: [Party] = [] } #Preview { KaraokeTabView() }
-
2:28 - Presentation sizing
import SwiftUI struct AllPartiesView: View { @State var showAddSheet: Bool = true var parties: [Party] = [] var body: some View { PartiesGridView(parties: parties, showAddSheet: $showAddSheet) .sheet(isPresented: $showAddSheet) { AddPartyView() .presentationSizing(.form) } } } struct PartiesGridView: View { var parties: [Party] @Binding var showAddSheet: Bool var body: some View { Text("PartiesGridView") } } struct AddPartyView: View { var body: some View { Text("AddPartyView") } } struct Party { static var all: [Party] = [] } #Preview { AllPartiesView() }
-
2:39 - Zoom transition
import SwiftUI struct PartyView: View { var party: Party @Namespace() var namespace var body: some View { NavigationLink { PartyDetailView(party: party) .navigationTransition(.zoom( sourceID: party.id, in: namespace)) } label: { Text("Party!") } .matchedTransitionSource(id: party.id, in: namespace) } } struct PartyDetailView: View { var party: Party var body: some View { Text("PartyDetailView") } } struct Party: Identifiable { var id = UUID() static var all: [Party] = [] } #Preview { @Previewable var party: Party = Party() NavigationStack { PartyView(party: party) } }
-
3:18 - Controls API
import WidgetKit import SwiftUI struct StartPartyControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.karaoke_start_party" ) { ControlWidgetButton(action: StartPartyIntent()) { Label("Start the Party!", systemImage: "music.mic") Text(PartyManager.shared.nextParty.name) } } } } // Model code class PartyManager { static let shared = PartyManager() var nextParty: Party = Party(name: "WWDC Karaoke") } struct Party { var name: String } // AppIntent import AppIntents struct StartPartyIntent: AppIntent { static let title: LocalizedStringResource = "Start the Party" func perform() async throws -> some IntentResult { return .result() } }
-
3:49 - Function plotting
import SwiftUI import Charts struct AttendanceView: View { var body: some View { Chart { LinePlot(x: "Parties", y: "Guests") { x in pow(x, 2) } .foregroundStyle(.purple) } .chartXScale(domain: 1...10) .chartYScale(domain: 1...100) } } #Preview { AttendanceView() .padding(40) }
-
4:18 - Dynamic table columns
import SwiftUI struct SongCountsTable: View { var body: some View { Table(Self.guestData) { // A static column for the name TableColumn("Name", value: \.name) TableColumnForEach(Self.partyData) { party in TableColumn(party.name) { guest in Text(guest.songsSung[party.id] ?? 0, format: .number) } } } } private static func randSongsSung(low: Bool = false) -> [Int : Int] { var songs: [Int : Int] = [:] for party in partyData { songs[party.id] = low ? Int.random(in: 0...3) : Int.random(in: 3...12) } return songs } private static let guestData: [GuestData] = [ GuestData(name: "Sommer", songsSung: randSongsSung()), GuestData(name: "Sam", songsSung: randSongsSung()), GuestData(name: "Max", songsSung: randSongsSung()), GuestData(name: "Kyle", songsSung: randSongsSung(low: true)), GuestData(name: "Matt", songsSung: randSongsSung(low: true)), GuestData(name: "Apollo", songsSung: randSongsSung()), GuestData(name: "Anna", songsSung: randSongsSung()), GuestData(name: "Raj", songsSung: randSongsSung()), GuestData(name: "John", songsSung: randSongsSung(low: true)), GuestData(name: "Harry", songsSung: randSongsSung()), GuestData(name: "Luca", songsSung: randSongsSung()), GuestData(name: "Curt", songsSung: randSongsSung()), GuestData(name: "Betsy", songsSung: randSongsSung()) ] private static let partyData: [PartyData] = [ PartyData(partyNumber: 1, numberGuests: 5), PartyData(partyNumber: 2, numberGuests: 6), PartyData(partyNumber: 3, numberGuests: 7), PartyData(partyNumber: 4, numberGuests: 9), PartyData(partyNumber: 5, numberGuests: 9), PartyData(partyNumber: 6, numberGuests: 10), PartyData(partyNumber: 7, numberGuests: 11), PartyData(partyNumber: 8, numberGuests: 12), PartyData(partyNumber: 9, numberGuests: 11), PartyData(partyNumber: 10, numberGuests: 13), ] } struct GuestData: Identifiable { let name: String let songsSung: [Int : Int] let id = UUID() } struct PartyData: Identifiable { let partyNumber: Int let numberGuests: Int let symbolSize = 100 var id: Int { partyNumber } var name: String { "\(partyNumber)" } } #Preview { SongCountsTable() .padding(40) }
-
4:42 - Mesh gradients
import SwiftUI struct MyMesh: View { var body: some View { MeshGradient( width: 3, height: 3, points: [ .init(0, 0), .init(0.5, 0), .init(1, 0), .init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5), .init(0, 1), .init(0.5, 1), .init(1, 1) ], colors: [ .red, .purple, .indigo, .orange, .cyan, .blue, .yellow, .green, .mint ] ) } } #Preview { MyMesh() .statusBarHidden() }
-
5:14 - Document launch scene
DocumentGroupLaunchScene("Your Lyrics") { NewDocumentButton() Button("New Parody from Existing Song") { // Do something! } } background: { PinkPurpleGradient() } backgroundAccessoryView: { geometry in MusicNotesAccessoryView(geometry: geometry) .symbolEffect(.wiggle(.rotational.continuous())) } overlayAccessoryView: { geometry in MicrophoneAccessoryView(geometry: geometry) }
-
7:04 - Window styling and default placement
Window("Lyric Preview", id: "lyricPreview") { LyricPreview() } .windowStyle(.plain) .windowLevel(.floating) .defaultWindowPlacement { content, context in let displayBounds = context.defaultDisplay.visibleRect let contentSize = content.sizeThatFits(.unspecified) return topPreviewPlacement(size: contentSize, bounds: displayBounds) } }
-
7:30 - Window Drag Gesture
Text(currentLyric) .background(.thinMaterial, in: .capsule) .gesture(WindowDragGesture())
-
8:18 - Push window environment action
struct EditorView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Play", systemImage: "play.fill") { pushWindow(id: "lyric-preview") } } }
-
8:47 - Hover effects
struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .clipShape(.capsule) .hoverEffect { effect, isActive, _ in effect.scaleEffect(isActive ? 1.05 : 1.0) } } }
-
9:14 - Modifier key alternates
Button("Preview Lyrics in Window") { // show preview in window } .modifierKeyAlternate(.option) { Button("Preview Lyrics in Full Screen") { // show preview in full screen } } .keyboardShortcut("p", modifiers: [.shift, .command])
-
9:32 - Responding to modifier keys
var body: some View { LyricLine() .overlay(alignment: .top) { if showBouncingBallAlignment { // Show bouncing ball alignment guide } } .onModifierKeysChanged(mask: .option) { showBouncingBallAlignment = !$1.isEmpty } }
-
9:55 - Pointer customization
ForEach(resizeAnchors) { anchor in ResizeHandle(anchor: anchor) .pointerStyle(.frameResize(position: anchor.position)) }
-
10:23 - Pencil squeeze gesture
@Environment(\.preferredPencilSqueezeAction) var preferredAction var body: some View { LyricsEditorView() .onPencilSqueeze { phase in if preferredAction == .showContextualPalette, case let .ended(value) = phase { if let anchorPoint = value.hoverPose?.anchor { lyricDoodlePaletteAnchor = .point(anchorPoint) } lyricDoodlePalettePresented = true } }
-
13:13 - Custom containers
struct DisplayBoard<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
13:35 - Custom containers with sectioning
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") .displayBoardCardRejected(true) } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } }
-
13:52 - Entry macro
extension EnvironmentValues { @Entry var karaokePartyColor: Color = .purple } extension FocusValues { @Entry var lyricNote: String? = nil } extension Transaction { @Entry var animatePartyIcons: Bool = false } extension ContainerValues { @Entry var displayBoardCardStyle: DisplayBoardCardStyle = .bordered }
-
14:12 - Default accessibility label augmentation
SongView(song) .accessibilityElement(children: .combine) .accessibilityLabel { label in if let rating = song.rating { Text(rating) } label }
-
14:52 - Previewable
#Preview { @Previewable @State var showAllSongs = true Toggle("Show All songs", isOn: $showAllSongs) }
-
15:06 - Programatic text selection
struct LyricView: View { @State private var selection: TextSelection? var body: some View { TextField("Line \(line.number)", text: $line.text, selection: $selection) // ... } }
-
15:19 - Getting selected ranges
InspectorContent(text: line.text, ranges: selection?.ranges)
-
15:29 - Binding to search field focus state
// Binding to search field focus state struct SongSearchView: View { @FocusState private var isSearchFieldFocused: Bool @State private var searchText = "" @State private var isPresented = false var body: some View { NavigationSplitView { Text("Power Ballads") Text("Show Tunes") } detail: { // ... if !isSearchFieldFocused { Button("Find another song") { isSearchFieldFocused = true } } } .searchable(text: $searchText, isPresented: $isPresented) .searchFocused($isSearchFieldFocused) } }
-
15:41 - Text suggestions
TextField("Line \(line.number)", text: $line.text) .textInputSuggestions { ForEach(lyricCompletions) { Text($0.attributedCompletion) .textInputCompletion($0.text) } }
-
15:59 - Color mixing
Color.red.mix(with: .purple, by: 0.2) Color.red.mix(with: .purple, by: 0.5) Color.red.mix(with: .purple, by: 0.8)
-
16:13 - Custom shaders
ContentView() .task { let slimShader = ShaderLibrary.slim() try! await slimShader.compile(as: .layerEffect) }
-
16:23 - React to scroll geometry changes
struct ContentView: View { @State private var showBackButton = false ScrollView { // ... } .onScrollGeometryChange(for: Bool.self) { geometry in geometry.contentOffset.y < geometry.contentInsets.top } action: { wasScrolledToTop, isScrolledToTop in withAnimation { showBackButton = !isScrolledToTop } } }
-
16:42 - React to scroll visibility changes
struct AutoPlayingVideo: View { @State private var player: AVPlayer = makePlayer() var body: some View { VideoPlayer(player: player) .onScrollVisibilityChange(threshold: 0.2) { visible in if visible { player.play() } else { player.pause() } } } }
-
16:54 - New scroll positions
struct ContentView: View { @State private var position: ScrollPosition = .init(idType: Int.self) var body: some View { ScrollView { // ... } .scrollPosition($position) .overlay { FloatingButton("Back to Invitation") { position.scrollTo(edge: .top) } } } }
-
18:17 - Gesture interoperability
struct VideoThumbnailScrubGesture: UIGestureRecognizerRepresentable { @Binding var progress: Double func makeUIGestureRecognizer(context: Context) -> VideoThumbnailScrubGestureRecognizer { VideoThumbnailScrubGestureRecognizer() } func handleUIGestureRecognizerAction( _ recognizer: VideoThumbnailScrubGestureRecognizer, context: Context ) { progress = recognizer.progress } } struct VideoThumbnailTile: View { var body: some View { VideoThumbnail() .gesture(VideoThumbnailScrubGesture(progress: $progress)) } }
-
18:34 - SwiftUI animations in UIKit and AppKit
let animation = SwiftUI.Animation.spring(duration: 0.8) // UIKit UIView.animate(animation) { view.center = endOfBracelet } // AppKit NSAnimationContext.animate(animation) { view.center = endOfBracelet }
-
18:57 - Representable animation bridging
struct BeadBoxWrapper: UIViewRepresentable { @Binding var isOpen: Bool func updateUIView(_ box: BeadBox, context: Context) { context.animate { box.lid.center.y = isOpen ? -100 : 100 } } }
-
19:59 - Volume baseplate visibility
struct KaraokePracticeApp: App { var body: some Scene { WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScaling(.trueScale) .volumeBaseplateVisibility(.hidden) } }
-
20:15 - React to volume viewpoint changes
struct MicrophoneView: View { @State var micRotation: Rotation3D = .identity var body: some View { Model3D(named: "microphone") .onVolumeViewpointChange { _, new in micRotation = rotateToFace(new) } .rotation3DEffect(micRotation) .animation(.easeInOut, value: micRotation) } }
-
20:38 - Control allowed immersion levels
struct KaraokeApp: App { @State private var immersion: ImmersionStyle = .progressive( 0.4...1.0, initialAmount: 0.5) var body: some Scene { ImmersiveSpace(id: "Karaoke") { LoungeView() } .immersionStyle(selection: $immersion, in: immersion) } }
-
21:00 - Preferred surrounding effects
struct LoungeView: View { var body: some View { StageView() .preferredSurroundingsEffect(.colorMultiply(.purple)) } }
-
21:33 - Custom text renderers
struct KaraokeRenderer: TextRenderer { func draw( layout: Text.Layout, in context: inout GraphicsContext ) { for line in layout { for run in line { var glow = context glow.addFilter(.blur(radius: 8)) glow.addFilter(purpleColorFilter) glow.draw(run) context.draw(run) } } } } struct LyricsView: View { var body: some View { Text("A Whole View World") .textRenderer(KaraokeRenderer()) } } #Preview { LyricsView() }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.