스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI의 새로운 기능
SwiftUI 사용법을 배워 모든 Apple 플랫폼을 위한 훌륭한 앱을 만드세요. SwiftUI의 최신 업데이트를 탐구하고 visionOS의 새로운 장면 타입을 발견하세요. 최신 데이터 흐름 기능으로 데이터 모델을 단순화하고, 인스펙터 뷰에 관해 배워 보세요. 개선된 애니메이션 API, 강력한 ScrollView 개선 사항, 깔끔한 테이블을 만들 수 있도록 향상된 기능, 집중 및 키보드 입력의 개선 등 많은 주제를 다룹니다.
챕터
- 1:05 - SwiftUI in more places
- 10:21 - Simplified data flow
- 18:46 - Extraordinary animations
- 27:18 - Enhanced interactions
리소스
관련 비디오
WWDC23
- 공간 컴퓨팅을 위한 SwiftUI
- 스크롤 뷰 너머
- 스프링 애니메이션 만들기
- 앱의 기호에 애니메이션 적용하기
- Swift Charts에서 파이 그래프와 상호 교환성 탐색하기
- Swift의 새로운 기능
- SwiftData 만나보기
- SwiftData로 앱 만들기
- SwiftUI 애니메이션 살펴보기
- SwiftUI 초점 요리책
- SwiftUI에서 고급 애니메이션 사용하기
- SwiftUI와 UIKit으로 접근성 높은 앱 구축하기
- SwiftUI용 MapKit 알아보기
- SwiftUI용 StoreKit 알아보기
- SwiftUI의 인스펙터: 디테일 발견하기
- SwiftUI의 Observation 알아보기
- watchOS용 앱 디자인하고 빌드하기
- Xcode Previews로 프로그램적인 UI 구축하기
-
다운로드
♪ 부드러운 힙합 연주곡 ♪ ♪ 안녕하세요 시청해 주셔서 감사합니다 저는 SwiftUI 팀의 엔지니어 Curt입니다 저는 Jeff입니다 역시 SwiftUI 팀의 엔지니어죠 SwiftUI의 새로운 기능을 소개하게 되어 기쁩니다 이제 SwiftUI를 더 많은 곳에 사용할 수 있으며 새로운 플랫폼이 추가됐죠 새로운 데이터 흐름을 통해 도메인 모델링을 단순화하여 이전보다 강력한 기능을 제공합니다 Inspector와 테이블의 개선 사항을 통해 데이터를 표시하는 멋진 방법을 제공하죠 애니메이션 API를 강화하여 여러분의 앱을 사용하는 사람들에게 더욱 아름다운 경험을 제공할 수 있습니다 프레임워크 전반에 걸쳐 멋진 상호작용을 제공할 수 있는 능력을 향상하기 위해 강력한 스크롤 뷰 개선 사항과 집중 및 키보드 입력의 개선 사항 및 버튼 및 메뉴와 같은 컨트롤의 커스텀 제작을 강화했습니다 SwiftUI를 사용할 수 있는 새로운 공간에 대해 빨리 말씀드리고 싶군요 헤드셋과 watchOS 10에서 새로운 위젯과 교차 프레임워크 통합까지 SwiftUI를 통해 여러분의 앱 사용자에게 즐거운 경험을 선사하도록 도와줄 겁니다 공간 컴퓨팅을 통해 SwiftUI가 선보이는 새 기능은 볼륨과 같은 새로운 3D 기능과 몰입형 공간을 통한 풍부한 경험 새로운 3D 제스처 효과와 레이아웃과 RealityKit과의 통합이 있습니다 제어 센터, 홈뷰와 같은 핵심적인 메뉴는 물론 익숙한 앱인 TV와 Safari Freeform과 Keynote의 몰입형 리허설과 같은 새로운 환경에 적용됐죠 SwiftUI는 이러한 사용자 경험의 중심에 있습니다 새로운 플랫폼에서 윈도우를 구성할 때 WindowGroup처럼 익숙한 장면 타입을 사용하죠 WindowGroup 장면은 2D 윈도우로 렌더링되며 깊이감을 구분하는 3D 컨트롤을 제공합니다 윈도우 안에서 익숙한 SwiftUI 컨테이너인 NavigationSplitView 또는 TabView를 선택하세요 컨테이너 안에서 다른 플랫폼과 마찬가지로 익숙한 SwiftUI 컨트롤을 모두 사용할 수 있죠 깊이감을 추가하고 싶으면 장면에 볼륨 스타일을 적용하세요 볼륨은 보드게임이나 건축 모형과 같은 3D 경험을 제한된 공간에서 제공합니다 다른 앱과 함께 표시되죠 여러분의 콘텐츠를 사용하며 메모 앱에 생각을 적고 Keynote 슬라이드를 작업할 수 있습니다 Model3D를 이용하여 볼륨에 고정 모형을 채우세요 조명 효과가 들어간 동적으로 상호작용하는 모형은 새로운 RealityView를 사용하세요 완벽한 몰입을 원한다면 앱에 ImmersiveSpaces를 추가하세요 새로운 ImmersiveSpace 장면은 몰입형 공간 경험을 정의할 수 있으며 주변 환경에 포함시키거나 완벽하게 에워쌀 수 있습니다 시스템이 다른 앱을 숨기고 여러분이 만든 세상에 들어갈 수 있도록 하죠 ImmersiveSpace를 혼합형 몰입 스타일과 사용하여 여러분의 앱을 현실 세계와 연결하고 사람들의 주변 환경과 여러분의 콘텐츠를 결합하세요 앱의 요소를 책상과 표면에 고정하고 현실 세계를 가상 오브젝트와 효과로 보완하고 풍성하게 만드세요 완벽한 몰입 스타일로 더 나아갈 수 있습니다 여러분의 앱이 완벽하게 제어하죠 Model3D와 RealityView를 이용하여 볼륨에서 작동하는 연결형 몰입 경험을 구축하기 바랍니다 새로운 플랫폼의 SwiftUI가 마법 같은 경험을 만들게 해 주죠 '공간 컴퓨팅을 위한 SwiftUI'를 통해 멋진 조합에 관해 더 알아보세요 SwiftUI로 공간을 채우는 경험을 제공할 수 있지만 Apple에서 가장 휴대성이 좋은 디스플레이에 대한 경험을 구축할 수 있죠 watchOS 10은 사용자 경험을 다시 설계하여 중요한 정보를 표시하고 한눈에 집중 콘텐츠를 전달하며 디스플레이의 형태와 정확도를 활용합니다 플랫폼 전반에 걸쳐 앱을 업데이트하여 아름다운 전체 화면 색상과 이미지를 활용했죠 새로운 디자인의 기반에는 watchOS 10을 위해 새롭게 강화된 SwiftUI 뷰가 있습니다 NavigationSplitView와 NavigationStack이 새롭게 변화하죠 TabView는 수직형 페이징 스타일이 추가되며 Digital Crown으로 조작할 수 있습니다 SwiftUI에 새로운 API를 추가하여 Apple Watch 앱에도 다채로운 기능을 사용할 수 있죠 새로운 containerBackground 모디파이어를 통해 미묘한 배경의 변화를 설정할 수 있어 콘텐츠를 눌렀을 때 움직입니다 watchOS에서 배경의 탭 뷰를 설정할 수도 있죠 새로운 멀티플랫폼 툴바 배치 모드인 topBarLeading과 topBarTrailing이 기존의 bottomBar에 추가되어 Apple Watch 앱에 작은 상세 윈도우를 완벽하게 배치할 수 있습니다 새로운 추가 기능 외에도 기존의 API를 처음으로 watchOS에 적용했는데 목록의 DatePicker와 selection이죠 여러분의 Apple Watch 앱을 새로운 기능으로 다듬을 수 있는 기회입니다 아직 Apple Watch 앱을 만들지 않았다면 시작하기에 좋은 때죠 이러한 경험을 설계하고 개발하는 방법을 배우고 싶다면 'watchOS 10을 위한 앱 설계 및 구축'을 시청하세요 아이디어를 작업에 적용하려면 'watchOS 10에 맞춰 앱 업데이트하기'를 보세요 watchOS 10의 스마트 스택 위젯은 앱을 이용하는 사람들이 한눈에 정보를 볼 수 있게 하죠 SwiftUI는 새로운 곳에 위젯이 나타날 수 있도록 핵심적인 역할을 하죠 iPadOS 17의 잠금 화면에 나타나는 위젯은 홈 화면의 위젯을 훌륭하게 보완합니다 iPhone의 화면 상시표시의 대기 모드에서 크고 선명한 위젯이 빛을 발하죠 macOS Sonoma의 데스크톱 위젯은 사람들이 매일 정보를 알 수 있게 해 줍니다 위젯이 새로운 곳에 자리를 잡으면서 팀에서 새로운 기능을 추가했는데요 이제 위젯이 상호작용 컨트롤을 지원한다는 소식을 전하게 되어 기쁩니다 위젯의 토글과 버튼은 App Intents를 사용하여 여러분의 앱 번들에 정의된 코드를 활성화할 수 있죠 또한 SwiftUI의 전환 및 애니메이션 모디파이어로 위젯에 움직임을 줄 수 있습니다 새로운 기능에 관해 자세히 알고 싶은 분은 '새로운 곳에 적용하는 위젯'과 '생동감 있는 위젯'을 확인하세요 상호작용하고 움직이는 위젯을 개발하고 다듬을 계획이라면 Xcode 미리보기가 큰 도움이 될 겁니다 미리보기는 Swift 5.9의 매크로를 이용하여 우아하고 새로운 구문을 제공하죠 미리보기를 선언 및 구성한 뒤 위젯 타입을 추가하고 테스트를 위한 타임라인을 정의하세요 Xcode Preview가 현재 위젯 상태를 보여 주고 타임라인을 통해 상태 사이의 움직임을 보여 줍니다 새로운 미리보기는 일반 SwiftUI 뷰는 물론 앱과 호환되죠 Mac 앱의 미리보기도 Xcode 안에서 조작할 수 있습니다 다른 세션인 'Xcode 미리보기로 프로그래밍적 UI 구축하기'를 통해 새로운 도구를 활용하는 법을 배우고 앱과 위젯 개발을 더 빠르게 진행하세요 미리보기를 지원하는 매크로를 포함하여 Swift 5.9의 개선 사항이 아주 많습니다 Swift 신규 사항의 개요가 궁금하시다면 'Swift의 새로운 기능'을 확인하세요 SwiftUI가 새로워진 또 다른 방식은 다른 Apple 프레임워크에 활용할 수 있는 Swift 전용 익스텐션입니다 여러 가지 프레임워크가 새롭거나 개선된 기능을 지원하며 특히 더 흥미로운 일부 기능을 소개하도록 하죠 MapKit은 엄청난 양의 업데이트를 통해 Apple의 놀라운 매핑 프레임워크의 기능을 SwiftUI 코드에 적용했습니다 SwiftUI와 MapKit을 임포트하여 훌륭한 기능을 사용하세요 뷰에 지도를 띄우고 커스텀 마커, 폴리라인과 사용자의 위치를 추가하세요 사용 가능한 컨트롤을 설정하세요 놀라운 지도를 SwiftUI 앱에 추가하는 법을 배우려면 'SwiftUI를 위한 MapKit 알아보기'를 시청하세요 두 번째 시즌을 맞은 Swift Charts에 놀라운 개선 사항이 추가됐으며 스크롤링 차트와 선택에 대한 자체 지원 그리고 모두 기다려 왔던 기능인 도넛과 파이 차트가 SectorMark와 함께 추가됐습니다 새로운 기능이 궁금하신 분은 'Swift Charts의 파이 차트와 상호작용성 탐구'를 시청하세요 충성 고객을 유치하고 유지하는 경험을 쌓고 싶다면 새로운 인앱 구매와 구독 상점의 용이함과 강력함에 빠져들 겁니다 맞춤형 마케팅 콘텐츠로 구독 상점 뷰를 제공하세요 브랜드에 어울리는 전면 배경을 설정하고 다양한 컨트롤 중에 선택하세요 'SwiftUI를 위한 StoreKit'을 시청하여 인앱 마케팅 경쟁력을 키우세요 새로운 플랫폼과 위젯부터 교차 프레임워크 통합과 watchOS의 기능을 활용한 SwiftUI가 Apple 개발자 경험을 더욱 개선했습니다 SwiftUI가 적용되는 새 플랫폼들을 보니 마음이 들뜨는군요 물론이죠 Apple의 모든 플랫폼에 적용되는 개선 사항도 많아요 맞습니다 이런 개선 사항을 활용한 앱을 개발할까요? 좋아요 제 아이디어에 대해 더 생각해 봤어요? - 개에 관한 거요? - 네 들새 관찰과 비슷한데 대상이 개예요 사람들이 정말 개 관찰 앱을 원할까요? 물론이죠, 투자자 설명 자료도 술술 쓸 수 있어요 100만 달러의 아이디어를 생각해 냈으니 앱 개발을 시작해야겠군요 좋은 앱은 좋은 데이터 모델에서 비롯되니까 우리의 앱 데이터에 적용할 수 있는 SwiftUI의 새로운 기능을 알아보도록 하죠 SwiftUI가 좋은 점 중 하나는 제 UI를 앱 상태의 기능으로 정의할 수 있다는 거예요 최고의 업그레이드 사항인 SwiftUI로 모델 종류를 정의하는 방법을 공유하죠 새로운 Observable 매크로입니다 익숙한 SwiftUI 패턴을 데이터 흐름으로 사용하게 해 주는 Observable 모델은 코드를 더 간결하고 성능에 맞춰 작성합니다 제가 밖에서 만난 개를 나타내는 데이터를 저장하기 위해 설정한 모델 클래스입니다 이 타입을 Observable로 만들기 위해 매크로를 추가할게요 그것만 하면 됩니다 ObservableObject와 달리 프로퍼티를 Published로 지정할 필요가 없죠 Observable 모델은 기존 SwiftUI 메커니즘에 데이터 흐름으로 쉽게 통합됩니다 제 DogCard 뷰를 예로 들어 볼게요 View에서 Observable을 사용하면 SwiftUI가 자동으로 여러분이 읽는 프로퍼티들과 종속 관계를 확립합니다 읽을 때 프로퍼티 래퍼를 사용할 필요가 없어 View 코드가 깔끔하죠 이 View는 isFavorite 속성을 읽고 있는데 그게 변하는 경우 재평가됩니다 읽은 속성에 대해서만 무효화가 발생하기 때문에 중간의 뷰를 통해 모델을 통과시키면서 불필요한 업데이트를 트리거하지 않죠 SwiftUI는 현재 상태와 뷰와의 관계를 정의하는 여러 도구를 포함하며 그중 몇 개는 ObservableObject와 함께 사용하도록 설계돼 있습니다 Observable을 사용하면 이 작업이 간단해지는데 State 및 Environment 동적 프로퍼티와 직접 작용하도록 설계돼 있기 때문이죠 읽기 전용 값을 모델링하는 것과 더불어 Observable이 가변 상태를 대표하기에도 좋은데 새로운 개 관찰에 대한 이 양식이 그렇습니다 이 모델은 동적 상태 프로퍼티를 이용하여 정의했고 해당 프로퍼티를 편집하는 양식 요소에 프로퍼티에 대한 바인딩을 통과시키고 있죠 마지막으로 Observable 타입을 Environment에 완벽히 통합했죠 우리 앱의 뷰들이 현재 사용자에 관한 정보를 얻을 수 있도록 Environment에 루트 뷰를 추가했습니다 그럼 사용자 프로필 뷰에서 값을 읽을 때 Environment 동적 프로퍼티를 이용하죠 여기서는 Environment의 키를 타입으로 사용하고 있는데 커스텀 키도 지원하죠 'SwiftUI의 Observation 발견하기'를 통해 새롭고 강력한 도구의 이용 방법을 더 알아보세요 Observable로 명확하고 간결한 코드를 작성할 수 있어 좋습니다 앱 개발의 성공적인 시작을 도왔죠 데이터 모델에 대한 변동 사항이 계속 남도록 하여 좋아하는 강아지들의 자료를 계속 추적하고 싶습니다 SwiftData는 데이터 모델링과 관리를 위한 새로운 프레임워크죠 빠르고 확장성이 있으며 SwiftUI와 함께 잘 작동합니다 SwiftData 모델은 전체가 코드로 대표되어 어떤 SwiftUI 앱에도 잘 맞죠 SwiftData의 개 모델 타입을 설정하기 위해 Observable 대신 Model 매크로를 사용하겠습니다 이것만 바꾸면 되죠 SwiftData에서 제공하는 지속성과 더불어 Observable 사용의 이점을 Model이 모두 받습니다 정말 강력하죠 개 관찰 앱의 메인 화면에서 최근에 만난 개들을 스크롤 화면으로 보여 줍니다 여기에 SwiftData를 사용하는 데 필요한 변경 사항을 살펴보죠 먼저 modelContainer를 앱의 정의에 추가하여 모델 타입에 제공하겠습니다 저의 View 코드에서 개의 배열을 바꿔 Query 동적 프로퍼티를 사용하도록 하죠 Query를 사용하면 SwiftData의 기반 데이터베이스에서 모델 값을 불러옵니다 새로운 개를 발견하여 데이터가 바뀌면 제 뷰가 무효화되겠죠 Query는 대규모 데이터 세트에 엄청나게 효율적이며 데이터를 반환하는 방식을 커스텀으로 설정할 수 있어서 정렬 순서를 제가 강아지를 발견한 날짜로 바꾸면 앱의 경험이 더 나아지죠 SwiftData는 문서의 데이터를 macOS와 iOS에 저장하기에도 좋습니다 앱에 사용할 인식표 느낌의 비주얼을 빠르게 프로토타이핑할 수 있는 방법을 찾고 싶어서 문서 기반의 앱을 만들어 Curt와 디자이너들과 협업하려고 하죠 문서 기반의 앱은 새로운 이니셜라이저를 사용하여 SwiftData의 기능을 활용할 수 있습니다 SwiftUI가 SwiftData에 있는 문서별 기반 스토리지를 사용하고 모델 컨테이너도 자동으로 설정하죠 SwiftData가 SwiftUI와 통합되는 방법이 궁금하다면 'SwiftData 만나보기'와 'SwiftData로 앱 만들기'를 시청해 보세요 SwiftData 지원과 더불어 iOS 17, iPadOS 17에서 실행될 경우 DocumentGroup이 수많은 플랫폼 어포던스를 얻게 되는데 자동 공유나 문서 이름 바꾸기 지원과 툴바의 되돌리기 컨트롤이 그 예입니다 inspector는 새로운 모디파이어로 현재 선택이나 맥락의 세부 정보를 보여 주죠 인터페이스의 별도 섹션으로 나타납니다 macOS에서는 inspector가 오른쪽의 사이드바로 나타나며 iPadOS에서는 보통 크기의 클래스로 나타나죠 작은 크기의 클래스에서는 시트로 나타납니다 Inspector의 모든 정보를 알아보고 싶다면 'SwiftUI의 Inspector: 세부 정보 발견하기'를 시청하세요 대화 상자는 iOS 17과 macOS Sonoma부터 새로운 커스터마이징 API가 적용됐습니다 새로운 모디파이어를 이용하여 이미지 내보내기 대화 상자에 유용한 정보를 주고 있는데 예를 들면 확인 버튼 레이블의 조정이죠 심각도가 높아지면 중요한 확인 대화 상자에 관심을 끌도록 도와주며 억제 토글을 추가하는 건 이후의 상호작용에서 대화 상자가 나타나지 않는 설정을 보여 줍니다 마지막으로 HelpLink를 추가하면 대화 상자의 목적에 관한 추가 정보를 보여 줄 수 있죠 목록과 테이블은 많은 앱의 핵심 요소이며 SwiftUI가 새로운 기능과 API를 추가하여 iOS 17과 macOS Sonoma에 맞춰 미세 조정했습니다 테이블은 열 정렬의 커스텀 항목과 가시성을 지원하죠 SceneStorage 동적 프로퍼티와 함께 사용했을 때 이러한 설정이 여러분의 앱에 지속해서 나타나게 됩니다 테이블에 커스텀 상태를 대표하는 값을 제공하고 각 열에 독특한 스테이블 식별자를 부여하세요 테이블은 OutlineGroup의 강력한 능력이 포함됐습니다 이는 계층 구조에 도움이 되는 대규모 데이터 세트에 적합한데 여기 있는 것처럼 제가 좋아하는 개와 반려인을 짝지을 수 있죠 새로운 DisclosureTableRow를 사용하여 다른 행을 포함하는 행을 대표하고 평소처럼 남은 테이블을 작성하면 됩니다 목록과 테이블 내의 섹션이 프로그램적 확장의 지원을 받았죠 제 앱의 사이드바에 사용하여 처음에는 축소된 위치 섹션을 보여 주며 확장도 가능합니다 새로운 이니셜라이저는 값에 바인딩하여 섹션의 현재 확장 상태를 반영하죠 더 작은 데이터 세트를 위해서 테이블에 새로운 스타일링 어포던스가 추가됐는데 행의 배경과 열의 제목이 보여지는 방법입니다 마지막으로 저의 별점과 같은 커스텀 컨트롤이 환경의 backgroundProminence 프로퍼티의 도움을 얻을 수 있죠 배경이 돋보일 때 덜 돋보이는 전경 스타일을 사용하면 커스텀 컨트롤과 목록이 잘 어울립니다 목록과 테이블의 모습을 세세하게 설정할 수 있는 API에 더하여 성능 면에서도 큰 개선을 이뤘는데 대규모 데이터 세트를 처리할 때 특히 그렇죠 이 내용과 여러분의 SwiftUI 뷰를 최적화하는 방법이 궁금하다면 'SwiftUI의 성능 파헤치기'를 확인해 보세요 Observable과 SwiftData에서 Inspector와 테이블 커스터마이징까지 여러분 앱의 데이터 작업이 새로운 경험으로 느껴집니다 Jeff가 작성한 데이터 모델과 테이블 덕분에 훌륭한 앱의 뼈대를 갖췄죠 이제 특별하고 새로운 애니메이션 API로 생기를 더해 보겠습니다 Apple TV 앱을 가지고 있다면 개 사진을 감상하기에 좋을 것 같군요 현재 시청자를 선택하는 애니메이션을 만들어 봤는데요 KeyframeAnimator API로 제작했습니다 KeyframeAnimator는 다수의 프로퍼티를 병렬로 사용할 수 있죠 먼저 애니메이터에 애니메이션 관련 프로퍼티를 포함하는 값을 주고 이퀘이터블 상태를 전달합니다 상태가 바뀌면 애니메이션이 트리거되죠 첫 번째 클로저에는 애니메이션 프로퍼티로 수정된 뷰를 만듭니다 예를 들면 로고의 수직 오프셋 값이죠 두 번째 클로저에는 이 프로퍼티가 시간에 따라 어떻게 바뀌는지 정의합니다 예를 들어 첫 번째 트랙은 verticalTranslation 프로퍼티의 애니메이션을 정의하죠 처음 4분의 1초간 스프링 애니메이션을 사용하여 로고를 30포인트 내립니다 그리고 삼차 곡선을 사용하여 비글이 뛰게 한 뒤 착지시키죠 마지막으로 스프링 애니메이션을 통해 원래 상태로 돌려놓습니다 추가 트랙에서 다른 애니메이션 프로퍼티를 정의하죠 모든 트랙이 병렬로 실행되어 멋진 애니메이션을 만듭니다 여러분의 앱에서 KeyFrameAnimator를 사용하려면 'SwiftUI의 고급 애니메이션 작업 헤쳐 나가기'를 확인해 보세요 제가 밖에서 달리는 동안 관찰한 개를 기록할 Apple Watch 앱도 개발 중이죠 지금은 간단하게 행복한 아이콘과 새로운 관찰을 등록하는 버튼만 있는데 버튼을 탭했을 때 애니메이션 효과를 주고 싶습니다 단계별 애니메이터를 사용하기에 적합한 상황이죠 단계별 애니메이터는 KeyframeAnimator보다 간단합니다 병렬 트랙 대신 단계를 차례대로 밟아 나가죠 이전 애니메이션이 끝나면 새로운 애니메이션이 시작됩니다 애니메이터에 연속된 단계를 전달하고 sightingCount가 바뀔 때마다 애니메이션을 실행하라고 하죠 그리고 첫 번째 클로저에 현재 단계에 따라 행복한 개의 회전과 크기를 설정합니다 두 번째 클로저는 SwiftUI에 단계별로 애니메이션 방식을 전달하죠 새로운 스프링 애니메이션을 사용하고 있습니다 이름이 마음에 들어요 스내피, 바운시 애니메이션을 누가 싫어할까요? grow 단계에서는 자체적으로 만든 스프링 애니메이션을 사용합니다 스프링은 지속 시간과 바운스를 통해 더 쉽게 표현할 수 있죠 새로운 스프링 기능은 SwiftUI 애니메이션을 쓰는 모든 곳에 사용할 수 있습니다 스프링 애니메이션은 자연스러운 느낌이 있죠 이전의 애니메이션과 속도감이 비슷하고 현실적인 마찰력을 통해 최종값으로 마무리합니다 현재 기본 애니메이션이며 iOS 17 이후 버전과 호환 버전에 제작된 앱에 적용되죠 애니메이션이 마음에 들지만 달리는 중에는 햅틱 피드백을 받는 것도 좋을 겁니다 햅틱 피드백은 촉각적 반응을 제공하는데 탭과 같은 동작으로 주의를 끌고 행동과 사건의 연결을 강화하죠 손목에 탭이 느껴지면 그냥 지나친 개가 한 마리도 없다는 확신이 들 겁니다 sensoryFeedback API로 햅틱 피드백을 설정할 수 있죠 햅틱 피드백을 재생하려면 sensoryFeedback 모디파이어를 첨부하여 원하는 피드백의 종류와 타이밍을 명시합니다 sensoryFeedback 모디파이어는 햅틱 피드백을 지원하는 모든 플랫폼에서 작동하죠 플랫폼마다 지원하는 피드백이 다르므로 Human Interface Guidelines를 참고하여 여러분 앱에 제일 어울리는 피드백을 확인해 보세요 첫 화면의 애니메이션도 시각 효과 모디파이어로 작업하고 있죠 시각 효과 모디파이어로 위치에 따른 개 사진을 업데이트합니다 GeometryReader도 필요 없죠 화면 안에서 초점이 바뀌는 시뮬레이션이 있습니다 여기 있는 빨간 점이 초점을 나타내죠 모든 강아지를 보여 주는 그리드와 좌표 공간을 연관 짓습니다 그리고 DogCircle 뷰 안에 visualEffect를 추가하죠 클로저가 수정할 콘텐츠와 기하 프록시를 받습니다 기하 프록시를 헬퍼 메서드로 통과시켜 크기를 계산하죠 기하 프록시를 사용하여 그리드 뷰의 크기를 받고 그리드 뷰와 단일 아이콘 틀의 상대적 위치 정보를 받습니다 그럼 어떤 개가 시뮬레이션의 초점에서 얼마나 떨어져 있는지 계산하고 초점이 맞춰진 강아지의 크기를 키우죠 시각 효과를 통해 GeometryReader 없이 모든 작업을 할 수 있습니다 그리고 다양한 크기에 자동으로 맞추죠
예를 하나 더 공유하겠습니다 최근에 개발한 기능인데 만났던 개의 주인에게 '좋은 개' 메시지를 보내는 거죠 개의 이름을 스타일 있게 바꿔 돋보이게 하면 재미있을 것 같았습니다 이제는 다른 텍스트 뷰에 있는 전경 스타일로 텍스트를 보간하여 쉽게 할 수 있죠 이것 보세요 슬라이더를 이용하여 스타일을 조절할 수 있죠 원리가 궁금한가요? 스타일을 정의하는 방법입니다 stripeSpacing과 stripeAngle을 애셋 카탈로그의 색상과 함께 커스텀 Metal 셰이더로 전달하죠 SwiftUI의 ShaderLibrary로 Metal 셰이더 함수를 SwiftUI의 형태 스타일에 적용할 수 있죠 여기서는 줄무늬로 Furdinand 이름을 렌더링했습니다
Metal 셰이더를 사용하고 싶다면 새로운 Metal 파일을 프로젝트에 추가하고 SwiftUI의 ShaderLibrary로 셰이더 함수를 호출하세요 이 예시에서 또 하나를 언급하고 싶습니다 슬라이더 트랙의 끝에 도달하면 기호가 통통 튀는 피드백을 출력하죠 macOS와 iOS의 슬라이더에 내제된 효과입니다 여러분의 기호에도 적용할 수 있으며 새로운 symbolEffect 모디파이어를 사용하면 되죠 SF Symbol이나 뷰 계층 구조의 모든 기호에 이 모디파이어로 애니메이션이 가능합니다 기호는 다양한 이펙트를 지원하며 펄스와 변동 색상이 적용된 지속 애니메이션을 포함하죠 크기에 따른 상태 변화 나타났다가 사라지기 대체도 있고 바운스를 이용한 이벤트 알림도 있습니다 '앱 기호에 애니메이션 적용하기' 세션에서 기호 효과를 이용하여 사람들을 만족시키는 사례를 알아보세요 이 예시를 마치기 전에 마지막 기능을 하나 언급하려고 합니다 텍스트의 단위를 보세요 이전에는 여기에 작은 대문자를 적용했는데 이제는 텍스트를 설정할 때 단위에 textScale 모디파이어를 적용할 수 있죠 Jeff와 제가 이 앱을 중국 시장에 출시한다면 단위가 제대로 된 크기로 나타날 겁니다 작은 대문자라는 개념이 중국어 타이포그래피에 없어도 상관없죠 앱이 여러 지역에서 잘 작동할 수 있는 또 다른 툴이 있습니다 태국어의 경우 글자의 높이가 높죠 이런 언어의 텍스트가 영어처럼 높이가 낮은 텍스트에 현지화되어 적용된다면 더 높은 글자로 인해 부산하고 잘린 느낌이 듭니다 이런 문제가 발생할 경우... 예를 들어 개 이름을 전 세계에서 모집했다고 하면... typesettingLanguage 모디파이어를 적용하면 되죠 그럼 SwiftUI에서 텍스트에 공간이 필요하다는 걸 알게 됩니다 새로운 API를 즐기며 사용하고 있지만 적절한 애니메이션을 선정하여 산만함을 주지 않는 게 중요하죠 SwiftUI 애니메이션의 기초를 배우고 싶다면 'SwiftUI 애니메이션 탐구하기'를 확인해 보세요 '스프링을 이용한 애니메이션'에서 Jacob의 도움을 받아 모두의 기기에 어울리는 애니메이션을 만드세요 SwiftUI의 새 애니메이션 API는 놀라운 규모를 자랑하죠 오늘 다룬 건 빙산의 일각입니다 더 많은 주제가 있으며 애니메이션 완료 핸들러부터 커스텀 애니메이션 제작까지 다양하죠 여러분도 저만큼 API 사용을 즐겼으면 좋겠습니다 새로운 애니메이션과 효과로 앱에 생동감을 주는 게 마음에 드네요 이제 상호작용 API를 통해 마무리 작업을 해 보죠 상호작용은 훌륭한 앱 경험의 중심에 있으며 iOS 17과 호환 버전에 업데이트된 API의 일부를 소개하겠습니다 최근에 만난 개 화면에 추가로 멋을 주기 위해 최종 마무리 작업을 해 보죠 개 카드에 시각 효과를 줘서 스크롤 뷰의 보이는 영역으로 드나들 때 적용하려고 합니다 scrollTransition 모디파이어는 Curt가 홈 화면에 적용한 visualEffect 모디파이어와 아주 유사하죠 스크롤 뷰의 항목에 효과를 적용하게 해 줍니다 크기와 투명도 효과를 사용하여 제가 원했던 마무리 작업을 추가 코드 몇 줄만으로 완료했죠 제가 좋아하는 강아지 공원들의 좌우 스크롤 목록도 추가하고 싶습니다 SwiftUI에서 추가한 기능으로 만들 수 있죠 수직으로 스택된 개 위에 수평으로 스택된 공원 카드를 넣겠습니다 새로운 모디파이어 containerRelativeFrame으로 수평 스크롤 뷰의 보이는 크기에 맞게 공원 카드의 크기를 조절하죠 count는 화면을 몇 개 조각으로 나눌지 명시합니다 span은 몇 개의 조각이 뷰에 들어오는지 설정하죠 좋아 보이는데 공원 카드가 제 위치에 걸리는 느낌을 주고 싶습니다 새로운 scrollTargetLayout 모디파이어로 쉽게 할 수 있죠 LazyHStack에 추가하면 스크롤 뷰를 수정하여 목표 레이아웃으로 뷰를 정렬합니다 뷰 정렬과 더불어 스크롤 뷰를 정의하여 페이징 동작을 사용할 수 있죠 진정한 커스텀 경험을 원한다면 여러분이 원하는 동작을 scrollTargetBehavior 프로토콜로 정의하세요 또한, 개가 스크롤 뷰의 최상단에 있을 때 상을 받아야 한다고 생각했습니다 새로운 scrollPosition 모디파이어는 최상단 항목 ID에 바인딩되며 스크롤할 때마다 업데이트되죠 이렇게 하면 언제나 최고의 개를 알 수 있습니다 이 내용과 스크롤 뷰의 다른 개선 사항이 궁금하시다면 '스크롤 뷰를 너머' 세션을 시청해 보세요 이제 이미지가 HDR 콘텐츠의 렌더링을 지원합니다 allowedDynamicRange 모디파이어를 적용하면 앱 갤러리 화면의 아름다운 이미지를 최고의 정확도로 보여 줄 수 있죠 하지만 가끔 쓰는 게 좋은 기능이며 이미지가 단독으로 나올 때 주로 사용합니다 SwiftUI로 작성한 앱은 손쉬운 사용 기능을 바로 사용할 수 있지만 우리가 새로 도입하는 손쉬운 사용 API로 더 발전시킬 수 있죠 모험을 좋아하는 이 개는 너무 멀리 떨어져 있어서 확대 제스처를 적용하여 줌인이 가능하게 했죠 또한 새롭게 추가된 accessibilityZoomAction 모디파이어를 뷰에 추가할 겁니다 이렇게 하면 VoiceOver와 같은 지원 기술로 제스처 없이 같은 기능에 접근할 수 있죠 동작의 방향에 따라 확대 수준을 업데이트하여 저 녀석의 꿍꿍이를 알아낼 수 있습니다 이미지 뷰를 확대합니다 이미지 Apple 플랫폼의 손쉬운 사용 기능을 더 알아보고 싶다면 다음 세션을 참고하세요 'SwiftUI와 UIKit으로 손쉬운 사용 앱 만들기'입니다 이제 색상이 정적 멤버 구문 사용을 지원하여 앱의 애셋 카탈로그에 정의된 커스텀 색상을 검색할 수 있죠 이 기능을 사용할 때 컴파일 시간 안전이 보장되며 오타로 인해 시간을 낭비할 일이 없습니다 앞에서 보여 준 문서 앱에서 툴바에 유용한 동작을 포함하는 메뉴를 추가했었죠 메뉴의 최상위 섹션은 ControlGroup이며 새로운 compactMenu 스타일을 통해 항목들을 수평으로 쌓은 아이콘으로 보여 줍니다 태그 색상 선택자는 피커로 정의되며 새로운 팔레트 스타일이 적용됐죠 이 스타일과 기호 이미지를 함께 사용하면 메뉴를 시각적으로 표현할 수 있으며 이 사례처럼 레이블의 색상으로 구분할 수 있습니다 마지막 paletteSelectionEffect 모디파이어는 피커에서 선택된 항목을 대표하는 기호 종류를 선택할 수 있게 해 주죠 이제 메뉴를 넣어서 버디의 인식표를 제일 좋아하는 색상인 테니스공 노란색으로 설정할 수 있습니다 테두리가 적용된 버튼은 사전 제작된 형태인 원이나 모서리가 둥근 사각형으로 정의할 수 있죠 테두리가 적용된 형태는 iOS watchOS, macOS에서 작동합니다 macOS와 iOS의 버튼은 드래그 동작에 반응하는데 에디터의 이 버튼도 반응하여 팝업 창을 열죠 새로운 springLoadingBehavior 모디파이어는 드래그 동작이 멈추거나 macOS의 강제 클릭 시 버튼이 동작을 트리거해야 한다고 표시합니다 tvOS의 버튼은 새로운 강조 호버 효과를 멋지게 활용할 수 있죠 우리 갤러리의 이미지에도 사용했고 버튼 레이블의 이미지 부분에도 이를 적용하여 플랫폼에 어울리는 효과를 완성했습니다 이 버튼도 테두리가 없는 스타일을 사용하는데 이제 tvOS에서 사용할 수 있죠 하드웨어 키보드는 앱의 기본적 상호작용을 촉진할 수 있는 수단입니다 하드웨어 키보드를 지원하는 플랫폼의 집중이 가능한 뷰는 onKeyPress 모디파이어를 사용하여 키보드 입력에 직접 반응할 수 있죠 이 모디파이어에는 이벤트에 맞는 키와 수행할 동작을 넣어야 합니다 집중과 관련된 내용을 더 알아보고 싶다면 '집중과 관련된 SwiftUI 요리책'을 시청하세요 스크롤 전환과 동작부터 버튼 스타일과 집중 상호작용까지 새로운 API가 풍부한 기능과 멋진 스타일의 앱을 만들도록 해 줍니다 앱 개발에 엄청난 진전이 있었어요 아주 좋아 보여요 새로운 API를 이용해서 재미있었어요 그건 사실이죠 SwiftUI로서는 매우 짜릿한 시간입니다 새로운 플랫폼이 생겼죠 Observable과 SwiftData의 우아한 기능이 SwiftUI와 잘 작동합니다 애니메이션 개선 사항도 놀라워요 스크롤 뷰도 잊지 말아요 언제나 즐거운 건 우리의 놀라운 개발자분들이 새로운 API를 활용하는 방법을 보는 거죠 시청해 주셔서 감사합니다 여러분의 개에게도 인사해 줘요 계속 멋진 작업 부탁드립니다 ♪
-
-
4:49 - watchOS 10
import SwiftUI #if os(watchOS) struct ContainerBackground_Snippet: View { @State private var selection: Int? @State var date = Date() var body: some View { NavigationSplitView { List(selection: $selection) { NavigationLink("Dates", value: -1) NavigationLink("Zero", value: 0) NavigationLink("One", value: 1) NavigationLink("Two", value: 2) } .containerBackground( Color.green.gradient, for: .navigation) } detail: { switch selection { case -1: DatePicker( "Time", selection: $date, displayedComponents: .hourMinuteAndSecond) .containerBackground( Color.yellow.gradient, for: .navigation) case let value?: DetailView(value: value) .containerBackground( Color.blue.gradient, for: .navigation) default: Text("Choose a link") } } } struct DetailView: View { var value: Int var body: some View { Text("\(value)") .font(.largeTitle) } } } #Preview { ContainerBackground_Snippet() } #endif
-
7:01 - Widget Previews
#Preview(as: .systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
7:28 - SwiftUI Preview
#Preview("good dog") { ZStack(alignment: .bottom) { Rectangle() .fill(Color.blue.gradient) Text("Riley") .font(.largeTitle) .padding() .background(.thinMaterial, in: .capsule) .padding() } .ignoresSafeArea() }
-
7:33 - Mac Preview
import SwiftUI struct MacPreview_Snippet: View { @State private var drinks = Drink.sampleData @State private var selection: Drink? var body: some View { NavigationSplitView { List(drinks, selection: $selection) { drink in NavigationLink(drink.name, value: drink) } } detail: { if let selection { DrinkCard(drink: selection) } else { ContentUnavailableView( "Select a drink", systemImage: "cup.and.saucer.fill") } } } } struct DrinkCard: View { var drink: Drink var body: some View { ZStack(alignment: .top) { Rectangle() .fill(Color.blue.gradient) Text(drink.name) .padding([.leading, .trailing], 16) .padding([.top, .bottom], 4) .background(.thinMaterial, in: .capsule) .padding() } } } struct Drink: Identifiable, Hashable { let id = UUID() var name: String static let sampleData: [Drink] = [ Drink(name: "Cappuccino"), Drink(name: "Coffee"), Drink(name: "Espresso"), Drink(name: "Latte"), Drink(name: "Macchiato"), ] } #Preview { MacPreview_Snippet() }
-
8:18 - MapKit
import SwiftUI import MapKit struct Maps_Snippet: View { private let location = CLLocationCoordinate2D( latitude: CLLocationDegrees(floatLiteral: 37.3353), longitude: CLLocationDegrees(floatLiteral: -122.0097)) var body: some View { Map { Marker("Pond", coordinate: location) UserAnnotation() } .mapControls { MapUserLocationButton() MapCompass() } } } #Preview { Maps_Snippet() }
-
8:46 - Scrolling Charts
import SwiftUI import Charts struct ScrollingChart_Snippet: View { @State private var scrollPosition = SalesData.last365Days.first! @State private var selection: SalesData? var body: some View { VStack(alignment: .leading) { VStack(alignment: .leading) { Text(""" Scrolled to: \ \(scrollPosition.day, format: .dateTime.day().month().year()) """) Text(""" Selected: \ \(selection?.day ?? .now, format: .dateTime.day().month().year()) """) .opacity(selection != nil ? 1.0 : 0.0) } .padding([.leading, .trailing]) Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales)) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30) .chartScrollPosition(x: $scrollPosition) .chartXSelection(value: $selection) } } } struct SalesData: Plottable { var day: Date var sales: Int var primitivePlottable: Date { day } init?(primitivePlottable: Date) { self.day = primitivePlottable self.sales = 0 } init(day: Date, sales: Int) { self.day = day self.sales = sales } static let last365Days: [SalesData] = buildSalesData() private static func buildSalesData() -> [SalesData] { var result: [SalesData] = [] var date = Date.now for _ in 0..<365 { result.append(SalesData(day: date, sales: Int.random(in: 150...250))) date = Calendar.current.date( byAdding: .day, value: -1, to: date)! } return result.reversed() } } #Preview { ScrollingChart_Snippet() }
-
9:00 - Donut and Pie Charts
import SwiftUI import Charts struct DonutChart_Snippet: View { var sales = Bagel.salesData var body: some View { NavigationStack { Chart(sales, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.6), angularInset: 1.5) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) } .padding() .navigationTitle("Bagel Sales") .toolbarTitleDisplayMode(.inlineLarge) } } } struct Bagel { var name: String var sales: Int static var salesData: [Bagel] = buildSalesData() static func buildSalesData() -> [Bagel] { [ Bagel(name: "Blueberry", sales: 60), Bagel(name: "Everything", sales: 120), Bagel(name: "Choc. Chip", sales: 40), Bagel(name: "Cin. Raisin", sales: 100), Bagel(name: "Plain", sales: 140), Bagel(name: "Onion", sales: 70), Bagel(name: "Sesame Seed", sales: 110), ] } } #Preview { DonutChart_Snippet() }
-
9:31 - StoreKit
import SwiftUI import StoreKit struct SubscriptionStore_Snippet { var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) .subscriptionStorePickerItemBackground(.thinMaterial) .storeButton(.visible, for: .redeemCode) } }
-
10:56 - Observable Model
import Foundation import SwiftUI @Observable class Dog: Identifiable { var id = UUID() var name = "" var age = 1 var breed = DogBreed.mutt var owner: Person? = nil } class Person: Identifiable { var id = UUID() var name = "" } enum DogBreed { case mutt }
-
11:22 - Observable View Integration
import Foundation import SwiftUI struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Observable class Dog: Identifiable { var id = UUID() var name = "" var isFavorite = false } }
-
12:22 - Observable State Integration
import Foundation import SwiftUI struct AddSightingView: View { @State private var model = DogDetails() var body: some View { Form { Section { TextField("Name", text: $model.dogName) DogBreedPicker(selection: $model.dogBreed) } Section { TextField("Location", text: $model.location) } } } struct DogBreedPicker: View { @Binding var selection: DogBreed var body: some View { Picker("Breed", selection: $selection) { ForEach(DogBreed.allCases) { Text($0.rawValue.capitalized) .tag($0.id) } } } } @Observable class DogDetails { var dogName = "" var dogBreed = DogBreed.mutt var location = "" } enum DogBreed: String, CaseIterable, Identifiable { case mutt case husky case beagle var id: Self { self } } } #Preview { AddSightingView() }
-
12:33 - Observable Environment Integration
import SwiftUI @main private struct WhatsNew2023: App { @State private var currentUser: User? var body: some Scene { WindowGroup { ContentView() .environment(currentUser) } } struct ContentView: View { var body: some View { Color.clear } } struct ProfileView: View { @Environment(User.self) private var currentUser: User? var body: some View { if let currentUser { UserDetails(user: currentUser) } else { Button("Log In") { } } } } struct UserDetails: View { var user: User var body: some View { Text("Hello, \(user.name)") } } @Observable class User: Identifiable { var id = UUID() var name = "" } }
-
13:59 - SwiftData Model Container
import Foundation import SwiftUI import SwiftData @main private struct WhatsNew2023: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Dog.self) } struct ContentView: View { var body: some View { Color.clear } } @Model class Dog { var name = "" var age = 1 } }
-
14:05 - SwiftData Query
import Foundation import SwiftUI import SwiftData struct RecentDogsView: View { @Query(sort: \.dateSpotted) private var dogs: [Dog] var body: some View { ScrollView(.vertical) { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog) } } } } struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Model class Dog: Identifiable { var name = "" var isFavorite = false var dateSpotted = Date.now } } #Preview { RecentDogsView() }
-
14:52 - SwiftData DocumentGroup
import SwiftUI import SwiftData import UniformTypeIdentifiers @main private struct WhatsNew2023: App { var body: some Scene { DocumentGroup(editing: DogTag.self, contentType: .dogTag) { ContentView() } } struct ContentView: View { var body: some View { Color.clear } } @Model class DogTag { var text = "" } } extension UTType { static var dogTag: UTType { UTType(exportedAs: "com.apple.SwiftUI.dogTag") } }
-
15:33 - Inspector
import SwiftUI struct InspectorContentView: View { @State private var inspectorPresented = true var body: some View { DogTagEditor() .inspector(isPresented: $inspectorPresented) { DogTagInspector() } } struct DogTagEditor: View { var body: some View { Color.clear } } struct DogTagInspector: View { @State private var fontName = FontName.sfHello @State private var fontColor: Color = .white var body: some View { Form { Section("Text Formatting") { Picker("Font", selection: $fontName) { ForEach(FontName.allCases) { Text($0.name).tag($0) } } ColorPicker("Font Color", selection: $fontColor) } } } } enum FontName: Identifiable, CaseIterable { case sfHello case arial case helvetica var id: Self { self } var name: String { switch self { case .sfHello: return "SF Hello" case .arial: return "Arial" case .helvetica: return "Helvetica" } } } } #Preview { InspectorContentView() }
-
16:10 - File Export Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ExportDialogCustomization: View { @State private var isExporterPresented = true @State private var selectedItem = "" var body: some View { Color.clear .fileExporter( isPresented: $isExporterPresented, item: selectedItem, contentTypes: [.plainText], defaultFilename: "ExportedData.txt") { result in handleDataExport(result: result) } .fileExporterFilenameLabel("Export Data") .fileDialogConfirmationLabel("Export Data") } func handleDataExport(result: Result<URL, Error>) { } struct Data: Codable, Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .plainText) } var text = "Exported Data" } }
-
16:19 - Confirmation Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ConfirmationDialogCustomization: View { @State private var showDeleteDialog = false @AppStorage("dialogIsSuppressed") private var dialogIsSuppressed = false var body: some View { Button("Show Dialog") { if !dialogIsSuppressed { showDeleteDialog = true } } .confirmationDialog( "Are you sure you want to delete the selected dog tag?", isPresented: $showDeleteDialog) { Button("Delete dog tag", role: .destructive) { } HelpLink { } } .dialogSeverity(.critical) .dialogSuppressionToggle(isSuppressed: $dialogIsSuppressed) } }
-
17:01 - Table Column Customization
import SwiftUI struct DogSightingsTable: View { private var dogSightings: [DogSighting] = (1..<50).map { .init( name: "Sighting \($0)", date: .now + Double((Int.random(in: -5..<5) * 86400))) } @SceneStorage("columnCustomization") private var columnCustomization: TableColumnCustomization<DogSighting> @State private var selectedSighting: DogSighting.ID? var body: some View { Table( dogSightings, selection: $selectedSighting, columnCustomization: $columnCustomization) { TableColumn("Dog Name", value: \.name) .customizationID("name") TableColumn("Date") { Text($0.date, style: .date) } .customizationID("date") } } struct DogSighting: Identifiable { var id = UUID() var name: String var date: Date } }
-
17:22 - DisclosureTableRow
import SwiftUI struct DogGenealogyTable: View { private static let dogToys = ["🦴", "🧸", "👟", "🎾", "🥏"] private var dogs: [DogGenealogy] = (1..<10).map { .init( name: "Parent \($0)", age: Int.random(in: 8..<12) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)], children: (1..<10).map { .init( name: "Child \($0)", age: Int.random(in: 1..<5) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)]) } ) } var body: some View { Table(of: DogGenealogy.self) { TableColumn("Dog Name", value: \.name) TableColumn("Age (Dog Years)") { Text($0.age, format: .number) } TableColumn("Favorite Toy", value: \.favoriteToy) } rows: { ForEach(dogs) { dog in DisclosureTableRow(dog) { ForEach(dog.children) { child in TableRow(child) } } } } } struct DogGenealogy: Identifiable { var id = UUID() var name: String var age: Int var favoriteToy: String var children: [DogGenealogy] = [] } }
-
17:45 - Programmatic Section Expansion
import SwiftUI struct ExpandableSectionsView: View { @State private var selection: Int? var body: some View { NavigationSplitView { Sidebar(selection: $selection) } detail: { Detail(selection: selection) } } struct Sidebar: View { @Binding var selection: Int? @State private var isSection1Expanded = true @State private var isSection2Expanded = false var body: some View { List(selection: $selection) { Section("First Section", isExpanded: $isSection1Expanded) { ForEach(1..<6, id: \.self) { Text("Item \($0)") } } Section("Second Section", isExpanded: $isSection2Expanded) { ForEach(6..<11, id: \.self) { Text("Item \($0)") } } } } } struct Detail: View { var selection: Int? var body: some View { Text(selection.map { "Selection: \($0)" } ?? "No Selection") } } }
-
17:54 - Table Display Customization And Background Prominence
import SwiftUI struct TableDisplayCustomizationView: View { private var dogSightings: [DogSighting] = (1..<10).map { .init( name: "Dog Breed \($0)", sightings: Int.random(in: 1..<5), rating: Int.random(in: 1..<6)) } @State private var selection: DogSighting.ID? var body: some View { Table(dogSightings, selection: $selection) { TableColumn("Name", value: \.name) TableColumn("Sightings") { Text($0.sightings, format: .number) } TableColumn("Rating") { StarRating(rating: $0.rating) .foregroundStyle(.starRatingForeground) } } .alternatingRowBackgrounds(.disabled) .tableColumnHeaders(.hidden) } struct StarRating: View { var rating: Int var body: some View { HStack(spacing: 1) { ForEach(1...5, id: \.self) { n in Image(systemName: "star") .symbolVariant(n <= rating ? .fill : .none) } } .imageScale(.small) } } struct StarRatingForegroundStyle: ShapeStyle { func resolve(in environment: EnvironmentValues) -> some ShapeStyle { if environment.backgroundProminence == .increased { return AnyShapeStyle(.secondary) } else { return AnyShapeStyle(.yellow) } } } struct DogSighting: Identifiable { var id = UUID() var name: String var sightings: Int var rating: Int } } extension ShapeStyle where Self == TableDisplayCustomizationView.StarRatingForegroundStyle { static var starRatingForeground: TableDisplayCustomizationView.StarRatingForegroundStyle { .init() } }
-
19:19 - Keyframe Animator
import SwiftUI struct KeyframeAnimator_Snippet: View { var body: some View { Logo(color: .blue) Text("Tap the shape") } } struct Logo: View { var color: Color @State private var runPlan = 0 var body: some View { VStack(spacing: 100) { KeyframeAnimator( initialValue: AnimationValues(), trigger: runPlan ) { values in LogoField(color: color) .scaleEffect(values.scale) .rotationEffect(values.rotation, anchor: .bottom) .offset(y: values.verticalTranslation) .frame(width: 240, height: 240) } keyframes: { _ in KeyframeTrack(\.verticalTranslation) { SpringKeyframe(30, duration: 0.25, spring: .smooth) CubicKeyframe(-120, duration: 0.3) CubicKeyframe(-120, duration: 0.5) CubicKeyframe(10, duration: 0.3) SpringKeyframe(0, spring: .bouncy) } KeyframeTrack(\.scale) { SpringKeyframe(0.98, duration: 0.25, spring: .smooth) SpringKeyframe(1.2, duration: 0.5, spring: .smooth) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.rotation) { LinearKeyframe(Angle(degrees:0), duration: 0.45) CubicKeyframe(Angle(degrees: 0), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) CubicKeyframe(Angle(degrees: 15), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) SpringKeyframe(Angle(degrees: 0), spring: .bouncy) } } .onTapGesture { runPlan += 1 } } } struct AnimationValues { var scale = 1.0 var verticalTranslation = 0.0 var rotation = Angle(degrees: 0.0) } struct LogoField: View { var color: Color var body: some View { ZStack(alignment: .bottom) { RoundedRectangle(cornerRadius: 48) .fill(.shadow(.drop(radius: 5))) .fill(color.gradient) } } } } #Preview { KeyframeAnimator_Snippet() }
-
20:35 - Phase Animator
import SwiftUI struct PhaseAnimator_Snippet: View { @State private var sightingCount = 0 var body: some View { VStack { Spacer() HappyDog() .phaseAnimator( SightingPhases.allCases, trigger: sightingCount ) { content, phase in content .rotationEffect(phase.rotation) .scaleEffect(phase.scale) } animation: { phase in switch phase { case .shrink: .snappy(duration: 0.1) case .spin: .bouncy case .grow: .spring( duration: 0.2, bounce: 0.1, blendDuration: 0.1) case .reset: .linear(duration: 0.0) } } .sensoryFeedback(.increase, trigger: sightingCount) Spacer() Button("There’s One!", action: recordSighting) .zIndex(-1.0) } } func recordSighting() { sightingCount += 1 } enum SightingPhases: CaseIterable { case reset case shrink case spin case grow var rotation: Angle { switch self { case .spin, .grow: Angle(degrees: 360) default: Angle(degrees: 0) } } var scale: Double { switch self { case .reset: 1.0 case .shrink: 0.75 case .spin: 0.85 case .grow: 1.0 } } } } struct HappyDog: View { var body: some View { ZStack(alignment: .center) { Rectangle() .fill(.blue.gradient) Text("🐶") .font(.system(size: 58)) } .clipShape(.rect(cornerRadius: 12)) .frame(width: 96, height: 96) } } #Preview { PhaseAnimator_Snippet() }
-
22:27 - Haptic Feedback
https://developer.apple.com/design/human-interface-guidelines/playing-haptics
-
22:35 - Visual Effects
import SwiftUI struct VisualEffects_Snippet: View { @State private var dogs: [Dog] = manySampleDogs @StateObject private var simulation = Simulation() @State private var showFocalPoint = false var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: itemSpacing) { ForEach(dogs) { dog in DogCircle(dog: dog, focalPoint: simulation.point) } } .opacity(showFocalPoint ? 0.3 : 1.0) .overlay(alignment: .topLeading) { DebugDot(focalPoint: simulation.point) .opacity(showFocalPoint ? 1.0 : 0.0) } .compositingGroup() } .coordinateSpace(.dogGrid) .onTapGesture { withAnimation { showFocalPoint.toggle() } } } var columns: [GridItem] { [GridItem( .adaptive( minimum: imageLength, maximum: imageLength ), spacing: itemSpacing )] } struct DebugDot: View { var focalPoint: CGPoint var body: some View { Circle() .fill(.red) .frame(width: 10, height: 10) .visualEffect { content, proxy in content.offset(position(in: proxy)) } } func position(in proxy: GeometryProxy) -> CGSize { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return .zero } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return CGSize(width: xOffset, height: yOffset) } } /// A self-updating simulation of a point bouncing inside a unit square. @MainActor class Simulation: ObservableObject { @Published var point = CGPoint( x: Double.random(in: 0.001..<1.0), y: Double.random(in: 0.001..<1.0) ) private var velocity = CGVector(dx: 0.0048, dy: 0.0028) private var updateTask: Task<Void, Never>? private var isUpdating = true init() { updateTask = Task.detached { do { while true { try await Task.sleep(for: .milliseconds(16)) await self.updateLocation() } } catch { // fallthrough and exit } } } func toggle() { isUpdating.toggle() } private func updateLocation() { guard isUpdating else { return } point.x += velocity.dx point.y += velocity.dy if point.x < 0 || point.x >= 1.0 { velocity.dx *= -1 point.x += 2 * velocity.dx } if point.y < 0 || point.y >= 1.0 { velocity.dy *= -1 point.y += 2 * velocity.dy } } } } extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { fileprivate static var dogGrid: Self { .named("dogGrid") } } private func magnitude(dx: Double, dy: Double) -> Double { sqrt(dx * dx + dy * dy) } private struct DogCircle: View { var dog: Dog var focalPoint: CGPoint var body: some View { ZStack { DogImage(dog: dog) .visualEffect { content, geometry in content .scaleEffect(contentScale(in: geometry)) .saturation(contentSaturation(in: geometry)) .opacity(contentOpacity(in: geometry)) } } } } private struct DogImage: View { var dog: Dog var body: some View { Circle() .fill(.shadow(.drop( color: .black.opacity(0.4), radius: 4, x: 0, y: 2))) .fill(dog.color) .strokeBorder(.secondary, lineWidth: 3) .frame(width: imageLength, height: imageLength) } } extension DogCircle { func contentScale(in geometry: GeometryProxy) -> Double { guard let gridSize = geometry.bounds(of: .dogGrid)?.size else { return 0 } let frame = geometry.frame(in: .dogGrid) let center = CGPoint(x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0) let xOffset = focalPoint.x * gridSize.width - center.x let yOffset = focalPoint.y * gridSize.height - center.y let unitMagnitude = magnitude(dx: xOffset, dy: yOffset) / magnitude(dx: gridSize.width, dy: gridSize.height) if unitMagnitude < 0.2 { let d = 3 * (unitMagnitude - 0.2) return 1.0 + 1.2 * d * d * (1 + d) } else { return 1.0 } } func contentOpacity(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func contentSaturation(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func opacity(for displacement: Double) -> Double { if displacement < 0.3 { return 1.0 } else { return 1.0 - (displacement - 0.3) * 1.43 } } func displacement(in proxy: GeometryProxy) -> Double { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return 0 } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return magnitude(dx: xOffset, dy: yOffset) / magnitude( dx: backgroundSize.width, dy: backgroundSize.height) } } private struct Dog: Identifiable { let id = UUID() var color: Color } private let imageLength = 100.0 private let itemSpacing = 20.0 private let possibleColors: [Color] = [.red, .orange, .yellow, .green, .blue, .indigo, .purple] private let manySampleDogs: [Dog] = (0..<100).map { Dog(color: possibleColors[$0 % possibleColors.count]) } #Preview { VisualEffects_Snippet() }
-
23:39 - Metal Shader
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
25:01 - Symbol Effect
import SwiftUI struct SymbolEffect_Snippet: View { @State private var downloadCount = -2 @State private var isPaused = false var scaleUpActive: Bool { (downloadCount % 2) == 0 } var isHidden: Bool { scaleUpActive } var isShown: Bool { scaleUpActive } var isPlaying: Bool { scaleUpActive } var body: some View { ScrollView { VStack(spacing: 48) { Image(systemName: "rectangle.inset.filled.and.person.filled") .symbolEffect(.pulse) .frame(maxWidth: .infinity) Image(systemName: "arrow.down.circle") .symbolEffect(.bounce, value: downloadCount) Image(systemName: "wifi") .symbolEffect(.variableColor.iterative.reversing) Image(systemName: "bubble.left.and.bubble.right.fill") .symbolEffect(.scale.up, isActive: scaleUpActive) Image(systemName: "cloud.sun.rain.fill") .symbolEffect(.disappear, isActive: isHidden) Image(systemName: isPlaying ? "play.fill" : "pause.fill") .contentTransition(.symbolEffect(.replace.downUp)) } .padding() } .font(.system(size: 64)) .frame(maxWidth: .infinity) .symbolRenderingMode(.multicolor) .preferredColorScheme(.dark) .task { do { while true { try await Task.sleep(for: .milliseconds(1500)) if !isPaused { downloadCount += 1 } } } catch { print("exiting") } } } } #Preview { SymbolEffect_Snippet() }
-
25:35 - Metal Shader (cont.)
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
26:11 - Typesetting Language
import SwiftUI struct TypesettingLanguage_Snippet: View { var dog = Dog( name: "ไมโล", language: .init(languageCode: .thai), imageName: "Puppy_Pitbull") func phrase(for name: Text) -> Text { Text( "Who's a good dog, \(name)?" ) } var body: some View { HStack(spacing: 54) { VStack { phrase(for: Text("Milo")) } VStack { phrase(for: Text(dog.name)) } VStack { phrase(for: dog.nameText) } } .font(.title) .lineLimit(...5) .multilineTextAlignment(.leading) .padding() } struct Dog { var name: String var language: Locale.Language var imageName: String var nameText: Text { Text(name).typesettingLanguage(language) } } } #Preview { TypesettingLanguage_Snippet() }
-
27:46 - ScrollView Transitions And Behaviors
import SwiftUI struct ScrollingRecentDogsView: View { private static let colors: [Color] = [.red, .blue, .brown, .yellow, .purple] private var dogs: [Dog] = (1..<10).map { .init( name: "Dog \($0)", color: colors[Int.random(in: 0..<5)], isFavorite: false) } private var parks: [Park] = (1..<10).map { .init(name: "Park \($0)") } @State private var scrolledID: Dog.ID? var body: some View { ScrollView { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog, isTop: scrolledID == dog.id) .scrollTransition { content, phase in content .scaleEffect(phase.isIdentity ? 1 : 0.8) .opacity(phase.isIdentity ? 1 : 0) } } } } .scrollPosition(id: $scrolledID) .safeAreaInset(edge: .top) { ScrollView(.horizontal) { LazyHStack { ForEach(parks) { park in ParkCard(park: park) .aspectRatio(3.0 / 2.0, contentMode: .fill) .containerRelativeFrame( .horizontal, count: 5, span: 2, spacing: 8) } } .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .padding(.vertical, 8) .fixedSize(horizontal: false, vertical: true) .background(.thinMaterial) } .safeAreaPadding(.horizontal, 16.0) } struct DogCard: View { var dog: Dog var isTop: Bool var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() if isTop { TopDog() } Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(dog.color.gradient) .frame(height: 400) } } struct TopDog: View { var body: some View { HStack { Image(systemName: "trophy.fill") Text("Top Dog") Image(systemName: "trophy.fill") } } } struct ParkCard: View { var park: Park var body: some View { RoundedRectangle(cornerRadius: 8) .fill(.green.gradient) .overlay { Text(park.name) .padding() } } } struct Dog: Identifiable { var id = UUID() var name: String var color: Color var isFavorite: Bool } struct Park: Identifiable { var id = UUID() var name: String } }
-
31:12 - Menu Enhancements
import SwiftUI struct DogTagEditMenu: View { @State private var selectedColor = TagColor.blue var body: some View { Menu { ControlGroup { Button { } label: { Label("Cut", systemImage: "scissors") } Button { } label: { Label("Copy", systemImage: "doc.on.doc") } Button { } label: { Label("Paste", systemImage: "doc.on.clipboard.fill") } Button { } label: { Label("Duplicate", systemImage: "plus.square.on.square") } } .controlGroupStyle(.compactMenu) Picker("Tag Color", selection: $selectedColor) { ForEach(TagColor.allCases) { Label($0.rawValue.capitalized, systemImage: "tag") .tint($0.color) .tag($0) } } .paletteSelectionEffect(.symbolVariant(.fill)) .pickerStyle(.palette) } label: { Label("Edit", systemImage: "ellipsis.circle") } .menuStyle(.button) } enum TagColor: String, CaseIterable, Identifiable { case blue case brown case green case yellow var id: Self { self } var color: Color { switch self { case .blue: return .blue case .brown: return .brown case .green: return .green case .yellow: return .yellow } } } }
-
32:30 - Highlight Hover Effect
import SwiftUI struct DogGalleryCard: View { @FocusState private var isFocused: Bool var body: some View { Button { } label: { VStack { RoundedRectangle(cornerRadius: 8) .fill(.blue) .frame(width: 888, height: 500) .hoverEffect(.highlight) Text("Name") .opacity(isFocused ? 1 : 0) } } .buttonStyle(.borderless) .focused($isFocused) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.