FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

What’s new in SwiftUI for iOS 15

Visual effects, remote URLs, and cleaned up APIs galore.

Paul Hudson       @twostraws

Expectations were always going to be high for SwiftUI this year, but the team didn’t disappoint – they’ve shipped a massive collection of improvements and features, including a new AsyncImage view for loading remote images, swipe actions for list rows, pull to refresh, plus shorter, simpler APIs for common uses. Alongside huge improvements to Swift itself (see What's new in Swift 5.5 for more on that), this is another significant leap forward for SwiftUI and I’m really keen to dive in.

Please keep in mind that these changes are very new – I'm pushing it as hard as I can, experimenting, refining, sharing, and learning all at the same time. If you have any feedback, please tweet me @twostraws.

You can watch the video below, or scroll down for links to articles.

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Get started

Sponsor Hacking with Swift and reach the world's largest Swift community!

The big stuff

More welcome improvements

An important animation deprecation

The one-parameter form of the animation() modifier has now been formally deprecated, mostly because it caused all sorts of unexpected behaviors. To see the problem yourself, try code like this:

struct ContentView: View {
    @State private var scaledUp = true

    var body: some View {
        VStack {
            Text("Hello, world!")
                .scaleEffect(scaledUp ? 2 : 1)
                .animation(.linear(duration: 2))
                .onTapGesture { scaledUp.toggle() }
        }
    }
}

That seems simple enough: animation the text getting larger or smaller when it’s tapped. However, we attached the animation to everything for the label, which means even something like rotating the screen would make the text label animate from its old position to its new position.

The solution here is to use animation(_:value:) to attach your animation to a specific value changing, like this:

struct ContentView: View {
    @State private var scaledUp = true

    var body: some View {
        VStack {
            Text("Hello, world!")
                .scaleEffect(scaledUp ? 2 : 1)
                .animation(.linear(duration: 2), value: scaledUp)
                .onTapGesture { scaledUp.toggle() }
        }
    }
}

Toggle buttons

iOS 15 provides a middle ground between Button and Toggle, which is a Toggle that looks like a button but flips its foreground and background colors in its on state. This is enabled using the .button toggle style, as shown in the following code:

struct ContentView: View {
    @State private var isOn = false

    var body: some View {
        Toggle("Filter", isOn: $isOn)
            .toggleStyle(.button)
            .tint(.mint)
    }
}

Automatic image selection for TabView

In iOS 15 SwiftUI now automatically selects the correct variant of an SF Symbols icon when used inside a TabView.

According to the iOS human interface guidelines icons ought to be filled when used inside a TabView, but according to the macOS human interface guidelines they should be stroked. To make this work well on both platforms, you can now specify the simple, unfilled form of the image and have SwiftUI use the correct variant as appropriate for the platform.

So, on iOS this will show the filled forms of each of our icons:

TabView {
    Text("View 1")
        .tabItem {
            Label("Home", systemImage: "house")
        }

    Text("View 2")
        .tabItem {
            Label("Account", systemImage: "person")
        }

    Text("View 3")
        .tabItem {
            Label("Community", systemImage: "theatermasks")
        }
}

Primary actions for menus

In iOS 15 menus can also have a primary action attached, which is triggered when the menu’s button is tapped rather than held down. So, you press and release to trigger the primary action, or hold down to get the full menu of options.

We could create a menu that supports both simple taps as well as a full set of options:

struct ContentView: View {
    var body: some View {
        Menu("Options") {
            Button("Order Now", action: placeOrder)
            Button("Adjust Order", action: adjustOrder)
            Button("Cancel", action: cancelOrder)
        } primaryAction: {
            justDoIt()
        }
    }

    func justDoIt() {
        print("Button was tapped")
    }

    func placeOrder() { }
    func adjustOrder() { }
    func cancelOrder() { }
}

And there’s more…

There’s a newer, simpler way to dismiss views programmatically. Rather than using the presentationMode environment key then calling presentationMode.wrappedValue.dismiss(), you should instead use the new .dismiss) environment key:

@Environment(\.dismiss) var dismiss

That uses Swift’s callAsFunction(), so with that in place you can now make a sheet dismiss itself by calling dismiss().

There's a new format parameter when creating Text views, allowing you to format the value as a list, as a percentage, as a measurement, and more. You can read about this change here: How to format text inside text views

iOS 15 lets you attach a role to your button to help SwiftUI know what kind of styling should be attached to the button. For example, if we had a Delete button we might mark it with the .destructive role so SwiftUI can highlight it in red when it makes sense:

Button("Delete", role: .destructive) {
    print("Perform delete")
}

Plus, there are stacks of improvements that have come about thanks to improvements in Swift itself. You can find out more in my article What’s New in Swift 5.5, but here’s the abridged version.

First, styles now use much simpler names: SidebarListStyle() becomes just .sidebar, and RoundedBorderTextFieldStyle() becomes just .roundedBorder.

Second, you can now use #if for postfix member expressions, like this:

Text("Welcome")
#if os(iOS)
    .font(.largeTitle)
#else
    .font(.headline)
#endif

And third, Swift 5.5 is able to implicitly convert between CGFloat and Double in most places where it is needed, which means you can pretty much remove CGFloat from your SwiftUI code.

Another great year!

Last year was huge for SwiftUI, but this year the framework is really maturing – we’re seeing old UIKit controls such as UIVisualEffectView and UISearchController being dramatically rethought for a SwiftUI world, and the results are pretty darn incredible.

Thanks to the SwiftUI team for doing such an incredible job this year, and to Apple’s developer publications team for working so hard to create fantastic documentation we can all learn from!

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Get started

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and a speaker at Swift events around the world. If you're curious you can learn more here.

Was this page useful? Let us know!

Average rating: 4.5/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.