Skip to main content

SwiftUI tips and tricks

About 11 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

SwiftUI tips and tricks 관련

SwiftUI by Example

Back to Home

SwiftUI tips and tricks | SwiftUI by Example

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.


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 @twostrawsopen in new window and let me know!

Similar solutions…
All SwiftUI property wrappers explained and compared | SwiftUI by Example

All SwiftUI property wrappers explained and compared
How to use Instruments to profile your SwiftUI code and identify slow layouts | SwiftUI by Example

How to use Instruments to profile your SwiftUI code and identify slow layouts
Building a menu using List | SwiftUI by Example

Building a menu using List
Answering the big question: should you learn SwiftUI, UIKit, or both? | SwiftUI by Example

Answering the big question: should you learn SwiftUI, UIKit, or both?
Frequently asked questions about SwiftUI | SwiftUI by Example

Frequently asked questions about SwiftUI

이찬희 (MarkiiimarK)
Never Stop Learning.