SwiftUI · · 12 min read

What's New in SwiftUI 4 for iOS 16

What's New in SwiftUI 4 for iOS 16

Earlier this week, Apple kicked off WWDC 22. The SwiftUI framework continues to be one of the main focuses of the conference. As expected, Apple announced a new version of SwiftUI that comes along with iOS 16 and Xcode 14.

This update comes with tons of features to help developers build better apps and write less code. In this tutorial, let me give you an overview of what’s new in SwiftUI 4.0.

SwiftUI Charts

You no longer need to build your own chart library or rely on third-party libraries to create charts. The SwiftUI framework now comes with the Charts APIs. With this declarative framework, you can present animated charts with just a few lines of code.

In brief, you build SwiftUI Charts by defining what it calls Mark. Here is a quick example:

import SwiftUI
import Charts

struct ContentView: View {
    var body: some View {
        Chart {
            BarMark(
                x: .value("Day", "Monday"),
                y: .value("Steps", 6019)
            )

            BarMark(
                x: .value("Day", "Tuesday"),
                y: .value("Steps", 7200)
            )
        }
    }
}

Whether you want to create a bar chart or line chart, we start with the Chart view. Inside the chart, we define the bar marks to provide the chart data. The BarMark view is used for creating a bar chart. Each BarMark view accepts the x and y value. The x value is used for defining the chart data for x-axis. In the code above, the label of the x-axis is set to Day. The y axis shows the total number of steps.

If you input the code in Xcode 14, the preview automatically displays the bar chart with two vertical bars.

swiftui-charts-bar

The code above shows you the simplest way to create a bar chart. However, instead of hardcoding the chart data, you usually use the Charts API with a collection of data. Here is an example:

swiftui-bar-chart

By default, the Charts API renders the bars in the same color. To display a different color for each of the bars, you can attach the foregroundStyle modifier to the BarMark view:

.foregroundStyle(by: .value("Day", weekdays[index]))

To add an annotation to each bar, you use the annotation modifier like this:

.annotation {
    Text("\(steps[index])")
}

By making these changes, the bar chart becomes more visually appealing.

swiftui-colored-bar-chart

To create a horizontal bar chart, you can simply swap the values of x and y parameter of the BarMark view.

swiftui-horizontal-bar-chart-ios

By changing the BarMark view to LineMark, you can turn a bar chart into a line chart.

Chart {
    ForEach(weekdays.indices, id: \.self) { index in
        LineMark(
            x: .value("Day", weekdays[index]),
            y: .value("Steps", steps[index])
        )
        .foregroundStyle(.purple)
        .lineStyle(StrokeStyle(lineWidth: 4.0))
    }
}

Optionally, you use foregroundStyle to change the color of the line chart. To change the line width, you can also attach the lineStyle modifier.

The Charts API is so flexible that you can overlay multiple charts in the same view. Here is an example.

swiftui-line-chart

Other than BarMark and LineMark, the SwiftUI Charts framework also provides PointMark, AreaMark, RectangularMark, and RuleMark for creating different types of charts.

Resizable Bottom Sheet

Apple introduced UISheetPresentationController in iOS 15 for presenting an expandable bottom sheet. Unfortunately, this class is only available in UIKit. If we want to use it in SwiftUI, we have to write additional code to integrate the component into SwiftUI projects. This year, Swift comes with a new modifier called presentationDetents for presenting a resizable bottom sheet .

To use this modifier, you just need to place this modifier inside a sheet view. Here is an example:

struct BottomSheetDemo: View {
    @State private var showSheet = false

    var body: some View {
        VStack {
            Button("Show Bottom Sheet") {
                showSheet.toggle()
            }
            .buttonStyle(.borderedProminent)
            .sheet(isPresented: $showSheet) {
                Text("This is the resizable bottom sheet.")
                    .presentationDetents([.medium])
            }

            Spacer()
        }
    }
}

The presentationDetents modifier accepts a set of detents for the sheet. In the code above, we set the detent to .medium. This shows a bottom sheet that takes up about half of the screen.

swiftui-bottom-sheet-medium-size

To make it resizable, you have to provide more than one detent for the presentationDetents modifier:

