Professional Documents
Culture Documents
SwiftUI Documentation - Views and Controls. Updated For iOS 14
SwiftUI Documentation - Views and Controls. Updated For iOS 14
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
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.
Framework Integration
Gestures
Preview
@ViewBuilder
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.
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.
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
/*"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.
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
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.
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.
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
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 .
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.
UITextView documentation.
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) {
Natalia Panferova
@natpanferova
!FAP$H=M$KJ<<?H$P<@=;OLAH=P$<F=$9@?E=PP$?>$;:?9<AHI$*AIH$AH$MA<F$299L=$AH$R?J@$;99
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 }
%$&'$"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.
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.
I(4'"/*"0.G9:;.*+.<=>H
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 }
J(K"2.G9:;.*+.<=>H
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.
In iOS 14, we now have Link , which does the action part of the
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 }
6"+4.G9:;.*+.<=>H
!F=$:=>;JL<$P<RL=$AP$6?@:=@=:6J<<?H'=HJ*<RL=
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.
6"+4!4##&+.GL"C$"1(#"5.*+.<=>H
See Menu above, which replaces MenuButton , but serves many of
,"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
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
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 }
%(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 .
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)
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 .
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 }
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.
L(#"%*1A"$.GMC5(#"5.*+.<=>H
There are now two new styles for DatePicker , called
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.
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
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
=-"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)
=(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
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.
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.
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 }
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 }
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
You either want a screen of your app to use the system colour
scheme, or you want to override it with your own.
=#"D#@&+#"+#,NC".G9:;.*+.<=>H
This modiGer tells the device what kind of suggestions would
be helpful when a user is typing.
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 }
=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:
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
=2*8#O&0%2(##"$@&2&$.GL"C$"1(#"5.*+.<=>H
The new .listItemTint modiGer above replaces
.listRowPlatterColor .
=&+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.
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)
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.
=&+%(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
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:
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
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.
easier way to control the Uniform Type IdentiGer for the data in
the pasteboard. The main diTerence between onPasteCommand
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
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 .
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
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.
=&+@-(+'".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
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
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 }
=A"NK&($5B-&$#14#.G9:;.*+.<=>H
=A"NK&($5B-&$#14#.G9:;.*+.<=>H
To quote Apple’s documentation for .keyboardShortcut:
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.
=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 .
DisplayTextView .
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.
=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.
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)
it the namespace.
=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 }
=5"7(42#FCCB#&$('".G9:;.*+.<=>H
This modiGer .defaultAppStorage changes what UserDefaults
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.
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 ‘?’.
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 are only two positions for an App Store overlay: .bottom
‘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: >) }
=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)) .
.previewContext(WidgetPreviewContext(family:
.systemSmall))
.previewContext(WidgetPreviewContext(family:
.systemMedium))
.previewContext(WidgetPreviewContext(family:
.systemLarge))
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()
,$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
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.
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.
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.
=#(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?
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.
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
.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)
platform
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:
Swift UI Lab
WWDC by Sundell
Swift by Sundell
LOSTMOA Blog
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.
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