-
SwiftUI의 새로운 기능
SwiftUI에 추가된 최신 기능을 살펴보고, 이러한 기능이 앱을 어떻게 향상할 수 있는지 알아보세요. 고성능 앱 빌드를 위한 직접 디스크 접근과 스냅샷 기반 비교 기능을 갖춘 새로운 Document 프로토콜과 리스트, 그리드 및 섹션의 콘텐츠를 리오더할 수 있는 새로운 API, 가시성 우선순위 및 자동 최소화 동작을 포함한 도구 막대 개선 사항을 소개합니다. 또한 모든 뷰에서의 쓸어넘기기 동작과 AsyncImage 캐싱 개선 사항 및 Observable 유형에 대한 지연 상태 초기화 등 확장된 프레젠테이션 API도 다룹니다.
챕터
- 0:00 - Introduction
- 2:12 - Refreshed look and feel
- 8:06 - Document-based apps
- 15:18 - Presentation and interaction
- 19:58 - Data flow and performance
- 27:25 - Next steps
리소스
- TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder
- State()
- ContentBuilder
- Swift Collections on GitHub
관련 비디오
WWDC26
-
비디오 검색…
안녕하세요, 저는 Steven이에요 UI Frameworks 팀에서 일하고 있습니다 저는 Julia입니다, 저도 UI Frameworks 엔지니어예요 SwiftUI의 새로운 기능에 대해 소개해 드릴 수 있어 기쁩니다 SwiftUI에 주요 업그레이드가 추가됐습니다 세련된 디자인과 성능 향상부터 앱과 상호작용하는 새로운 방식 강력한 새 문서 API까지 다양한 새로운 기능을 소개해 드릴 예정입니다 그런데 먼저… 저는 스티커를 좋아해요 노트북이 스티커로 가득 덮여 있거든요 저도 스티커를 정말 좋아해요 하지만 현실에서 스티커를 붙일 수 있는 장소는 한정되어 있잖아요 그래서 무한한 스티커 가능성을 열어줄 방법을 생각해냈어요 바로 앱이에요 저희 스티커 앱을 만나보세요 먼저 사진을 선택해 볼게요 Apple Park에서 Steven과 함께 찍은 이 사진이 정말 마음에 들어요 직장에 반려동물을 데려올 수 있으면 좋겠어요 분명 이 공간을 정말 좋아할 거예요 제 강아지 Pretzel 스티커를 장면에 드래그해 볼게요 강아지의 개성에 맞게 크기를 조정해 볼게요 이제 제 고양이 Kishka도 추가할게요 이 아이는 개성이 훨씬 더 크거든요 이야말로 이상적인 하루네요 이렇게 하루 종일 하고 싶지만 다룰 내용이 많으니 스크립트에 집중하는 게 중요하겠죠 저희 앱은 SwiftUI의 다양한 새로운 기능 향상을 활용하여 멋진 디자인과 뛰어난 성능을 갖춘 일류 사용자 경험을 구축했습니다 먼저 앱이 얻게 되는 아름다운 새 디자인을 안내해 드릴게요 2027 릴리즈와 크기 조정을 위한 도구 모음 콘텐츠 최적화 방법도 함께요 Julia가 새로운 API에 대해 알려드릴 거예요 앱에서 강력한 문서 기능을 활성화하는 프레젠테이션과 상호작용의 개선 사항도 포함해서요 마지막으로 앱이 원활하게 작동하도록 유지하는 방법을 알려드릴게요 성능과 데이터 흐름의 향상된 기능들을 중심으로요 2027 릴리즈에서 새로워진 앱의 디자인부터 시작해 볼게요 앱을 빌드하고 실행하면 Liquid Glass 디자인이 업데이트된 모습으로 자동으로 적용됩니다 코드를 한 줄도 변경하지 않고도 이 디자인을 얻을 수 있습니다 Liquid Glass는 세련된 디자인을 갖추고 있으며 새로운 Liquid Glass 슬라이더에 자동으로 반응해 색조를 조절합니다 macOS에서도 iOS처럼 Liquid Glass 커스텀 요소를 "대화형"으로 표시하면 사용자 클릭에 더 유연하게 반응합니다 마우스 포인터와 잘 최적화되어 있어서 Mac에서 자연스럽게 사용할 수 있습니다
Mac처럼 iPad 앱도 비활성 상태일 때 독특한 모습을 자동으로 갖추며 아이콘과 텍스트가 흐릿해져 어떤 창이 활성 상태인지 명확히 알려줍니다 여기서처럼 탭하면 저희 앱과 Files 앱 사이를 전환할 수 있어요 저희 앱에 적용된 개선 사항들이 정말 마음에 들어요 특히 앱의 디자인이 새로워진 게 더욱 좋습니다 코드 변경 없이 가능했으니까요 물론 앱의 외형을 더 세밀하게 조정하는 방법도 있습니다 사이드바의 커스텀 계정 버튼은 다른 탭 레이블과 함께 흐릿해집니다 appearsActive 환경 값을 사용해서 조건부로 버튼의 불투명도를 줄입니다 창이 비활성 상태일 때요 iPad와 Mac 메뉴 바는 이제 기본적으로 최소한의 아이콘만 표시하며 주요 동작에만 사용합니다 하지만 labelStyle titleAndIcon modifier를 Store 메뉴 항목에 적용하면 아이콘이 표시되어 눈에 띄게 할 수 있습니다 스티커가 늘어날수록 Mac과 iPad에서 앱의 크기 조정 기능이 더욱 유용하게 느껴집니다 iOS 27에서는 iPhone 앱도 크기 조정이 가능해집니다 Xcode 27에서 Live Preview에 크기 조정 핸들이 추가되어 테스트할 수 있습니다 앱이 대화형 크기 조정에 어떻게 반응하는지요 즉시 미리 볼 수 있습니다 iPhone Mirroring을 사용할 때 앱이 어떻게 동작하는지 또는 iPad에서 iPhone 앱으로 실행할 때요 이미 잘 작동하고 있어요 특히 iPad와 Mac에서 앱의 크기 조정이 잘 되도록 했기 때문입니다 SwiftUI로 만든 앱은 이런 기능을 자동으로 갖게 되지만 UIKit과 SwiftUI를 모두 사용하는 앱이라면 추가로 고려해야 할 사항이 있을 수 있습니다 화면 지오메트리를 올바르게 파악하는 방법 뷰 크기를 정할 때 기기 유형 대신 크기 클래스를 사용하는 방법 인터페이스 방향 변경에 대응하는 방법 등이죠 UIKit과 SwiftUI를 모두 사용하는 앱에서 크기 조정 준비에 대해 더 자세히 알아보려면 "Modernize your UIKit app"을 확인하세요
저희 앱에는 전체 스토어 기능도 있어서 새로운 스티커 팩을 다운로드할 수 있습니다 제가 제일 좋아하는 건 WWDC26 스티커 팩이에요 스토어 화면에는 쇼핑 카트를 포함한 몇 가지 탭이 있습니다 쇼핑 카트 탭은 화면의 하단 우측에 표시됩니다 스토어 콘텐츠가 있는 다른 탭과 구분되도록요 이런 특별한 탭 배치를 적용하려면 새로운 prominent 탭 역할을 사용하여 눈에 띄게 합니다 저희 앱의 다양한 기능을 고려할 때 도구 모음은 가장 중요한 동작에 빠르게 접근할 수 있는 좋은 방법입니다 앱에 기능을 더 추가할수록 도구 모음 항목 목록이 점점 더 늘어날 거예요 앱 크기를 조정할 때 이 점이 특히 중요합니다 앱 창 크기를 조정하면 도구 모음 항목이 시스템에 의해 자동으로 조정됩니다 공간에 맞지 않는 일부 항목은 숨겨지게 됩니다 수평 공간이 더 제한된 iPhone에서는 모든 도구 모음 버튼을 표시할 공간이 충분하지 않습니다 실행 취소, 다시 실행, 공유처럼 중요한 동작이 오버플로 메뉴에 숨겨집니다 이때 새로운 도구 모음 API가 활용됩니다 도구 모음 공간이 부족할 때 어떤 버튼을 계속 표시할지 지정할 수 있습니다 가장 중요한 ToolbarItemGroup은 편집 버튼을 포함하고 있어요 실행 취소와 다시 실행이죠 하지만 현재는 숨겨져 있습니다 시스템에 이 버튼들을 계속 표시하는 것이 중요하다는 걸 알려주고 싶습니다 새로운 visibilityPriority modifier를 추가하면 됩니다 우선순위를 high로 설정하면 됩니다 이제 실행 취소와 다시 실행 버튼이 표시됩니다
일부 동작은 오버플로 메뉴에 두는 것이 더 좋습니다 자주 사용하지 않는 동작들이에요 사진을 교체하는 버튼이나 페이지를 이미지로 내보내거나 스티커를 지우는 버튼들이죠 이 버튼들을 항상 오버플로 메뉴에 배치하려면 새로운 ToolbarOverflowMenu 컨테이너로 그룹화합니다 이제 필요할 때 메뉴에서 찾을 수 있습니다 마지막으로 공유 버튼은 절대 숨겨지지 않도록 하고 싶어요 아는 모든 분께 스티커 페이지를 보내는 걸 잊지 않으려고요 새로운 topBarPinnedTrailing 배치를 사용하면 공유 버튼이 항상 우측에 표시됩니다 이제 도구 모음이 완벽하게 설정되어 앱의 모든 중요한 기능에 접근할 수 있고 창이나 화면 크기에 관계없이 사용할 수 있습니다 보여드리고 싶은 도구 모음 개선 사항이 하나 더 있습니다 컬렉션에 스티커가 많아서 스크롤할 때 최대한 많은 공간이 확보되었으면 좋겠어요 그래서 새로운 toolbarMinimizeBehavior modifier를 추가하고 "onScrollDown"으로 설정합니다 navigationBar 배치에 적용합니다 이제 시스템이 자동으로 탐색 바를 스크롤할 때 자동으로 숨깁니다 앱의 디자인과 사용감이 훌륭하다고 생각합니다 하지만 내부적으로 더 많은 것들이 작동하고 있어요 저희 앱은 스티커 페이지를 열고 저장할 수 있으며 페이지를 이미지로 내보내는 기능도 지원합니다 이 모든 것은 Julia가 소개할 강력한 새 기능 덕분입니다 앱에는 이런 기능들이 있고 그 이상도 있습니다 새로운 SwiftUI Document API 덕분입니다 먼저 새로운 Document API의 개요를 설명해 드릴게요 그리고 앱 구축의 기반으로 어떻게 활용하는지도요 지금까지 SwiftUI는 문서 기반 앱을 지원해왔습니다 FILE_DOCUMENT 및 REFERENCE_FILE_DOCUMENT 프로토콜을 통해서요 2027 릴리즈에서는 이 기반을 확장하는 새로운 API를 소개하게 되어 기쁩니다 PixelMator PRO 같은 문서 기반 앱에 익숙하실 수도 있어요 또는 Pages나 저희가 매일 사용하는 Xcode처럼요 이런 앱들은 기본적으로 다양한 기능을 갖추고 있습니다 새 문서를 위한 Command+N 같은 키보드 단축키를 포함해서요 문서 열기를 위한 Command-O도 있고요 문서에 변경 사항이 있을 때 알려주는 편집 표시기도 있습니다 스마트 자동 저장 메커니즘과 그 밖에 더 많은 기능도요 Document API는 앱이 내부적으로 그리고 UI 측면에서도 개선할 수 있는 여러 기능을 제공합니다 이 중 세 가지를 다룰 텐데요 문서 생성 컨텍스트 디스크 읽기 및 쓰기 성능 향상 그리고 문서 URL에 직접 접근하는 일급 지원입니다 저희 앱에서 문서를 생성하는 방법을 살펴볼게요 기본적으로 앱에서는 빈 스티커 페이지로 시작할 수 있습니다 하지만 사용자가 더 빠르게 시작할 수 있도록 도와주고 싶어요 그래서 사진으로 페이지를 만드는 버튼도 추가했습니다 새로운 DocumentCreationSource API를 사용하여 blank와 photo 두 가지 소스를 선언하고 각각에 대한 NewDocumentButton을 시작 장면에 추가합니다 이 버튼 중 하나를 선택하면 SwiftUI가 소스를 문서에 전달합니다 context 매개변수를 통해 생성 클로저로요 initializer에서 컨텍스트를 확인하고 소스가 "photo"이면 문서가 열릴 때 사진 선택기가 바로 표시됩니다 이제 한 번만 탭하면 사진에 스티커를 바로 붙일 수 있습니다
문서 기반 앱은 많은 데이터를 읽고 씁니다 자주 업데이트해야 하는 복잡한 UI도 있을 수 있고요 새 API는 이런 작업을 최적화하는 훌륭한 방법을 제공합니다 앱이 원활하게 실행될 수 있도록요 앱의 body에 첫 번째 Scene으로 DocumentGroup을 선언하여 앱을 문서 아키텍처에 맞게 설정합니다 StickerDocument 클래스는 문서 유형을 설명합니다 뷰에 데이터를 제공하고 디스크에서 데이터를 읽는 방법과 다시 쓰는 방법을 설명합니다 Document API는 최신 Observation 프레임워크와 함께 작동하므로 Observable 매크로를 사용합니다 이것만으로도 성능이 향상됩니다 뷰는 의존하는 프로퍼티가 변경될 때만 업데이트됩니다 읽기와 쓰기를 최대한 빠르고 효율적으로 만드는 것이 목표입니다 새 API의 최적화 포인트를 살펴볼게요 쓰기를 위해 문서를 WritableDocument 프로토콜에 준수시킵니다 세 가지 요구 사항이 있습니다 첫째, 앱이 쓸 수 있는 형식 목록입니다 저희 앱은 사진과 스티커가 포함된 커스텀 패키지 형식을 지원합니다 둘째, 쓰기용 현재 문서 콘텐츠를 반환하는 snapshot 메서드입니다 콘텐츠를 나타내기 위해 커스텀 PageSnapshot 구조체를 사용합니다 쓰기에 필요한 모든 것이 포함되어 있습니다 배경 이미지 스티커 좌표 그리고 스티커 자체입니다 특정 시점의 문서 스냅샷으로 역할을 합니다 세 번째 요구 사항을 충족하기 위해 Writer를 제공합니다 Writer는 DocumentWriter 프로토콜을 준수하며 지정된 형식으로 문서를 디스크에 쓰는 방법을 알고 있습니다 요청된 콘텐츠 유형을 지정합니다 저희 앱에서는 "stickerDocument"입니다 DocumentWriter 프로토콜에는 Snapshot의 개념이 있습니다 PageSnapshot 유형이 여기에 딱 맞습니다 DocumentWriter의 유일한 요구 사항은 쓰기 메서드입니다 성능을 최적화할 수 있는 여러 기회를 제공합니다 첫째, write 메서드는 nonisolated이며 비동기입니다 비용이 많이 드는 디스크 쓰기 작업을 백그라운드에서 수행할 수 있어서 앱의 응답성이 유지됩니다
실제로 업데이트가 필요한 패키지 부분만 씁니다 현재 스냅샷과 이전 스냅샷을 비교하여 결정합니다 모든 최적화에도 불구하고 디스크 작업은 눈에 띄는 시간이 걸릴 수 있어서 SwiftUI는 쓰기 진행 상황을 보고할 수 있는 progress 매개변수를 제공합니다 Foundation Subprogress API를 사용해서요
문서 유형이 디스크에서 읽는 방법을 알도록 StickerDocument 클래스를 ReadableDocument 프로토콜에 준수시킵니다 ReadableDocument는 WritableDocument의 쌍둥이입니다 비교해 볼게요 각 프로토콜은 지원되는 콘텐츠 유형 목록이 필요합니다 WritableDocument는 스냅샷을 제공하고 ReadableDocument는 그것을 적용하는 방법을 알고 있습니다 WritableDocument에는 친구 프로토콜이 있습니다: DocumentWriter입니다 ReadableDocument의 친구는 DocumentReader입니다 디스크 관련 무거운 작업을 모두 처리합니다 이제 Sticker 앱이 이 페이지처럼 파일을 읽고 쓸 준비가 됐습니다 제가 꽤 그럴듯한 해적인 것 같죠
추가할 기능이 하나 더 있습니다 페이지를 이미지로 저장하는 기능이에요 앱이 없는 사람들과도 공유할 수 있도록요 Document API를 사용하면 writer를 확장하여 페이지를 다른 형식으로 저장할 수 있습니다 Core Graphics를 사용한 PNG처럼요 쓰기 가능한 콘텐츠 유형 정의로 돌아가 .PNG를 목록에 추가합니다 이제 추가 형식을 지원하기 위해 이전에 구현한 write 메서드를 다시 살펴봅니다 앱이 여러 형식을 처리할 수 있으므로 콘텐츠 유형 검사를 추가합니다 앱이 지원하는 각 유형에 대해서요 PNG의 경우 Core Graphics를 사용하여 스티커와 배경 사진을 하나의 이미지로 합치고 URL에 씁니다 더 많은 유형을 추가하려면 어떤 형식으로든 문서를 작성할 수 있습니다 어떤 프레임워크든 사용하여 다른 콘텐츠 유형을 추가하기만 하면 됩니다 이것이 저희 앱이 문서를 저장하도록 설정한 방식입니다 이제 스티커 컬렉션을 살펴볼 시간입니다 프레젠테이션과 상호작용에 관한 훌륭한 개선 사항들이 있습니다 때로는 방대한 스티커 컬렉션이 조금 혼잡하게 느껴질 수 있습니다 그때 재정렬 가능한 컨테이너 API가 도움이 됩니다 앱의 인스펙터에서 스티커 컬렉션을 두 가지 방식으로 탐색할 수 있습니다 스티커 컬렉션을 앱의 인스펙터에서요 첫 번째는 각 스티커를 이름과 함께 표시하는 "List"입니다 드래그하여 순서를 바꿔서 스티커를 정리하고 싶습니다 그래서 Reorderable API를 사용합니다
ForEach에 Reorderable modifier를 추가하고 List에 reorderContainer modifier를 추가합니다 그런 다음 클로저에서 제가 작성한 헬퍼 함수 difference.apply를 호출하여 스티커 배열을 업데이트합니다 내부적으로 apply 함수는 오픈 소스 swift-collections 패키지를 사용하여 순서 변경 사항을 적용합니다 자세한 내용은 swift.org를 방문하세요
SwiftUI가 드래그 상호작용과 애니메이션을 자동으로 처리합니다 그리드로 스티커를 정리할 수도 있어서 한 번에 더 많은 스티커를 쉽게 탐색할 수 있습니다 Reorderable API는 List만이 아니라 모든 컨테이너에서 작동합니다 즉, 기존 코드를 목록에서 가져와서 LazyVGrid에 재사용할 수 있습니다 재정렬 코드는 완전히 동일하게 유지됩니다 동일한 대화형 재정렬 동작을 완전히 다른 컨테이너에서도 동일한 코드로 구현할 수 있습니다 이제 이 API는 처음으로 watchOS에도 재정렬 기능을 제공합니다
재정렬 기능은 아직 시작에 불과합니다 재정렬 가능한 컨테이너의 모든 기능에 대해 자세히 알아보려면 코드 실습 세션을 확인하세요 "Build powerful drag and drop in SwiftUI"
이동 중에는 iPhone에서 스티커 페이지를 꾸미는 걸 좋아합니다 UI 하단의 시트에서 필요한 스티커를 바로 찾을 수 있습니다 목록에서 스티커를 제거하기 위한 스와이프 동작도 설정했습니다 목록 항목에 swipeActions modifier와 Delete 버튼을 추가하면 됩니다
목록을 더 자유롭게 커스터마이즈하고 싶어서 LazyVStack으로 전환하기로 했습니다 이제 SwiftUI는 List만이 아니라 모든 뷰에서 스와이프 동작을 지원합니다 ForEach를 List에서 LazyVStack으로 옮기고 업데이트된 항목 스타일을 적용합니다 그리고 swipeActionsContainer modifier를 추가합니다 이 스크롤 뷰의 항목들 전체에서 스와이프 동작을 조정합니다
앞서 말했듯이 저는 스티커를 정말 좋아해요 그래서 사진을 꾸미기 시작하면 솔직히 말씀드리면 조금 과해지는 경향이 있어요 그러다 보면 공간을 만들기 위해 스티커를 삭제해야 할 때도 있습니다 다 마음에 들어도 말이죠
사진에 배치된 각 스티커에 컨텍스트 메뉴를 추가했습니다 Delete 버튼으로 빠르게 삭제할 수 있도록요
버튼을 탭하면 stickerToDelete State 변수가 현재 스티커로 설정됩니다
confirmationDialog modifier도 추가했습니다 이제 확인 다이얼로그는 시트에서 사용하는 항목 바인딩 패턴을 지원합니다 stickerToDelete 바인딩을 modifier에 전달합니다 Delete 버튼을 탭하여 stickerToDelete에 값을 설정하면 확인 다이얼로그가 나타납니다 이는 alert에서도 작동합니다
프레젠테이션과 관련된 훌륭한 개선 사항들을 소개해 드렸는데 상호작용도 있습니다, 이게 전부가 아니에요 2027 릴리즈에서 SwiftUI에는 더 많은 개선 사항이 있습니다 이런 개선 사항 중 많은 부분이 이미 사용 중인 API를 더 좋게 만들어 줍니다 앱을 변경하지 않아도요 훌륭한 성능은 앱이 반응적이고 세련되게 느껴지도록 하는 데 중요합니다 데이터가 앱을 통해 흐르는 방식에 신중하게 접근하는 것이 가장 좋은 방법 중 하나입니다 앱의 성능을 최상의 상태로 유지하기 위한요 Steven이 데이터 흐름과 성능의 주요 개선 사항을 알려드릴 거예요 감사합니다, Julia 앱에 추가한 스티커 스토어가 정말 멋집니다 스티커 팩을 다운로드할 수 있어서 좋아요 덕분에 AsyncImage의 개선 사항을 활용할 수 있거든요 AsyncImage는 인터넷에서 이미지 에셋을 로드하는 훌륭한 방법입니다 화면에 나타날 때 로드되죠 스크롤하면서 더 보여줄 때 잘 작동합니다 Kishka와 Pretzel의 귀여운 스티커들이 더 많이 나타나거든요 하지만 지금까지 AsyncImage는 이미지를 메모리에 유지하지 않았습니다 다시 위로 스크롤하면 화면에 다시 나타날 때 재로드되었죠 위로 스크롤할 때 이미지가 즉시 표시되었으면 합니다 맨 위로 스크롤할 때요 2027 릴리즈에서는 그렇게 됩니다 AsyncImage는 이제 표준 HTTP 캐싱을 지원하여 이미지가 기본적으로 캐시됩니다 서버의 캐시 헤더를 따르며 코드 변경 없이 작동합니다 모든 앱에서 자동으로 활성화됩니다 Xcode 27로 만든 앱은 새로운 API를 활용하여 다운로드 방식을 커스터마이즈할 수 있습니다 다운로드 방식을요 이미지 다운로드 방식을 더 세밀하게 제어하려면 직접 URLRequest를 만들어 AsyncImage에 전달합니다 이를 통해 요청별로 다양한 커스터마이즈가 가능합니다 캐시 정책을 지정하는 것처럼요 더 큰 캐시처럼 오래 지속되는 설정이 필요하다면 커스텀 URLSession을 직접 만들어서 필요한 용량으로 URLCache를 설정할 수 있습니다 그런 다음 asyncImageURLSession modifier에 세션을 전달하여 사용합니다
이제 맨 위로 스크롤하면 이미지가 자동으로 로드됩니다 캐시에서요
SwiftUI는 앱이 데이터를 모델링하고 저장하는 다양한 방법을 제공합니다 그리고 그 데이터를 뷰에 전달하는 방법도요 앱 데이터를 저장하는 좋은 방법은 Observable 클래스를 사용하는 것입니다 여기서는 StickerStore 클래스로 그렇게 하고 있습니다 StickerStoreView가 초기화되면 StickerStore 클래스의 새 인스턴스가 만들어지고 State 변수에 할당됩니다 이 인스턴스는 뷰의 수명 동안 유지됩니다 하지만 부모 뷰가 업데이트되어 StickerStoreView가 다시 초기화되면 어떻게 될까요 이전 릴리즈에서는 초기화할 때마다 StickerStore의 새 인스턴스가 만들어졌습니다 하지만 원래 인스턴스가 여전히 State 변수에 저장되어 있습니다 그래서 새 인스턴스는 그냥 버려졌습니다 뷰가 다시 초기화될 때마다 이런 일이 반복됩니다 클래스의 주 저장 인스턴스가 안정적으로 유지되고 있는데도요
2027 릴리즈에서는 처음으로 State 프로퍼티를 사용하여 초기화되고 저장되는 클래스가 이제 lazy가 되었습니다 한 번만 초기화된다는 의미입니다 이는 State가 변환된 덕분입니다 Dynamic Property에서 매크로로요
이제 StickerStoreView가 처음 초기화될 때 StickerStore 클래스의 새 인스턴스가 이전과 같이 만들어집니다 하지만 이후 초기화에서는 새 클래스 인스턴스가 만들어지지 않습니다 이 동작은 하위 호환되어 릴리즈에 백포트되었습니다 @Observable이 처음 도입된 릴리즈까지요 iOS 17, macOS 14 및 이에 맞는 릴리즈부터 시작합니다
일부 경우에는 State 매크로 도입이 소스 변경을 일으킬 수 있습니다 예를 들어 @State 변수에 기본값을 지정하고 init 내에서 동일한 @State 변수에 값을 할당하면 Xcode가 초기화 전 사용에 대한 오류를 표시합니다 이 오류를 해결하려면 불필요한 기본값 할당을 제거하세요 State 매크로가 코드에 미치는 영향에 대한 자세한 내용은 문서를 확인하세요 좋은 런타임 성능을 유지하는 것은 앱이 원활하게 작동하는 데 중요합니다 하지만 또 다른 종류의 성능도 있습니다 앱 개발 경험에 큰 영향을 줄 수 있는 성능이에요 앱에 복잡하고 깊이 중첩된 뷰가 있다면 이 오류를 만났을 수도 있습니다 "컴파일러가 합리적인 시간 내에 이 표현식을 타입 검사할 수 없습니다" 왜 이런 일이 발생할까요 이 뷰에는 Section, Group, ForEach가 콘텐츠를 감싸고 있습니다 이 표현식을 타입 검사하려면 먼저 컴파일러가 사용할 Section 오버로드를 선택해야 합니다 Section은 View를 생성하는 빌더로 초기화할 수 있습니다 또는 TableRowContent로도요 어느 것을 사용할지 알려면 컴파일러가 두 옵션을 모두 시도해야 합니다 제 코드에서 Section 빌더는 Group을 반환합니다 컴파일러는 Section이 생성하는 콘텐츠 유형을 알 수 없습니다 중첩된 Group의 콘텐츠 유형을 파악하기 전까지는요 이번에는 옵션이 더 많습니다 중첩된 ForEach에 대해서도 컴파일러가 각각을 시도해야 합니다 그리고 ForEach 빌더도 자체 옵션이 있습니다 이것들도 검사해야 합니다 아직 콘텐츠에도 도달하지 못했습니다 이런 경로들을 모두 시도하면 타입 검사 비용이 점점 커집니다 하지만 제 코드의 Section, Group, ForEach는 뷰를 만들고 있다는 걸 압니다 그래서 이 결정 트리에서 실제로 유효한 경로는 하나뿐입니다 이렇게 복잡한 선택 대신 빌더들이 생성 유형에 제약받지 않고 생성 유형에 제약받지 않는다면 대신 콘텐츠를 조합하기만 한다면요 2027 릴리즈에서는 바로 그것이 SwiftUI가 하기 시작하는 일입니다 가장 일반적인 빌더 집합이 이제 단일 initializer를 공유하여 단 하나의 명확한 경로만 남깁니다 이것은 여러 다른 빌더 유형이 하나로 통합되었기 때문에 가능합니다 단일 빌더 ContentBuilder로요 SwiftUI API 전반에 걸쳐 통합된 빌더를 지향하는 한 걸음입니다 ContentBuilder는 모든 최소 배포 타겟에서 사용할 수 있습니다 내부적으로는 기존 ViewBuilder의 진화이기 때문입니다 ContentBuilder는 Xcode 27로 빌드할 때 SwiftUI의 타입 검사 성능을 크게 향상시킵니다 2027 릴리즈를 타겟으로 하든 이전 릴리즈를 타겟으로 하든 마찬가지입니다 Xcode 27에 포함된 새로운 에이전트 스킬도 소개하게 되어 기쁩니다 2027 릴리즈의 새로운 기능을 앱에 도입하는 데 도움이 되며 앱의 성능과 코드 정확성을 향상시킬 수 있습니다 SwiftUI Specialist Skill은 앱에서 SwiftUI 모범 사례를 따르는 데 도움을 줍니다 What's New In SwiftUI Skill은 새로운 API 도입을 안내합니다 2027 릴리즈의 API요 이 두 스킬은 Xcode 27의 Coding Assistant에서 접근할 수 있습니다 다른 도구에서 이 스킬을 사용하려면 내보낼 수 있습니다 "xcrun agent skills export" 명령으로요 워크플로에 가져올 수 있는 마크다운 파일이 생성됩니다 SwiftUI의 흥미로운 새 개선 사항들을 살펴봤습니다 이제 여러분 차례입니다 2027 릴리즈를 위해 Xcode 27에서 프로젝트를 빌드하는 것부터 시작하세요 그런 다음 앱의 업데이트된 디자인을 확인하세요 문서 기반 앱이 있다면 새로운 Document API가 어떻게 개선할 수 있는지 살펴보세요 앱을 더 좋게 만들 수 있는지요 Xcode 27에서 SwiftUI 에이전트 스킬을 사용해 보세요 새로운 API와 모범 사례를 도입하기 위해서요 아쉽지만 일정에 맞춰 마무리해야 할 것 같습니다 앱에 이 개선 사항들을 적용하면서 저희만큼 즐거우시길 바랍니다 저희도 즐거웠거든요 끝까지 함께해 주셔서 감사합니다
-
-
3:20 - appearsActive environment value
struct SidebarFooterView: View { @Environment(\.appearsActive) private var appearsActive var body: some View { MyAccountView() .opacity(appearsActive ? 1 : 0.5) } } -
3:34 - Menu icon visibility
CommandMenu("Stickers") { Button { openStore() } label: { Label("Store", systemImage: "bag.fill") .labelStyle(.titleAndIcon) } } // Other menu items } -
5:12 - Prominent tab role
TabView { Tab { EventsTab() } Tab { HolidaysTab() } Tab { FunTab() } Tab(role: .prominent) { CartTab() } } -
6:15 - Toolbar item visibility and overflow menu
// Toolbar item visibility priority StickerPageView() .toolbar { ToolbarItemGroup { UndoButton() RedoButton() } .visibilityPriority(.high) ToolbarOverflowMenu { ChoosePhotoButton() ExportAsImageButton() ClearAllStickersButton() } ToolbarItem(placement: .topBarPinnedTrailing) { ShareButton() } } -
7:37 - Minimize toolbar on scroll with toolbarMinimizeBehavior
// Minimize toolbar when scrolling ScrollView { StickerListView() } .toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar) -
9:47 - Document creation sources with context parameter
// Use the context to create a document @main struct Stickers: App { var body: some Scene { DocumentGroupLaunchScene("Create a Sticker Page") { NewDocumentButton("New Sticker Page", source: .blank) NewDocumentButton("Sticker Page from Photo…", source: .photo) } DocumentGroup { /* ... */ } } } extension DocumentCreationSource { static let blank = Self(id: "blank") static let photo = Self(id: "photo") } -
10:01 - Use the context to create a document
@main struct Stickers: App { var body: some Scene { DocumentGroupLaunchScene("Create a Sticker Page") { NewDocumentButton("New Sticker Page", source: .blank) NewDocumentButton("Sticker Page from Photo…", source: .photo) } DocumentGroup { document in StickerPageDocumentView(document) } { configuration, context in StickerPageDocument(configuration: configuration, context: context) } } } -
10:43 - Document app declaration
@main struct Stickers: App { var body: some Scene { DocumentGroup { /* ... */ } WindowGroup { /* ... */ } } } -
11:25 - Implement document writing
@Observable final class StickerDocument { // ... } -
11:34 - Implement document writing: list writable formats
@Observable final class StickerDocument { static let writableDocumentTypes: [UTType] = [.stickerDocument] // ... } import UniformTypeIdentifiers extension UTType { static let stickerDocument = UTType(exportedAs: "stickerdocument") } -
11:45 - Implement document writing: provide snapshot
@Observable final class StickerDocument { static let writableDocumentTypes: [UTType] = [.stickerDocument] @MainActor func snapshot(contentType: UTType) async throws -> sending PageSnapshot { /* ... */ } // ... } -
11:54 - Implement document writing: represent the snapshot
struct PageSnapshot { var background: Image var metadata: StickerPlacements var stickers: [Image] } struct StickerPlacements { /* ... */ } -
12:13 - Implement document writing: provide a DocumentWriter
@Observable final class StickerDocument { static let writableDocumentTypes: [UTType] = [.stickerDocument] @MainActor func snapshot(contentType: UTType) async throws -> sending PageSnapshot { makeSnapshot() } func writer(configuration: sending WriteConfiguration) -> sending Writer { Writer(contentType: configuration.contentType) } } -
12:33 - DocumentWriter: Snapshot
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot // ... } -
12:36 - DocumentWriter: PageSnapshot as Snapshot
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot let contentType: UTType // ... } -
12:42 - DocumentWriter protocol implementation
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot let contentType: UTType nonisolated func write( snapshot: sending PageSnapshot, to destination: URL, previous: sending PageSnapshot?, progress: consuming Subprogress ) async throws { // write .stickerDocument } } -
13:18 - Progress reporting during writing
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot let contentType: UTType nonisolated func write( snapshot: sending PageSnapshot, to destination: URL, previous: sending PageSnapshot?, progress: consuming Subprogress ) async throws { // report progress… // write .stickerDocument } } -
13:27 - Implement document reading with ReadableDocument protocol
extension StickerDocument: ReadableDocument { } -
14:35 - Add PNG to supported formats list
@Observable final class StickerDocument: WritableDocument { static let writableContentTypes: [UTType] = [.stickerDocument, .png] } -
14:48 - Add content type checks
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot let contentType: UTType nonisolated func write( snapshot: sending PageSnapshot, to destination: URL, previous: sending PageSnapshot?, progress: consuming Subprogress ) async throws { if contentType.conforms(to: .stickerDocument) { // write .stickerDocument } else if contentType.conforms(to: .png) } } -
14:56 - Writing multiple formats including PNG
struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot let contentType: UTType nonisolated func write( snapshot: sending PageSnapshot, to destination: URL, previous: sending PageSnapshot?, progress: consuming Subprogress ) async throws { if contentType.conforms(to: .stickerDocument) { // write .stickerDocument } else if contentType.conforms(to: .png) { let context = CGContext(/* ... */) context.draw(/* ... */) } } } -
15:58 - Reorderable list with reorderContainer
List { ForEach(stickers) { sticker in StickerListItemView(sticker: sticker) } .reorderable() } .reorderContainer(for: Sticker.self) { difference in difference.apply(to: &stickers) } -
16:14 - Apply changes to a reorderable list's data source
import OrderedCollections // from https://github.com/apple/swift-collections extension ReorderDifference where CollectionID == ReorderableSingleCollectionIdentifier { func apply(to values: inout [some Identifiable<ItemID>]) { var dictionary = OrderedDictionary(uniqueKeys: values.map { $0.id }, values: values) let destinationOffset: Int? = switch destination.position { case .before(let destination): dictionary.keys.firstIndex(of: destination) case .end: nil } dictionary.move(keys: sources, to: destinationOffset ?? values.endIndex) values = dictionary.values.elements } } -
16:48 - Reorderable grid with LazyVGrid
LazyVGrid { ForEach(stickers) { sticker in StickerListItemView(sticker: sticker) } .reorderable() } .reorderContainer(for: Sticker.self) { difference in difference.apply(to: &stickers) } -
18:12 - Swipe actions on List
List { ForEach(stickers) { sticker in StickerListItemView(sticker: sticker) .swipeActions { DeleteButton(sticker: sticker) } } } -
18:15 - Swipe actions on any view
ScrollView { LazyVStack { ForEach(stickers) { sticker in StickerListItemView(sticker: sticker) .swipeActions { DeleteButton(sticker: sticker) } } } } .swipeActionsContainer() -
18:54 - Confirmation dialog with item binding
struct StickerCanvasView: View { var stickers: [Sticker] @State private var stickerToDelete: Sticker? var body: some View { ZStack { ForEach(stickers) { sticker in PlacedStickerView(sticker: sticker) .contextMenu { // ... } } } .confirmationDialog( "Delete?", item: $stickerToDelete ) { sticker in DeleteStickerButton(sticker) } } } -
19:35 - Alert with item binding
struct StickerCanvasView: View { var stickers: [Sticker] @State private var stickerToDelete: Sticker? var body: some View { ZStack { ForEach(stickers) { sticker in PlacedStickerView(sticker: sticker) .contextMenu { // ... } } } .alert( "Delete?", item: $stickerToDelete ) { sticker in DeleteStickerButton(sticker) } } } -
21:18 - AsyncImage with URLRequest and custom URLSession
@Observable class StickerStore { static let imageSession: URLSession = { let config = URLSessionConfiguration.default config.urlCache = URLCache( memoryCapacity: 64 * 1024 * 1024, diskCapacity: 256 * 1024 * 1024) return URLSession(configuration: config) }() } ForEach(pets) { pet in AsyncImage(request: URLRequest( url: pet.imageURL, cachePolicy: .returnCacheDataElseLoad) ) } .asyncImageURLSession(StickerStore.imageSession) -
23:08 - @State converted to macro for lazy initialization
@Observable class StickerStore { } struct StickerStoreView: View { // store is now lazily initialized, only // created once for the lifetime of the view @State private var store = StickerStore() var body: some View { // ... } } -
23:48 - @State macro init assignment error
struct StickerPageView: View { @State private var page = StickerPage() let title: String init(title: String) { self.page = StickerPage(title: title) // Variable 'self.title' used before being initialized self.title = title } var body: some View { // ... } } -
24:02 - Fixed @State macro init assignment error
struct StickerPageView: View { @State private var page: StickerPage // Removed default value to fix error let title: String init(title: String) { self.page = StickerPage(title: title) self.title = title } var body: some View { // ... } } -
26:07 - @ContentBuilder
@ContentBuilder func stickerLibraryView() -> some View { // ... }
-
-
- 0:00 - Introduction
This video covers the refreshed Liquid Glass look and feel, new document-based app APIs, presentation and interaction improvements, and data flow and performance enhancements.
- 2:12 - Refreshed look and feel
Apps automatically adopt the updated Liquid Glass appearance on 2027 OS releases without code changes. Covers interactive Liquid Glass elements, the inactive window appearance on iPadOS, toolbar customization with overflow menus and pinned placements, minimize-on-scroll behavior, and guidance for building resizable apps using size classes.
- 8:06 - Document-based apps
Expanded document APIs for SwiftUI apps, including the new DocumentCreationSource API for custom new-document flows, performance improvements for reading and writing large documents, and first-class support for direct document URL access via the FileDocument and ReferenceFileDocument protocols.
- 15:18 - Presentation and interaction
New reorderable container APIs let users drag to reorder items in any container — List, LazyVGrid, and on watchOS for the first time. Also covers swipe actions on arbitrary views beyond List, and item binding-driven confirmation dialogs.
- 19:58 - Data flow and performance
AsyncImage now supports standard HTTP caching by default, with new APIs for custom URLRequest and URLSession configurations. The @State property wrapper is converted to a macro, making class initialization lazy to prevent redundant allocations — back-ported to iOS 17, macOS 14, and aligned releases.
- 27:25 - Next steps
Key takeaways and recommended next steps: build in Xcode 27 to see the updated Liquid Glass look in your app, adopt the new Document APIs for document-based apps, and explore the SwiftUI agent skills.