뷰와 컨트롤 — 여러분이 기다려온 SwiftUI 2 문서
iOS 14, iPadOS 14, WatchOS 7 및 MacOS Big Sur 용으로 업데이트 됨
2020 년 초, 저는 The Complete SwiftUI Documentation You'veen Waiting For 라는 긴 Medium 게시물을 썼습니다 .
이것이 애플이 제공하는 불충분 한 문서로 인해 남겨진 틈을 메우려 고 할 때 배운 것을 공유하는 방법이었습니다. 내 게시물이 많은 사람들에게 도움이되는 것 같았지만 6 개월 늦게 썼습니다.
이제 Apple의 2020 개발자 컨퍼런스가 끝났으므로 SwiftUI에 몇 가지 새로운 기능이 제공되었으므로이 업데이트가 내 문서가 그 어느 때보 다 도움이되기를 바랍니다.
이것은 포스트 당 하나의 챕터로 시리즈로 출시 될 것입니다.
이 장의 이름은 Apple의 SwiftUI 문서에있는 장 이름과 일치합니다.
나는 그들 중 누구도 이것만큼 길지 않을 것이라고 보장 할 수 있습니다.
- 보기 및 제어
- 앱 구조 및 동작
- 레이아웃 및 프레젠테이션보기
- 그리기 및 애니메이션
- 프레임 워크 통합
- 상태 및 데이터 흐름
- 제스처
- 시사
The View Protocol
@ViewBuilder
New and Updated Views
ColorPicker (NEW in 2.0)
SpriteView (NEW in 2.0)
TextEditor (NEW in 2.0)
SignInWithAppleButton (NEW in 2.0)
ProgressView (NEW in 2.0)
GaugeView (NEW in 2.0)
Label (NEW in 2.0)
Link (NEW in 2.0)
Menu (NEW in 2.0)
MenuButton (Deprecated in 2.0)
Text (Updated in 2.0)
Image (Updated in 2.0)
Button (Updated in 2.0)
PasteButton (Updated in 2.0)
Toggle (Updated in 2.0)
DatePicker (Updated in 2.0)
New and Updated View Modifiers
.matchedGeometryEffect (NEW in 2.0)
.help (NEW in 2.0)
.accessibility(inputLabels:) (NEW in 2.0)
.accessibility(selectionIdentifier:) (Deprecated in 2.0)
.scaleEffect (Updated in 2.0)
.imageScale (NEW in 2.0)
.accentColor (Updated in 2.0)
.preferredColorScheme (Updated in 2.0)
.textContentType (NEW in 2.0)
.listItemTint (NEW in 2.0)
.listRowPlatterColor (Deprecated in 2.0)
.onLongPressGesture (Updated in 2.0)
.onOpenURL (NEW in 2.0)
.onPasteCommand (NEW in 2.0)
.onDrag and .onDrop (Updated in 2.0)
.onChange (NEW in 2.0)
.keyboardShortcut (NEW in 2.0)
.focusedValue and @FocusedBinding (NEW in 2.0)
.prefersDefaultFocus and .focusScope (NEW in 2.0)
.fullScreenCover (NEW in 2.0)
.defaultAppStorage (NEW in 2.0)
.appStoreOverlay (NEW in 2.0)
.toolbar (NEW in 2.0)
.previewContext (NEW in 2.0)
.userActivity, .onContinueUserActivity (NEW in 2.0)
.tabItem (Updated in 2.0)
.contextMenu (Updated in 2.0)
.navigationTitle and .navigationSubtitle (NEW in 2.0)
.navigationViewStyle (Updated in 2.0)
.navigationBarTitle (Deprecated in 2.0)
.navigationBarItems (Deprecated in 2.0)
Styles on iOS, iPadOS, Mac Catalyst and tvOS (NEW in 2.0)
Styles Only on macOS (NEW in 2.0)
Next Steps
아직 모르는 경우 SwiftUI는 View
프로토콜을 사용하여 재사용 가능한 인터페이스 요소를 만듭니다. 뷰는 그들이 사용하는 수단 값 유형입니다 Struct
대신의 Class
정의.
실제로 이것은 실제로 무엇을 의미합니까?
구조체는 상속을 허용하지 않습니다. 구조체는 View
프로토콜 을 따르지만 View
Apple이 제공 한 기본 클래스에서 상속하지 않습니다 .
이것은 UIView
UIKit의 거의 모든 것이 상속되는. A는 UIView
기본적으로 프레임이 할당되지 않고 UIViewController
서브 클래스 의 서브 뷰로 추가되지 않으면 볼 수 없습니다 .
당신이 사용은 사용자 인터페이스의 기초로 대신 스토리 보드의 SwiftUI하는 새로운 Xcode 프로젝트를 생성하면 자동으로 SwiftUI의 예를 제공 할 것 View
이라고 ContentView
.
ContentView
구조체 안에 body라는 변수가 있음을 알 수 있습니다 . 이것은 View
프로토콜 의 유일한 요구 사항이며 some
Swift 5.1의 새로운 키워드를 사용합니다 .
당신은 의지 할 수 스택 오버플로 스레드 더 나은 내가 할 수있는 것보다 어떻게이 키워드 수단을 설명하기 :
"이것을"역 "의 일반적인 자리 표시 자로 생각할 수 있습니다. 호출자가 만족하는 일반 제네릭 플레이스 홀더와는 달리 ... 불투명 한 결과 유형은 구현에 의해 충족되는 암시 적 제네릭 플레이스 홀더입니다. 여기서 가장 중요한 것은 반환하는 함수 some P
가 특정 단일 콘크리트의 값을 반환하는 것입니다. 이 부합 함을 선언을하는 입력합니다 P
. "
@ViewBuilder
이것은 여러 뷰에서 단일 뷰를 구성 할 수있는 일종의 함수 빌더입니다. 이 속성을 View 본문 위에 추가하고 cmd- 클릭하고 정의로 이동을 선택하면 흥미로운 내용이 많이 표시됩니다. 아마도 가장 중요한 부분은 다음과 같습니다.
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
Xcode 12에서 body 속성은 @ViewBuilder
.
이것이 왜 중요합니까? 이제 body 속성에 직접 자식으로 최대 10 개의 뷰를 넣을 수 있습니다. 이전에는 뷰 를 ,, 또는 의 레이아웃에 영향을주지 않고의 Group
이점을 얻을 수있는 방법 인 . 이는 새 버전의 SwiftUI에서 더 이상 필요하지 않으며 그 결과 의 사용 이 더 틈새 시장이 될 것입니다.@ViewBuilder
VStack
HStack
ZStack
Group
뷰 를 사용하려고 할 때 body 속성에 직접 배치하는 것은 모호하므로 여전히 뷰를 VStack
또는 HStack
에 배치해야합니다.
수정 자보기
2020 년에 새로 추가 된보기에 대해 알아보기 전에보기 수정자가 무엇인지 다시 살펴 보겠습니다. 새로운 뷰와 함께 표시되므로 나중에 설명하기 위해 기다리는 것은 의미가 없습니다.
모든 뷰는 ViewModifier 프로토콜을 준수하는 구조체로 수정할 수 있습니다. 모든 프로토콜에 필요한 것은 일반 뷰를 반환하는 body (content : Content)라는 함수입니다. View를 직접 만들 수 없기 때문에 수정자가 호출 될 때까지 ViewModifier에 전달하는 유형을 알 수 없습니다. 콘텐츠는 구체적인 유형에 대한 프록시 역할을합니다. 뷰의 본문과 같은 반환 유형은 구현에서 유추됩니다.
다음과 같이 표면 아래에서 일어나는 일을 볼 수 있도록 사용자 지정 수정 자의 예를 살펴 보겠습니다.
보시다시피 .modifier(YourModifier())
ViewModifier를 호출 하기 위해 추가 하는 것이 가능 하지만 View
확장 을 사용 하고 깨끗한 호출 사이트를 제공하는 것이 훨씬 더 합리적 입니다.
첫 번째 방법 인 blue(_:)
은 기본적으로하는 일을 .modifier(YourModifier())
합니다. 구성하여 ModifiedContent
, 우리는 결국 View
수식을 가지고 그 적용했다. 그러나 동일한 결과를 얻기 위해 인스턴스 메소드 .modifier(YourModifier())
를 호출하여이를 덜 복잡하게 만들 수 View
있습니다.
두 번째 메서드 b(_:)
인는 필요할 .modifier(YourModifier())
때마다 호출 할 필요가 없습니다. 이것이 표준 수정자가 보이는 방식이며 View
.
분명히 b(_:)
속성, 메서드 또는에 대한 꽤 끔찍한 이름 ViewModifier
이지만 식별자를 점차 짧게 만들어 가장 간단한 식별자를 나타냅니다.
먼저 2020 년에 완전히 새로 워진 모든 뷰를 볼 수 있고, 그다음에 올해 업데이트 된 2019 년 뷰를 볼 수 있습니다.
그런 다음 마지막에 어떤 View Modifier가 새로 추가되거나 업데이트되었는지 확인합니다.
2.0의 새로운 기능 : ColorPicker
iOS 개발자를위한 색상 선택기가 포함 된 적이 없습니다. 이전에 타사 제품을 사용해 보았지만 종속성으로 인해 향후 모든 프로젝트 및 배포 대상과의 호환성을 보장하기 위해 다른 개발자에게 의존하게됩니다. WWDC 전 마지막 몇 주 동안 마침내 SwiftUI에서 생각할 수있는 모든 종류의 색상 선택기 컨트롤을 만들기로 결정했습니다. 이를 통해 이러한 컨트롤 의 Swift Package에서 색상 선택기 를 만들고 모든 프로젝트에 대한 색상 선택기를 만들 수 있습니다.
그런 다음 WWDC가 나왔고 이제 iOS 14 용 ColorPicker가 있습니다.이 새로운 컨트롤은 매우 유능 해 보이며 어디에서나 색상을 선택할 수있는 스포이드와 같은 기능이 있습니다. 스포이드를 사용하여 ColorPicker의 UI 자체에서 색상을 선택할 수도 있으므로 이것이 무료로 제공되는 강력한 새 기능이라는 것이 분명해 보입니다. 하지만 제가 해결할 수있는 한, 새로운 색상 선택기에는 변경할 수없는 매우 엄격한 컨트롤 세트가 있습니다. 공식 문서가 아직 업데이트되지 않은 경우 ColorPicker가 제공하는 내용을 캔버스, 팔레트 또는 슬라이더로만 제한하는 것과 같이 변경할 수있는 방법이없는 것처럼 보입니다.
대신이 세 가지 옵션은 사용자가 세그먼트 선택기로 선택합니다.
ColorPicker가 앱에서 어떻게 보이는지에 대한 유용한 스크린 샷 및 애니메이션 GIF는 Using a ColorPicker with SwiftUI를 확인하십시오 .
2.0의 새로운 기능 : SpriteView
SpriteKit은 2D 게임 제작을위한 Apple의 프레임 워크입니다. 스프라이트는 무엇보다도 플레이어, 적, 발사체를 나타내는 데 사용되는 작은 비트 맵입니다. 게임에서 한 번에 화면에 많은 스프라이트가있을 수 있으므로 성능을 염두에두고이를 수행하도록 설계된 도구를 사용하는 것이 좋습니다. 이제 SpriteKit에서를 표시하는 SwiftUI 뷰 SKScene
를 생성하여 게임을 생성 한 다음 해당 게임을 SwiftUI 뷰를 배치 할 위치에 둘 수 있습니다.
내 예에서는 오른쪽에서 오는 적에게 발사체를 발사하는 간단한 사각형 스프라이트가 있습니다. 그들이 왼쪽 끝까지 가면 당신은 생명을 잃습니다. 그중 하나를 쓰러 뜨리면 점수가 올라갑니다.
나중에 만들 SpriteKit 씬이 필요한 SwiftUI를 먼저 살펴 보겠습니다.
데이터를 저장 하는 ObservableObject
호출 GameModel
과 ContentView
게임을 표시 하는 구조체가 있습니다. 의 상단에는 현재 점수, 우리의 생명 수, 이전에 기록 된 최고 점수를 알려주는를 VStack
표시합니다 HUDView
. 수명이 다하면 다시 시작 버튼이 나타납니다. 이 모든 작업은 에서 new modifier로 관찰 @Published
중인 GameModel
객체 의 속성을 변경하는 것 .onChange
입니다 ContentView
.
기본적으로 우리는 언제 restartGame
가 참인지 말하고 GameScene
, 게임을 처음부터 일시 중지를 해제하고 다시로드해야한다는 메시지를 우리에게 보내고 싶습니다 .
높은 점수는 편리한 방법으로 @AppStorage
데이터를 저장 하는 속성 래퍼를 사용하여 기록됩니다 UserDefaults
. 이 문서의 다른 장에서 새 속성 래퍼에 대한 자세한 정보를 제공 할 것이지만 중요한 것은이 래퍼가있는 속성이 지속적으로 저장되고 다음에 앱이로드 될 때 쉽게 불러올 수 있다는 것입니다. 플레이어가 생명이 없을 때 게임 오버 상태도 있습니다.이 상태는 플레이어가 게임을 다시 시작하기 위해 재시작 버튼을 탭해야합니다. 이것은 점수를 재설정하고, 적을 제거하고, 게임 시작과 마찬가지로 적의 스폰을 다시 시작합니다.
다음은 SpriteKit 코드입니다.
Swift로 작성된 SpriteKit 게임이므로 게임의 논리에 대해 너무 걱정하지 마십시오. SpriteKit에 대해 잘 모르는 경우에는 분명히 알지 못하지만 시작하는 데 도움이되는 많은 자습서가 있습니다.
알아야 할 중요한 점 SpriteView
은 SwiftUI에 2D 게임을 삽입하는 쉬운 방법을 제공한다는 것입니다.
2.0의 새로운 기능 : TextEditor
WWDC 2020 이전에는 TextField
또는 .NET을 사용하는 iOS에서만 텍스트 편집을 처리 할 수있었습니다 SecureTextField
. 이들은 기본적으로 동일한 텍스트 필드이며 SecureTextField
암호 필드와 마찬가지로 문자를 검은 색 원으로 대체하여 입력하는 내용을가립니다. 이러한 텍스트 필드의 중요한 유사점은 한 줄만 허용한다는 것입니다. 즉, 여러 줄 편집을위한 유일한 옵션 은 UIKit UIViewRepresentable
에서 변환 UITextView
하는 데 사용 하는 것입니다 .
이것은 비교적 복잡하지만 더 많은 속성을 허용합니다. 입력 된 텍스트에서 탭할 수있는 URL을 만드는에 대한 UITextView
변경을 허용 dataDetectorTypes
합니다. 현재 텍스트는를 사용하여 새 텍스트로 바꿀 수 있으며 clearsOnInsertion
를 호출하여 지정된 문자열이 표시 될 때까지 스크롤 할 수 있습니다 scrollRangeToVisible
. 자세한 정보는 UITextView 문서를 확인하십시오 .
이러한 속성에 액세스 할 수는 없지만 새로운 속성 TextEditor
은 TextField
.
이제 TextEditor
한 줄을 만드는 것처럼 쉽게 여러 줄을 만들 수 있습니다 TextField
.
에 적용 할 Text
수있는 모든 항목을에 적용 할 수 있습니다 TextEditor
. 사용자 지정 글꼴과 함께 새로운 동적 유형 구문 을 사용하려고했을 때 텍스트가 TextStyle
. 이것은 첫 번째 베타의 버그이거나 내가 한 방식의 문제 일 수 있습니다. 내가 문서 편집기의 기능을 만들 수 어쨌든, 가장 좋은 예는과 글꼴 크기를 변경 할 수 있도록했다 Stepper
과 함께 글꼴 무게를 Picker
. A ColorPicker
를 사용하여 전경 (글꼴) 색상을 선택할 수 있지만 배경이 현재 작동하지 않는 것 같습니다.
TextEditor
systemBackground
색상 의 불투명 한 배경이있는 것처럼 보이며 배경을 추가하면 배경이 뒤에 배치됩니다.
배경이 전혀 보이지 않습니다.
해결 방법이 있는데, 아마도 지금은 우리가 할 수있는 전부일 것입니다 :
TextEditor는 아래 스크린 샷과 같이 테두리를 지원합니다.
2.0의 새로운 기능 : SignInWithAppleButton
Apple로 로그인은 이메일과 암호를 제공하지 않고도 앱에 안전하게 로그인하는 방법으로 iOS 13에 도입되었습니다. Apple로 로그인은 생체 인식을 사용하여 본인이 Apple ID를 소유 한 사람임을 인증 한 다음 앱에 자동으로 생성 된 전달 주소와 암호를 앱에 보냅니다.
다음 예제는 SignInWthAppleButton
허용되는 최대 크기 (너비 ≤ 375)에서 를 표시합니다 . 이보기에는 제약이 있습니다. Apple은 방금 기존 ASAuthorizationAppleIDButton 을으로 래핑 한 것 같습니다 UIViewRepresentable
. 이는 작년에 SwiftUI에 버튼을 가져 오기 위해 필요한 작업입니다. 그러나 iOS 13에서 Apple로 로그인 을 구현하는 데 필요한 단계를 확인 하면 코디네이터에서 위임 프로토콜을 설정하는 추가 작업 이 필요하지 않습니다 .
Apple의 예에서 채택한 내 버전은 인증 결과를 Text
앱에 출력하고 인쇄합니다. 처음 시도 할 때 로그인 버튼이 아무 작업도하지 않는다는 것을 알 수 있습니다. 그것은 확실히 제 경험이었습니다.
두 번 누르면 예상대로 출력이 표시됩니다.
ProgressView (2.0의 새로운 기능)
UIActivityIndicatorView 는 미정 인로드 상태에 대한 스피너를 표시 할 수있는 UIKit 컨트롤입니다. UIViewRepresentable로 래핑하지 않고 직접 사용할 수 없었지만 이제는 이에 상응하는 것이 있습니다! 매개 변수없이 ProgressView를 생성하면 스피너가 표시되지만 진행률 값을 전달하면 수평 진행률 표시 줄로 표시 될 수 있습니다.
진행률 표시 줄 양식은 웹 페이지가로드 될 때 와 함께 자주 사용되는 UIProgressView 와 유사합니다 WKWebView
.
중간 예제에 이름, 값 및 총 매개 변수가 있음을 알 수 있습니다. 일반적으로 이렇게 ProgressView
하면 하단 예제와 같이 가로 진행률 표시 줄로 표시됩니다. 그러나 .progressViewStyle
수정자를 적용 CircularProgressViewStyle
하여 기본값과 일치하는 색조로에 전달 했음을 알 수 accentColor
있습니다. 진행률 표시 줄이 아닌 결과는 파란색 원형 스피너입니다.
스타일이 적용될 때 "다운로드 중"레이블은 유지 ProgressView
되지만는 원형 이 되어야하며 이로 인해 자동 동작이 취소 된다는 사실을 보여주는 그림입니다 .
GaugeView (2.0의 새로운 기능)
GaugeView
View
WatchOS 7 전용 의 유일한 새로운 기능 입니다. 이것은 값이 척도에있는 위치를 표시하는 비교적 간단한 지표입니다. 아래의 예에서는 이 표시 Slider
되는 값을 변경 하기 위해를 추가했습니다 Gauge
. Slider는 실제로 현재 값이 자체 파란색 막대에있는 위치를 표시하므로 약간 이상하게 보입니다. 게이지 스타일링은 매우 어렵습니다. 또한 호출되는 기본 게이지 스타일은 LinearGaugeStyle
사용 Color.primary
의 전경 색상으로, 그리고 사용 .foregroundColor
또는 .accentColor
수정하면이 변경되지 않습니다. 마찬가지로 CircularGaugeStyle
사용 Color.gray
, 그리고 이것은 변경할 수 없습니다.
게이지에서 현재 값이있는 위치를 나타내는 엄지 손가락 또는 원은 적어도의 경우 마스크처럼 보입니다 LinearGaugeStyle
. 추가 .background(Color.blue)
하면 원의 색상이 변경됩니다.
라벨 (2.0의 새로운 기능)
이것은 Apple의 현재 더 큰 SF Symbols 무료 아이콘 컬렉션의 심볼을 더 큰 컨텍스트를 제공하는 텍스트와 결합하는 비교적 간단한 방법입니다. 내 예제에서 나는 비교하고 Label
를 사용하여 해당과 함께 HStack
. 거기의 정렬 의한 최초의 엑스 코드 (12) 베타 버그했습니다 Label
의 Image
텍스트로 잘못 정렬 할 수는 있지만, 지금은 수정되었습니다. 에 대한 두 가지 옵션이 있습니다. 그 .labelStyle
중 하나 Text
는 Image
.
링크 (2.0의 새로운 기능)
저는 항상 iOS 앱에서 하이퍼 링크를 사용할 수 없다는 것이 조금 아쉽다고 생각했습니다. 물론 URL을 여는 파란색 텍스트가있는 버튼을 만들 수 있지만 이렇게하려면 매번 작성하려는 것보다 더 많은 코드가 필요합니다. 이 예제에서는 Button
원래 형식으로 코드를 얻을 수있는만큼 작기 때문에 제목 문자열 (또는 지역화 된 문자열 키) 만 받는 편리한 이니셜 라이저를 사용하고 있습니다.
iOS 14에는 이제 위 Link
의 작업 부분을 수행하는이 Button
있습니다. 나는 Apple의 문서 가 '!'를 사용하여 URL을 안전하지 않게 언 래핑하는 방식이 마음에 들지 않았습니다 . 왜냐하면 이것은 코드 샘플에서 권장하는 매우 나쁜 습관이기 때문입니다. 물론, URL을 IP 주소로 변환하는 IANA (Internet Assigned Numbers Authority) 소유 사이트 인 example.com/TOS.html에 연결되기 때문에이 특정 URL이 성공적으로 생성되었음을 알 수 있습니다 .
그러나 URL 문자열이 유효하다는 인간의 확신에 맡기면 조만간 실수하게 될 것입니다.
선택 사항을 안전하지 않게 풀 때 예기치 않게 nil을 찾는 앱은 즉시 충돌합니다.
그렇기 때문에 위의 예제는 Apple의 예제보다 몇 줄 더 걸리지 만 안전하게 수행합니다. 이 예제는 SwiftUI의 첫 번째 버전에서 선택적 바인딩 ( if let
또는 guard let
) 이 부족하여 실제로 방해가됩니다. 대신 URL이 존재하는지 확인하기 위해 URL을 nil과 비교하는 것으로 제한되어 있습니다. 이 비교에서 URL이 nil이 아님을 확인하더라도, 이것이 Button
먼저 래핑을 풀지 않고 에서 사용할 수 있다는 의미는 아닙니다 . 이것이 Button
액션 에 약간 혼란스러운 추가 단계가있는 이유 입니다. 선택적으로 URL을 바인딩하여 nil이 아닌지 확인합니다.
in the action assertionFailure
뒤에 else 문 을 넣을 수 있었지만 예제 와의 일관성을 위해 를 추가하고 싶었습니다 . 포함하는 else 문 은 필요하지 않습니다. 클로저 의 유일한 점유자 주변의 모든 if 문 은 if 조건이 거짓 일 때 반환 됩니다. 그러나 URL이 nil이면 어떤 일이 발생할지 보여주기 위해 이것을 명시 적으로 추가하고 싶었습니다. 사용자는 아무것도 볼 수 없지만 디버그 모드에서 개발자에 대해 어설 션이 트리거됩니다.if let
Button
EmptyView
Link
EmptyView
ViewBuilder
EmptyView
이렇게하면 URL이 nil임을 알 수 있지만 최종 사용자에게 충돌을 일으키지 않습니다.
이제 SwiftUI가 지원하는 경우 URL과 같은 속성을 직접 만들고 해당 데이터를 사용하는 뷰를 만들 수 있습니다. 이전과 마찬가지로 링크는 URL을 생성 할 수있을 때만 표시되지만이 경우를 확인하기 위해 여러 번 확인할 필요는 없습니다.
메뉴 (2.0의 새로운 기능)
MenuButton
이 (가)로 대체되었습니다 Menu
. 원본은 드롭 다운 메뉴 였고 대체품도 그리 다르지 않습니다. 새 이름이 추가하는 가장 중요한 것은 메뉴와 그 안의 항목에 대해 이야기하고 있으므로 명확성이므로 버튼이라고 부르는 것은별로 의미가 없습니다. Menu
스타일에 대한 몇 가지 다른 옵션이 제공됩니다. 기본 스타일은 BorderedButtonMenuStyle
입니다.이 때문에 DefaultButtonMenuStyle
오른쪽은 똑같이 BorderlessButtonMenuStyle
보이고 가운데는 다르게 보입니다.
그외 메뉴에 "풀다운는"사실에 대한 참조를 제거하고 새로운 스타일에서 유일한 변화는이 MenuButton
라고 Menu
하고, .menuButtonStyle
수정이 지금이라고합니다 .menuStyle
.
첫 번째 베타에서는 Menu
대체하는 컨트롤과 마찬가지로 macOS에서만 사용할 수있었습니다.
그러나 베타 3는 iOS에서도 메뉴 지원을 추가했습니다 .
MenuButton (2.0에서 더 이상 사용되지 않음)
Menu
를 대체 MenuButton
하지만 동일한 기능을 많이 제공 하는 위를 참조하십시오 .
텍스트 (2.0에서 업데이트 됨)
텍스트는 뷰를 만드는 가장 간단한 구성 요소 일 것입니다. 대부분의 경우를 전달 String
하여 생성하고 이것이 표시되는 콘텐츠가됩니다. 이 문서의 원래 버전에는 지역화, ObservableObjects 및 하위 문자열을 포함하여 문서를 만드는 다른 모든 방법이 포함되어 있습니다. 이 예제의 맨 아래에서 찾을 수 있지만 2020 년에는 예제 맨 위에 포함 된 많은 새로운 이니셜 라이저가 있습니다.
첫 번째는 .NET Framework에서 상속하는 모든 일반 객체와 모든 클래스를 사용할 수 있으므로 매우 흥미 롭습니다 Formatter
. 이것은 Formatter
Apple이 제공 하는 유형이거나 내 예에서와 같이 Formatter
사용자 정의 유형을 위해 특별히 만든 유형일 수 있습니다. a Formatter
가 구현 되는 방법을 정확히 알지 못했기 때문에 선택적으로 개체를 사용자 지정 클래스의 인스턴스에 바인딩하고 다른 모든 상황에서는 nil을 반환했습니다.
주의점 걸리는 일이 LocalizedStringKey
, tableName
, bundle
, 및 comment
용도 별도의 파일이 필요합니다 .strings
파일 확장자를. 이 initializer에 대한 Apple의 문서 에서 언급했듯이 유일한 필수 매개 변수는 키의 문자열입니다. 이러한 다른 매개 변수가 무엇을 필요로하는지 알 수 있도록 대부분의 자세한 예제를 제공했습니다.
의 기본 tableName
IS Localizable
하는 문자열 파일의 표준 이름. Local
이 매개 변수가 필요한 이유를 보여주기 위해 일부러 내 이름을 지정했습니다 . 번들은 기본적으로 기본 번들이므로이 Bundle.main
경우 전달 은 중복됩니다. 주석은 상황에 맞는 정보를 제공해야하지만이 예제에서는 문자열 Comment
.
Text
이제 Image
using 보간을 포함 하거나 Date
. 세 가지 새로운 글꼴 : caption2
, title2
및 title3
. 이들은 UIFont의 일부로 사용할 수 있었으므로 SwiftUI로 만든 것은 놀라운 일이 아닙니다. 새로운 수정자를 사용하면 텍스트가 대문자인지 소문자인지와 고정 폭 또는 세리프 디자인을 사용할지 여부를 선택할 수 있습니다.
Dynamic Type을 사용하여 사용자 정의 글꼴을 시스템 스타일만큼 액세스 할 수 있도록 만드는 새로운 방법은 포함하지 않았습니다. 이를 위해 Hacking With Swift의 Dynamic Type 가이드에 링크 할 것입니다. 거기에서 말한 내용 만 반복 할 것입니다.
이미지 (2.0에서 업데이트 됨)
SF Symbols 2는 Mac 앱에서의 사용을 지원합니다. Image(systemNamed:)
Xcode 11에서 SF 기호를 사용 하려고 하면 "외부 인수 레이블 'systemNamed :'in call"오류가 발생합니다. 즉, macOS에는 표시 할 방법이 없기 때문에 기본 Mac 앱 또는 Catalyst 앱에서도 SF Symbols를 사용할 수 없습니다. 아마도 macOS Catalina에서 실행되는 SF Symbols Mac 앱은 .NET을 사용할 수 없었기 때문에 심볼의 썸네일로 PNG를 사용했을 것 Image(systemNamed:)
입니다.
어쨌든 Xcode 12 및 macOS 11 Big Sur부터는 이러한 경고가 표시되지 않으며 Image(systemNamed:)
기본 macOS 및 Mac Catalyst 앱에서 사용할 수 있습니다 .
버튼 (2.0에서 업데이트 됨)
이제 CardButtonStyle
tvOS에는 색상이 덜 눈에 띄는 작은 버튼을 만드는 옵션 이 있습니다 . 위의 예는 Button
스타일이 전혀없이 생성 된를 보여 DefaultButtonStyle
주므로를 사용하므로 차이를 확인할 수 있습니다. 새로운 Button
것은 훨씬 더 미묘하며 더 밝은 색상이 주어진 옵션보다 덜 중요한 옵션에 사용할 수 있습니다.
CardButtonStyle
Button
어떤 이유로 패딩이 없기 때문에 에 패딩을 추가해야했습니다 . 이것은 Xcode 12 베타 2에서만 테스트하고 있으므로 이후 베타에서 변경 될 수 있습니다.
PasteButton (2.0에서 업데이트 됨)
이 컨트롤을 사용하면 MacOS에 정보를 붙여 넣을 수 있지만 iOS에서는 사용할 수 없습니다. UTI로 표현되는 다양한 데이터 유형을 사용할 수 있습니다. Apple의 문서를 인용하기 위해 "Uniform Type Identifiers는 다른 앱에서로드, 저장 또는 열 수있는 리소스에 대한 공통 유형을 선언합니다." Xcode 11에서는 .NET Framework를 만들 때 이러한 문자열을 배열에 제공해야했습니다 PasteButton
.
이 버튼을 구현할 때 도움이 될 모든 유형의 UTI 문자열을 찾을 수있는 함수를 예제에 포함했습니다.
이제 UTType
지원하려는 유형을 훨씬 쉽게 만들 수있는 라는 새로운 구조 가 있습니다. 이 구조의 이니셜 라이저에 Xcode 11에서 작동했던 문자열을 전달하거나 제공된 많은 시스템 선언 유형 중 하나를 사용할 수 있습니다 .
문자열을받는 이니셜 라이저는 선택 사항을 반환하므로 사용하는 경우 사용하는 문자열이 올바른지 확인해야합니다. 내가 사용하고 UTType.text
내 예를 들어,하지만 문자열에서 수동으로 구성 아래 나는 예를 포함. 느낌표를 사용하여 옵션을 강제로 풀지 않는 것이 좋습니다. UTType
시스템 선언 유형에서 얻은 유형이 nil이 아니므로 동일한 유형이 문자열에서 생성되지 않음 을 보여주고 싶었습니다 .
필요한 유형 식별자를 결정했으면에서 가져온 데이터를 처리해야합니다 NSItemProvider
. 내 예제는 배열의 첫 번째 항목 만 붙여 넣지 만 다른 데이터 유형과 여러 항목을 처리 할 수있는 방법을 명확하게 보여줍니다.
Here’s a list of the types that conform to NSItemProviderWriting
, and can therefore be used for pasting with the PasteButton
:
CNContact
CNMutableContact
CSLocalizedString
MKMapItem
NSAttributedString
NSMutableString
NSString
NSTextStorage
NSURL
NSUserActivity
UIColor
UIImage
Toggle (Updated in 2.0)
The default style on iOS, SwitchToggleStyle, now allows us to choose a tint colour that is only shown when the bool the Toggle has a Binding to is true. While the default Toggle tint on iOS and iPadOS is green, on Mac Catalyst it is blue. Using the new SwitchToggleStyle with the tint colour option in a native Mac app currently displays a switch as we would expect, but the tint colour is still the default blue.
The top Toggle
, which I added without a ToggleStyle
for comparison, is displayed in the macOS default of CheckboxToggleStyle
. This is a checkbox that displays a checkmark when on and does not have tint colour options. I’ve added an accentColor
modifier to this Toggle, which shows that macOS actually does allow the Toggle tint to be changed when the user has not selected ‘Accent Color’ in the Highlight Colour dropdown menu in the General section of their System Preferences.
This can be used to change either style of Toggle
; I just didn’t add the accentColor
modifier to the second one to show how the SwitchToggleStyle
with tint colour has no effect.
DatePicker (Updated in 2.0)
There are now two new styles for DatePicker
, called GraphicalDatePickerStyle
and CompactDatePickerStyle
.
You might notice that the version of GraphicalDatePickerStyle
on macOS on the right is much smaller than the one that is shared by iOS, iPadOS, and Mac Catalyst on the left. I could only get the DatePicker
to display correctly when I allowed it a minimum height of 400. Any less and the DatePicker
clips some of the text — in particular, the part that is used to set a time.
This causes the width to scale proportionally, meaning that on a small device like iPhone 8, the DatePicker
exceeds the screen width. Perhaps this will be fixed in a future beta, as I was using Xcode 12 beta 2. Unfortunately, it seems that the WheelDatePickerStyle, which is available on iOS, iPadOS and Mac Catalyst but not a native macOS app, still continues to have a minimum width that leaves very little space for its label on small screens.
CompactDatePickerStyle
is now the default on iOS, iPadOS and Mac Catalyst. This is essentially a button that displays the current value, and displays a tiny calendar similar to the GraphicalDatePickerStyle
when the button is tapped. This allows you to have a very compact display for the current date, without needing to display an entire calendar at all times.
My example has the conditional compilation flag #if os(iOS) || targetEnvironment(macCatalyst)
around the WheelDatePickerStyle
example.
This means that no matter what platform you build it for, you will see all of the DatePicker
variations that are supported on that platform.
New and Updated View Modifiers in 2.0
.matchedGeometryEffect (NEW in 2.0)
As you might be able to tell from the name, .matchedGeometryEffectt
is an animation effect that animates changes in size and position.
Like Sarun’s example, I used a Text
and a Shape
, but mine is a Circle
instead of a RoundedRectangle
(totallydifferent). Instead of a VStack
that turns into an HStack
on the basis of a bool, I thought it would be interesting to position two views in a variety of configurations. These configurations are relative to the TextPosition
enum, and you can guess what the value of that means. The values are left, centre, right, top and bottom. When the Text
is in the centre, the Circle
adjusts for this by reducing its opacity, making it easier to see the text that is layered on top of it in a ZStack
.
My example is relatively basic, so check out these better sources:
- Hacking With Swift: matchedGeometryEffect()
- SwiftUI Lab MatchedGeometryEffect Part 1 & Part 2
- Apple’s Fruta sample app code
The accessibility modifier .help provides tooltips in MacOS and an accessibility hint that works on both MacOS and iOS. My example shows how this new modifier actually overrides any accessibility hint that was previously applied and vice versa. When I ran this on iOS, VoiceOver read the first text as “Label 1, Help 1” because the help modifier was added after the hint. The second Text is read as “Label 2, Hint 2” because the hint was added after the help modifier.
This behaviour is similar on MacOS, except that a tooltip displaying the help text is displayed, regardless of whether help is overridden by an accessibility hint. In other words, although the second Text is still read by VoiceOver as “Label 2, Hint 2”, hovering over it with the mouse still displays “Help 2”.
In Mac Catalyst, it seems that neither the help modifier or the accessibility hint are read.
The tooltip functionality of help also seems absent from Catalyst.
.accessibility(inputLabels:) (NEW in 2.0)
Input labels are used by Voice Control (NOT VoiceOver) and Full Keyboard Access. When Voice Control is enabled, speaking a command such as ‘tap Input’ would press this Button
. This gives you an array of different labels for your UI elements that are not visible to VoiceOver.
In other words, they are ways that someone can describe the Button verbally.
.accessibility(selectionIdentifier:) (Deprecated in 2.0)
This identifier was previously used by Picker
to identify the current selection.
.scaleEffect (Updated in 2.0)
This isn’t new, but one of the original modifiers has been fixed. In Xcode 11, .scaleEffect(x: 2) causes Y to be scaled to zero. In Xcode 12, the default parameters are both 1, which means you can keep one the same and scale one of them without setting both.
Calling .scaleEffect() in Xcode 11 scaled the entire View to zero!
Try the example above in each version of Xcode and you’ll see what I mean.
In Xcode 11, both rectangles are invisible, whereas in Xcode 12 they appear as a square and a rectangle.
.imageScale (NEW in 2.0)
imageScale
seems to only apply to symbols. I have tried passing the name of an image file into the initializer for Image, but the imageScale
seems to have no effect.
It will, however, work on Label
, which also has an initializer that takes a system name for one of the provided SF Symbols.
.accentColor (Updated in 2.0)
The first example below was possible on iOS 13, macOS Catalyst 13, tvOS 13, and watchOS 6. The only platform it wasn’t supported on was macOS, but that has changed in 2020. With a Button
this changes the font colour, which wasn’t possible before on that platform.
In my second example, I’ve used foregroundColor
to change the font colour of a Button
on Mac. This works, but it is no longer necessary in Xcode 12.
Now we can use accentColor
to change the font colour on all platforms, which allows me to use the version of Button
that takes a string and doesn’t require me to create a Text
.
.preferredColorScheme (Updated in 2.0)
The existing colorScheme(_ colorScheme: ColorScheme)
modifier is now deprecated in the current versions of Apple operating systems. The purpose of this modifier was to override the system colour scheme for a single View and its subviews, while preferredColorScheme
was used to override the colour scheme for the entire presentation. This refers to the popover or window that the View is being presented in.
While this modifier is not new, the new addition in 2020 is the fact that the parameter is now optional. To illustrate this, my example contains a Toggle that changes whether the sheet should be dark or not. With colorScheme
, it was impossible to have the ternary expression sheetIsDark ? .dark : nil
, but now we can with preferredColorScheme
.
It might seem like a disadvantage that preferredColorScheme
only applies to the presentation, not each individual View, but I’m not sure why you’d want to apply a colour scheme to only one View.
You either want a screen of your app to use the system colour scheme, or you want to override it with your own.
If you do want individual Views to act as if the colour scheme is different, you can just change the colours they would be in either scheme.
.textContentType (NEW in 2.0)
This modifier tells the device what kind of suggestions would be helpful when a user is typing.
You might notice that this modifier takes a different type on Mac compared to the other platforms. Not only is Mac incapable of using any of the UITextContentType declarations, but it is also restricted to using NSTextContentType which currently only has three options. When I tried them out, I couldn’t see any difference in the suggestions they offer on the MacBook Touch Bar, but maybe I was doing it wrong somehow.
I noticed that there were no suggestions for usernames and passwords, which is probably unnecessary.
I didn’t have time to go through them all and note the intricacies of each, but I’ve provided an array of them so that my code sample isn’t too long.
See what differences you can find and let me know!
.listItemTint (NEW in 2.0)
Changing the tint of a list item has a different effect depending on the platform. To quote Apple’s official documentation on the new .listItemTint
modifier:
The containing list’s style will apply that tint as appropriate. watchOS uses the tint color for its background platter appearance. Sidebars on iOS and macOS apply the tint color to their ItemLabel
icons, which otherwise use the accent color by default.
After looking for something called ItemLabel
, I realised that this does not seem to be something that exists. However, any item in a List
can be a Label
, so I tried that and it worked.
I’m not sure why they specified “Sidebars on iOS and macOS” either as it seems to work universally for any List
, and iOS doesn’t have sidebars.
That’s iPadOS, it’s totally different!
The fact that watchOS uses the tint for the background is a huge difference. The Label
icon is not tinted at all, and the tint is used for any item, not just Label
. The .listItemTint
modifier can directly take a colour, but it can also take a structure called ListItemTint
(with a capital letter). You can give it a .preferred
variety, which can be overridden by a parent, or a .fixed
variety, which cannot. While the .monochrome
style of ListItemTint
makes icons grey on iOS, on watchOS it makes the background of the list item black. On iOS, you’ll notice that the .accentColor
and Color.blue
variations are the same, while on watchOS the accent colour is grey and not blue.
.listRowPlatterColor (Deprecated in 2.0)
The new .listItemTint
modifier above replaces .listRowPlatterColor
.
This deprecated modifier was only available on WatchOS, and took only a colour. With .listItemTint
, you can use ListItemTint
, so you aren’t just limited to a colour. The new modifier overrides the old one too, so even if .listRowPlatterColor
is used after .listItemTint
, the colour passed to .listRowPlatterColor
will be used instead.
.onLongPressGesture (Updated in 2.0)
It is now possible to use SwiftUI to add a long press gesture to a View on tvOS.
You need to be able to add focus to your View, so I’ve used the focusable()
modifier. You can use this modifier with no parameters, but I’ve used it with a closure in order to change the colour of my custom buttons to show when they have focus. If I didn’t do this, there would be no indication of which button is selected at any given time. I’ve used an enum to contain four button states: unfocused, focused, pressing, and pressed. It isn’t necessary to have a closure for the pressing state, but I made this to show that you can run code before a long press has reached its minimum duration. The default duration is 0.5 seconds, so you probably only need to make changes to the UI if you also increase this duration.
This code will work on any platform from 2019, but will only work on tvOS 14.0 from 2020.
.onOpenURL (NEW in 2.0)
This modifier has nothing to do with opening URLs for websites. These URLs are solely the ones that can be opened by your app, and your app alone.
The onOpenURL
modifier is for SwiftUI apps that use the new SwiftUI App lifecycle that does not use AppDelegate or SceneDelegate. If your project has these files in it, you probably won’t be able to get this modifier to work, like I couldn’t. When creating a project, be sure to choose ‘SwiftUI App’ as the Life Cycle option instead of ‘UIKit App Delegate’.
To create a unique URL scheme for your app, select the Info tab of your project settings, whether that be for an iOS or macOS target. Without changing the Custom Target Properties at the top of the screen, you’ll see that there is already a URL types section at the bottom of the screen. Opening this and clicking the ‘+’ button will allow you to create a new URL scheme for your app, providing that it is unique and not the same as any other app on a user’s device. For the purposes of this example I have called it my-scheme
, but you can choose anything.
The Identifier field is optional, but you may want to use it, as the URL type is simply referred to as Untitled in this menu without it.
Now that we have a URL scheme in place, we can do the rest in code. In the example below, I’ve added the ability to use a TextField to change what link is opened. The URL scheme you set up needs to be set as a constant in the ContentView struct. If you are using my-scheme as I was, you don’t need to do anything. I’ve extended URL, Character and String in order to filter the input from the TextField.
Because characters that are allowed in a URL are set as a constant called allowedCharacters
, we can be certain that any text added to the TextField can successfully create a URL. If a URL was created with nothing but illegal characters, this code would filter them all out, leaving us with an empty string. This is one way that the code could fail, as a URL cannot be created with an empty string. Luckily we are applying the URL scheme separately, and the URL can be constructed with only the scheme and nothing else, so an empty string is fine in this case.
Whether you type a URL or not, the link that says “Open” will open that URL, and the onOpenURL modifier will add that URL to a list. Since we are doing this inside the app that those URLs are opened in, we don’t actually go anywhere. If you want to see a link actually do something, try entering a URL that starts with the URL scheme you set in Safari. This will ask you if you want to open the URL in your app, and then you will be taken back to the app.
If everything is working, the URLs you enter in Safari should also be added to the opened URLs list.
.onPasteCommand (NEW in 2.0)
This one took a long time to figure out. The official documentation for onPasteCommand says the modifier adds “an action to perform in response to the system’s Paste command.” But don’t go thinking that the closure you give to it will run when you paste into a TextField
. The modifier only seems to work on SwiftUI Views, rather than the ones like TextField that wrap UIKit controls with UIViewRepresentable
.
But how do you allow a View to accept pasting if it’s not a TextField
? After all, trying to do this without a TextField causes an error sound, and the Edit > Paste option in the default menu bar is greyed out. The answer lies in a transcript from Session 231 of WWDC 2019, when the original version of this modifier was released:
However, I want to point out something that really makes onPaste different than onDrop. The first part is that there’s no location parameter in the closure. And that’s a key to what’s really going on here. When you do drag and drop, user is directly targeting via the cursor or the touch location which view should accept the drop but a paste command is more indirect. The user is either choosing paste from the menu or is perhaps using a keyboard shortcut or the great new gestures that exist in iOS. The way we solve the problem of knowing which view that the paste command should go to is with the focus system.
If you follow the link the official documentation for onPasteCommand, you’ll notice that there is absolutely no mention of the focus system whatsoever. So what is the focus system? SwiftUI’s guide to Focus does a good job of explaining it, so I’ll just summarise the important parts that relate to this modifier. As shown on the left window of the screenshot above, focus requires the setting in System Preferences on macOS that allows you to navigate between focusable items with the tab button.
Now that you have that enabled, my code example should work.
Essentially, we have a Text
at the top, and this automatically takes focus when the app runs. You’ll notice that it’s surrounded by a blue outline, assuming you have the default Accent colour of ‘Multicolor’ selected in System Preferences > General. If you have chosen a different Accent colour, the Text
will be outlined in that colour. Now when you select Edit > Copy from the menu bar, or press the equivalent cmd+C key combination, we run a closure for copying to the clipboard.
This isn’t strictly necessary for using onPasteCommand
, but it’s an easier way to control the Uniform Type Identifier for the data in the pasteboard. The main difference between onPasteCommand
in 2020 and the original from 2019 is the existence of the UTType
structure. In 2019, the types that the closure would accept were given as strings like “public.utf8-plain-text”.
This leaves a high probability of human error when typing the strings, and it is also not easy to find what strings apply to the type you want.
With the UTType
structure, we have a huge number of constants that will likely cover anything you’re likely to want to paste. You can also make your own by conforming to the UTTypeContent
and/or UTTypeData
protocols. I’ve included a constant that shows an array of all the provided data types if you want to take a look. In the onCopyCommand
closure, you might notice that we’re converting the string we want to copy to NSString
.
Here’s a list of the types that conform to NSItemProviderWriting
, and can, therefore, be used for pasting with an NSItemProvider
:
CNContact
CNMutableContact
CSLocalizedString
MKMapItem
NSAttributedString
NSMutableString
NSString
NSTextStorage
NSURL
NSUserActivity
UIColor
UIImage
I’ve also included an example of PasteButton
so that you can see how similar onPasteCommand
is.
The closure takes exactly the same array of UTType instances, although the parameter is labelled supportedContentTypes
instead.
.onDrag and .onDrop (Updated in 2.0)
The two Views below have no awareness of one another. They don’t share an ObservableObject. The top one does not pass a Binding<String>
to the bottom one, nor does it pass a constant to its initialiser. The only link between them is that the top applies an .onDrag
modifier that provides data of the UTType.utf8PlainText
variety, and the bottom applies an .onDrop
modifier that expects UTType.utf8PlainText
.
For more info about UTType, see .onPasteCommand
above.
The TextField
is used to generate a string, which is something unique that only one View knows about. Once it has been generated, it is displayed as an orange rectangle with rounded corners. This orange shape has been given an .onDrag
modifier that provides an NSItemProvider
for the underlying data, which has been converted to NSString
because this class conforms to the NSItemProviderWriting
protocol.
Providing some sort of NSItemProvider
is all your View needs to do in order to become draggable, but you have to have somewhere to drop it in order for dragging to be useful.
That’s where .onDrop
comes in.
In DragView
, we have a similar TextWithBackground
which is hidden initially due to it having a clear background and no text.
Unlike .onDrag
, .onDrop
requires a delegate that conforms to the DropDelegate protocol. Unlike some delegate types, DropDelegate can be a structure and does not need to be a class. The only mandatory requirement of this protocol is that it has a function called performDrop
, which does exactly what it sounds like. You attempt to read the data and, if successful, you return true to confirm that dragging happened. If there is an error at any point, you return false.
This is the only part of my code that is slightly complicated, but it bears a lot of similarity to the code used in PasteButton
and .onPasteCommand
above.
.onChange (NEW in 2.0)
One of the annoying things about the original version of SwiftUI is the lack of property observers. The only way to use something like didSet
, which allows you to run a closure every time a property’s value changes, is to use it in an ObservableObject
class. Since these are regular Swift classes, they cannot use the@State
property wrapper, which only works on structures. But they can use property observers in the usual way that we expect, even on properties that are marked as @Published
and are therefore accessible from SwiftUI.
Because structures are value types, and self
is immutable, the structure needs to be completely recreated when changes occur. In the case of @State
, the property is already being observed. Changes to this property cause the structure to be completely recreated since it cannot be mutated.
To quote the official documentation for the@State
property wrapper:
A State
instance isn’t the value itself; it’s a means of reading and writing the value. To access a state’s underlying value, use its variable name, which returns the wrappedValue
property value.
So the property inside your structure is not actually the value you think it is — it’s a wrapper that actually saves it inside a totally separate and invisible structure. That’s why the documentation of the wrapper declares it as @propertyWrapper struct State<Value>
, because it's not just modifying the behaviour of the property, it's storing it elsewhere. That’s why trying to use the didSet
property observer doesn’t work: your instance of the property wrapper doesn’t ever change, only its wrappedValue property does.
So how do we do anything on the basis of these changes?
In the first year of SwiftUI’s existence, using property wrappers in an ObservableObject
was about the only way. In 2020, we have a new modifier called .onChange
, and this allows any View in your structure to run a closure when a @State
property’s value changes. If a TextField
has a Binding<String>
, it can use the modifier to run code when the string is changed. But the View observing the changes does not need a direct Binding
to react to changes, and the .onChange
modifier for a Button
in another part of the layout would have no more or less access to this ability.
To make things a bit more interesting, my example uses a Binding
instead of a State
as the property for onChange
to observe. The value that .onChange
observes can actually be any type that conforms to Equatable
. For a great look at all the ways that Equatable affects SwiftUI Views, check out SwiftUI Lab’s tutorial on EquatableView. Notice how ContentView
, which does not contain an .onChange
modifier, is still affected by the logic in the modifier of its child OnChangeView
.
For instance, the TextField
in ContentView
is emptied when the Toggle
in OnChangeView
is turned off, despite it having no Binding
to that control or even knowledge of its existence.
This is perhaps one of the most powerful capabilities of this new modifier, as it allows Views anywhere in the hierarchy to judge the situation and require that Views elsewhere should also be redrawn according to the properties that it cares about.
.keyboardShortcut (NEW in 2.0)
To quote Apple’s documentation for .keyboardShortcut:
Pressing the control’s shortcut while the control is anywhere in the frontmost window or scene, or anywhere in the macOS main menu, is equivalent to direct interaction with the control to perform its primary action.
Below is an example of two Button
s with associated keyboard shortcuts. The first button has .upArrow
as the key, but no modifier is specified. You might think that this means pressing the up arrow will perform the action of the Button
, but that would be wrong. The Command button is the default modifier, and so this Button
actually requires the Cmd + Up combination to be pressed.
The second button does explicitly state a modifier, allowing it to use the shift key instead of command. If you read about the onPasteCommand
modifier above, you would’ve read that on macOS there is a System Preferences option that enables navigating through focusable items with the tab key. If that option is enabled, you will find the top Button
is highlighted by default. Tab will move focus to the second button, and Shift + Tab will move it back to the first.
No matter which Button
is focused, the keyboard shortcuts you set will still work.
This is perhaps the most useful aspect of these shortcuts, as they continue to work despite the fact that the spacebar will perform the function of the currently focused Button
.
.focusedValue and @FocusedBinding (NEW in 2.0)
This is a new way to pass data between Views. Instead of having an ObservableObject
, we save data using a FocusedValueKey
.
In the following example, DisplayTextView
is able to show the text you type into TextFieldView
, despite the fact that a Binding<String> or String constant is not passed between the Views.
The magic here is enabled by the structure and extension at the bottom.
The FocusedValueKey
protocol requires that conforming structures have a typealias
for the value they store.
Once you have a structure that defines a typealias
for your key, you’ll need to define a getter and setter for that value. This extension of FocusedValue
defines \.text
as the key that we will use to read and write the value. Notice that the getter and setter both use the FocusedTextKey
type as a subscript for FocusedValues
. Now we just need to write a value to the key in TextFieldView
, and then we need to read from it in DisplayTextView
.
The .focusedValue(\.text, $text)
modifier on the TextField
saves the value to the key.
The @FocusedBinding(\.text) var text: String?
property in DisplayTextView
subscribes it to changes in the associated value. Notice that it is an optional, because the value does not have to be set. As Apple’s official documentation says, “Unlike EnvironmentKey
, FocusedValuesHostKey
has no default value requirement, because the default value for a key is always nil
.” I assume the original name was FocusedValuesHostKey
, because the documentation still mentions this despite the fact it no longer exists.
In other words, you can set up a key without giving it a value, and your code will still run.
When you do set up a value, you will need to unwrap it as I did using the nil coalescing operator ‘??’.
The next question you might have is why these values would be needed. After all, they seem to be global, at least in the context of a single window. We wouldn’t want to keep all our data at this scope, and having a lot of them might make it hard to debug. A more complex example by an Apple Frameworks Engineer on the Apple Developer Forums shows an interesting use case. When a Mac app has separate commands, such as Shift, Cmd + D in that example, you may still want to access data in the app despite the fact that the commands are at the WindowGroup
scope.
Now you can!
.prefersDefaultFocus and .focusScope (NEW in 2.0)
On tvOS 14 and watchOS 7, we now have the ability to declare what user interface element we want to be focused by default. On tvOS, this matters because pressing the Touch surface of the Siri remote performs the action of the Button
with focus. On watchOS, the focused element is controlled by moving the Digital Crown.
In a VStack
, the top View will always gain focus by default, unless we take steps to prevent this behaviour.
To do this, we need to define the scope in which the focus system can be overridden. Declaring a focus scope requires a @Namespace
, a subject on which there’s more detail in the .matchedGeometryEffect
section above. The important thing is that I gave a Namespace.ID
with my property, which I also called namespace
. I passed this to the .focusScope
modifier on the VStack, and now we have our scope.
The only other thing I need to do is use the .prefersDefaultFocus
modifier on the bottom Button
, passing it the namespace.
Now the bottom Button
will be focused by default, despite the fact that it is not at the top of the VStack
.
.fullScreenCover (NEW in 2.0)
You probably won’t be shocked to learn that, unlike .sheet
, the .fullScreenCover
modifier presents a modal View that covers the full screen. I made an example that allows you to infinitely create sheets and full-screen covers, as this shows you important information about how they work. A sheet can be swiped to dismiss it, but a full-screen cover cannot. A View can allow both kinds of modal, and the modal itself can be identical in either case.
I have provided the @Environment(\.presentationMode)
property in the modal, as without swiping to dismiss, the full-screen cover could not be dismissed.
.defaultAppStorage (NEW in 2.0)
This modifier .defaultAppStorage
changes what UserDefaults
an entire View’s @AppStorage
properties are saved to. If you’re unfamiliar with @AppStorage
or UserDefaults
, this is a way of saving simple information that persists after the user has quit the app. The @AppStorage
property wrapper will not be covered specifically here, but it will be covered as part of the State and Data Flow chapter of this revised version of my documentation.
To quote Axel Kee’s post When to use UserDefaults, Keychain, or Core Data:
Previously, we have explained that UserDefaults saves data into plist. Using apps such as iExplorer, users can access the Library/Preferences folder of their iPhone and read / modify the UserDefaults plist data easily (eg: Change the boolean value of “boughtProVersion” from false to true, or change the amount of coins). Don’t ever store a boolean for checking if user has bought in-app purchase in UserDefaults! User can change it very easily (without jailbreaking) and get your goodies for free! 😬
Other than in-app purchase status, you shouldn’t store user password / API Keys in UserDefaults for the same reason as well.
.appStoreOverlay (NEW in 2.0)
In order to recommend an app made by yourself or others, you need to know the 10-digit App ID. This is relatively easy if you own the app, as you’ll find it in the App Information section of App Store Connect. However, if you don’t know the App ID for an app, head to iTunes Link Maker and search for it, making sure to change the Media Type to Apps so you don’t just get a bunch of music. Whatever app you choose will give you a direct link, which ends in a number followed by a query.
The 10-digit code is found between the letters ‘id’ and the question mark ‘?’.
For instance, the Apple Developer app has the link:
https://apps.apple.com/us/app/apple-developer/id640199958?mt=8
The App ID is therefore 640199958.
There’s a screenshot of my example at the start of this section. I added a TextField
, into which you can type or paste an App ID, a Button
to toggle the appearance of the App Store overlay, and a Toggle to change the position.
There are only two positions for an App Store overlay: .bottom
and .bottomRaised
, hence the name for the Toggle being ‘raised’.
.toolbar (NEW in 2.0)
Toolbars can now be easily created on all platforms using ToolBarItem
s. These can be Button
s or any View.
In terms of placing a ToolBarItem, there are many options.
Instead of rewording what he said, I’m going to quote Majid’s excellent post Mastering toolbars in SwiftUI:
automatic — The item is placed in the default section that varies depending on the current platform.
primaryAction — The item represents a primary action. Usually, SwiftUI places this item in the navigation bar on iOS or on top of other views on watchOS.
There are placement options that we can use only in toolbars presented by a modal view.
confirmationAction — The item represents a confirmation action for a modal interface. You can use it in your sheets to confirm saving action.
cancellationAction — The item represents a cancellation action for a modal interface.
destructiveAction — The item represents a destructive action for a modal interface. You can use it in your modal screens that delete some data.
There are also a bunch of platform-specific placement options.
bottomBar — The item is placed in the bottom toolbar. It is available only on iOS.
navigationBarLeading — The item is placed in the leading area of the navigation bar. It is available only on iOS and macOS.
navigationBarTrailing — The item is placed in the trailing area of the navigation bar. It is available only on iOS and macOS.
.previewContext (NEW in 2.0)
When you want to preview one of the widgets that you can make with the new WidgetKit framework, you run into a problem. The previews that might work for other SwiftUI views show a full-screen app on a device, or a custom size and shape if you choose .previewLayout(.fixed(width: 300, height: 300))
.
Instead of us needing to manually choose a size that matches what a widget looks like, we now have a new modifier that allows us to choose from the three sizes that widgets can be.
As far as I know, these are the only options for .previewContext
:
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewContext(WidgetPreviewContext(family: .systemMedium))
.previewContext(WidgetPreviewContext(family: .systemLarge))
I made an example that includes an actual widget, if you want to see the code for that.
My example above uses a Date
extension to get a string for the time. There isn’t much reason for this, but in a real widget you are required to have a Date
object in your TimelineEntry
structures. The rest of the View uses the environment variable \.widgetFamily
to access the value we passed to the WidgetPreviewContext
. I am able to use computed properties based on this value forbackgroundColor
and text
, so the View ends up completely different in each case.
When I tried on Xcode 12 beta 2, I was unable to use a switch statement inside the body property. When I did, even if I had the same View based on the computed properties in each case of the switch statement, I would get the ‘medium’ colour and text for each of the previews, even though they were all displayed as differently sized widgets. This might be fixed by the time you read this.
What about if you want to make your own PreviewContext
? I tried my best, but it’s somewhat difficult due to the inaccessible nature of Apple’s WidgetPreviewContext
implementation. The protocol requires that you allow subscripts from instances of a structure that conforms to PreviewContextKey
, and you return a value that matches the typealias
inside that key structure. That type could be literally anything, as it has no protocol requirements or restrictions of any kind.
When I tried to make my own PreviewContext, I wanted the ability to make widgets way too big. Widgets have a maximum size, so I thought it would be interesting to try and display them larger than they could ever be. To do this, I used an enum, just like the WidgetFamily
. The enum has a computed property called size
, and this will be used to define the size of my widget previews. Instead of using the subscript, I decided to just make a new version of the previewContext
modifier.
After all, we have no way of knowing how the original modifier actually works.
My custom version of .previewContext
optionally binds the custom value passed to it, casting it to the my LargePreviewContext
type. When this succeeds, I get the size for the preview from the Custom enum case stored there, and return the view with a custom preview of that size and shape. If the optional binding fails, I simply use the regular version of previewContext.
You might notice that my PreviewProvider
at the bottom uses both kinds of previewContext
. Using the system version, my custom PreviewContext
is completely ignored. Using my custom version, it is made successfully. This was my attempt at replicating the basic underlying principles of these protocols. It may be that Apple doesn’t store any size information in these structures, as accessing the WidgetFamily
enum case inside might provide the system with enough information to provide the size from elsewhere.
However it works under the surface, .previewContext
is a mysterious new capability that might lead to more ways to customise previews in the future.
If you figure out a better way to make use of it, let me know!
.userActivity and .onContinueUserActivity (NEW in 2.0)
Now that we can make make SwiftUI apps without an App Delegate, we need to be able to add the functionality that would usually be there. That is, assuming we do not take steps to add App Delegate to a SwiftUI app. The .onOpenURL
modifier is anexample of another closure that takes an action when the app resumes.
The main use for .onContinueUserActivity
that I found was opening the app from a Spotlight search. The example below creates a list displaying 10 UUIDs. These unique identifiers will be used to identify each item when they become searchable. When the app starts, the UUIDs are generated and saved to UserDefaults
.
Only when a Button
in the List
is tapped is the UUID indexed, creating a CSSearchableItem
and assigning it a CSSearchableIndex
.
If this function prints “saved successfully”, the UUID you chose has been indexed.
I found it difficult to get my UUIDs to appear in search, but that might be because I didn’t add search terms. To do this, use the keywords
property of your CSSearchableItemAttributeSet
, which is just an optional array of strings. Without doing that, I managed to still find my UUIDs by searching the name of my app. Although the app itself will come up, you will also see an option to search within the app, shown as a search result with your your app’s name next to a magnifying glass icon.
Tapping this should bring up any UUIDs you have indexed.
When you tap an item that has been indexed, the app will open. Without an App Delegate, how do we know what to do when it does? The .onContinueUserActivity
modifier does exactly that, giving a function that will run when any search activity opens the app. I didn’t need to pass a function though, as I could easily have used a trailing closure instead. The important thing is that I optionally bind the ID of the item so that I can be sure I have a UUID.
I set the UUID to a State
property and present an alert that displays that data for you to see. The UUID should match the description of the item you selected in search.
I have provided an empty implementation of the version of .onContinueUserActivity
that takes a string for activityType
instead, although I was unable to get it working.
The .userActivity
modifier “advertises” an NSUserActivity.
You can see that this updates when the UUID changes. In my case, I found there was a long delay of about 30 seconds before the change was printed here. This is when your activity, whatever it is, would be available for handoff if that was implemented.
In any case, the important thing to know about these modifiers is that they relate to NSUserActivity, Handoff and Spotlight Search.
.tabItem (Updated in 2.0)
There isn’t a lot to say about this one, so I’ll post Apple’s example with the addition of the @available
attribute at the top.
Notice anything?
TabView
, along with the modifier .tabItem
that allows you to create the icon that represents that page on the tab bar, is new to WatchOS. Although it was available on Mac, iOS, iPadOS and tvOS last year, it has only just come to the Watch this year. What form could it possibly take, you might ask? It resembles a UIPageViewController
from UIKit, with each page requiring you to swipe horizontally from one to the other. The although the .tabItem
modifier exists, neither the Text
nor the Image
that Apple’s example provides are visible.
Instead we get dots, much in the same way that UIPageViewController
makes use of a UIPageControl
, which Apple describes as "a horizontal series of dots, each of which corresponds to a page in the app’s document or other data-model entity.”
.contextMenu (Updated in 2.0)
Like .tabItem
above, .contextMenu
was new last year but has come to a new platform this year, and like .tabItem
above, I've used one of Apple’s examples. This time I put in a little more effort, as adapting their example for tvOS actually requires that the Text
be made .focusable
, which is covered in detail earlier in this post. Once a View can become the focused element on the screen, it can also receive a long press gesture. If it has a .contextMenu
modifier, this brings up the list of options you provided to it.
The fact that I was able to use a Text
means that you don’t need a Button
to do this, but as I said the View you use must use the .focusable()
modifier for this to work.
Here’s how I did it.
.navigationTitle and .navigationSubtitle (NEW in 2.0)
I can’t do much better than Apple’s official documentation for navigationTitlethis time:
A view’s navigation title is used to visually display the current navigation state of an interface. On iOS and watchOS, when a view is navigated to inside of a navigation view, that view’s title is displayed in the navigation bar. On iPadOS, the primary destination’s navigation title is reflected as the window’s title in the App Switcher. Similarly on macOS, the primary destination’s title is used as the window title in the titlebar, Windows menu and Mission Control.
And here’s Apple’s official documentation for navigationSubtitle:
A view’s navigation subtitle is used to provide additional contextual information alongside the navigation title. On macOS, the primary destination’s subtitle is displayed with the navigation title in the titlebar.
My example goes through the options for navigationBarTitleDisplayMode
with a Picker
, so you can see what they all look like. I restricted this example to iOS, because NavigationBarItem.TitleDisplayMode
options are compatible with macOS.
.navigationViewStyle (Updated in 2.0)
WatchOS now has the ability to use .navigationViewStyle
, but it seems the only provided value for it is StackNavigationViewStyle
. The only other option on any platform isDoubleColumnNavigationViewStyle
, and you can bet that's not coming to WatchOS any time soon!
.navigationBarTitle (Deprecated in 2.0)
There is, in fact, no navigation bar on macOS, which is one of the reasons why the more generic .navigationTitle
is replacing .navigationBarTitle
. Not all of the pages of the documentation where .navigationBarTitle
appears show it as deprecated, but the one for Text
does. It seems like there may be mistakes where it isn’t deprecated everywhere even though it should be. For instance, it is currently not deprecated on Mac Catalyst 13.0, but this seems to make little sense if it’s deprecated on iOS 13.0.
.navigationBarItems (Deprecated in 2.0)
To add a Button
to the leading or trailing positions of a navigation bar, use a toolbar with ToolbarItem(placement: .navigationBarLeading)
and/or a ToolbarItem(placement: .navigationBarTrailing)
.
Styles on iOS, iPadOS, Mac Catalyst and tvOS (NEW in 2.0)
Here’s an example that uses both .labelStyle and .indexViewStyle, which are both new in 2020 and unavailable on macOS:
Here are the options for PageIndexViewStyle
, which is the only thing you can pass to .indexViewStyle
at the moment:
.automatic
: Background will use the default for the platform.interactive
: Background is only shown while the index view is interacted with.always
: Background is always displayed behind the page index view.never
: Background is never displayed behind the page index view
I couldn’t get them to work, but here they are! These styles relate to the window that is presented, so I can only assume that it’s the window you present in front of your View. The .groupBoxStyle
modifier is new, but we can only use DefaultGroupBoxStyle
with it unless we make our own custom ones.
The names of these are at least clear enough that you can imagine roughly what they do, even if I couldn’t give a working example.
Next Steps
SwiftUI is only a year old as I’m writing this, and there are already a wealth of resources out there. My writing would not be possible without the following websites:
As I said at the start of the post, If you have requests for more detail on a subject, or if you think I’ve made a mistake, let me know in a response below.
Thanks for reading!
댓글 없음:
댓글 쓰기