This article is best viewed on Chrome due to limitations with linking to sections on Medium.
Apple’s SwiftUI documentation covers a lot of the basics of SwiftUI. But what about those gaps that still exist? We all know that the documentation is likely to be updated massively when WWDC comes in June 2020, but we can’t wait that long!
Here is everything I’ve learned about every page of SwiftUI’s existing documentation. I won’t repeat what Apple has provided, but I’ll try to add what they don’t say. I’ll use the same categories for the sake of consistency, and definitely not because I’m too lazy to think of my own.
Each category title will link directly to Apple’s version.
I don’t expect anyone to read this entire post. If you do, I admire your dedication! I guess the Better Programming editors have to look it over, and I’m very grateful to them.
What I recommend is that although you may not need all of this now, you may need it in the future, so it would probably help to bookmark it and come back to it later when you have gaps in your knowledge.
I have gaps in my knowledge too, which is why I promise to update this post as often as I can. If there are areas you think I should clarify or cover in more detail, let me know!
Table of Contents
VIEWS AND CONTROLS
- SwiftUI 2 changes
- The View protocol
- Text
- Text ViewModifiers
- Standard text modifiers
- TextField
- TextField ViewModifiers
- SecureTextField
- SecureTextField ViewModifiers
- Font
- Image
- SF Symbols
- Button
- ButtonStyle
- NavigationView and NavigationLink
- EditButton
- MenuButton
- PasteButton
- Toggle
- Creating a custom ToggleStyle
- Picker
- DatePicker
- Slider
- Stepper
VIEW LAYOUT AND PRESENTATION
- HStack, VStack, and ZStack
- List, ScrollView, ForEach, and DynamicViewContent
- Identifiable
- Axis
- Form
- Group
- GroupBox
- Section
- Spacer
- Divider
- TabView
- VSplitView and HSplitView
- Alert
- ActionSheet
- EmptyView
- EquatableView
- AnyView
- TupleView
DRAWING AND ANIMATION
- Animation
- Animatable and AnimatableData
- AnimatablePair
- EmptyAnimatableData
- AnimatableModifier
- withAnimation (Implicit Animation)
- AnyTransition
- InsettableShape
- FillStyle
- ShapeStyle
- GeometryEffect
- Angle
- Edge and EdgeInsets
- Rectangle, RoundedRectangle, Circle, Ellipse, and Capsule
- Path
- ScaledShape, RotatedShape, and OffsetShape
- TransformedShape
- Color
- ImagePaint
- Gradients (Linear/Angular/Radial)
- GeometryReader and GeometryProxy
- CoordinateSpace
FRAMEWORK INTEGRATION
- UIHostingController
- UIViewRepresentable
- UIViewControllerRepresentable
- DigitalCrownRotationalSensitivity
STATE AND DATA FLOW
- State
- Binding
- ObservedObject
- EnvironmentObject
- FetchRequest and FetchedResults
- DynamicProperty
- Environment
- PreferenceKey
- LocalizedStringKey
GESTURES
- Gestures
PREVIEWS
- The PreviewProvider protocol
See also: SwiftUI 2 changes
The View protocol
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
definition.
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 different 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 Overflow 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 satisfied by the caller… An opaque result type is an implicit generic placeholder satisfied 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 specific single concrete type that conforms to P
.”
Let’s get started looking at the example views that Apple provides that conform to the View
protocol.
Text
See also: Text (Updated in 2.0)
That example project that you get when creating a SwiftUI Xcode project includes probably the most simple building block for a view, and it’s called Text
.
In most cases, you’ll be passing a String
to the constructor of this, and that will be the content it displays.
Here are some examples of all the initializers for Text
:
Note that the last initializer, the one that takes a LocalizedStringKey
, tableName
, bundle
, and comment
, requires a separate file that uses the .strings
file 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 file. 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
.
The Text
s here are embedded in a VStack
because the body variable’s opaque result type has to be set to one type. In other words, it can’t be set to several items because this could involve several types.
Although VStack
can contain up to ten views inside it, each of these can be a VStack
, HStack
, or Group
and each can have ten types inside it.
Scroll down to HStack, VStack, and ZStack for more details on these.
Text ViewModifiers
Text, like all Views, can be modified by structs that conform to the ViewModifier protocol.
Let’s see an example of a custom modifier so you can see what’s happening under the surface with these:
As you can see, it would be possible to add .modifier(YourModifier())
to call a ViewModifier, but it makes a lot more sense to use a View
extension and give a clean call site.
This is the way the standard modifiers look, so making a View
extension will make your modifiers look much more like the default ones.
It will be difficult to create a ViewModifier
that is easier to write than the defaults without this, as starting your modifier with the word “modifier” and calling its constructor adds unnecessary complexity.
Standard text modifiers
In this example, I put a red border around the VStack
that holds the font alignments. This was to show that the boundaries of this VStack
are limited because the Text
s inside have a fixed maximum size.
Without this, alignment for the Text
s has no effect, as the VStack
container will expand to accommodate the Text
s inside.
To align to a leading (left)or trailing (right) edge, we need to define where that leading or trailing edge is going to be. This can also be achieved by fixing the width of the VStack
itself.
Notice how this example has created extensions on Text
instead of View
. This is because you can only guarantee that the received type will be a Text
in this case.
It is impossible to do the equivalent to greenStrikethrough
or redUnderline
by creating a ViewModifier
or View
extension because these take a generic View
that may not be a Text
.
Now that you know this, you can make your own custom functions that customize Text
, without the intermediary step of creating ViewModifier
s I mentioned above.
TextField
If you want a user to enter text, you’ll need a binding to store that data. This first example uses a State
variable, which stores the string locally in the SwiftUI struct and doesn’t actually save it anywhere you can use it or store it permanently.
If you want to hold onto your data and be able to give it a computed value, you’ll need an ObservableObject
. This essentially gives you a regular Swift file to store your data in, which will get pretty useful once you start adding controls that can modify data values.
You don’t need to provide a didSet
closure for the string value that you saved, I’m just providing an example to show when didSet
runs. My DataModel
class is a normal Swift class, so its didSet
closure runs and prints the new value of the string.
However, since SwiftUI views are value types that are created dynamically, the didSet
callback does not print anything to the console when you modify the local State
variable.
Did you notice how some of the later initializers use a Float
instead of a String
?
These initializers will take any type, but be careful to pass in one of the provided Formatter
classes, or make one yourself.
In my example, I use NumberFormatter
, which will not let you input any character that isn’t a number. This makes it easy for me to save my Float
without worrying that the app will crash because I can’t convert a string of letters into the Float
that stores the TextField
’s value.
The other initializers also have two closures, the first of which is onEditingChanged
.
Apple’s documentation on thisTextField
initializer doesn’t mention what the bool inside the closure indicates, but testing seems to show that it relates to the TextField
being given focus.
Have you ever called resignFirstResponder
on a UITextField
in UIKit?
This essentially dismisses the keyboard because the UITextField
no longer needs focus. Even if you could bring the keyboard back at some point, text would not be inserted into that UITextField
unless you made it the first responder again.
That all relates to UIResponder
, an abstract interface from which UIView
, UIViewController
, and basically everything else in UIKit inherits.
We don’t know how SwiftUI events are handled to the same extent, but I’m using the phrase first responder as it should be familiar to anyone who has used UITextField
.
The bool in onEditingChanged
can be called fieldActive
or anything else you want that makes it clear to you.
The important thing is that when you start to edit a TextField
, onEditingChanged
is called with a bool that is set to true. When you press the keyboard’s return key, the onCommit
block is called, after which onEditingChanged
is called with a bool that is set to false
.
TextField ViewModifiers
For a more detailed explanation of what ViewModifiers are, see Text ViewModifiers.
You cannot currently change the foreground color of the placeholder text in a TextField
. At the time that I’m writing this, you cannot use any keyboard type for a TextField
other than the default when displaying TextField
s in a List
.
I found that trying to display TextField
s in a List
caused them to overlap each other too. Here are all of the keyboard types which seem to work pretty well when presented in a VStack
:
SecureTextField
Essentially the same as TextField
above with the added benefit of hiding the characters you enter, which is useful for passwords. As with TextField
above, you can choose a variety of keyboard types, of which only numberPad
is shown here.
SecureTextField ViewModifiers
For a more detailed explanation of ViewModifiers, see Text ViewModifiers.
Similar to TextField
, you can change the foreground or background colors, add a border, and use different TextFieldStyle
s, but you cannot change the foreground color of the placeholder text at this time.
Font
I can’t expand much on what Apple’s documentation says about Font
, so I’ve provided a simple way to use custom fonts in the same way as Apple’s standard fonts:
Notice how I’ve made extensions for both Font
and View
. You don’t have to use extensions, as you can see when I use Font.custom
directly. All of these methods result in the same Text
, so it’s just a matter of which code you find to be the cleanest.
The absolute easiest to write is the View
extension, which doesn’t require you to pass anything into the function.
The Font
extension is more consistent with the way standard Apple fonts are assigned, for example, .font(.headline)
.
Image
See also: Image (Updated in SwiftUI 2.0)
Images in SwiftUI are much easier than in UIKit. Instead of needing to create a UIImage(named: “Your file name”)
and assigning it to yourUIImageView.image
, Image
is about as easy to create as Text
.
Just pass it a String
and it’ll set it to a file with that name. If you launch your app and it doesn’t have a file with that name, you’ll get a useful console message saying:
No image named ‘Your file name’ found in asset catalog for main bundle.
Image is not resizable by default
You must call the .resizable()
modifier on your Image
before making changes to its size in subsequent modifiers.
The scaledToFit
modifier will lock the aspect ratio of your image and scale it to the maximum size it can be without being too large for the screen.
The scaledToFill
modifier also scales your image, but it does not lock the aspect ratio and, subsequently, is likely to stretch or shrink your image to fit the available space.
SF Symbols
If you aren’t familiar with them, SF Symbols is a library of over 1500 symbols that Apple provides in nine weights from ultralight to black.
To use these in your images, simply label the String
you pass into your Image as systemName
. It’s probably worth downloading the SF Symbols Mac app so that you can find out what the system name is for the symbols you want to use.
Using SF Symbols gives your app a consistent look that will probably be taking over the iOS ecosystem in the coming years due to the flexibility and accessibility of these free symbols.
Button
See also: Button (Updated in 2.0)
A Button
has no appearance of its own. In other words, you will need to give your Button
a Label
, which itself is any concrete type that conforms to View
.
The most obvious example of this is a Text
that will give information on what your button will do. At the time that I’m writing this, the only thing Apple specifies in the documentation (other than how to create and style them) is that buttons are triggered differently depending on your operating system.
On iOS, you tap on it, on tvOS, you press enter when the button is selected, and in a macOS app with or without Catalyst, which Apple doesn’t mention, you click with a mouse or trackpad.
The constructor requires that you give an action. This can be an empty set of curly braces, but it has to be there in this form at the very least.
As well as specifying your functionality in the curly braces, which can get verbose pretty fast, you can also specify the name of a function without curly braces and without the ()
call operator. This is not binding the action to a variable, which means that you do not need the $
operator that you’ll find on controls that take a binding such as a Toggle
.
ButtonStyle
Some controls allow you to choose existing styles, such as those that conform to ButtonStyle
in this case. That also means that you can create your own custom styles for a Button
, details of how to do that can be found onSwiftUI Lab’s Custom Styling tutorial.
As you might see in the comment I made on that post, SliderStyle
does not currently exist (although it is documented on Apple’s website). Let’s go through the existing styles for buttons and see them in action.
Note that some are only available on MacOS.
NavigationView and NavigationLink
See also: NavigationView (Updated in 2.0)
Embedding your Views in a NavigationView
allows you to set a navigation title and link to other Views. Similarly to Button
, a NavigationLink
requires a Label
which is basically any struct that conforms to the View
protocol.
In most cases, this will probably be a Text
or Image
, but it can also be any custom view that you create.
The View
that is the destination of your link slides in from the right on an iPhone, and each successive NavigationLink
slides in the same way. When returning to the initial View
, you can swipe from the left edge or use the back button in the top-left of the navigation bar.
This example is from my watch app Dog HQ that shows a scrolling list of full-sized dog photos, each of which links to a zoomed-in version.
This is why I need to pass the index to the constructor of my zoomed-in DogView
, so that I know which dog I want to be the destination.
Combining a List
, which scrolls vertically and expands to any size I want, with a ForEach
allows me to create 50 rows and pass that index into the closure with a name I specify.
The iteration for the ForEach
could be a sequence that has a maximum of the number of items in an array, or the array could be passed into the constructor for a List
and accessed inside the closure with a name you specify.
Obviously, I’m just scratching the surface with this array. The array could contain complex types, such as a custom class that has a string property called imageName
, a number value, or perhaps even an instance of another class, which you can access using the dot syntax.
The navigation bar at the top of the screen can contain a leading and trailing button. The main use for this seems to be adding an EditButton
, which is described in detail below.
EditButton
An edit button is pretty useful when you have a List
of items and you want to make it possible to delete some of them. Tapping it takes you into edit mode (unsurprisingly), showing a red circle with a horizontal line through it on each row.
Tapping Edit will slide the row to the left, revealing a delete button on the right end that acts as a final confirmation.
You still need to implement a function that will handle deleting the data from the list, otherwise, your changes will only be visual and your data won’t actually be deleted in the way you expect.
For more information on using EditButton
, see List, ScrollView, ForEach, and DynamicViewContent.
MenuButton
MenuButton
is only available on macOS apps, so I’ve provided a Mac app example that uses all of the standard .menuButtonStyle
options.
From left to right, these styles are BorderlessButtonMenuButtonStyle
, BorderlessPullDownMenuButtonStyle
, and PullDownMenuButtonStyle
.
If the one on the far right looks a lot like the one next to it, it’s because it uses DefaultMenuButtonStyle
.
Since the default MenuButton
has the appearance of PullDownMenuButtonStyle
, these look exactly the same.
PasteButton
See also: PasteButton (Updated in 2.0)
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 UTI types.
I’ve included a function in my example that lets you find the UTI string for any type, which will probably help you when implementing this button. Once you have decided what type identifiers you need, you will need to handle the data that you get from the NSItemProvider
.
I’ve shown an example where I only paste the first 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
Toggle
See also: Toggle (Updated in 2.0)
Toggle
is the SwiftUI equivalent of UISwitch
in UIKit. Instead of having an IBAction
function that links your Swift code to a UISwitch
on a Storyboard and runs when its value changes, SwiftUI uses bindings.
Without marking a variable as State
(within the struct) or Published
(in an outside class conforming to ObservableObject
), SwiftUI will not redraw the contents of the View
when the value changes.
This is an essential part of the binding process, especially marking outside code as Published
, as this is the only way that SwiftUI will even be aware of that variable’s existence.
Creating a custom ToggleStyle
I noticed that the initializer for Toggle
can take a struct called ToggleStyleConfiguration
, and I spent a while trying to figure out how to construct this myself.
What I found, with a lot of help from SwiftUI Lab’s excellent tutorial on custom styling, was that the protocol ToggleStyle
provides the ability to make your own custom styles.
Part of the way it allows you to do this is the following line:
typealias ToggleStyle.Configuration = ToggleStyleConfiguration
Using a typealias
here is just a way of referring to the struct with a more succinct local name. This is probably so that the makeBody
function, seen below, can have the same declaration signature as the similar protocols ButtonStyle
, PickerStyle
, and TextFieldStyle
:
func makeBody(configuration: Self.Configuration) -> some View
Instead, I decided I would change how the label is treated by completely ignoring the label that is passed in and giving two dynamic labels that change based on the toggle’s isOn
state:
There are two examples here, but they look exactly the same. One uses the rather verbose form using the .toggleStyle
modifier, just as the standard ToggleStyle
s do.
The other uses an extension on Toggle
that returns this verbose form, providing a clean call site but becoming inconsistent with the way the standard ToggleStyle
s look.
It’s up to you which of these you prefer. It goes without saying that you do not need to have local variables in the MyToggleStyle
struct, a lack of which would remove the need to pass values into the constructor.
I only did this to show how you can pass custom values in, but you cannot change the signature of the makeBody
function.
In other words, makeBody
can only take a Self.Configuration
parameter. By constructing a struct with uninitialized variables, we have another way to pass values alongside the isOn
binding and Label
from the Toggle
constructor.
MyToggleStyle
does not make use of configuration.label
, which is the value of Text(“This label will never be seen”)
we added. It isn’t necessary to add this label, as a Toggle
can be constructed without it, but it was worth pointing out how a custom ToggleStyle
can hide whatever it wants.
Since makeBody
returns some View
, you can return whatever you want. You could return a Text
, Button
, Image
, or even a VStack
, although I have no idea why you’d want to do that.
Picker
As was mentioned in the Hacking With Swift tutorial on Pickers, the default behavior of a Picker
inside a Form
is to take you to another where you can choose an option.
On iOS you must put the Form
inside a NavigationView
, otherwise, this navigation will not occur. Outside of a Form
, the DefaultPickerStyle
will be WheelPickerStyle
.
I have also included SegmentedPickerStyle
which has a similar appearance to UISegmentedControl
in UIKit
.
DatePicker
See also: DatePicker (Updated in 2.0)
DatePicker
is similar to Picker
, but doesn’t have all the same styles. When used inside a Form
, the DatePicker
only takes up a single line.
As you can see in the screenshot above, the default DatePicker
in a Form
has a label and the current date. Tapping it will cause a DatePicker
to slide out underneath.
The DatePicker
that slides out is exactly the same as the WheelDatePickerStyle
, which is why it looks like it is displayed now when actually I just have a WheelDatePickerStyle
underneath it.
I added a Picker
that you can use to try out different date formats, just to show how you could change the format of a DatePicker
at runtime.
Slider
A slider allows you to swipe the thumb, a white circle, between a minimum and maximum value. This is similar to UISlider
in UIKit. When you create it you have to set a closed range so that SwiftUI knows what the minimum and maximum values will be.
The step can be set to any amount, potentially saving you from needing to convert a long Float
to an Int
if you don’t need your value to be a decimal.
This also helps you to increase or decrease the amount of accuracy that the slider position is recorded in, potentially making it easier to make calculations by excluding decimal places past the step amount you specify.
Stepper
A Stepper
in SwiftUI is basically identical to a UIStepper
in UIKit
. It consists of a connected minus and plus button.
Not all of the initializers require you to set a binding variable to store the value. Many of them take closures that are called when you decrement, increment, or edit the value of the Stepper
.
View Layout and Presentation
HStack, VStack, and ZStack
Although they are always written vertically, these stacks arrange their children in different directions.
VStack
is a useful starting point for any app, as you can quickly fill a phone screen with up to ten children (and all of their descendants).
HStack
will use the available horizontal space to layout its children which might not allow a lot of space on a portrait-oriented phone screen. This is useful when you want to put a Text
label next to a control, such as in a List
(see below).
List, ScrollView, ForEach, and DynamicViewContent
See also: List (Updated in 2.0)
See also: ForEach & DynamicViewContent (Updated in 2.0)
As was mentioned in the example for NavigationLink
, a List
is a scrolling view that will grow vertically to accommodate a dynamic number of rows. Similar to UITableView
in UIKit
, but without any of the work.
You can either add static data to the List
in much the same way as a VStack
, placing one View
on top of another, or you can use a ForEach
.
ForEach
lets you loop through a collection such as an array and display a vast amount of data in a standardized way each time.
ScrollView
enables scrolling on whichever VStack
or HStack
is embedded inside it. The default ScrollView
scrolls vertically, even if the direct child of the ScrollView
is an HStack
.
This means that you have to use ScrollView(.horizontal)
if you intend to override this behavior. You can still use them with a ForEach
as you would a List
, but the extra layer of a VStack
or HStack
makes this a more complicated way.
VStacks
, of course, do not have rows that have a similar appearance to UITableView
cells in UIKit
. A List
that is made up of Text
, for instance, will just pile those Texts
on top of one another without dividers.
It would be possible to make a custom View
that imitates these rows, or gives your rows a totally different appearance.
But it’s probably best to use List
unless you need horizontal scrolling.
List
would also support custom rows and it has other features that a ScrollView
with a VStack
lacks.
When an EditButton
has been added to a View
that contains a List
, you can rearrange or delete items in the List
.
If you don’t have a method that gets called in this situation, the row of your List
will disappear, but you will still have the data behind it unaffected. Next time you start the app after swiping to delete a row, that row will return because the underlying data has not been modified.
In Hacking With Swift’sonDelete
tutorial, you can see how the .onDelete
modifier works. This gives you the ability to pass in a method that will run when the user swipes to delete an item in your List
.
DynamicViewContent
is the return type for the .onDelete
modifier, but all it means is that the ForEach
content needs to be updated.
ForEach
is another View
struct, which means it can be changed dynamically itself when the underlying data changes.
As you can see in my example, .onMove
is pretty similar to .onDelete
. The real problems occur when you try to use .onInsert
, which I couldn’t get working.
The way I expected it to work is in the insert()
function, and this method may start working in future versions of SwiftUI.
For some reason, .onInsert
takes an array of UTType
identifiers in the form of strings. These specify the types that we expect to be inserted into the ForEach
’s underlying data, which in this case is NSString
.
As an example of how to create a UTI type identifier, I created an NSItemProvider
from the string and printed it. This outputs the UTI type string for NSString
, and this is what I quoted in my onInsert
call.
Even so, the method I provided called inserted()
is never called. This seems to indicate that the functionality of .onInsert
has not been added. I only tried it inside a List
and a VStack
, so maybe it works somewhere.
Let me know if you got onInsert
to work, as there are no examples of it anywhere online.
Identifiable
ForEach
loops in SwiftUI require that each item in an array is Identifiable
, meaning that each member has its own unique identifier.
In the following example, I start with an array of strings. Since String
conforms to the Hashable
protocol, it is not necessary for a unique identifier to be provided, as \.self
provides the hashValue
.
Conforming to this protocol in my custom classes would require me to provide a hash(into:)
function that combines the essential components into an integer hashValue
that uniquely identifies each instance.
I would also be required to overload the ==
operator which compared the same properties that I combined in the hash(into:)
function.
Learn more about the Hashable protocol in Apple’s documentation.
When I create myUnhashableType
, I do not conform to the Hashable
protocol.
As a result, using an ID of \.self
does not work, as can be seen in the comment above the second ForEach
. This creates an error that prevents compilation, unless this ForEach
is commented out or removed.
However, the myIdentifiableType
has a much easier way of being identified in the loop. The Identifiable
protocol only requires that a variable by the name of ID exists, and is unique to each instance.
To do this, I simply use UUID, which generates a Universally Unique Identifier each time a new instance is created.
This even lets me avoid the need to specify an identifier in the ForEach
, because conforming to Identifiable
tells the ForEach
exactly what it needs to identify each instance.
Axis
This is simply an enum containing the cases .horizontal
and .vertical
. It is used to represent the two directions that content can be arranged in.
A ScrollView
, for instance, has a property called axes
which is an Axis.Set
. This essentially means that you can change axes to contain either .horizontal
, .vertical
, or both. This changes the directions in which you can scroll.
Form
Form
gives you an interface not unlike that of the iOS Settings menu. You can separate parts of the interface into Section
s, and controls have a much more pleasant appearance than they would have in a List
.
Group
Apple describes this as simply:
“An affordance for grouping view content.”
Instead of having an impact on the layout like a VStack
or HStack
would do, a Group
does not change the layout at all. Instead, it allows you to treat up to ten children as if it were one child. For instance, a VStack
can only have ten children, which limits you to ten View
s.
But if all of those ten children are Group
s, each of those groups can have ten children, leading to a total of 100 View
s being displayed by one VStack
.
The fact that they are treated as one View
also allows you to apply modifiers such as .foregroundColor(.red)
or .frame(width: 300)
to an entire group, instead of having to set this for each View
or place the View
s in a layout such as a VStack
.
GroupBox
GroupBox
is a container for View
s with an optional label, and is only available on macOS.
Section
The screenshot above shows a Form
that is divided into three Section
s.
As you can see, the Form
has gaps between the three Section
s, which can be seen as thinner rows that show a darker background color.
Spacer
In the screenshot above, I’ve shown the similarities and differences between Rectangle
and Spacer
.
When rectangleShown
is true, the Text
s in the HStack
are pushed to the sides and the Text
s in the VStack
are pushed to the top and bottom. The Rectangle
is essentially resizing the height of its parent HStack
’s height to occupy all available vertical space.
If you set rectangleShown
to false, the Rectangle
will disappear but the Text
s will not move.
This is because a Spacer
with a maximum size of infinity acts the same way as a Rectangle
. It has the ability to increase the size of its parent to occupy all available space.
But change spacerMaxSize
to false and the Spacer
will shrink down to the height of the Text
s, which are otherwise the basis of the HStack
’s height.
The Text
s in the HStack
are still pushed to the sides because the HStack
itself has a maxWidth
of infinity by default.
In summary, Spacer
s only grow to the size of their parent by default, and won’t increase the size of their parent unless they are given a maximum size of infinity.
Views like Rectangle
s have an infinite maximum size by default and will increase the size of their parent unless they are given a maximum size equal to that of their parent.
Divider
A Divider
puts a line between View
s in a layout. In a VStack
, they are horizontal lines, while in an HStack
, they are vertical lines.
Setting .background(Color.red
) on the Dividers
would give you red dividers. Otherwise, they are set to the default based on the color scheme currently selected.
TabView
See also: TabView (Updated in 2.0)
The screenshot above comes from implementing Apple’sTabView
example.
There isn’t a lot I can add to that.
VSplitView and HSplitView
These versions of VStack
and HStack
allow the user to drag the dividers to change the size of each split area.
Unsurprisingly, VSplitView
lays its children out vertically while HSplitView
does so horizontally. This is only available on macOS, so you cannot use this in iOS or tvOS projects.
Alert
Alert
s are pretty easy to create but they don’t conform to the View
protocol as you might expect. You cannot place a value of type Alert
in a VStack
or anywhere else you think it might be shown.
Below, I’ve provided an example of the three main scenarios for creating alerts. Note that I’ve added actions for the first two alerts, but this is not required. You could have an action on either alert button, both, or neither, it’s up to you.
In alert1
, you want a default action, in this case called “OK”, that confirms you want an action to happen. This prints “you did something” to the console.
The button next to it is a cancel button, created here using the default Alert.Button.cancel
which automatically provides the expected text and takes no action when it is pressed.
alert2
is very similar, printing “You tried to delete something” to the console when it is pressed. The difference here is that the button is of the type Alert.Button.destructive
, which means that the button will be red to indicate an action that makes a permanent and potentially negative change.
alert3
is the simplest kind of alert, where you can have a title and optional message but only one button that dismisses the Alert
.
ActionSheet
As they work in the exact same way, I’ve replicated the example for Alert
s above with ActionSheets
.
The major difference is that they take an array of buttons, meaning there’s no limit to how many buttons you can add. This is in contrast to Alert
, which only has the option of one or two buttons.
EmptyView
EmptyView
has a fairly descriptive name. It is an invisible View
that takes up no space.
The example I give below draws a specific contrast between EmptyView
and Spacer
. Spacer
can be given a specific frame size and will fill that space, while EmptyView
will just ignore a frame modifier.
Spacer
will fill all available space by default, which is why I have to limit it to a height of 20 for this example.
Perhaps one of the most useful aspects of EmptyView
is that it can be returned as the body of any View
struct. This means you can create an empty View
without getting errors because the body is empty.
EquatableView
SwiftUI Lab has a great tutorial onEquatableView
that explains it better than I can.
AnyView
Since View
is a protocol, you cannot make an instance of View
itself. This means you cannot create an array of type [View]
, but you can make one of [AnyView]
.
Below is an example of an array of type [AnyView]
and how you might display its contents using a ForEach
. You cannot pass the array itself into the constructor of the ForEach
, as AnyView
does not conform to Hashable
which is required for that.
Instead, I’ve created a sequence that goes from the first index to the last, and used this index as a subscript for the array inside.
I’ve also provided a Button
that shuffles the array, just to show you that the underlying type of an AnyView
does not matter. What was previously a Text
can become an Image
, and AnyView
just redraws the content.
TupleView
If you aren’t familiar with it, here’s an explanation of tuple from the Swift language documentation:
“A compound type is a type without a name, defined in the Swift language itself. There are two compound types: function types and tuple types.
A compound type may contain named types and other compound types. For example, the tuple type (Int, (Int, Int))
contains two elements. The first is the named type Int
, and the second is another compound type (Int, Int)
.”
In many ways, a tuple
is like a struct with no body inside curly braces. If a struct has properties that are not initialized with a default value, you are forced to initialize these in brackets when you create it.
Tuples don’t have initializers though, so those brackets are assigned to a tuple
with the equals sign. Unlike initializing struct properties, labels are optional.
I provided a second example that creates a new kind of tuple twoTexts
, with labels for the two values that must match the original twoTexts
if they are used.
I didn’t add myTwoTexts
to the body
of ContentView
, mainly to draw attention to the fact that TupleView
does not require the use of a VStack
despite the fact that it displays content from several View
s.
You cannot create an array that mixes types this way. You can create an array of Text
or Image
, but not of View
because it has protocol requirements. Creating an array of mixed types is inferred, as you cannot cast from Any
to View
, Text
, or Image
.
There is a way to create an array of mixed types, and it is through the use of AnyView
.
Scroll up to AnyView
above for a mixed array example using that.
Unlike most of my examples, I’ve provided ContentView_Previews
for my TupleView
to show how it looks. I used a fixed size because we are working with small View
s, and it makes it easier to see that a TupleView
displays every View
in a separate preview.
With normal View
s, you would need to create a Group
and specify which View
s you wanted to be in separate previews. For more on previews, see what I wrote about thePreviewProvider
protocol near the end of this post.
Drawing and Animation
Animation
Here’s an example that uses the default types of Animation
. To use them, simply make changes to your View
s and use the .animation(.spring())
modifier to add an animation.
If you want to be specific about what changes you want to perform, see withAnimation
. If you make a custom shape with custom properties, you will need to specify them as animatableData
(see below).
Animatable and AnimatableData
Animatable
is a protocol for telling SwiftUI how to animate your custom Shape
.
Without explicitly declaring that a struct conforms to the protocol, you can conform by declaring a property called animatableData
that tells SwiftUI what you can animate.
In my example, I’ve created a Square
shape, as Rectangle
already exists but Square
doesn’t.
Conforming to Shape
requires that your shape has a function called path(in:)
which basically takes the frame rectangle of your shape and requires you to generate a Path
that SwiftUI can use to draw the shape.
All I do is decide which length is shorter, the width or the height. On an iPhone in portrait mode, this is the width.
When I draw the path, I make the shape equal to this shorter length in both directions, instead of using rect.maxX
or rect.maxY
to stretch the square into the provided rectangular space.
In the Y-direction, I also apply an offset so that the square can be moved up or down from its starting position in the center of the screen.
The important part is that I provide a variable called animatableData
, with a getter and setter that provides access to the offset
variable.
The Stepper
s have a step of 25, meaning they move the Square
by 25 every time the number changes. Why is this important? This is a big enough change that it would be a jarring movement if there was no animation.
Try disabling the animation and you’ll see what I mean.
If you’re confused about my use of a GeometryReader
, you can find my definition for that in this post.
AnimatablePair
AnimatablePair
relates to the Animatable
protocol mentioned above, so I won’t repeat the basics here. AnimatablePair
allows you to condense two animatableData
into one value. It’s really that simple.
Here’s a version of the example from Animatable
above that allows the Square
to have both an xOffset
and a yOffset
:
EmptyAnimatableData
In the AnimatablePair
and AnimatableData
examples above, I explicitly informed SwiftUI of what properties I expected to animate in my custom shape.
Since Shape
itself conforms to the Animatable
protocol, the default implementation is inherited. All the default implementation does is create an animatableData
property that is set to a type of EmptyAnimatableData
.
This allows children of Shape
to conform to the Animatable
protocol without actually setting their animatableData
.
If they want to override this value, they can, as I did in the examples above.
AnimatableModifier
AnimatableModifier
allows you to produce a modified view that has an animation.
I’ve produced a version of the example from the section above on the Animatable
protocol that uses the AnimatableModifier
protocol instead. In the existing example, I had a stepper that moved a square up and down by 25 each time, animating it as it goes.
In this new version, I am using a blue Rectangle
that fills the entire screen, and apply the AnimatableSquare
modifier.
Note that I use a View
extension to avoid using the awkward .modifier(SquareAnimatable(offset: offset))
syntax usually required for custom modifiers.
The SquareAnimatable
modifier adds a red Square
as an overlay to any View
. That overlay takes its offset
value from the View
that creates it, meaning the parent View
can change that value and SquareAnimatable
will move the square and animate the change.
The Square
shape in this example is actually declared inside the scope of the SquareAnimatable
modifier, which means that the parent View
with the Stepper
passing it an offset value has no idea how to even make the Square
that it’s controlling!
When I originally did this, I passed it the offset through as a @Binding
.
Don’t do this!
The setter in the animatableData
tries to set the value that was passed through, and you get a runtime warning that says:
Modifying state during view update, this will cause undefined behavior
Essentially, the problem is that we are trying to change that initial @State
property called offset while the View
is in the middle of being created. It wouldn’t be a problem if it was a Button
.
withAnimation (Implicit animation)
An easy way to get animation into your SwiftUI View
s is to place code inside a withAnimation
block.
This is similar to the block used in UIView.animate(withDuration:Animations:)
in UIKit
, but it does not take a duration by default.
You can pass in an Animation
object such as withAnimation(.linear(duration: 5))
to add greater control over how the animation looks. You even have a choice about whether to pass a duration into .linear
.
For more details on Animation
, see the definition for that at the beginning of this section.
Note that adding the .animation(.default)
modifier to the Rectangle
would have the same effect in this case, although it’s worth pointing out the difference.
The .animation(.default)
modifier is an example of implicit animation. You are basically saying that we expect any changes to the Rectangle
to be animated.
This means that if we added another Button
that increased the width, this would also be animated, even if we don’t specify these changes as withAnimation
.
If you want full control over what aspects of a View
can be animated, you need to be explicit by specifying exactly which value changes should animate using withAnimation
.
AnyTransition
This is similar to AnyView
, which allows you to treat a view as generic and an opaque return type.
When combining multiple transitions, you can use the combined
method to add transitions together. You can also add an Animation
to a transition, the result of which is an AnyTransition
object.
InsettableShape
strokeBorder
, which allows you to draw borders that cut into the area of the shape, requires that a Shape
conforms to the InsettableShape
protocol.
See this Hacking With Swift tutorial for more information.
FillStyle
FillStyle
only has two options, both of which are bools.
The even-odd rule relates to how SwiftUI decides which parts of a path should be filled. To quote the specification for SVG:
“This rule determines the “insideness” of a point on the canvas by drawing a ray from that point to infinity in any direction and counting the number of path segments from the given shape that the ray crosses.
If this number is odd, the point is inside; if even, the point is outside.”
In practice, this causes a shape with a fill that is distorted to overlap itself to have no fill on the part that overlaps. There are examples of this effect in the SVG specification, and the Wikipedia page for the even-odd rule.
If you use false for the isEOFilled
parameter, the non-zero method is used. This ensures that all enclosed spaces are filled, not just the ones that don’t overlap.
Anti-aliasing is the process of smoothing jagged edges or ‘jaggies’. When resolutions are low, jaggies result from the fact that raster images are made up of a grid of square pixels.
Although straight horizontal and vertical lines can be rendered at low resolutions without jaggies, any difference in angle from those axes causes jaggies to appear, due to the “staircase effect” of trying to represent a line that is not perpendicular to one of the axes with a grid of square pixels.
If you use true for the isAntialiased
parameter, some amount of blurring is used to soften the jaggies. Otherwise, jaggies will occur.
ShapeStyle
ShapeStyle
is used to create View
s from Shape
s. The background modifier, which surprisingly has no documentation at the current time, is declared as follows:
extension View {
@inlinable public func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background : View
}
In fact, you might be surprised to learn that you can create a View
that has only a Color
as the body, despite the fact that Color
doesn’t conform to the View
protocol directly.
Instead, Color
conforms to ShapeStyle
, which conforms to View
itself.
What seems to be happening is that a Rectangle
is being created with the Color
, filling it according to ShapeStyle
’s default implementation:
ImagePaint
, Border
, Stroke
, Fill
, and Gradients
all seem to use ShapeStyle
in some way to create foregrounds and backgrounds.
GeometryEffect
GeometryEffect
allows you to create custom animations, many of which give a 3D effect similar to the provided rotation3DEffect
modifier.
SwiftUI Lab has a great tutorial on using GeometryEffect.
Angle
You can create an Angle
with degrees or radians, both of which are required to be a Double
.
Once created, the degrees or radians of your Angle
can be accessed as properties. Angles
are used in making RadialGradients
, adding arcs to Paths
, and in RotationEffect
and RotationGesture
.
Edge and EdgeInsets
Edge
is an enum containing the values .bottom
, .leading
, .top
, and .trailing
. It seems to only be used in two modifiers: .edgesIgnoringSafeAreas
and .padding
.
EdgeInsets
is used in places like the .listRowInsets
and .resizable
modifiers.
Rectangle, RoundedRectangle, Circle, Ellipse, and Capsule
You can create these shapes easily as they are provided as Views
in SwiftUI. The image below shows the difference between them.
Circle
seems to be the only one that locks its aspect ratio, so even giving it a frame with more width than height (as I do in the code below) does not stretch the Circle
like it does the Ellipse
.
There is no equivalent called Square
, so you can only create a square by using a Rectangle
and giving it an equal width and height.
Hopefully, the side-by-side comparison shows how a Capsule
differs from a RoundedRectangle
. I provided the RoundedRectangle
with a cornerRadius
of 15, which is why it has a visible top edge.
If I set a RoundedRectangle
’s cornerRadius
to 50% of its width, which is 50 in this case, it has an almost indistinguishable appearance from the Capsule
.
In summary, a Capsule
is like a RoundedRectangle
with a cornerRadius
that is always equal to 50% of its width.
A Rectangle
is also identical to a RoundedRectangle
with a cornerRadius
of 0
.
Path
To repeat what I said in the section on Animatable
and AnimatableData
:
“Conforming to Shape
requires that your shape has a function called path(in:)
that basically takes the frame rectangle of your shape and requires you to generate a Path
which SwiftUI can use to draw the shape.”
The Apple tutorial Drawing Paths and Shapes uses paths directly in the body property of a View
, without conforming to Shape
.
ScaledShape, RotatedShape, and OffsetShape
These transformed Shapes
are pretty easy to use, as they merely require that you pass in a Shape
and the necessary parameters for the transformation.
For my scaled Rectangle
, I scaled it by 0.5 in both directions. A Rectangle
will usually take up all of the available space, so it is noticeable that this one is relatively small and centered in the top third of the VStack
.
The rotated and offset rectangles had to be scaled down using their frame, otherwise they would overlap the others.
This screenshot was taken in Light Mode, so the default foregroundColor
is black. I changed the overlaid Text
s for the first two to white, but it should be noted that this would not work in Dark Mode.
In Dark Mode, the Rectangle
s will take on the default foregroundColor
, which is white. So, this text would be hidden, while the Text
for the offset Rectangle
would be hidden by the default black background.
Always think about Light and Dark mode when using default background and foreground colors in SwiftUI.
I put the OffsetShape
example in a Group
which has its own fixed height at 200.
This is because offsetting a shape does not increase the size of the space it is allocated, so while the space allocated would be 100 due to the height of the OffsetShape
, moving it down and to the right would merely move the OffsetShape
off of the bottom of the screen.
RotatedShape
, similarly, does not increase the allocated space to account for rotation.
Note that the overlaid Text
doesn’t get offset with the OffsetShape
, creating an amusing effect where the Text
is left where the OffsetShape
should be.
TransformedShape
A TransformedShape
is similar to the ScaledShape
,RotatedShape
, andOffsetShape
examples above, except it takes a single parameter of a CGAffineTransform
in its initializer.
A Core Graphics Affine Transform represents a transformation as a 3 x 3 matrix, meaning it can represent many transformations in a single instance. Since a 3 x 3 matrix always has [0, 0, 1] in the far-right column, all changes made here are in the first two.
If you don’t know how matrix multiplication works, check out this zany website.
Although you can construct matrices with the GAffineTransform
class, either at initialization or later by adjusting the properties for the individual positions a, b, c, d, tₓ, and tᵧ, the easiest way to use the class is to use the constructors and instance methods Apple provides.
In my example, I use the constructor that takes a translation to try and center the Rectangle
in the middle of the screen. Then I scale it and rotate it by 45 degrees.
Note that you need to assign the result of the instance methods to the variable itself, which is why I use the awkward syntax affineTransform = affineTransform.scaledBy(x: 0.4, y: 0.4)
.
I originally assumed that calling the method directly would work, as Xcode does not warn you in its usual way that:
Result of call to scaledBy(x:y:) is unused
Color
SwiftUI has its own Color
class that is cross-platform, meaning it works on macOS, tvOS, iOS, and watchOS. This is in contrast to NSColor
, which only works on macOS, and UIColor
which works basically everywhere else.
Color
in SwiftUI can be initialized using NSColor
, UIColor
, Red/Green/Blue (RGB), Hue/Saturation/Lightness (HSL), or White/Opacity.
An important distinction with these initializers is that Color
labels the transparency parameter as “opacity”, not “alpha” as in the other color classes.
Unlike UIColor
, Color
cannot be initialized from CGColor
or CIColor
. To use these, simply pass them into the initializer for NSColor
(on macOS) or UIColor
(everywhere else) and pass the result into the initializer for Color
.
In the following example, I display three types of color in SwiftUI, or at least, I would if SwiftUI would let me:
As you can see, although I can construct CGColor
, UIColor
, and Color
using the same variables, I must convert the first two to Color
if they are to be used in my View
.
You might also notice that Color
requires these variables to be of type Double
, not CGFloat
.
It might seem like this signals the beginning of the end for the Core Graphics Float, but SwiftUI still uses it all over the place, perhaps most notably to set the width and height of a View
’s frame.
Perhaps one day, SwiftUI will require a Double
to set the frame of a View
, and that will surely mark a turning point.
ImagePaint
Gradients (Linear/angular/radial)
GeometryReader and GeometryProxy
GeometryReader
allows you to capture the geometry of the View
s on the screen.
In the example above, the ZStack
in which the entire View
is contained is itself embedded in a GeometryReader
, a closure which does not affect layout but requires that one argument be passed in.
Apple’s tutorial calls this argument geometry, so I have done the same in my example. The object being passed in is a GeometryProxy
, which gives us two properties and one method.
The method, frame(in:)
, allows you to pass in CoordinateSpace.local
or CoordinateSpace.global
to get the frame relative to the direct parent (local) or relative to the highest level parent (global).
This example shows two GeometryReader
s being used on an iPhone 11 Pro Max. The green area is a VStack
which is the first parent of the View
.
The GeometryProxy
passed into the GeometryReader
closure shows that the first parent has safe area insets of 44 at the top and 34 at the bottom. The top is to avoid placing the view underneath the notch.
On iPhone 8 and other devices that still have home buttons, the top safe area has a size of 20 to avoid the status bar.
Only iPhones without home buttons have this bottom safe area of 34, which allows the user to swipe up to go home and switch apps.
To ignore safe areas with any view, use the .edgesIgnoringSafeArea(.all)
modifier. The other options are .bottom
, .leading
, .top
, and .trailing.
For more information on these options, see Edge.
It’s worth noting that size seems to be the same in most situations. In global and local scope, the parent VStack
and the smaller red VStack
both have a static size.
Where they differ is their minX
and minY
values. When I first put the small red VStack
inside an HStack
, the minX
value in global space didn't change.
Only when I inserted a Spacer
did the VStack
move right by eight, creating the situation where both the global position minX
and minY
are different.
CoordinateSpace
The coordinate space was covered in more detail in the GeometryReader
andGeometryProxy
section above.
The main difference is that the local origin of a View
is (0,0), but due to its position on the screen that View
’s global position may differ from this.
CoordinateSpace
is an enum that provides the .local
and .global
options when using the frame(in:)
function of a GeometryProxy
object.
The only other place where the enum seems to be used is in DragGesture
. The initializer for DragGesture
can take a parameter of minimumDistance
, which is the movement required before action is taken, and a case of the coordinateSpace
enum.
With this coordinateSpace
set, the Value
struct that is passed into the onChanged
closure for the gesture will give relative or universal coordinates in its startLocation
and location
CGPoint
properties.
Framework Integration
UIHostingController
If you create a new SwiftUI project in Xcode and go to the SceneDelegate
Swift file, you’ll notice that the top function is called func scene(_ :, willConnectTo:, options:)
and it contains code that initializes an instance of the ContentView
struct.
Next, the function gets the current UIWindowScene
, which is basically the manager for using multiple windows on iPad. If you have an iPhone, multiple windows are not possible, so you’re merely managing the one window.
Inside the if let windowScene = scene as? UIWindowScene
block, you’ll notice that SceneDelegate
immediately gets the current window. This isn’t difficult, because we haven’t set up multiple windows on iPad at this point, so there is only one window.
Then, somewhat like the initial UIViewController
on a Storyboard
, we set the rootViewController
.
Creating a new UIHostingController
allows us to display our SwiftUI.
There are tutorials like SwiftUI Lab’s Dismissing Modals that show you the value of creating your own custom UIHostingController
.
I can’t provide a better example than that, but if you don’t have a specific problem to solve like that, you may not ever need to create a custom UIHostingController
.
The main change you’ll need to make in SceneDelegate
is to pass an EnvironmentObject
into your ContentView
. For more on that, see EnvironmentObject
.
UIViewRepresentable
UIViewRepresentable
allows you to create SwiftUI View
s from UIViews
in UIKit.
In this example, I create a multiline TextField
which currently isn’t possible in SwiftUI. As there is no equivalent of UITextView
, I create this using UIViewRepresentable
with a simple ObservableObject
that saves the data permanently to UserDefaults
.
If you type into the MultiTextField
, your changes are automatically and instantly saved, so the text will be the same next time you launch the app.
UIViewControllerRepresentable
Instead of representing individual views as in UIViewRepresentable
above, you can even represent entire UIViewController
instances from UIKit.
To replicate my example, you’ll need to create a Storyboard
in your SwiftUI project, and call it the default name of “Storyboard”.
Add a UIViewController
to the Storyboard
, and select it in the view hierarchy. Set the Class
to ViewController
and the Storyboard ID
to initialVC
.
Add a UILabel
, which I centered using constraints.
I won’t explain UIKit constraints in this post, but there are many tutorials on it online.
You don’t need the UILabel
I added at the top that says “This is a ViewController from a Storyboard”, that was just to make it obvious when it works.
You also don’t have to make the background blue, I just thought that would make the distinction more obvious when we mix SwiftUI and the UIViewController
.
You don’t have to create your ViewController
instance from a Storyboard
, I just thought this would be a useful example for a lot of people.
All that you need to include in UIViewConrollerRepresentable
is makeUIViewController(context:)
which initializes the ViewController
and updateUIViewController(_:, context:)
which reacts to changes in SwiftUI.
Notice that I have a @Binding
property that is linked to my ContentView
struct, meaning that changes I make in a SwiftUI TextField
update this property in UIKitVC
, while updateUIViewController(_:, context:)
passes these changes to the actual UILabel
in the ViewController
class.
DigitalCrownRotationalSensitivity
This is an enum of sensitivities for the rotating dial on the side of the Apple Watch. It comes in .low
, .medium
, and .high
varieties.
State and Data Flow
State
Any variables that you want to store locally in a View
struct should be marked with this. If you add a variable and don’t add @State
to it, you cannot use it to store the value of a control.
This is because @State
variables can be changed at runtime, and the SwiftUI View
will redraw itself on that basis.
For instance, if you have a Button
that changes the value bound to a Slider
, that Slider
would move to reflect the change you made despite the fact that you didn’t move the Slider
itself.
Here are three of the main SwiftUI controls with their corresponding @State
variables, and a reset Button
that changes them all:
Binding
If you want to affect a @State
property in the parent of a child View
, you’ll need to pass it in and mark it as @Binding
. This gives the child View
the same direct access to the parent’s @State
variable as the parent has.
In the example below, I use the @State
example above to present a sheet containing a child View
.
As the local @Binding
properties are uninitialized, you are required to initialize them when you create the PresentedView
struct in the sheet modifier.
If you’re unsure about the following line:
Environment(\.presentationMode) var presentationMode
ObservedObject
In my examples for controls such as Toggles
and TextFields
, I showed a simple way to access data from a Swift class conforming to the ObservableObject
protocol.
I show this in the same code snippet for simplicity, but it should really be in a separate Swift file. Here’s the most basic example of binding a Swift class as an @ObservedObject
.
This means that any changes made to variables marked @Published
in your Swift file will notify your SwiftUI file to update its view accordingly.
EnvironmentObject
Adding an EnvironmentObject
is pretty similar to adding an ObservedObject
.
The structure of the DataModel
class here is exactly the same, but we are marking it as EnvironmentObject
inside ContentView
and not setting it to DataModel.shared
.
Instead, we are merely declaring it with a name and type, and the DataModel.shared
is passed in using the SceneDelegate
Swift file.
I have included what needs to be changed in SceneDelegate
to pass the EnvironmentObject
in. Bear in mind that any subsequent views that you navigate to, such as with a NavigationLink
or by presenting a sheet, will need to have the same EnvironmentObject
passed to them when they are created.
I’ve added a sheet to my example which you can present by pressing a button, just to show how the environmentObject
is passed (although it’s the same as in SceneDelegate
):
FetchRequest and FetchedResults
There’s a good Hacking With Swift tutorial onFetchRequest
.
DynamicProperty
For SwiftUI View
s to be refreshed when their underlying data changes, we need a protocol to encapsulate this underlying concept.
At least some of the conforming types Binding
, Environment
, EnvironmentObject
, FetchRequest
, GestureState
, ObservedObject
, and State
should be familiar to anyone who has worked with data in SwiftUI.
These properties are set before the body of the View
is redrawn.
Environment
Environment gives you access to settings related to your device’s settings. For instance, ColourScheme.dark
allows you to preview what your app will look like in the new Dark Mode, and to contrast that side-by-side with the more conventional ColourScheme.light
.
Contrast can be increased in the Accessibility settings, so ColorSchemeContrast.increased
or ColorSchemeContrast.standard
are the only options.
The result of selecting bold text in Accessibility settings is shown by LegibilityWeight.bold
, otherwise the default is LegibilityWeight.regular
.
Every View
has an associated PresentationMode
struct that stores one property and has one method. The property is the bool isPresented
, which tells you if the View
is active.
The method is dismiss()
, which allows you to remove the current View
from the screen and return to whatever View
presented it.
I could go on, but it probably makes more sense to give an example that lists how to bind every single Environment
value and view the data in a List
:
PreferenceKey
Any View
can use the modifier .preferredColorScheme(.dark)
to force its appearance to be that of Dark Mode even if the device is set to Light Mode.
What this does is set the PreferredColorSchemeKey
struct’s value property to that of ColorScheme.dark
. This can be overridden by forcing a View
to use Light Mode with .colorScheme(.light)
, which is why the first modifier only indicates a preference and not a mandatory state.
It’s possible to call .preferredColorScheme(nil)
to indicate no preference, which causes the default color scheme to be used, whereas .colorScheme(nil)
cannot be called.
Why can’t .colorScheme(nil)
be called?
PreferredColorSchemeKey
conforms to the PreferenceKey
protocol, which requires not just a value but also a defaultValue
which can be used when no value was set.
The .colorScheme(.light)
modifier doesn’t set a struct value at all, and merely returns the View
with the required color scheme.
LocalizedStringKey
LocalizedStringKey
can be created from a string and will attempt to use it to find a corresponding value in Localisable.strings
or another file used for internationalization.
If no value is found, the key itself is used instead.
For an example of using LocalizedStringKey
, see Text.
Gestures
Gestures
I know this is supposed to be a replacement for their documentation, but Apple’s tutorial covers Gestures pretty well. I don’t want to give examples that are similar to those provided, so I’ll just leave this section as it is.
When you’ve mastered the basics, Apple has another tutorial on combining Gestures into more complex interactions. Both of these articles also have a list of standard Gesture types at the bottom, all of which seem to be very well-documented.
If you find a page of the Gestures documentation that needs further explanation, let me know and I’ll do my best to explain it here.
Previews
The PreviewProvider protocol
Creating a struct that conforms to this provider allows you to create a collection of View
s. Creating a Group
allows you to create multiple previews, each of which can have a different platform.
You can use VStack
, HStack
, and ZStack
for this, but this produces the bizarre result of displaying multiple previews on a single screen, even if you choose different preview devices.
As well as specifying devices, you can also specify platforms. The current options for this are iOS, macOS, tvOS, and watchOS.
By default, the PreviewLayout
value is set to .device
, which displays what the device looks like and fits the preview inside it. Setting it to .sizeThatFits
seems to give a container the size of the device, but without showing the device bezels around the container.
Finally, setting previewLayout
to fixed
allows you to set a custom width and height for the container, which may be useful when you aren’t too bothered about what your View
will look like on a device.
Using .environment
allows you to preview in Dark Mode and to see how your app works in different localizations.
Thanks for reading!