Download as pdf or txt
Download as pdf or txt
You are on page 1of 115

3?J$F;Q=$!$>@==$O=OK=@U?HLR$P<?@R$L=><$<FAP$O?H<FV$*AIH$J9$>?@$'=:AJO$;H:$I=<$;H$=T<@;$?

H=

!"#$%&'()#!#$*+,-!.,$/$0&%.'#1!2!,&1$3&.45#$6##1$+2,!,17$-&8

!"#$%&'()&*+(,-+.%
.9:;<=:$>?@$A&*$BCD$A(;:&*$BCD$+;<EF&*$GD$;H:$';E&*$6AI
*J@

8?K$*<J@I=?H -?LL?M
2JI$C$·$CN$OAH$@=;:

2$9F?<?$?>$P?O=$QA=MP$;H:$E?H<@?LP$KR$2LK@=EF<$-A=<S$>@?O$(AT;K;R

!"##"$
!"##"$
At the start of 2020, I wrote a long Medium post called The
%$&'$())*+'
Complete
2:QAE=$>?@ SwiftUI Documentation You’ve Been Waiting For.
9@?I@;OO=@PV

This was my way of sharing what I learned when I tried to Gll in


-?LL?M

the gaps left by the insuIcient documentation provided by


Apple. Although my post seemed to help a lot of people, I also
wrote
WXC it six months late.

B
Now that Apple’s 2020 developer conference is over, SwiftUI
has been given some new capabilities, so hopefully this update
will make my documentation more helpful than ever before.

This will be released as a series, with one chapter per post.

The names of these chapters correspond with the chapter


names in Apple’s SwiftUI documentation.

I can guarantee that none of them will be as long as this one, if


that’s something you’re worried about!

Views and Controls

App Structure and Behavior

View Layout and Presentation

Drawing and Animation

Framework Integration

State and Data Flow

Gestures
Preview

I encourage you to contact me in a response below or on my


Twitter proGle if you spot any mistakes or a subject you think I
should cover in more detail.

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

,-"./*"0.%$&#&1&2
If you aren’t already aware, SwiftUI uses the View protocol to
create reusable interface elements. Views are value types,
which means they use a Struct instead of a Class deGnition.

What does this actually mean, in practice?

Structs do not allow inheritance. Although your structs


conform to the View protocol, they do not inherit from a base
class called View that Apple has provided.
This makes it diTerent from UIView , from which almost

everything in UIKit inherits. A UIView basically cannot be seen


without being assigned a frame and being added as a subview
of a UIViewController subclass.

If you create a new Xcode project that uses SwiftUI instead of


Storyboard as the basis of its user interface, you’ll automatically
be given an example of a SwiftUI View called ContentView .

You’ll notice that inside the ContentView struct, there is a


variable called body. This is the sole requirement of the View

protocol, and it makes use of the some keyword, which is brand


new to Swift 5.1.

You can rely on a Stack Over[ow thread to explain what this


keyword means better than I ever could:

“You can think of this as being a “reverse” generic placeholder.


Unlike a regular generic placeholder which is satis;ed by the
caller… An opaque result type is an implicit generic placeholder
satis;ed by the implementation… The main thing to take away
from this is that a function returning some P is one that returns a
value of a speci;c single concrete type that conforms to P .”

3/*"0!4*25"$
This is a kind of function builder that allows you to construct a
single View from multiple Views. If you add this attribute above
your View body, cmd-click it, and choose Jump to deGnition,
you’ll see a bunch of interesting stuT. Perhaps the most
important part is this:

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

This is the function that runs when you put ten Views inside a
VStack to lay them out vertically. The fact that there isn’t a
buildBlock that takes eleven Views is the reason you can’t keep
adding children to a VStack indeGnitely. There’ll be more about
this in the View Layout and Presentation chapter of this
documentation, but I’m mentioning it for a speciGc reason.

In Xcode 12, the body property is assumed to be


@ViewBuilder .

Why does this matter? Now you can put up to ten Views as
children directly in your body property. Previously, this was
only possible by putting your Views inside a Group , which is a

way to get the beneGts of @ViewBuilder without the layout


implications of VStack , HStack , or ZStack . This is no longer

necessary in the new version of SwiftUI, and the uses of Group

will become more niche as a result.

You should still probably lay your Views out in a VStack or


HStack , as putting them directly in a body property is

ambiguous when you come to use it.

/*"0.6&5*7*"$8
Before we dive into the Views that are new in 2020, let’s do a
refresher on what View ModiGers are. They’ll be shown
alongside the new Views, so it doesn’t really make sense to wait
to explain what they are until later.

All Views can be modiGed by structs that conform to the


ViewModiGer protocol. All the protocol requires is a function
called body (content: Content) that returns a generic View.
Because View cannot be created directly, the type that we pass
to the ViewModiGer is unknown until the modiGer is called.
Content acts as a proxy for that concrete type. The return type,
like the body of any View, is inferred from the implementation.

Let’s see an example of a custom modiGer so you can see what’s


happening under the surface with these:

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 VStack {
6 Text("Blue")
7 .foregroundColor(.blue)
8 .padding(10)
9 Text("Blue")
10 .modifier(BluePaddingModifier(padding: 10))
11 Text("Blue")
11 Text("Blue")
12 .blue(10)
13 Text("Blue")
14 .b(10)
15 }
16 }
17 }
18
19 struct BluePaddingModifier: ViewModifier {
20 let padding: CGFloat
21 func body(content: Content) -> some View {
22 content
23 .foregroundColor(.blue)
24 .padding(padding)
25 }
26 }
27
28 extension View {
29
30 func blue(_ padding: CGFloat) -> some View {
31 let modifier = BluePaddingModifier(padding: padding)
32 return ModifiedContent(content: self, modifier: modifier)
33 }
34

As you can see, it would be possible to add


.modifier(YourModifier()) to call a ViewModiGer, but it makes
a lot more sense to use a View extension and give a clean call
site.

The Grst method, blue(_:) , does essentially what

.modifier(YourModifier()) does. By constructing


ModifiedContent , we end up with a View that has the modiGer
applied. But we can make this less complicated by calling the
instance method .modifier(YourModifier()) on our View to get
the same result.
The second method, b(_:) , removes the need to call

.modifier(YourModifier()) every time we need it. This is the


way the standard modiGers look, and it reduces the amount of
code inside our View .

Obviously b(_:) is a pretty terrible name for a property,


method or ViewModifier , but I made the identiGers gradually

shorter to indicate which one is the simplest.

First we’ll see all of the Views that are completely new in 2020,
then the Views from 2019 that have been updated this year.

Then, we’ll see what View ModiGers are new or updated at the
end.

9:;.*+.<=>?.@&2&$%*1A"$
There has never been a colour picker included for iOS
developers. I’ve used third-party ones before, but dependencies
lead to a reliance on other developers to ensure compatibility
with all your future projects and deployment targets. In the
Gnal weeks before WWDC, I Gnally decided to create every kind
of colour picker control I could think of in SwiftUI. This would
allow me to create colour pickers from a Swift Package of these
controls and make a colour picker for any project.

Then WWDC came along, and now we have ColorPicker for iOS
14. This new control seems very capable and has features such
as an eyedropper that allows you to pick colours from
anywhere. You can even use the eyedropper to pick colours
from the ColorPicker’s UI itself, so it seems clear that this is a
powerful new capability that we get for free. But as far as I can
work out, the new colour picker has a very rigid set of controls
that cannot be changed. Unless the oIcial documentation has
not been updated yet, it would appear that there is no way to
change what the ColorPicker oTers, such as restricting it to only
a canvas, a palette or only sliders.

Instead, these three options are selected with a segmented


picker by the user.

1 struct ColourPickerView: View {


2 @State private var bgColor =
3 Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2)
4
5 var body: some View {
6 VStack {
7 ColorPicker("Alignment Guides", selection: $bgColor)
8 .frame(width: 500, height: 1000)
9 }
10 }
11 }

For some useful screenshots and animated GIFs of how the


ColorPicker will look in your app, check out Using a
ColorPicker with SwiftUI.

9:;.*+.<=>?.BC$*#"/*"0
SpriteKit is Apple’s framework for making 2D games. Sprites
are small bitmaps that are used to represent players, enemies,
and projectiles, among other things. Since you may have many
sprites on the screen at once in a game, it makes sense to use
tools that are designed to do this with performance in mind. It
is now possible to create a SwiftUI View that will show a
SKScene from SpriteKit, allowing you to create a game and then
put that game anywhere you would put a SwiftUI View.
In my example, I have a simple square sprite that Gres
projectiles at enemies that come from the right. If they get all
the way to the left, you lose a life. If you take one of them
down, your score increases.

Let’s look at the SwiftUI Grst, which requires a SpriteKit scene


that we’ll create later.

1 import SwiftUI
2 import SpriteKit
3
4 class GameModel: ObservableObject {
5 static let startLives = 5
6 static let startScore = 0
7 static let shared = GameModel()
8 @Published var lives = startLives
9 @Published var score = startScore
10 @Published var gameOver = false
11 @Published var restartGame = false {
12 didSet {
13 if restartGame {
14 gameOver = false
15 lives = GameModel.startLives
16 score = GameModel.startScore
17 }
18 }
19 }
20 }
21
22 struct ContentView: View {
23 @ObservedObject var data = GameModel.shared
24 @State var scene = GameScene(size: CGSize(width: 300, height: 400))
25
26 var body: some View {
27 VStack {
28 HUDView(data: data)
29 SpriteView(scene: self.scene)
30 .onChange(of: data.restartGame) { shouldRestart in
31 data.restartGame = false
32 scene.restart()
33 }
34 }
35 }
36 }
37
38 struct HUDView: View {
39 @ObservedObject var data: GameModel
40 @AppStorage("highScore") var highScore = 0
41 var body: some View {
42 HStack {
43 Text("Score: \(data.score)")
44 Text("High score: \(highScore)")
45 if self.data.gameOver {
46 Button("Restart") {
47 if data.score > highScore {
48 highScore = data.score
49 }
50 data.restartGame = true

We have an ObservableObject called GameModel that stores our


data, and a ContentView struct that displays our game. At the
top of a VStack we’re displaying the HUDView , which tells us

what the score currently is, how many lives we have, and what
high score has been previously recorded. When we run out of
lives, the restart button appears. All this does is alter a
@Published property in the GameModel object that we are
observing with the new .onChange modiGer in ContentView .

Essentially we’re saying when restartGame is true, we want to


send a message to our GameScene that it should unpause and
reload the game from the beginning.

The high score is recorded using the @AppStorage property


wrapper, which saves data to UserDefaults in a convenient
way. I will give more information about the new property
wrappers in another chapter of this documentation, but the
important thing is that properties with this wrapper are saved
persistently, and can be recalled easily the next time the app is
loaded. When the player is out of lives, there is also a game over
state, that requires the player to tap a restart button in order to
begin the game again. This resets the score, removes the
enemies, and restarts the spawning of enemies as the beginning
of the game did.

Here’s the SpriteKit code:

1
2 import SpriteKit
3
4 class GameScene: SKScene, SKPhysicsContactDelegate {
5 static let projectileSize = CGFloat(10)
6 static let playerSize = CGFloat(15)
7 static let enemySize = CGFloat(30)
8 let data = GameModel.shared
9
10 var playerPosition: CGPoint {
11 CGPoint(x: size.width * 0.1, y: size.height * 0.5)
12 }
13
14 override func didMove(to view: SKView) {
15 physicsWorld.contactDelegate = self
16 startGame()
17 }
18
19 func startGame() {
20 addWalls()
21 addPlayer()
22 run(SKAction.repeatForever(SKAction.sequence([SKAction.run(addEnemy), SKAction.
23 }
24
25 func restart() {
26 removeAllChildren()
27 self.scene?.view?.isPaused = false
28 startGame()
28 startGame()
29 }
30
31 func addPlayer() {
32 let player = SKSpriteNode(color: UIColor.red, size: CGSize(width: Self
33 player.position = playerPosition
34 addChild(player)
35 }
36
37 func addWalls() {
38 let wallFrames = [
39 CGRect(x: 0, y: frame.midY, width: 10, height: size.height),
40 CGRect(x: size.width, y: frame.midY, width: 10, height: size.height
41 CGRect(x: frame.midX, y: size.height, width: size.width, height: 10
42 ]
43 wallFrames.forEach {
44 let wall = SKSpriteNode(color: UIColor.orange, size: $0.size)
45 wall.position = CGPoint(x: $0.minX, y: $0.minY)
46 wall.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: wall.size
47 wall.zPosition = 1
48 wall.physicsBody?.isDynamic = false
49 addChild(wall)
50 }
51 }
52
53 func didBegin(_ contact: SKPhysicsContact) {
54 guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node
55 if [nodeA.name, nodeB.name].contains("projectile") && [nodeA.name, nodeB.
56 [nodeA, nodeB].forEach {
57 $0.removeAllActions()
58 $0.name = "projectile"
59 $0.physicsBody?.affectedByGravity = true
60 $0.run(SKAction.sequence([SKAction.wait(forDuration: 2), SKAction.
61 }
62 self.data.score += 1
63 }
64 }
65
66 func random() -> CGFloat {
67 return CGFloat(Float(arc4random()) / 4294967296)
68 }
69
70 func random(min: CGFloat, max: CGFloat) -> CGFloat {
71 return random() * (max - min) + min
72 }
73
74 func addEnemy() {
75 let enemy = SKSpriteNode(color: UIColor.blue, size: CGSize(width: Self
76 let actualY = random(min: Self.enemySize / 2, max: size.height - Self
77 enemy.position = CGPoint(x: size.width + Self.enemySize, y: actualY)
78 enemy.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: Self.enemySize
79 guard let physicsBody = enemy.physicsBody else {
80 return
81 }
82 physicsBody.affectedByGravity = false
83 physicsBody.contactTestBitMask = physicsBody.collisionBitMask
84 enemy.name = "enemy"
85 addChild(enemy)
86 let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
87 let actionMove = SKAction.move(to: CGPoint(x: -enemy.size.width/2, y
88 duration: TimeInterval(actualDuration))
89 let actionMoveDone = SKAction.removeFromParent()
90 enemy.run(SKAction.sequence([actionMove, actionMoveDone]), completion
91 if self.data.lives > 0 {
92 self.data.lives -= 1
93 }
94 if self.data.lives <= 0 {
95 self.data.gameOver = true
96 self.scene?.view?.isPaused = self.data.gameOver
97 }
98 })
99 }
100
101 override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent
102 guard let touchLocation = touches.first?.location(in: self) else {

Don’t worry too much about the logic of the game, as this is a
SpriteKit game written in Swift. If you don’t know much about
SpriteKit, as I clearly don’t, there are many tutorials that will
help you to get started.

The important thing to know about SpriteView is it gives you


an easy way to embed 2D games in your SwiftUI.
9:;.*+.<=>?.,"D#:5*#&$
Before WWDC 2020, we were only able to handle text editing in
iOS with TextField or SecureTextField . These are basically the

same text Geld, only the SecureTextField obscures what you


are typing by replacing the characters with black circles as any
password Geld would. The important similarity between these
text Gelds is that they only allow a single line. This meant that
the only option for multiline editing was to use
UIViewRepresentable to convert UITextView from UIKit:

1 struct ContentView : View {


2 @State private var text = ""
3
4 var body: some View {
5 MultilineTextView(text: $text)
6 }
7 }
8
9 struct MultilineTextView: UIViewRepresentable {
10 @Binding var text: String
11
12 func makeUIView(context: Context) -> UITextView {
13 let view = UITextView()
14 view.isScrollEnabled = true
15 view.isEditable = true
16 view.isUserInteractionEnabled = true
17 return view
18 }
19
20 func updateUIView(_ uiView: UITextView, context: Context) {
21 uiView.text = text

This is relatively complicated, but it also allows many more


properties. UITextView allows changes to dataDetectorTypes ,
which create tappable URLs from the typed text. The current
text can be replaced by new text using clearsOnInsertion , and

we can scroll until a speciGed string is visible by calling


scrollRangeToVisible . For more information, check out the

UITextView documentation.

Although we don’t have access to these properties, the new


TextEditor is a big upgrade from TextField .

We can now create a multiline TextEditor just as easily as we


create a single line TextField .

1 struct TextEditingView: View {


2 @State private var fullText: String = "This is some editable text..."
3
4 var body: some View {
5 TextEditor(text: $fullText)
6 }
7 }

Anything that can be applied to Text can be applied to


TextEditor . When I tried to use the new Dynamic Type syntax

with a custom font, the text didn’t seem to scale according to


the TextStyle . This could be a bug with the Grst beta or a

problem with the way I did it. Either way, the best example I
could make of the capabilities of TextEditor was to allow
changing the font size with a Stepper and the font-weight with
a Picker . A ColorPicker can be used to select the foreground
(font) colour, but be aware that the background does not seem
to work at the moment.
1 import SwiftUI
2
3 enum WeightType: String, CaseIterable {
4 case black, bold, heavy, light, medium, regular, semibold, thin, ultraLight
5
6
7 var weight: Font.Weight {
8 switch self {
9
10 case .black:
11 return .black
12 case .bold:
13 return .bold
14 case .heavy:
15 return .heavy
16 case .light:
17 return .light
18 case .medium:
19 return .medium
20 case .regular:
21 return .regular
22 case .semibold:
23 return .semibold
24 case .thin:
25 return .thin
26 case .ultraLight:
27 return .ultraLight
28 }
29 }
30 }
31
32 struct TextEditorView: View {
33 @State private var text: String = ""
34 @State private var fontWeight = WeightType.regular
35 @State private var fontSize = CGFloat(12)
36 @State private var customFont = true
37 @State private var foregroundColor = Color.primary
38 @State private var colourPickerShown = true
39 var body: some View {
40 ScrollView(.vertical) {
41 VStack {
42 Picker(selection: $fontWeight, label: EmptyView()) {
43 ForEach(WeightType.allCases, id: \.self) { fontWeight in
44 Text(fontWeight.rawValue)
45 }
45 }
46 }
47 .frame(height: 130)
48 .offset(y: -25)
49
50 Stepper(value: $fontSize, in: 1...100) {
51 Text("Font size: \(String(format: "%.1f", fontSize))")
52 }
53 HStack {
54 Button("Delete all") {
55 self.text = ""
56 }
57 .padding()
58 .background(Color.secondary)
59 .foregroundColor(.white)
60 .cornerRadius(5)
61 ColourPickerButton(dismissSheet: true, buttonTitle: "Text colour
62 RoundedRectangle(cornerRadius: 5)
63 .aspectRatio(1, contentMode: .fit)
64 .foregroundColor(foregroundColor)
65
66 }
67 .frame(height: 50)
68 TextEditor(text: $text)
69 .font(.system(size: fontSize, weight: self.fontWeight.weight))
70 .foregroundColor(foregroundColor)
71 .frame(height: 300)
72 .padding()
73 .border(Color.secondary, width: 4)
74
75 }
76 .padding(.horizontal)
77 }
78 }
79 }
80
81
82 struct ColourPickerButton: View {
83 let dismissSheet: Bool
84 let buttonTitle: String
85 let previousColour: Color
86 @Binding var colour: Color
87 @State var colourInitialised = false
88 @Binding var colourPickerShown: Bool
89 var body: some View {
90 Button(buttonTitle) {
91 colourPickerShown.toggle()
92 }
93 .padding()
94 .background(Color.secondary)
95 .foregroundColor(.white)
96 .cornerRadius(5)
97 .onChange(of: colour) { _ in
98 if dismissSheet && colour == previousColour {
99 self.colourPickerShown.toggle()
100 }
101 }
102 .sheet(isPresented: $colourPickerShown) {

TextEditor seems to have an opaque background of the


systemBackground colour, and adding a background to it just
puts a background behind this.

Your background will not be visible at all.

There is a workaround, which is probably all we can do for


now:

Natalia Panferova
@natpanferova

Looks like with the current version of #SwiftUI TextEditor on


iOS 14, we can only set the background color by going via the
appearance property on UITextView. May be it will change in
the future updates?
11:20 AM · Jul 12, 2020

91 See Natalia Panferova’s other Tweets

TextEditor does support a border, though, as shown in the


screenshot below.
9:;.*+.<=>?.B*'+E+;*#-FCC2"!4##&+
Sign in with Apple was introduced in iOS 13 as a way to
securely sign in to apps without having to give an email and
password. Sign in with Apple uses biometrics to authenticate
that you are the person who owns your Apple ID, and then
sends the app an automatically generated forwarding address
and password to the app.

!FAP$H=M$KJ<<?H$P<@=;OLAH=P$<F=$9@?E=PP$?>$;:?9<AHI$*AIH$AH$MA<F$299L=$AH$R?J@$;99

The following example displays the SignInWthAppleButton at


the maximum size its constraints allowed (width ≤ 375). You
heard right: this View has constraints. It seems that Apple just
wrapped the existing ASAuthorizationAppleIDButton
in UIViewRepresentable , which is exactly what we needed to do
to get the button into SwiftUI last year. But if you see what steps
were required to implement sign in with Apple in iOS 13, we
are spared the extra task of setting up the delegate protocols in
the coordinator.

1 import SwiftUI
2 import AuthenticationServices
3
4 struct SignInWithAppleView: View {
5 @State var output = ""
6
7 func output(_ result: Result<ASAuthorization, Error>) {
8 switch result {
9 case .success (let authResults):
10 self.output = "Authorization successful\n\n\(authResults)\n\(authResults.
11 case .failure (let error):
12 self.output = "Authorization failed: \(error.localizedDescription)"
13 }
14 print(self.output)
15 }
16
17
18 var body: some View {
19 VStack {
20 Text("Tap the button to authenticate.\nYou must be signed into an Apple ID on t
21 .foregroundColor(.black)
22 .lineLimit(nil)
23 SignInWithAppleButton(
24 .signIn,
25 onRequest: { request in
26 request.requestedScopes = [.fullName, .email]
27 },
28 onCompletion: { result in
29 output(result)
30 })
31 .frame(maxWidth: 375)
31 .frame(maxWidth: 375)
32 .aspectRatio(7, contentMode: .fit)
33 Text(output)
34 .foregroundColor(.black)
35
36 }

My version, adapted from Apple’s example, outputs the result


of the authentication to a Text in the app as well as printing it.
You might notice that the sign-in button doesn’t do anything
the Grst time you try; that was certainly my experience.

Tapping a second time should show the output as expected.

%$&'$"88/*"0.G9:;.*+.<=>H
UIActivityIndicatorView is a UIKit control that allows you to
show a spinner for a loading state that is indeterminate. We
were unable to use this directly without wrapping it in
UIViewRepresentable, but now we have an equivalent!
Constructing ProgressView without any parameters causes it to
display a spinner, but passing it a progress value allows it to be
shown as a horizontal progress bar.
The progress bar form resembles the UIProgressView, which is
often used in combination with WKWebView when a webpage is
loading.

1 struct ProgressingView: View {


2 @State private var progress = 0.5
3
4 var body: some View {
5 VStack(spacing: 15) {
6 ProgressView()
7 ProgressView("Downloading", value: progress, total: 1)
8 .progressViewStyle(CircularProgressViewStyle(tint: .accentColor))
9 ProgressView(value: progress)
10 .accentColor(.red)
11 Slider(value: $progress, in: 0...1)
12 }
13 }

You may notice that the middle example has a name, a value,
and a total parameter. Normally this would cause the
ProgressView to be displayed as a horizontal progress bar, like
the bottom example. However, you’ll notice that I have applied
the .progressViewStyle modiGer, passing it to the
CircularProgressViewStyle with a tint that matches the default
accentColor . Instead of being a progress bar, the result is a blue

circular spinner.

This is an illustration of the fact that while the “Downloading”


label is preserved when a style is applied, the ProgressView

must become circular, and this cancels out the automatic


behaviour.

I(4'"/*"0.G9:;.*+.<=>H

GaugeView is the only new View that is exclusive to WatchOS 7.


This is a relatively simple indicator for showing where a value is
on a scale. In my example below, I have also added a Slider for
changing the value that the Gauge is displaying. This looks a bit
strange, as the Slider actually displays where the current value
is on its own blue bar. Styling a Gauge is pretty diIcult. The
default gauge style, which is also called LinearGaugeStyle , uses

Color.primary as its foreground colour, and using the


.foregroundColor or .accentColor modiGers will not change
this. Similarly, the CircularGaugeStyle uses Color.gray , and
this cannot be changed.

1 import SwiftUI
2
3 struct GaugeView: View {
4 let sliderValue: Double
5 var body: some View {
6 VStack {
7 Gauge(value: sliderValue, in: 0...1) {
8 Text("Gauge")
9 }
10 .frame(maxHeight: .infinity)
11 }
12 }
13 }
14
15 struct ContentView: View {
16 @State var isCircular = false
17 @State var sliderValue = Double()
18 var body: some View {
19 VStack {
20 Toggle(isOn: $isCircular) {
21 Text("Circular")
22 }
23 if isCircular {
24 GaugeView(sliderValue: sliderValue)
25 .gaugeStyle(CircularGaugeStyle())
26 }
27 else {
28 GaugeView(sliderValue: sliderValue)
29 .gaugeStyle(LinearGaugeStyle())
30 }
31 Text("\(sliderValue)")
32 Slider(value: $sliderValue, in: 0...1) {
33 Text("Slider")
34 }

The thumb or circle that indicates where the current value is on


the Gauge looks like a mask, at least in the case of
LinearGaugeStyle . Adding .background(Color.blue) changes
the colour of the circle.

J(K"2.G9:;.*+.<=>H

1 struct LabelView: View {


2 var body: some View {
3 VStack {
4 Label("Games", systemImage: "gamecontroller")
5 HStack {//Roughly equivalent to
6 Image(systemName: "gamecontroller")
7 Text("Games")
8 }
9 }
10 }
11 }

This is a relatively simple way to combine a symbol from


Apple’s now even larger SF Symbols collection of free icons
with some text that gives greater context. In my example, I am
comparing a Label with the equivalent using an HStack . There

was a bug in the Grst Xcode 12 beta that caused the alignment
of a Label 's Image to be misaligned with the text, but this has
now been Gxed. There are two options for the .labelStyle , one

of which shows only the Text and one of which only shows the
Image .

J*+A.G9:;.*+.<=>H
I always thought it was a bit of a shame that hyperlinks were
not possible in iOS apps. Of course, you can create a button
with blue text that opens a URL, but this requires more code
than you necessarily want to write every time. In this example,
I’m using the convenience initialiser of Button that only takes a
title string (or localized string key), as this is as small as I could
get the code in its original form.

1 struct LinkStyleView: View {


2 let urlString = "https://medium.com/better-programming/the-complete-swiftui-documen
3
4 var body: some View {
5 Group {
6 if URL(string: urlString) != nil {
7 //The old way to create a Link-style Button
8 Button("Read more") {
9 if let url = URL(string: urlString) {
10 UIApplication.shared.open(url, options: [:], completionHandler
11 }
12 }
13 }
14 else {
15 EmptyView()
16 .onAppear { assertionFailure("URL was nil") }
17 }
18 }

In iOS 14, we now have Link , which does the action part of the

Button above for us. I didn’t like how Apple’s documentation


unsafely unwraps a URL using the ‘!’ operator, because this is an
extremely bad practice that I’m surprised they would
encourage in a code sample. Sure, they may know that this
particular URL is created successfully because they link to
example.com/TOS.html, a site owned by the Internet Assigned
Numbers Authority (IANA) that convert URLs to IP addresses.
But if you leave it up to your human certainty that a URL string
is valid, sooner or later you’re going to make a mistake.

Any app that unexpectedly Gnds nil when unsafely unwrapping


an optional will crash instantly.

That’s why my example above takes a few more lines than


Apple’s example, but it does it safely. This example is really
hampered by the lack of optional binding ( if let or guard

let ) in the Grst version of SwiftUI, as I am instead restricted to

comparing my URL to nil to ensure it exists. When this


comparison conGrms that the URL is not nil, this still doesn’t
mean I can use it in the Button without unwrapping it Grst.
This is why there’s a slightly confusing additional step in the
Button action, which optionally binds the URL to ensure that it
is not nil.

I could have put the assertionFailure in an else statement


after the if let in the Button action, but I wanted to add the
EmptyView for consistency with my Link example. An else
statement containing EmptyView is not required, as any if
statement around the only occupant of a ViewBuilder closure
will return EmptyView when the if condition is false. But I
wanted to add this explicitly to show what would happen if our
URL was nil. The user would see nothing, but an assertion
would be triggered for the developer in debug mode.

This would allow us to be aware that the URL was nil, but
without causing a crash for the end-user.
1 struct LinkView: View {
2 let urlString = "https://medium.com/better-programming/the-complete-swiftui-documen
3
4 var body: some View {
5 Group {
6 if let url = URL(string: urlString) {
7 //The new way to create a Link
8 Link("Read more", destination: url)
9 }
10 else {
11 EmptyView()
12 .onAppear { assertionFailure("URL was nil") }
13 }
14 }
15 }

Now that SwiftUI supports if let, I can directly create properties


like the URL and create Views that use that data. Just as before,
the link is only shown when the URL can be created, but we
don’t need to do multiple checks just to make sure that’s the
case.

6"+4.G9:;.*+.<=>H

!F=$:=>;JL<$P<RL=$AP$6?@:=@=:6J<<?H'=HJ*<RL=

MenuButton has been replaced by Menu . The original was a


drop-down menu, and the replacement isn’t too diTerent. The
main thing the new name adds is clarity, as we are talking
about a menu and the items inside it, so calling it a button
doesn’t make much sense. Menu comes with a few diTerent
options for styles. The default style is BorderedButtonMenuStyle ,

which is why the DefaultButtonMenuStyle on the right looks the


same, while the BorderlessButtonMenuStyle in the middle looks
diTerent.

1 import SwiftUI
2
3 @available(OSX 10.16, *)
4 @available(iOS, unavailable)
5 @available(tvOS, unavailable)
6 @available(watchOS, unavailable)
7 struct MenuView : View {
8 @State var selectedOption = "Select an option"
9 var body : some View {
10 HStack {
11 Menu(selectedOption) {
12 Button(action: {self.selectedOption = "Option 1"}) {
13 Text("Option 1")
14 }
15 Button(action: {self.selectedOption = "Option 2"}) {
16 Text("Option 2")
17 }
18 Button(action: {self.selectedOption = "Option 3"}) {
19 Text("Option 3")
20 }
21 }
22 .menuStyle(BorderedButtonMenuStyle())
23 Menu(selectedOption) {
24 Button(action: {self.selectedOption = "Option 1"}) {
25 Text("Option 1")
26 }
27 Button(action: {self.selectedOption = "Option 2"}) {
28 Text("Option 2")
29 }
30 Button(action: {self.selectedOption = "Option 3"}) {
31 Text("Option 3")
31 Text("Option 3")
32 }
33 }
34 .menuStyle(BorderlessButtonMenuStyle())
35 Menu(selectedOption) {
36 Button(action: {self.selectedOption = "Option 1"}) {
37 Text("Option 1")
38 }
39 Button(action: {self.selectedOption = "Option 2"}) {
40 Text("Option 2")
41 }
42 Button(action: {self.selectedOption = "Option 3"}) {
43 Text("Option 3")
44 }
45 }

Aside from the new styles, which remove any reference to the
fact that it is a “pull down” menu, the only changes are that
MenuButton is called Menu and the .menuButtonStyle modiGer is
now called .menuStyle .

In the Grst beta Menu was only available on macOS, just like the
control it replaces.

However, beta 3 added support for Menu on iOS too.

6"+4!4##&+.GL"C$"1(#"5.*+.<=>H
See Menu above, which replaces MenuButton , but serves many of

the same functions.

,"D#.GMC5(#"5.*+.<=>H
,"D#.GMC5(#"5.*+.<=>H
Text is perhaps the simplest building block for creating Views.
In most cases, you’ll be passing a String to create it, and that
will be the content it displays. The original version of this
documentation included all of the other ways to create it,
including from localisations, ObservableObjects, and
substrings. Those can be found at the bottom of this example,
but we also have many new initialisers in 2020 that are
included at the top of the example.

The Grst one is quite exciting, as it can take any generic object
and any class that inherits from Formatter . This could be any of

the Formatter types that Apple provides or, as in my example, a


Formatter that you create speciGcally for your custom type. I
wasn’t exactly sure how a Formatter is implemented, so I just
optionally bound the object to an instance of my custom class,
and returned nil in all other situations.

Note that the one that takes a LocalizedStringKey , tableName ,

bundle , and comment requires a separate Gle that uses the


.strings Gle extension. As is mentioned in Apple’s
documentation for this initializer, the only required parameter
is the string for the key. I gave a verbose example mostly so that
you can see what these other parameters require.

The default of tableName is Localizable , the standard name for

a strings Gle. I deliberately named mine Local to show why I


would need this parameter. The bundle is the main bundle by
default, so passing Bundle.main is redundant in this case. The
comment should give contextual information, but in this
example, I’ve just given it the string Comment .

1 /*
2 Separate file called Localizable.strings
3 "string_key" = "This string is in the default file";
4 Separate file called Local.strings
5 "string_key" = "This string is in another file";
6 */
7 import SwiftUI
8
9 final class DataModel: ObservableObject {
10 static let shared = DataModel()
11 @Published var string = "This is an ObservedObject string"
12 }
13
14 class MyFormatter: Formatter {
15 override func string(for obj: Any?) -> String? {
16 if let customObj = obj as? CustomType {
17 return customObj.name + "\n" + customObj.text
18 }
19 else {
20 return nil
21 }
22 }
23 }
24
25 class CustomType: NSObject {
26
27 let name: String
28 let text: String
29 init(name: String, text: String) {
30 self.name = name
31 self.text = text
32 }
33 }
34
35 struct ContentView: View {
36 @ObservedObject var data = DataModel.shared
37 let substring: Substring = "This is a substring"
38 let string = "This is a string"
39 let codeUpdated = Date(timeIntervalSince1970: 1594548848)
40 var body: some View {
40 var body: some View {
41 VStack {
42 Group {
43
44 //NEW: Use a custom type and a custom formatter to display it
45 Text(customObject, formatter: formatter)
46
47 //NEW: Add an Image with string interpolation
48 Text("\(Image(systemName: "gamecontroller")) Games")
49
50 //NEW: Add an Image with string interpolation
51 Text(Image(systemName: "gamecontroller"))
52
53 //NEW: A range between two dates
54 Text(ClosedRange<Date>(uncheckedBounds: (lower: codeUpdated, upper
55
56 //NEW: A range between a date and an added duration (1 day in seconds)
57 Text(DateInterval(start: codeUpdated, duration: 86400))
58
59 //NEW: Use the new DateStyle to specify formatting as a duration, not a date
60 Text(codeUpdated, style: .relative)
61
62 //NEW: Change the case of text
63 Text("Make this uppercase")
64 .textCase(.uppercase)
65 Text("MAKE THIS LOWERCASE")
66 .textCase(.lowercase)
67 Text("KEEP THIS THE SAME")
68 .textCase(.none)
69 }
70 Group {
71 //NEW: Font styles
72 Text("Caption 2")
73 .font(.caption2)
74 Text("Title 2")
75 .font(.title2)
76 Text("Title 3")
77 .font(.title3)
78
79 //NEW: Font designs
80 Text("Monospaced")
81 .font(.system(.body, design: .monospaced))
82 Text("Serif")
83 .font(.system(.body, design: .serif))
84 }
84 }
85 Group {
86 //This is a substring
87 Text(substring)
88
89 //This is a string
90 Text(string)
91

Text can now include an Image using interpolation, or


represent a Date . There are three new fonts: caption2 , title2

and title3 . These were available as part of UIFont, so it’s not

surprising that they’ve made it to SwiftUI. New modiGers allow


you to select whether the text is uppercase or lowercase and
whether it uses a monospaced or serif design.

I haven’t included the new way to use Dynamic Type to make


your custom fonts as accessible as the system styles. For that, I’ll
link to Hacking With Swift’s Dynamic Type guide, as I would
only be repeating what was said there.
E)('".GMC5(#"5.*+.<=>H
SF Symbols 2 brings support for use in Mac apps. Attempting to
use Image(systemNamed:) to use an SF Symbol in Xcode 11
causes the error “Extraneous argument label ‘systemNamed:’ in
call”. This means that you could not use SF Symbols in any
native Mac app or even a Catalyst app, as macOS had no way of
displaying them. Presumably, the SF Symbols Mac apps that
ran on macOS Catalina used PNG for the thumbnails for the
symbols, as they could not have been using
Image(systemNamed:) .

Anyway, as of Xcode 12 and macOS 11 Big Sur, you will not get
those warnings and can use Image(systemNamed:) in native
macOS and Mac Catalyst apps.

!4##&+.GMC5(#"5.*+.<=>H
There is now a CardButtonStyle option on tvOS, which makes a
smaller button that is less prominently coloured. The example
above shows a Button that was created without any style
whatsoever, and so it takes on the DefaultButtonStyle , so you

can see the diTerence. The new Button is much more subtle
and could be used for options that are less important than those
that are given the brighter colour.

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 VStack {
6 Button("DefaultButtonStyle") {
7 print("Default button pressed")
8 }
9 Button(action: { print("CardButtonStyle pressed")}) {
10 Text("CardButtonStyle")
11 .padding()
12 }
13 .buttonStyle(CardButtonStyle())
14 }
15 }

I had to add padding to the CardButtonStyle Button , as it for

some reason has no padding otherwise. This may change in


later betas, as I am only testing this on Xcode 12 beta 2.

%(8#"!4##&+.GMC5(#"5.*+.<=>H
%(8#"!4##&+.GMC5(#"5.*+.<=>H
This control allows you to paste information on MacOS, but it is
not available on iOS. It can take a variety of data types, which
are expressed as UTIs. To quote Apple’s documentation,
“Uniform Type IdentiGers declares common types for resources
to be loaded, saved, or opened from other apps.” In Xcode 11, it
was necessary to provide these strings in an array when
creating a PasteButton .

I’ve included a function in my example that lets you Gnd the


UTI string for any type, which will probably help you when
implementing this button.

Now we have a new structure called UTType , which makes it a

lot easier to create the types that you want to support. You can
either pass the initializer for this structure a string that
would’ve worked in Xcode 11, or you can use one of the many
system-declared types that are provided.

1 import SwiftUI
2 import UniformTypeIdentifiers
3
4 @available(OSX 10.16, *)
5 @available(iOS, unavailable)
6 @available(tvOS, unavailable)
7 @available(watchOS, unavailable)
8 struct PasteButtonView: View {
9 @State var text = String()
10 let utType = UTType.text
11 //The declaration above is equivalent to
12 //let utType = UTType("public.utf8-plain-text")!
13
14 var body: some View {
15 VStack {
16 Text(text)
16 Text(text)
17 PasteButton(supportedContentTypes: [utType], payloadAction: { array
18 guard let firstItem = array.first else {
19 return
20 }
21 firstItem.loadDataRepresentation(forTypeIdentifier: "public.utf8-plain-text
22 (data, error) in
23
24 guard let data = data else {
25 return
26 }
27 let loadedText = String(decoding: data, as: UTF8.self)
28 self.text = loadedText
29
30 //This call just shows how to find print the UTI type of any type conformin
31 self.getUTITypeString(for: loadedText)
32 })
33 })
34 }
35 .frame(width: 200, height: 200)
36 }
37 func getUTITypeString(for item: Any) {
38 if let item = item as? NSItemProviderWriting {
39 let provider = NSItemProvider(object: item)
40 print(provider)

The initializer that takes a string returns an optional, so if you


use it you have to be sure that the string you use is correct. I
have used UTType.text for my example, but I included an
example below that constructed it manually from a string. Note
that it is not recommended to force unwrap an optional using
the exclamation mark. I just wanted to show that the UTType

you get from the system declared type is not nil, so neither
would the equivalent constructed from a string.

Once you have decided what type identiGers you need, you will
need to handle the data that you get from the NSItemProvider .

My example only pastes the Grst item in the array, but


hopefully, it makes it clear how you could handle other data
types and multiple items.

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

You can also conform to this protocol with your own custom
types, allowing you to paste custom types of data.

,&''2".GMC5(#"5.*+.<=>H
,&''2".GMC5(#"5.*+.<=>H
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.

1 import SwiftUI
2
3 struct ToggleTintView: View {
4 @State var toggleIsOn = true
5 var body: some View {
6 VStack {
7 Toggle(isOn: $toggleIsOn) {
8 Text("Toggle")
9 }
10 Toggle(isOn: $toggleIsOn) {
11 Text("Toggle")
12 }
13 .toggleStyle(SwitchToggleStyle(tint: .red))
14 }
15 .padding()
16 }

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 modiGer 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 modiGer to the second one to show how


the SwitchToggleStyle with tint colour has no eTect.

L(#"%*1A"$.GMC5(#"5.*+.<=>H
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 Gxed 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.

1 import SwiftUI
2
3 struct ContentView: View {
4 @State var date = Date()
5 var body: some View {
6 ScrollView(.vertical) {
7 DatePicker("No style", selection: $date)
8 DatePicker("DefaultDatePickerStyle", selection: $date)
9 .datePickerStyle(DefaultDatePickerStyle())
10 DatePicker("GraphicalDatePickerStyle", selection: $date)
11 .datePickerStyle(GraphicalDatePickerStyle())
12 .frame(height: 400)
13 DatePicker("CompactDatePickerStyle", selection: $date)
14 .datePickerStyle(CompactDatePickerStyle())
15 #if os(iOS) || targetEnvironment(macCatalyst)
16 DatePicker("WheelDatePickerStyle", selection: $date)
17 .datePickerStyle(WheelDatePickerStyle())
18 #else
19 DatePicker("FieldDatePickerStyle", selection: $date)
20 .datePickerStyle(FieldDatePickerStyle())
21 DatePicker("StepperFieldDatePickerStyle", selection: $date)
22 .datePickerStyle(StepperFieldDatePickerStyle())
23 #endif
24 }
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 [ag #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.

9"0.(+5.MC5(#"5./*"0.6&5*7*"$8.*+.<=>

=)(#1-"5I"&)"#$N:77"1#.G9:;.*+.<=>H
As you might be able to tell from the name,
.matchedGeometryEffectt is an animation eTect 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 (totally diTerent).


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 conGgurations. These conGgurations 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 .

1 import SwiftUI
2
3 struct MatchedTextView: View {
4 let namespace: Namespace.ID
5 let colour: Color
6 var body: some View {
7 Text("Tap to move!")
8 .fontWeight(.semibold)
9 .foregroundColor(colour)
10 .matchedGeometryEffect(id: "text", in: namespace)
11 }
12 }
13
14 struct MatchedRoundedView: View {
15 let namespace: Namespace.ID
16 let colour: Color
17 let isCentre: Bool
18 var body: some View {
19 Circle()
20 .foregroundColor(colour)
20 .foregroundColor(colour)
21 .opacity(isCentre ? 0.5 : 1)
22 .frame(width: 60, height: 60)
23 .matchedGeometryEffect(id: "rect", in: namespace)
24 }
25 }
26
27 struct MatchedGeometryEffectView: View {
28 @State private var position: TextPosition = .bottom
29 @Namespace private var namespace
30
31 var body: some View {
32 Group {
33 switch position {
34 case .bottom:
35 VStack {
36 MatchedRoundedView(namespace: namespace, colour: position.colour
37 MatchedTextView(namespace: namespace, colour: position.colour)
38 }
39 case .left:
40 HStack {
41 MatchedTextView(namespace: namespace, colour: position.colour)
42 MatchedRoundedView(namespace: namespace, colour: position.colour
43 }
44 case .centre:
45 ZStack {
46 MatchedRoundedView(namespace: namespace, colour: position.colour
47 MatchedTextView(namespace: namespace, colour: position.colour)
48 }
49 case .right:
50 HStack {
51 MatchedRoundedView(namespace: namespace, colour: position.colour
52 MatchedTextView(namespace: namespace, colour: position.colour)
53 }
54 case .top:
55 VStack {
56 MatchedTextView(namespace: namespace, colour: position.colour)
57 MatchedRoundedView(namespace: namespace, colour: position.colour
58 }
59 }
60 }
61 .frame(maxWidth: .infinity, maxHeight: .infinity)
62 .background(Color.primary.colorInvert())
63 .onTapGesture {
64 withAnimation {
65 position.next()
66 }
67 }
68 }
69 }
70
71 enum TextPosition: CaseIterable {
72 case bottom, left, centre, right, top
73
74 var colour: Color {
75 switch self {
76 case .bottom:
77 return .orange
78 case .left:
79 return .red
80 case .centre:
81 return .green
82 case .right:
83 return .blue
84 case .top:
85 return .purple

My example is relatively basic, so check out these better


sources:

Hacking With Swift: matchedGeometryETect()

SwiftUI Lab MatchedGeometryETect Part 1 & Part 2

Apple’s Fruta sample app code

=-"2C.G9:;.*+.<=>H
The accessibility modiGer .help provides tooltips in MacOS and
an accessibility hint that works on both MacOS and iOS. My
example shows how this new modiGer actually overrides any
accessibility hint that was previously applied and vice versa.
When I ran this on iOS, VoiceOver read the Grst text as “Label 1,
Help 1” because the help modiGer was added after the hint. The
second Text is read as “Label 2, Hint 2” because the hint was
added after the help modiGer.

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 List {
6 //VoiceOver: Label 1, Help 1
7 Text("Help 1 overrides Hint 1")
8 .accessibility(hint: Text("Hint 1"))
9 .help(Text("Help 1"))
10 .accessibility(label: Text("Label 1"))
11
12 //VoiceOver: Label 2, Hint 2
13 Text("Hint 2 overrides Help 2")
14 .help(Text("Help 2"))
15 .accessibility(hint: Text("Hint 2"))
16 .accessibility(label: Text("Label 2"))
17 }
18 .frame(minWidth: 200, minHeight: 200)

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 modiGer or the


accessibility hint are read.

The tooltip functionality of help also seems absent from


Catalyst.

=(11"88*K*2*#NG*+C4#J(K"28?H.G9:;.*+.<=>H
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 diTerent labels for your UI elements that


are not visible to VoiceOver.

In other words, they are ways that someone can describe the
Button verbally.

1 import SwiftUI
2
3 struct InputLabelsView : View {
4 var body: some View {
5 Button("My Button") {
6 print("My Button pressed")
7 }
8 .accessibility(label: Text("Label"))
9 .accessibility(inputLabels: [Text("Input"), Text("Labels")])
10 }
11 }
=(11"88*K*2*#NG8"2"1#*&+E5"+#*7*"$?H
GL"C$"1(#"5.*+.<=>H
This identiGer was previously used by Picker to identify the
current selection.
=81(2":77"1#.GMC5(#"5.*+.<=>H
This isn’t new, but one of the original modiGers has been Gxed.
In Xcode 11, .scaleETect(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 .scaleETect() in Xcode 11 scaled the entire View to


zero!

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 VStack {
6 Rectangle()
7 .frame(width: 50, height: 50)
8 .scaleEffect()
9
10 Rectangle()
11 .frame(width: 50, height: 50)
12 .scaleEffect(x: 2)
13 }
14 .frame(width: 350, height: 600)
15 }

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.
=*)('"B1(2".G9:;.*+.<=>H
imageScale seems to only apply to symbols. I have tried passing
the name of an image Gle into the initializer for Image, but the
imageScale seems to have no eTect.

1 import SwiftUI
2
3 struct Symbols: View {
4 var body: some View {
5 HStack {
6 Image(systemName: "square")
7 .imageScale(.small)
8 Image(systemName: "circle")
9 .imageScale(.medium)
10 Image(systemName: "triangle")
11 .imageScale(.large)
12 }
13 .frame(width: 100)
14 }
15 }
16
17 struct Labels: View {
18 var body: some View {
19 VStack {
20 Label("Games", systemImage: "gamecontroller")
21 .imageScale(.small)
22 Label("Games", systemImage: "gamecontroller")
23 .imageScale(.medium)
24 Label("Games", systemImage: "gamecontroller")
25 .imageScale(.large)
26 }

It will, however, work on Label , which also has an initializer

that takes a system name for one of the provided SF Symbols.


=(11"+#@&2&$.GMC5(#"5.*+.<=>H
The Grst 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.

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 VStack {
6 //Only possible on Mac in Xcode 12
7 Button("Button") {
8 print("Button pressed")
9 }
10 .accentColor(.red)
11 //Possible on Mac in Xcode 11
12 Button(action: {
13 print("Button pressed")
14 }) {
15 Text("Button")
16 .foregroundColor(.red)
17 }
18 }

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 .
=C$"7"$$"5@&2&$B1-")".GMC5(#"5.*+.<=>H
The existing colorScheme(_ colorScheme: ColorScheme)

modiGer is now deprecated in the current versions of Apple


operating systems. The purpose of this modiGer 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.

1 import SwiftUI
2
3 struct ContentView: View {
4 @State var showingSheet = false
5 @State var sheetIsDark = true
6 var body: some View {
7 VStack {
8 Toggle(isOn: $sheetIsDark) {
9 Text("Sheet is dark")
10 }
11 Button("Present sheet") {
12 showingSheet = true
13 }
14 .preferredColorScheme(.light)
15
16 .sheet(isPresented: $showingSheet) {
17 Button("Dismiss") {
18 showingSheet = false
19 }
20 .preferredColorScheme(sheetIsDark ? .dark : nil)
21 }
22 }
23 .padding()
24 .frame(width: 300, height: 500)
While this modiGer 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


diTerent, you can just change the colours they would be in
either scheme.

=#"D#@&+#"+#,NC".G9:;.*+.<=>H
This modiGer tells the device what kind of suggestions would
be helpful when a user is typing.

You might notice that this modiGer takes a diTerent 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 diTerence in the suggestions they oTer on the
MacBook Touch Bar, but maybe I was doing it wrong somehow.

1 import SwiftUI
2
3 struct TextContentTypeView: View {
4 #if os(macOS)
5 let contentTypes: [NSTextContentType] = [.username, .password, .oneTimeCode
6 #else
7 let contentTypes: [UITextContentType] = [.URL, .addressCity, .addressCityAndState
8 #endif
9
10 @State var text = ""
11
12 var body: some View {
13 Form {
14 ForEach(contentTypes, id: \.self) {
15 contextType in
16 TextField("Enter text for \(contextType.rawValue)", text: $text)
17 .textContentType(contextType)
18 }
19 }

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 diTerences you can Gnd and let me know!

=2*8#E#"),*+#.G9:;.*+.<=>H
=2*8#E#"),*+#.G9:;.*+.<=>H
Changing the tint of a list item has a diTerent eTect depending
on the platform. To quote Apple’s oIcial documentation on the
new .listItemTint modiGer:

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 speciGed “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 diTerent!

1 import SwiftUI
2
3 public struct ListItemTintView: View {
4 public var body: some View {
5 NavigationView {
6 List {
7 Label(".listItemTint(.fixed(.accentColor))", systemImage: "gamecontroller
8 .listItemTint(.fixed(.accentColor))
9 Label(".listItemTint(.fixed(Color.blue))", systemImage: "gamecontroller
10 .listItemTint(.fixed(Color.blue))
11 Label(".listItemTint(.fixed(Color.red))", systemImage: "gamecontroller
12 .listItemTint(.fixed(Color.red))
13 Label(".listItemTint(.monochrome)", systemImage: "gamecontroller")
14 .listItemTint(.monochrome)
15 Label(".listItemTint(.red)", systemImage: "gamecontroller")
16 .listItemTint(.red)
17 Text(".listItemTint(.fixed(.accentColor))")
18 .listItemTint(.fixed(.accentColor))
19 Text(".listItemTint(.fixed(Color.red))")
20 .listItemTint(.fixed(Color.red))
21 Text(".listItemTint(.monochrome)")
22 .listItemTint(.monochrome)
23 Text(".listItemTint(.red)")
24 .listItemTint(.red)
25 }
26 //Allows Sidebar on iOS and macOS
27 //.listStyle(SidebarListStyle())
28 //Content to show separate from Sidebar
29 Text("Content")
The fact that watchOS uses the tint for the background is a
huge diTerence. The Label icon is not tinted at all, and the tint
is used for any item, not just Label . The .listItemTint

modiGer 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.

=2*8#O&0%2(##"$@&2&$.GL"C$"1(#"5.*+.<=>H
The new .listItemTint modiGer above replaces
.listRowPlatterColor .

This deprecated modiGer 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

modiGer overrides the old one too, so even if


.listRowPlatterColor is used after .listItemTint , the colour

passed to .listRowPlatterColor will be used instead.

=&+J&+'%$"88I"8#4$".GMC5(#"5.*+.<=>H
=&+J&+'%$"88I"8#4$".GMC5(#"5.*+.<=>H
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() modiGer. You can use this modiGer 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.

1 import SwiftUI
2
3 enum ButtonState: String {
4 case unfocused, focused, pressing, pressed
5 var colour: Color {
6 switch self {
7 case .unfocused:
8 return .white
9 case .focused:
10 return .red
11 case .pressing:
12 return .orange
13 case .pressed:
14 return .green
15 }
16 }
17 }
18
19 struct ContentView: View {
20 @State var selectedIndex = 0
21 var body: some View {
22 VStack {
23 CustomButton(index: 0, currentIndex: $selectedIndex)
24 CustomButton(index: 1, currentIndex: $selectedIndex)
25 }
26 }
27 }
28
29 struct CustomButton: View {
30 let index: Int
31 @Binding var currentIndex: Int
32 @State var buttonState = ButtonState.unfocused
33
34 var isFocused: Bool {
35 return index == currentIndex
36 }
37
38 var focusedState: ButtonState {
39 return isFocused ? .focused : .unfocused
40 }
41
42 var body: some View {
43 Text(buttonState.rawValue)
44 .foregroundColor(.black)
45 .frame(maxWidth: .infinity, maxHeight: .infinity)
46 .background(buttonState.colour)
47 .focusable(true) {
48 focused in if focused { currentIndex = index }
49 buttonState = focusedState
50 }
51 .onChange(of: currentIndex) {
52 currentIndex in buttonState = focusedState

This code will work on any platform from 2019, but will only
work on tvOS 14.0 from 2020.

=&+PC"+MOJ.G9:;.*+.<=>H
This modiGer 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 modiGer is for SwiftUI apps that use the new
SwiftUI App lifecycle that does not use AppDelegate or
SceneDelegate. If your project has these Gles in it, you probably
won’t be able to get this modiGer 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 IdentiGer Geld 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
Glter the input from the TextField.

1 import SwiftUI
2
3 extension URL {
4 /// All symbols allowed in a URL
5 static let allowedCharacters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN
6 }
7
8 extension Character {
9 //Check a character is allowed when constructing a URL
10 var isAllowedInURL: Bool {
11 return URL.allowedCharacters.contains(self)
12 }
13 }
14
15 extension String {
16 /// Remove all characters not allowed in a URL from the string
17 /// - Precondition: String must not be empty and contain at least one character all
18 /// - Postcondition: A valid String for creating a URL
19 var urlString: String {
20 precondition(self.isEmpty == false, "String cannot be empty")
21 let returnValue = self.filter { $0.isAllowedInURL }
22 precondition(URL(string: returnValue) != nil, "urlString \(returnValue
23 return returnValue
24 }
25 }
26
27 extension URL {
27 extension URL {
28 /// Creates a URL from the provided string after removing all illegal characters
29 /// - Precondition: String must not be empty and contain at least one character all
30 init(unsafeString: String) {
31 guard let url = URL(string: unsafeString.urlString) else {
32 preconditionFailure("Unsafe string \(unsafeString) was not made safe by String.
33 }
34 self = url
35 }
36 }
37
38 struct ContentView: View {
39 //Whatever scheme you entered in your project settings under URL types
40 let myURLScheme = "my-scheme://"
41 @State var text = ""
42 @State var openedURLs = [String]()
43 var fullURL: URL {
44 return URL(unsafeString: myURLScheme + text)
45 }
46
47 var body: some View {
48 Form {
49 HStack {
50 Text(myURLScheme)
51 TextField("Add text to URL", text: $text)
52 }
53 Link(destination: fullURL) {
54 Text("Open")
55 }
56 .onOpenURL { url in
57 openedURLs.append(url.absoluteString)

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 Glter 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 Gne in this case.

Whether you type a URL or not, the link that says “Open” will
open that URL, and the onOpenURL modiGer 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.

=&+%(8#"@&))(+5.G9:;.*+.<=>H
This one took a long time to Ggure out. The oIcial
documentation for onPasteCommand says the modiGer 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 modiGer 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 modiGer was released:

However, I want to point out something that really makes onPaste


diIerent than onDrop. The ;rst 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.

3?J$OJP<$=H;KL=$<F=$Y=RK?;@:$H;QAI;<A?H$?9<A?H$AH$O;E&*$*RP<=O$(@=>=@=HE=P$>?@$<FAP
>=;<J@=$<?$M?@Y

If you follow the link the oIcial 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
modiGer. 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 diTerent 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 IdentiGer for the data in
the pasteboard. The main diTerence 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 Gnd 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
1 import SwiftUI
2 import UniformTypeIdentifiers
3
4 @available(OSX 10.16, *)
5 @available(iOS, unavailable)
6 @available(tvOS, unavailable)
7 @available(watchOS, unavailable)
8 struct OnPasteCommandView: View {
9 /// The UTType we are expecting
10 /// ```
11 /// //Equivalent to this:
12 /// let utType = UTType("public.utf8-plain-text")!
13 /// ```
14 let utType = UTType.utf8PlainText
15 /// The text displayed when you paste
16 @State var text = "Where text goes"
17 /// The text displayed under the PasteButton
18 @State var buttonText = "Where text goes"
19
20 /// A string that will be coped in onCopyCommand
21 let copyString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ei
22
23 /// All the provided types that you can copy and paste
24 let utTypes: [UTType] = [.aiff, .aliasFile, .appleProtectedMPEG4Audio, .appleProtect
25
26 /// Load the pasted data
27 /// - Parameters:
28 /// - array: The array of pasted items
29 /// - text: The text that should be updated with the result
30 func loadPastedString(from array: [NSItemProvider], to text: Binding<String
31
32 guard let lastItem = array.last else {
33 assertionFailure("Nothing to paste")
34 return
35 }
36 lastItem.loadDataRepresentation(forTypeIdentifier: utType.identifier) {
37 (data, error) in
38 guard error == nil else {
39 assertionFailure("Could not load data: \(error.debugDescription)")
40 return
41 }
42 guard let data = data else {
43 assertionFailure("Could not load data")
44 return
45 }
45 }
46 text.wrappedValue = String(decoding: data, as: UTF8.self)
47 }
48 }
49
50 var body: some View {
51 VStack {
52 Text("Make sure you tick \nSystem Preferences > Keyboard > \nUse keyboard naviga
53 Text("Press Cmd + C then press tab")
54 .padding()
55 .focusable()
56 .onCopyCommand {
57 [NSItemProvider(object: NSString(string: copyString))]
58 }
59 Text("Press Cmd + V then press tab")
60 .focusable()
61 .onPasteCommand(of: [self.utType]) {
62 array in loadPastedString(from: array, to: $text)
63 }
64 Text("onPasteCommand pasted below:\n\(text)")
65 .padding(.horizontal, 10)
66 .lineLimit(4)

Once you have copied when the Grst Text has focus, you can
press tab to cycle focus to the next Text . Pasting while this

Text is highlighted runs the onPasteCommand closure, which


receives an array of NSItemProvider instances. To convert the
data from the latest item back into a string, we Grst need to load
the data representation of the item and convert it to a string.
Then we use a binding to the string displayed by the Text

underneath, so we can immediately see that the operation was


successful.

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.

=&+L$('.(+5.=&+L$&C.GMC5(#"5.*+.<=>H
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 modiGer that provides data of the
UTType.utf8PlainText variety, and the bottom applies an
.onDrop modiGer 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 modiGer
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.

1 import SwiftUI
2 import UniformTypeIdentifiers
3
4 struct DragAndDropView: View {
5 var body: some View {
6 GeometryReader { geometry in let height = geometry.size.height / 2
7 VStack {
8 DragView()
9 .frame(height: height)
10 DropView()
11 .frame(height: height)
12 }
13 }
14 }
15 }
16
17 struct TextWithBackground: View {
18 let text: String
19 var body: some View {
20 Text(text)
21 .padding()
22 .background(text.isEmpty ? Color.clear : Color.orange)
23 .cornerRadius(5)
24 }
25 }
26
27 struct DragView: View {
28 @State var text = ""
29 var body: some View {
30 VStack(spacing: 10) {
31 TextField("Enter text here", text: $text)
32 .padding()
33 .background(Color(UIColor.secondarySystemBackground))
34 Text(text.isEmpty ? "Type text above" : "Drag the text below")
35 Group {
36 TextWithBackground(text: text)
37 .onDrag { NSItemProvider(object: self.text as NSString) }
38 }
39 .frame(maxWidth: .infinity, maxHeight: .infinity)
40 }
41 }
42 }
43
44 struct DropView: View {
45 @State var droppedText = ""
46 @State var dropEntered = false
47 let utType = UTType.utf8PlainText
48 var body: some View {
49 VStack {
50 Text(dropEntered ? "Drop text here" : "Drag text here")
51 Group {
52 TextWithBackground(text: droppedText)
53 }
54 .frame(maxWidth: .infinity, maxHeight: .infinity)
55 }
56 .background(self.dropEntered ? Color.blue : Color(UIColor.secondarySystemBackgrou
56 .background(self.dropEntered ? Color.blue : Color(UIColor.secondarySystemBackgrou
57 .onDrop(of: [utType], delegate: OnDropDelegate(text: $droppedText, dropEntered
58 }
59 }
60
61 struct OnDropDelegate: DropDelegate {
62 @Binding var text: String
63 @Binding var dropEntered: Bool
64 let utType = UTType.utf8PlainText
65
66 func dropEntered(info: DropInfo) {
67 self.dropEntered = true
68 }
69 func dropExited(info: DropInfo) {
70 self.dropEntered = false
71 }
72 func performDrop(info: DropInfo) -> Bool {
73 var returnValue = false
74 if let item = info.itemProviders(for: [utType]).first {
75 item.loadDataRepresentation(forTypeIdentifier: utType.identifier) {
76 (data, error) in
77 guard error == nil else {
78 assertionFailure("Could not load data: \(error.debugDescription
79 return

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 conGrm 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.

=&+@-(+'".G9:;.*+.<=>H
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 oIcial 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 Grst year of SwiftUI’s existence, using property wrappers


in an ObservableObject was about the only way. In 2020, we
have a new modiGer 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 modiGer 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 modiGer for a Button in
another part of the layout would have no more or less access to
this ability.

1 import SwiftUI
2
3 struct ContentView: View {
4 @State var text = ""
5 @State var toggleIsOn = true
6 var body: some View {
7 VStack {
8 TextField("Enter some text", text: $text)
9 .onChange(of: toggleIsOn) { value in
10 if toggleIsOn && text.isEmpty {
10 if toggleIsOn && text.isEmpty {
11 text = "Here's some text"
12 }
13 else if !toggleIsOn {
14 text = ""
15 }
16 }
17 Toggle(isOn: $toggleIsOn) {
18 Text("TextField has text in it")
19 }
20 .onChange(of: text) { value in
21 toggleIsOn = !text.isEmpty
22 }
23 }

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 aTects SwiftUI Views, check out SwiftUI


Lab’s tutorial on EquatableView. Notice how ContentView ,

which does not contain an .onChange modiGer, is still aTected


by the logic in the modiGer of its child OnChangeView .

For instance, the TextField in ContentView is emptied when


the Toggle in OnChangeView is turned oT, 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 modiGer, 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.

=A"NK&($5B-&$#14#.G9:;.*+.<=>H
=A"NK&($5B-&$#14#.G9:;.*+.<=>H
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 Grst button has .upArrow as the key, but no


modiGer is speciGed. 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 modiGer,


and so this Button actually requires the Cmd + Up
combination to be pressed.

1 import SwiftUI
2
3 @available(iOS 14.0, OSX 10.16, tvOS 14.0, *)
4 @available(watchOS, unavailable)
5 struct KeyboardShortcutView: View {
6 var body: some View {
7 VStack {
8 Button("Press Cmd + Up") {
9 print("Button 1 pressed")
10 }
11 .keyboardShortcut(.upArrow)
12 Button("Press Shift + Down") {
13 print("Button 2 pressed")
14 }
15 .keyboardShortcut(.downArrow, modifiers: .shift)
16 }
17 .padding(20)
18 }
The second button does explicitly state a modiGer, allowing it to
use the shift key instead of command. If you read about the
onPasteCommand modiGer 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 Gnd the top Button is highlighted by
default. Tab will move focus to the second button, and Shift +
Tab will move it back to the Grst.

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 .

=7&148"5/(24".(+5.3Q&148"5!*+5*+'
G9:;.*+.<=>H
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.

1 struct ContentView: View {


2 var body: some View {
3 VStack {
4 TextFieldView()
5 DisplayTextView()
6 }
7 .padding(20)
8 }
9 }
10
11 struct TextFieldView: View {
12 @State var text = ""
13 var body: some View {
14 TextField("", text: $text)
15 .focusedValue(\.text, $text)
16 }
17 }
18
19 struct DisplayTextView: View {
20 @FocusedBinding(\.text) var text: String?
21 var body: some View {
22 Text(text ?? "Blank")
23 }
24 }
25
26 struct FocusedTextKey : FocusedValueKey {
27 typealias Value = Binding<String>
28 }
29 extension FocusedValues {
30 var text: FocusedTextKey.Value? {

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 deGnes a typealias for your


key, you’ll need to deGne a getter and setter for that value. This
extension of FocusedValue deGnes \.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) modiGer 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 oIcial 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!

=C$"7"$8L"7(42#Q&148.(+5.=7&148B1&C"
G9:;.*+.<=>H
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 deGne 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 modiGer on
the VStack, and now we have our scope.

1 import SwiftUI
2
3 @available(iOS, unavailable)
4 @available(OSX, unavailable)
5 @available(tvOS 14.0, *)
6 @available(watchOS 6.0, *)
7 struct ContentView: View {
8 @Namespace var namespace
9 var body: some View {
10 VStack {
11 Button("Usually the default"){
12 print("Top button pressed")
13 }
14 Button("Prefers default"){
15 print("Bottom button pressed")
16 }
17 .prefersDefaultFocus(in: namespace)
18 }
19 .focusScope(namespace)

The only other thing I need to do is use the


.prefersDefaultFocus modiGer 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 .

=7422B1$""+@&R"$.G9:;.*+.<=>H
You probably won’t be shocked to learn that, unlike .sheet , the
.fullScreenCover modiGer presents a modal View that covers
the full screen. I made an example that allows you to inGnitely
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.

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 ModalView(canDismiss: false)
6 }
7 }
8
9 struct ModalView: View {
10 @Environment(\.presentationMode) var presentationMode
11 @State var fullScreenCovered = false
12 @State var sheetIsPresented = false
13 let canDismiss: Bool
14 var body: some View {
15 VStack {
16 Button("Full screen") {
17 fullScreenCovered.toggle()
18 }
19 .fullScreenCover(isPresented: $fullScreenCovered) {
20 ModalView(canDismiss: true)
21 .background(Color.yellow)
22 }
23 Button("Sheet") {
24 sheetIsPresented.toggle()
25 }
26 .sheet(isPresented: $sheetIsPresented) {
27 ModalView(canDismiss: true)
28 .background(Color.orange)
29 }
30 if canDismiss {
31 Button("Dismiss") {
32 presentationMode.wrappedValue.dismiss()
33 }
34 }

I have provided the @Environment(\.presentationMode)

property in the modal, as without swiping to dismiss, the full-


screen cover could not be dismissed.

=5"7(42#FCCB#&$('".G9:;.*+.<=>H
This modiGer .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
speciGcally 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.

1 import SwiftUI
2
3 struct ContentView: View {
4 var body: some View {
5 VStack {
6 StandardAppStorageView()
7 .defaultAppStorage(UserDefaults(suiteName: "group.YourGroupName.YourApp
8 GroupAppStorageView()
9 .defaultAppStorage(.standard)
10
11 }
12 }
13 }
14 struct GroupAppStorageView: View {
15 @AppStorage("Text") var text = ""
16 //Equivalent to:
17 //@AppStorage("Text", store: UserDefaults(suiteName: "group.YourGroupName.YourApp")
18 var body: some View {
19 VStack {
20 TextField("Enter text", text: $text)
21
22 Text(text)
23 }
24 }
25 }
26 struct StandardAppStorageView: View {
27 @AppStorage("Text") var text = ""
28 var body: some View {
29 VStack {
30 TextField("Enter text", text: $text)

=(CCB#&$"PR"$2(N.G9:;.*+.<=>H
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 Gnd 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.

1 import SwiftUI
2 import StoreKit
3
4
5 struct AppStoreOverlayView: View {
6 @State var isPresented = false
7 @State var appID = "1440427080"
8 @State var raised = false
9
10 func togglePresentation() {
11 //Dismiss keyboard
12 UIApplication.shared.windows.forEach { $0.endEditing(false) }
13 //Show/Hide overlay
14 isPresented.toggle()
15 }
16
17 var body: some View {
18 VStack {
19 Toggle(isOn: $raised) {
20 Text("Raised position")
21 }
22 .onChange(of: self.raised) { _ in
23 togglePresentation()
24 }
25 HStack {
26 TextField("Enter an App ID", text: $appID)
27 .keyboardType(.numberPad)
28 Button(isPresented ? "Dismiss" : "Open") {
29 togglePresentation()
30 }
31 .appStoreOverlay(isPresented: $isPresented) {
32 SKOverlay.AppConfiguration(appIdentifier: appID, position: raised
33 }
34 }

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’.

=#&&2K($.G9:;.*+.<=>H

1 import SwiftUI
2
3 public struct ToolbarView: View {
4 @State var data: [String]
5 init() {
6 var data = [String]()
7 for _ in 1...5 {
8 data.append(UUID().uuidString)
9 }
10 self._data = State<[String]>(initialValue: data)
11 }
12
13 public var body: some View {
14 NavigationView {
15 List(data, id: \.self) { uuid in
16 Text(uuid)
17 .lineLimit(1)
18 }
19 .navigationTitle("UUID Generator")
20 .toolbar {
21 ToolbarItem(placement: .primaryAction) {
21 ToolbarItem(placement: .primaryAction) {
22 Button("Add") {
23 data.append(UUID().uuidString)
24 }
25 .padding(5)
26 .background(Color.blue)
27 .foregroundColor(.white)
28 .cornerRadius(5)
29 }
30 ToolbarItem(placement: .navigationBarLeading) {
31 Button("Delete All") { data.removeAll() }
32 }
33 ToolbarItem(placement: .bottomBar) {
34 Button("Sort A > Z") { data.sort() }
35 }
36 ToolbarItem(placement: .bottomBar) {
37 Button("Sort A > Z") { data.sort(by: >) }

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.
conGrmationAction — The item represents a con;rmation action
for a modal interface. You can use it in your sheets to con;rm
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-speci;c 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.

=C$"R*"0@&+#"D#.G9:;.*+.<=>H
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 modiGer 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))

As you can see, we are required to initialise


WidgetPreviewContext , which conforms to the PreviewContext

protocol. We pass in a case of the WidgetFamily enum, and this


provides conGguration that the preview needs. Instead of
making a complete widget, I decided to use my example to
make a normal View. I did this because this requires less code,
but also to show that previewContext can work for any View.

I made an example that includes an actual widget, if you want


to see the code for that.

1 import SwiftUI
2 import WidgetKit
3
4 extension Date {
5 var timeString: String {
6 let hour = Calendar.current.component(.hour, from: self)
7 let minutes = Calendar.current.component(.minute, from: self)
8 return "\(hour):\(minutes < 10 ? "0\(minutes)" : "\(minutes)")"
9 }
10 }
11
12 struct ContentView: View {
12 struct ContentView: View {
13 @Environment(\.widgetFamily) var family
14
15 var text: String {
16 switch family {
17 case .systemSmall: return "Small size"
18 case .systemMedium: return "Medium size"
19 case .systemLarge: return "Large size"
20 default: return "Default size"
21 }
22 }
23
24 var backgroundColor: Color {
25 switch family {
26 case .systemSmall: return .red
27 case .systemMedium: return .green
28 case .systemLarge: return .blue
29 default: return .orange
30 }
31 }
32 var body: some View {
33 Text("\(Date().timeString)\n\(text)")
34 .font(.largeTitle)
35 .fontWeight(.bold)
36 .frame(maxWidth: .infinity, maxHeight: .infinity)
37 .background(backgroundColor)
38 .multilineTextAlignment(.center)
39
40 }
41 }
42
43 struct ContentView_Previews: PreviewProvider {
44 static var previews: some View {
45 ContentView()

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 for backgroundColor and text , so the View

ends up completely diTerent 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 diTerently sized widgets. This might be Gxed by
the time you read this.

,$9?AH<=:$?J<$F?M$LA<<L=$JP=>JL$AH>?@O;<A?H$E;H$K=$>?JH:$AH$<F=$:?EP$>?@$<FAP$L?HI$K=>?@=$,
>?JH:$?J<$MF;<$(@=QA=M%?H<=T<$AP$JP=:$>?@D$;H:$,$P<ALL$:?H4<$YH?M$=T;E<LR$F?M$A<$M?@YP

What about if you want to make your own PreviewContext ? I

tried my best, but it’s somewhat diIcult 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 deGne the size of my

widget previews. Instead of using the subscript, I decided to


just make a new version of the previewContext modiGer.

After all, we have no way of knowing how the original modiGer


actually works.

1 struct LargePreviewContextKey: PreviewContextKey {


2 static var defaultValue: Custom = .veryLarge
3 typealias Value = Custom
4 }
5 enum Custom {
6 case large, veryLarge, tooBig
7
8 var size: CGSize {
9 switch self {
10
11 case .large:
12 return CGSize(width: 400, height: 400)
13 case .veryLarge:
14 return CGSize(width: 500, height: 500)
15 case .tooBig:
16 return CGSize(width: 600, height: 600)
17 }
18 }
19 }
19 }
20 struct LargePreviewContext: PreviewContext {
21 subscript<Key>(key: Key.Type) -> Key.Value where Key : PreviewContextKey {
22 return key.defaultValue
23 }
24 let custom: Custom
25 public init(family: Custom) {
26 self.custom = family
27 }
28 }
29
30 extension View {
31 public func previewContext<C>(custom: C) -> some View where C : PreviewContext
32 return Group {
33 if let context = custom as? LargePreviewContext {
34 self.previewLayout(.fixed(width: context.custom.size.width, height
35 else {
36 self.previewContext(custom)
37 }
38 }
39 }
40 }
41
42 struct ContentView_Previews: PreviewProvider {
43
44 static var previews: some View {

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 Ggure out a better way to make use of it, let me know!

=48"$F1#*R*#N.(+5.=&+@&+#*+4"M8"$F1#*R*#N
G9:;.*+.<=>H
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 modiGer
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 identiGers 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 diIcult 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 Gnd 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.

1 import SwiftUI
2 import CoreSpotlight
3 import MobileCoreServices
4
5 struct UserActivityView: View {
6 @State var data: [String]
7 @State var openedID = String()
8 @State var alertPresented = false
9 init() {
10 if let stringArray = UserDefaults.standard.stringArray(forKey: "data"
11 self._data = State<[String]>(initialValue: stringArray)
12 }
13 else {
14 var data = [String]()
15 for _ in 1...10 {
16 data.append(UUID().uuidString)
17 }
18 self._data = State<[String]>(initialValue: data)
19 UserDefaults.standard.set(data, forKey: "data")
20 }
21 }
22 func indexItem(atIndex index: Int) {
23 let uuid = data[index]
24 let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText
25 attributeSet.title = "A search item for my app"
26 attributeSet.contentDescription = uuid
27
28 let item = CSSearchableItem(uniqueIdentifier: uuid, domainIdentifier:
29 CSSearchableIndex.default().indexSearchableItems([item]) { error in
30 if let error = error {
31 assertionFailure("Failed to index with error\n\(error)")
32 } else {
33 print("Saved successfully")
34 }
35 }
36 }
37
38 func presentAlert(_ userActivity: NSUserActivity) {
39 if let id = userActivity.userInfo?[CSSearchableItemActivityIdentifier]
40 openedID = id
41 alertPresented = true
42 }
43 }
44
45 var body: some View {
46 List(0..<data.count, id: \.self) { index in
47 Button("\(data[index])") {
48 indexItem(atIndex: index)
49 }
50 }
51 .userActivity("com.MyCompany.MyApp.searchActivity", element: openedID) {
52 string, activity in
53 if !string.isEmpty {print("Updated \(string) \(activity.activityType
54 }
55 .onContinueUserActivity("com.MyCompany.MyApp.searchActivity") { activity

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 modiGer 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 modiGer “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 handoT if that was
implemented.

In any case, the important thing to know about these modiGers


is that they relate to NSUserActivity, HandoT and Spotlight
Search.

=#(KE#").GMC5(#"5.*+.<=>H
=#(KE#").GMC5(#"5.*+.<=>H
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?

1 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 7.0, *)


2 struct TabItem: View {
3 var body: some View {
4 TabView {
5 View1()
6 .tabItem {
7 Image(systemName: "list.dash")
8 Text("Menu")
9 }
10
11 View2()
12 .tabItem {
13 Image(systemName: "square.and.pencil")
14 Text("Order")
15 }
16 }
17 }
18 }
19
20 struct View1: View {
21 var body: some View {
22 Text("View 1")
23 }
24 }
25
26 struct View2: View {
27 var body: some View {

TabView , along with the modiGer .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 modiGer 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.”
=1&+#"D#6"+4.GMC5(#"5.*+.<=>H
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
eTort, 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 modiGer, 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() modiGer for this to work.
Here’s how I did it.




♠ ♥



1 import SwiftUI
2
3 struct ContentView: View {
4 @State var selectedIndex = -1
5 var selection: String {
6 switch selectedIndex {
7 case 0: return " "
8 case 1: return " "
9 case 2: return " "
10 case 3: return " "
11 default: return ""
12 }
13 }
14
15 var body: some View {
16 VStack {
17 Text("Favourite card suit:")
18 Text(selection)
19 .font(.system(size: 200))
20 .focusable()
21 .padding()
22 .contextMenu {
23 Button(" - Hearts") {selectedIndex = 0}
24 Button(" - Clubs") {selectedIndex = 1}
25 Button(" - Spades") {selectedIndex = 2}
26 Button(" - Diamonds") {selectedIndex = 3}
27 }
28 Text("Long press to make a choice")

=+(R*'(#*&+,*#2".(+5.=+(R*'(#*&+B4K#*#2"
G9:;.*+.<=>H
I can’t do much better than Apple’s oIcial documentation for
navigationTitle this 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 re[ected 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 oIcial 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.

1 import SwiftUI
2 #if os(iOS)
3 struct ContentView: View {
4 @State var barStyle = BarStyle.automatic
5
6 enum BarStyle: String, CaseIterable {
7 case automatic, inline, large
8
9 var style: NavigationBarItem.TitleDisplayMode {
10 switch self {
11
12 case .automatic:
13 return .automatic
14 case .inline:
15 return .inline
16 case .large:
17 return .large
18 }
19 }
20 }
21
22 var body: some View {
23 Group {
24 List {
25 Picker(selection: $barStyle, label: Text(".navigationBarTitleDisplayMode
26 ForEach(BarStyle.allCases, id: \.self) {
27 barStyle in Text(barStyle.rawValue)
28 }
29 }.pickerStyle(SegmentedPickerStyle())
30 ForEach(0...50, id: \.self) { _ in
31 Text("Example list item")
32 }
33 }
34 .navigationTitle(".navigationTitle")

=+(R*'(#*&+/*"0B#N2".GMC5(#"5.*+.<=>H
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 is DoubleColumnNavigationViewStyle , and you can bet


that's not coming to WatchOS any time soon!
=+(R*'(#*&+!($,*#2".GL"C$"1(#"5.*+.<=>H
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) .

B#N2"8.&+.*PBS.*%(5PBS.6(1.@(#(2N8#.(+5
#RPB.G9:;.*+.<=>H
Here’s an example that uses both .labelStyle and
.indexViewStyle, which are both new in 2020 and unavailable
on macOS:
1 import SwiftUI
2
3 @available(OSX, unavailable)
4 struct TabIndexStyleView: View {
5 @State private var selected = 1
6
7 var body: some View {
8 VStack {
9 TabView(selection: self.$selected) {
10 Label("\(selected)", systemImage: "gamecontroller")
11 .labelStyle(DefaultLabelStyle())
12 .frame(maxWidth: .infinity, maxHeight: .infinity)
13 .background(Color.red)
14 .tag(1)
15 Label("\(selected)", systemImage: "gamecontroller")
16 .labelStyle(IconOnlyLabelStyle())
17 .frame(maxWidth: .infinity, maxHeight: .infinity)
18 .background(Color.green)
19 .tag(2)
20 Label("\(selected)", systemImage: "gamecontroller")
21 .labelStyle(TitleOnlyLabelStyle())
22 .frame(maxWidth: .infinity, maxHeight: .infinity)
23 .background(Color.blue)
24 .tag(3)
25 }
26 .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
27 .tabViewStyle(PageTabViewStyle())
28 }
29 .frame(maxWidth: .infinity, maxHeight: .infinity)

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

B#N2"8.P+2N.&+.)(1PB.G9:;.*+.<=>H
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 modiGer is new, but we can only use
DefaultGroupBoxStyle with it unless we make our own custom
ones.

1 import SwiftUI
2
3 @main
4 struct MyApp: App {
5 var body: some Scene {
6 WindowGroup {
7 ContentView()
8 .groupBoxStyle(DefaultGroupBoxStyle())
9 .presentedWindowStyle(TitleBarWindowStyle())
10 .presentedWindowStyle(HiddenTitleBarWindowStyle())
11 .presentedWindowToolbarStyle(UnifiedWindowToolbarStyle())
12 .presentedWindowToolbarStyle(ExpandedWindowToolbarStyle())
13 .presentedWindowToolbarStyle(UnifiedCompactWindowToolbarStyle())
14 }
15 }

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.
9"D#.B#"C8
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:

Hacking with Swift

Swift UI Lab

Swift with Majid

WWDC by Sundell

Swift by Sundell

LOSTMOA Blog

If you’ve got a great resource to share with the community, let


me know and I’ll gladly add it to this list.

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!


B*'+.4C.7&$.,-".!"8#.&7.!"##"$.%$&'$())*+'
6R$6=<<=@$(@?I@;OOAHI

2$M==YLR$H=MPL=<<=@$P=H<$=Q=@R$-@A:;R$MA<F$<F=$K=P<$;@<AEL=P$M=
9JKLAPF=:$<F;<$M==YV$%?:=$<J<?@A;LPD$;:QAE=D$E;@==@$?99?@<JHA<A=PD$;H:
O?@=Z$!;Y=$;$L??Y

3?J@$=O;AL 7=<
<FAP
H=MPL=<<=@
6R$PAIHAHI$J9D$R?J$MALL$E@=;<=$;$'=:AJO$;EE?JH<$A>$R?J$:?H4<$;L@=;:R$F;Q=$?H=V$8=QA=M$?J@$(@AQ;ER$(?LAER
>?@$O?@=$AH>?@O;<A?H$;K?J<$?J@$9@AQ;ER$9@;E<AE=PV

(@?I@;OOAHI A&* *MA>< *MA><JA '?KAL=

J"($+.)&$"= 6(A".6"5*4) B-($".N&4$


'=:AJO$AP$;H$?9=H$9L;<>?@O N&4$8= #-*+A*+'=
MF=@=$BG[$OALLA?H$@=;:=@P -?LL?M$<F=$M@A<=@PD ,>$R?J$F;Q=$;$P<?@R$<?$<=LLD
E?O=$<?$>AH:$AHPAIF<>JL$;H: 9JKLAE;<A?HPD$;H:$<?9AEP YH?ML=:I=$<?$PF;@=D$?@$;
:RH;OAE$<FAHYAHIV$"=@=D <F;<$O;<<=@$<?$R?JD$;H: 9=@P9=E<AQ=$<?$?>>=@$\
=T9=@<$;H:$JH:APE?Q=@=: R?J4LL$P==$<F=O$?H$R?J@ M=LE?O=$F?O=V$,<4P$=;PR
Q?AE=P$;LAY=$:AQ=$AH<?$<F= F?O=9;I=$;H:$AH$R?J@ ;H:$>@==$<?$9?P<$R?J@
F=;@<$?>$;HR$<?9AE$;H:$K@AHI AHK?TV$#T9L?@= <FAHYAHI$?H$;HR$<?9AEV$+@A<=
H=M$A:=;P$<?$<F=$PJ@>;E=V ?H$'=:AJO
)=;@H$O?@=

2K?J< "=L9 )=I;L

You might also like