SwiftUI tips and tricks
SwiftUI tips and tricks 관련
Updated for Xcode 15
SwiftUI is packed with powerful headline features, but there are also dozens of smaller tips and tricks that will help you write better apps.
I’ve tried to summarize all the tips I’ve come across so far below, and where applicable I’ve also provided links to my more in-depth SwiftUI tutorials to help give you extra context.
Resume the live preview
Having a live preview of your layouts while you code is a great feature of Xcode, but often you’ll see it pause because you changed a lot and Xcode couldn’t keep up.
Rather than constantly reaching for your trackpad to press Resume, here’s the most important keyboard shortcut for SwiftUI developers: press Option+CMd+P to make the preview window reload immediately, and resume its live updates.
Make @State
private
Apple provides us with three ways to use state in our apps: @State
is for simple local properties, @ObservedObject
is for complex properties or properties that are shared between views, and @EnvironmentObject
is for properties that are indirectly shared potentially by many views.
Because @State
is specifically designed for use by the local view, Apple recommends marking @State
properties as private
to really re-enforce that they aren’t designed to be accessed elsewhere:
@State private var score = 0
Prototype with constant bindings
If you’re just trying out a design and don’t want to have to create bindings to use things like text fields and sliders, you can use a constant binding instead. This will allow you to use the object with a realistic value.
For example, this creates a text field with the constant string “Hello”:
TextField("Example placeholder", text: .constant("Hello"))
.textFieldStyle(.roundedBorder)
Important
If you’re using Xcode 12 you need to use RoundedBorderTextFieldStyle()
rather than .roundedBorder
.
And this creates a slider with a constant value of 0.5:
Slider(value: .constant(0.5))
Note
Constant bindings like this one are just for testing and illustration purposes – you can’t change them at runtime.
Presenting test views
Another useful tip while you’re prototyping is that you can present any kind of view rather than a full detail view – even when working with a navigation stack.
For example, if you had a list of users and wanted to make sure that tapping one of them worked, you could use a navigation link that points to a text view rather than a fully fledged custom custom view, like this:
struct ContentView: View {
let users = (1...100).map { number in "User \(number)" }
var body: some View {
NavigationStack {
List(users, id: \.self) { user in
NavigationLink {
Text("Detail for \(user)")
} label: {
Text(user)
}
}
.navigationTitle("Select a user")
}
}
}
This allows you to make one screen complete before going on to design the real detail view.
Add customizations for an individual platform
Sometimes you have one SwiftUI view that works great on both iOS and macOS, but needs just a tiny modification – perhaps a little more padding on iOS, or slightly different styling.
For these times I recommend the following view extensions, which add iOS()
, macOS()
, tvOS()
, and watchOS()
methods for just this purpose:
extension View {
func iOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
#if os(iOS)
return modifier(self)
#else
return self
#endif
}
}
extension View {
func macOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
#if os(macOS)
return modifier(self)
#else
return self
#endif
}
}
extension View {
func tvOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
#if os(tvOS)
return modifier(self)
#else
return self
#endif
}
}
extension View {
func watchOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
#if os(watchOS)
return modifier(self)
#else
return self
#endif
}
}
Each of those follow the same pattern: they create a new method using the platform name, each of which accept a function that is able to transform the current view somehow. Inside the method there is a compiler check to see whether the current platform manages the one we expect – if so we apply the modifier function, otherwise we don’t.
Those platform checks – #if os(iOS)
, etc – are done at compile time, which means they will be optimized away. In fact, it’s likely the compiler will be able to optimize these methods entirely because they are so simple.
Use them like this:
Text("Hello World")
.iOS { $0.padding(10) }
Go past the 10 view limit
All containers in SwiftUI must return no more than ten children, which is usually fine for most purposes. However, if you need to have more than 10 views, if you need to return more than one view from your body
property, or if you need to return several different kinds of view, you should use a group like this:
struct ContentView: View {
var body: some View {
List {
Group {
Text("Row 1")
Text("Row 2")
Text("Row 3")
Text("Row 4")
Text("Row 5")
Text("Row 6")
}
Group {
Text("Row 7")
Text("Row 8")
Text("Row 9")
Text("Row 10")
Text("Row 11")
}
}
}
}
Groups are purely logical containers – they don’t affect your layouts.
Use semantic colors
SwiftUI is designed to work with themed user interfaces out of the box, which means it provides both semantic and adaptive colors by default. Although it might be tempting to use your own custom colors, you should at least check first whether you have something in the default SwiftUI set.
For example, Color.red
isn’t the pure red of RGB(255, 0, 0), but instead slightly lighter or slightly darker based on the environment – it adapts automatically without us needing to think about it.
Similarly, Color.primary
, Color.secondary
, and Color.accentColor
all refer to fixed values that are provided by the environment, allowing us to structure and highlight content in a standardized way.
Rely on adaptive padding
SwiftUI lets us control precisely how much padding to apply around views, like this:
Text("Row 1")
.padding(10)
While it’s tempting to always control padding like this to “get things just right”, if you use the padding()
modifier without any parameters you get adaptive padding – padding that automatically adjusts itself to its content and its environment.
So, if your app is running on an iPad with a regular size class you’ll get more padding than if the user moves it down to a split view – all without having to write any code.
Combine text views
You can create new text views out of several small ones using +
, which is an easy way of creating more advanced formatting. For example, this creates three text views in different colors and combines them together:
struct ContentView: View {
var body: some View {
Text("Colored ")
.foregroundStyle(.red)
+
Text("SwifUI ")
.foregroundStyle(.green)
+
Text("Text")
.foregroundStyle(.blue)
}
}
How to make print()
work
If you press play in the SwiftUI preview to try out your designs, you’ll find that any calls to print()
are ignored. If you’re using print()
for testing purposes – e.g. as simple button tap actions – then this can be a real headache.
Fortunately, there’s a simple fix: right-click on the play button in the preview canvas and choose “Debug Preview”. With that small change made you’ll find your print()
calls work as normal.
Relying on the implicit HStack
When you create a list of items, it’s common to want to get the iOS-standard look of having an image on the left then some text on the right.
Well, if you’re using a dynamic list of items – i.e., a list that’s attached to an array of data – then you actually get a HStack
for free inside your list, so there’s no need to make one by hand.
So, this code will create a list based on picture names from an array, and relies on the implicit HStack
to arrange the image and text side by side:
struct ContentView: View {
let imageNames = ["paul-hudson", "swiftui"]
var body: some View {
List(imageNames, id: \.self) { image in
Image(image).resizable().frame(width: 40)
Text(image)
}
}
}
Splitting up large views
If you find yourself with a large view you might find it easier to break it up into several smaller views and compose those together to get the same result. One of the great features of SwiftUI is that there’s no performance difference because it flattens its view hierarchy, but it certainly makes maintenance easier!
For example, here’s a list that shows an image, a title, and a subtitle for every users:
struct ContentView: View {
let users = ["Paul Hudson", "Taylor Swift"]
var body: some View {
NavigationStack {
List(users, id: \.self) { user in
Navigation {
Text("Detail View")
} label: {
Image(systemImage: "person.crop.circle")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(user)
.font(.headline)
Text("Occupation: Programmer")
}
}
}
.navigationTitle("Users")
}
}
}
Even though it’s not really that complicated, you still need to read it carefully to understand what it’s going on.
Fortunately, we can take parts of the view out into a separate view to make it easier to understand and easier to re-use, and Xcode makes it a cinch: just Cmd-click on the navigation link and choose Extract Subview. This will pull the code out into a new SwiftUI view, and leave a reference where it was.
Note
If your subview relies on data from the parent you’ll need to pass that along yourself.
Read more
Better previewing
One of the many benefits of SwiftUI is that we get instant previews of our layouts as we work. Even better, we can customize those previews so that we can see multiple designs side by side, see how things look with a navigation stack, try out dark mode, and more.
For example, this creates a preview for ContentView
that shows three different designs side by side: extra large text, dark mode, and a navigation stack:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
ContentView()
.preferredColorScheme(.dark)
NavigationStack {
ContentView()
}
}
}
Tips
Make sure you zoom out or scroll around in the preview window to see all the different previews.
Create custom modifiers
If you find yourself regularly repeating the same set of view modifiers – for example, making a text view have padding, be of a specific size, have fixed background and foreground colors, etc – then you should consider moving those to a custom modifier rather than repeating your code.
For example, this creates a new PrimaryLabel
modifier that adds padding, a black background, white text, a large font, and some corner rounding:
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.black)
.foregroundStyle(.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
You can now attach that to any view using .modifier(PrimaryLabel())
, like this:
struct ContentView: View {
var body: some View {
Text("Hello World")
.modifier(PrimaryLabel())
}
}
Animate changes easily
SwiftUI has two ways for us to animate changes to its view hierarchy: animation()
and withAnimation()
. They are used in different places, but both have the effect of smoothing out changes to the views in our app.
The animation()
method is used on bindings, and it asks SwiftUI to animate any changes that result in the binding’s value being modified. For example, here’s a view that has a Toggle
to show or hide a label:
struct ContentView: View {
@State private var showingWelcome = false
var body: some View {
VStack {
Toggle("Toggle label", isOn: $showingWelcome)
if showingWelcome {
Text("Hello World")
}
}
}
}
When the toggle is changed, the text view below it will appear or disappear immediately, which isn’t a great experience. However, if we used animation()
we could make the view slide in and out smoothly when the toggle is changed:
Toggle("Toggle label", isOn: $showingWelcome.animation())
You can even control the kind of animation you want, like this:
Toggle("Toggle label", isOn: $showingWelcome.animation(.spring()))
When you’re working with regular state rather than bindings, you can animate changes by wrapping them in a withAnimation()
call.
For example, here’s our same view except now it shows or hide the welcome label using a button press:
struct ContentView: View {
@State private var showingWelcome = false
var body: some View {
VStack {
Button("Toggle label") {
showingWelcome.toggle()
}
if showingWelcome {
Text("Hello World")
}
}
}
}
As with before that will cause the welcome label to appear and disappear immediately, but if we wrap our changes in withAnimation()
they will be animated instead:
withAnimation {
showingWelcome.toggle()
}
And it’s customizable in exactly the same way as animation()
:
withAnimation(.spring()) {
showingWelcome.toggle()
}
Showing multiple alerts in a view
If you try to attach multiple alert()
modifiers to a single view, you’ll find your code doesn’t work as you expect – one alert will work but the other won’t.
To fix this, you should attach your alerts to different parts of your view hierarchy, such as to the button or other view that triggers the alert to appear.
Publishing new values from a binding
Last but not least, to avoid problems when sending update notifications from a publisher – e.g. calling send()
on a PassthroughSubject
or updating any @Published
property – you should make sure you’re always on the main thread.
As with UIKit and most other UI frameworks, you can do all the background work you want in your SwiftUI apps, but you should only ever manipulate the user interface on the main thread. Because state changes automatically trigger a refresh of your body
, it’s important that you make sure you perform those state changes on the main thread.
What are your tips?
The SwiftUI tips and tricks above are ones I’ve come across watching WWDC sessions, asking questions in the labs, and writing lots and lots of code while moving my own projects from UIKit.
I’d love to hear what tips you have – send me a tweet @twostraws and let me know!