.presentationDetents([.medium, .large])

You should now see a drag bar indicating that the sheet is resizable. If you want to hide the drag indicator, attach the presentationDragIndicator modifier and set it to .hidden:

.presentationDragIndicator(.hidden)

Other than the preset detents such as .medium, you can create a custom detent using .height and .fraction. Here is another example:

.presentationDetents([.fraction(0.1), .medium, .large])

When the bottom sheet first appears, it only takes up around 10% of the screen.

MultiDatePicker

swiftui-ios-multidatepicker

The latest version of SwiftUI comes with a new date picker for users to choose multiple dates. Below is the sample code:

struct MultiDatePickerDemo: View {

    @State private var selectedDates: Set<DateComponents> = []

    var body: some View {
        MultiDatePicker("Choose your preferred dates", selection: $selectedDates)
            .frame(height: 300)
    }
}

NavigationStack and NavigationSplitView

NavigationView is deprecated in iOS 16. Instead, it’s replaced by the new NavigationStack and NavigationSplitView. Prior to iOS 16, you use NavigationView to create a navigation-based interface:

NavigationView {
    List {
        ForEach(1...10, id: \.self) { index in
            NavigationLink(destination: Text("Item #\(index) detail")) {
                Text("Item #\(index)")
            }
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
}

By pair it with NavigationLink, you can create a push and pop navigation.

swiftui-navigation-stack

Since NavigationView is deprecated, iOS 16 provides a new view called NavigationStack. This new view allows developers to create the same type of navigation-based UIs. Here is an example:

NavigationStack {
    List {
        ForEach(1...10, id: \.self) { index in
            NavigationLink {
                Text("Item #\(index) Detail")
            } label: {
                Text("Item #\(index)")
            }
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
}

The code is very similar to the old approach, except that you use NavigationStack instead of NavigationView. So, what’s the enhancement of NavigationStack?

Let’s check out another example:

NavigationStack {
    List {
        NavigationLink(value: "Text Item") {
            Text("Text Item")
        }

        NavigationLink(value: Color.purple) {
            Text("Purple color")
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
    .navigationDestination(for: Color.self) { item in
        item.clipShape(Circle())
    }
    .navigationDestination(for: String.self) { item in
        Text("This is the detail view for \(item)")
    }
}

The list above is simplified with only two rows: Text item and Purple color. However, the underlying type of these two rows are not the same. One is a text item and the other is actually a Color object.

In iOS 16, the NavigationLink view is further improved. Instead of specifying the destination view, it can take a value that represents the destination. When this pairs with the new navigationDestination modifier, you can easily control the destination view. In the code above, we have two navigationDestination modifiers: one for the text item and the other is for the color object.

When a user selects a particular item in the navigation stack, SwiftUI checks the item type of the value of the navigation link. It then calls up the destination view which associates with that specific item type.

swiftui-navigation-link-value

This is how the new NavigationStack works. That said, it’s just a quick overview of the new NavigationStack. With the new navigationDestination modifier, you can programmatically control the navigation. Say, you can create a button for you to jump directly to the main view from any detail views of the navigation stack. We will have another tutorial for that.

ShareLink for Data Sharing

iOS 16 introduces the ShareLink control for SwiftUI, allowing developers to present a share sheet. It’s very easy to use ShareLink. Here is an example:

struct ShareLinkDemo: View {
    private let url = URL(string: "https://www.appcoda.com")!

    var body: some View {
        ShareLink(item: url)
    }
}

Basically, you provide the ShareLink control with the item to share. This presents a default share button. When tapped, the app shows a share sheet.

swiftui-sharelink

You can customize the share button by providing your own text and image like this:

ShareLink(item: url) {
    Label("Share", systemImage: "link.icloud")
}

To control the size of the share sheet, you can attach the presentationDetents modifier:

ShareLink(item: url) {
    Label("Share", systemImage: "link.icloud")
}
.presentationDetents([.medium, .large])

Table for iPadOS

A new Table container is introduced for iPadOS that makes it easier to present data in tabular form. Here is a sample code which shows a table with 3 columns:

struct TableViewDemo: View {

    private let members: [Staff] = [
        .init(name: "Vanessa Ramos", position: "Software Engineer", phone: "2349-233-323"),
        .init(name: "Margarita Vicente", position: "Senior Software Engineer", phone: "2332-333-423"),
        .init(name: "Yara Hale", position: "Development Manager", phone: "2532-293-623"),
        .init(name: "Carlo Tyson", position: "Business Analyst", phone: "2399-633-899"),
        .init(name: "Ashwin Denton", position: "Software Engineer", phone: "2741-333-623")
    ]

    var body: some View {
        Table(members) {
            TableColumn("Name", value: \.name)
            TableColumn("Position", value: \.position)
            TableColumn("Phone", value: \.phone)
        }
    }
}

You can create a Table from a collection of data (e.g. an array of Staff). For each column, you use TableColumn to specify the column name and values.

swiftui-table-ipados

Table works great on iPadOS and macOS. The same table can be automatically rendered on iOS but only the first column is displayed.

Expandable Text Field

TextField on iOS 16 is greatly improved. You can now use the axis parameter to tell iOS whether the text field should be expanded. Here is an example:

Form {
    Section("Comment") {
        TextField("Please type your feedback here", text: $inputText, axis: .vertical)
            .lineLimit(5)
    } 
}

The lineLimit modifier specifies the maximum number of lines allowed. The code above initially renders a single-line text field. As you type, it automatically expands but limits its size to 5 lines.

expandable-textfield-swiftui-ios

You can change the initial size of the text field by specifying a range in the lineLimit modifier like this:

Form {
    Section("Comment") {
        TextField("Please type your feedback here", text: $inputText, axis: .vertical)
            .lineLimit(3...5)
    } 
}

In this case, iOS displays a text field displays a three-line text field by default.

swiftui-text-field

Gauge

SwiftUI introduces a new view called Gauge for displaying progress. The simplest way to use Gauge is like this:

struct GaugeViewDemo: View {
    @State private var progress = 0.5

    var body: some View {
        Gauge(value: progress) {
            Text("Upload Status")
        }
    }
}

In the most basic form, a gauge has a default range from 0 to 1. If we set the value parameter to 0.5, SwiftUI renders a progress bar indicating the task is 50% complete.

swiftui-gauge

Optionally, you can provide labels for the current, minimum, and maximum values:

Gauge(value: progress) {
    Text("Upload Status")
} currentValueLabel: {
    Text(progress.formatted(.percent))
} minimumValueLabel: {
    Text(0.formatted(.percent))
} maximumValueLabel: {
    Text(100.formatted(.percent))
}

Instead of using the default range, you can also specify a custom range like this:

Gauge(value: progress, in: 0...100) {
  .
  .
  .
}

The Gauge view provides a number of styles for you to customize. Other than the linear style as shown in the figure above, you can attach the gaugeStyle modifier to customize the style:

circular-gauge

ViewThatFits

ViewThatFits is a great addition to SwiftUI allowing developers to create more flexible UI layouts. This is a special type of view that evaluates the available space and presents the most suitable view on screen.

Here is an example. We use ViewThatFits to define two possible layouts of the button group:

struct ButtonGroupView: View {
    var body: some View {
        ViewThatFits {
            VStack {
                Button(action: {}) {
                    Text("Buy")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .buttonStyle(.borderedProminent)
                .padding(.horizontal)

                Button(action: {}) {
                    Text("Cancel")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .tint(.gray)
                .buttonStyle(.borderedProminent)
                .padding(.horizontal)
            }
            .frame(maxHeight: 200)


            HStack {
                Button(action: {}) {
                    Text("Buy")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .buttonStyle(.borderedProminent)
                .padding(.leading)

                Button(action: {}) {
                    Text("Cancel")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .tint(.gray)
                .buttonStyle(.borderedProminent)
                .padding(.trailing)
            }
            .frame(maxHeight: 100)

        }
    }
}

One group of the buttons is aligned vertically using the VStack view. The other group of buttons is aligned horizontally. The maxHeight of the vertical group is set to 200, while that of the horizontal group is set to 100.

What ViewThatFits does is that it evaluates the height of the given space and presents the best fit view on screen. Say, you set the frame height to 100 like this:

ButtonGroupView()
    .frame(height: 100)

ViewThatFits determines that it’s best to present the horizontally-aligned button group. Let’s say, you change the frame’s height to 150. The ViewThatFits view presents the vertical button group.

swiftui-button-group-viewthatfits

Gradient and Shadow

swiftui-gradient-shadow

The new version of SwiftUI lets you easily add linear gradient. Simply add the gradient modifier to Color and SwiftUI automatically generates the gradients. Here is an example:

Image(systemName: "trash")
    .frame(width: 100, height: 100)
    .background(in: Rectangle())
    .backgroundStyle(.purple.gradient)
    .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))
    .font(.system(size: 50))

You can also use the shadow modifier to apply shadow effect. Here is the line of code for adding a drop shadow style:

.foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))

Grid API

SwiftUI 4.0 introduces a new Grid API for composing grid-based layout. You can arrange the same layout by using VStack and HStact. The Grid view, however, makes it a lot easier.

swiftui-grid-gridrow

To create a 2×2 grid, you can write the code like this:

Grid {
    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }

    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }
}

Inside the Grid view, it’s a collection of GridRow that embeds the grid cells.

swiftui-grid-api

Let’s say, the second row presents a single icon view and you want it to span across two columns. You can attach the gridCellColumns modifier and set the value to 2:

Grid {
    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }

    GridRow {
        IconView(systemName: "trash")
            .gridCellColumns(2)
    }
}

The Grid view can be nested to compose more complex layouts like the one displayed below:

swiftui-multi-grid

AnyLayout and Layout Protocol

The new version of SwiftUI provides AnyLayout and the Layout protocol for developers to create customized and complex layouts. AnyLayout is a type-erased instance of the layout protocol. You can use AnyLayout to create a dynamic layout that responds to users’ interactions or environment changes.

For instance, your app initially arranges two images vertically using VStack. When a user taps the stack view, it changes to a horizontal stack. With AnyLayout, you can implement the layout like this:

struct AnyLayoutDemo: View {

    @State private var changeLayout = false

    var body: some View {
        let layout = changeLayout ? AnyLayout(HStack(spacing: 0)) : AnyLayout(VStack(spacing: 0))


        layout {
            Image("macbook-1")
                .resizable()
                .scaledToFill()
                .frame(maxWidth: 300, maxHeight: 200)
                .clipped()

            Image("macbook-2")
                .resizable()
                .scaledToFill()
                .frame(maxWidth: 300, maxHeight: 200)
                .clipped()

        }
        .animation(.default, value: changeLayout)
        .onTapGesture {
            changeLayout.toggle()
        }

    }
}

We can define a layout variable to hold an instance of AnyLayout. Depending on the value of changeLayout, this layout changes between horizontal and vertical layouts.

By attaching the animation to the layout, the layout change will be animated.

swiftui-anylayout-demo

The demo lets users change the layout by tapping the stack view. In some applications, you may want to change the layout based on the device’s orientation and screen size. In this case, you can capture the orientation change by using the .horizontalSizeClass variable:

@Environment(\.horizontalSizeClass) var horizontalSizeClass

And then you update the layout variable like this:

let layout = horizontalSizeClass == .regular ? AnyLayout(HStack(spacing: 0)) : AnyLayout(VStack(spacing: 0))

Say, for example, you rotate an iPhone 13 Pro Max to landscape, the layout changes to horizontally stack view.

swiftui-anylayout-sizeclass

In most cases, we use SwiftUI’s built-in layout containers like HStack and VStack to compose layouts. What if those layout containers are not good enough for arranging the type of layouts you need? The Layout protocol introduced in iOS 16 allows you to define your own custom layout. This is a more complex topic, so we will discuss this new protocol in another tutorial.

Summary

This year, Apple once again delivered tons of great features for the SwiftUI framework. The Charts API, the revamp of navigation view, and the introduction of AnyLayout will definitely help you build more elegant and engaging UIs. I’m still exploring the new APIs of SwiftUI. If I miss any great updates, please do leave a comment below and let me know.

Note: We are updating our Mastering SwiftUI book for iOS 16. If you want to start learning SwiftUI, check out the book here. You will receive a free update later this year.

Read next