Bottom sheets, fixed grids, and a wholly new way to handle navigation
WWDC was back in person for the first time since 2019, and once again was absolutely packed with new features for app developers – huge new features in Swift 5.7, new APIs such as WeatherKit and Swift Charts, and, of course, lots of goodies for SwiftUI developers.
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. I’ll be adding more soon!
You can watch the video below, or scroll down for links to articles.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Additional: It’s now possible to create multi-column tables on iOS and iPadOS, but I think it’s a bit buggy in beta 1. I’ve documented it here in case you’d like to try it yourself: How to create multi-column lists using Table.
This release comes with a whole batch of small tweaks, fixes, and improvements to make the SwiftUI experience smoother all around.
First, you can get a beautifully simple linear gradient by appending .gradient
to whatever color you’re using:
Rectangle().fill(.blue.gradient)
This works with any color, and is always just the smallest hint of a gradient to help bring your UI to life:
struct ContentView: View {
let colors: [Color] = [.blue, .cyan, .green, .yellow, .orange, .red, .purple]
var body: some View {
VStack {
ForEach(colors, id: \.self) { color in
Rectangle().fill(color.gradient)
}
}
}
}
There’s new API for creating shadows for shapes, both regular drop shadows:
Circle()
.fill(.red.shadow(.drop(color: .black, radius: 10)))
.padding()
And also now inner shadows:
Circle()
.fill(.red.shadow(.inner(color: .black, radius: 10)))
.padding()
The lineLimit()
modifier has been upgraded to handle ranges as well as simple integers:
Text("This is some longer text that is limited to a specific range of lines, so anything more than six lines will cause the text to clip.")
.lineLimit(3...6)
.frame(width: 200)
Some modifiers have been upgraded to support parameters, so they can be adjusted at runtime:
struct ContentView: View {
@State private var useBold = false
@State private var useItalic = false
var body: some View {
VStack {
Text("Welcome SwiftUI 4.0")
.bold(useBold)
.italic(useItalic)
Toggle("Use Bold", isOn: $useBold)
Toggle("Use Italic", isOn: $useItalic)
}
.padding()
.font(.title)
}
}
We can animate all sorts of text characteristics just by using a plain withAnimation()
call, even in ways you might think impossible. For example, we can animate the weight of a font from very thin to very thick:
struct ContentView: View {
@State private var useBlack = false
var body: some View {
Text("Hello, world!")
.font(.largeTitle)
.fontWeight(useBlack ? .black : .ultraLight)
.onTapGesture {
withAnimation {
useBlack.toggle()
}
}
}
}
Next, we can now create text fields that automatically grow with their content, optionally turning them into scrolling views when they pass a line limit:
struct ContentView: View {
@State private var bio = ""
var body: some View {
TextField("Enter your bio", text: $bio, axis: .vertical)
.textFieldStyle(.roundedBorder)
.lineLimit(...5)
.padding()
}
}
Moving on, it’s now possible to bind a Toggle
to an array of Booleans, which is helpful for times when you want to enable or disable several values all at once. For example, we could write some code to let the user subscribe to individual newsletters, or have one toggle to switch them all:
struct EmailList: Identifiable {
var id: String
var isSubscribed = false
}
struct ContentView: View {
@State private var lists = [
EmailList(id: "Monthly Updates", isSubscribed: true),
EmailList(id: "News Flashes", isSubscribed: true),
EmailList(id: "Special Offers", isSubscribed: true)
]
var body: some View {
Form {
Section {
ForEach($lists) { $list in
Toggle(list.id, isOn: $list.isSubscribed)
}
}
Section {
Toggle(sources: $lists, isOn: \.isSubscribed) {
Text("Subscribe to all")
}
}
}
}
}
It is at last now possible to animate foregroundColor()
:
struct ContentView: View {
@State private var useRed = false
var body: some View {
Text("WWDC22")
.font(.largeTitle.bold())
.foregroundColor(useRed ? .red : .blue)
.onTapGesture {
withAnimation {
useRed.toggle()
}
}
}
}
And one last thing before we move onto the biggest change of all: SwiftUI for iOS 16 leans heavily on a new Transferable
protocol for sending data around the system. This is a newer, simpler, Swiftier replacement for NSItemProvider
, and you’ll see it used by PasteButton
, ShareLink
, drag and drop, and more.
NSItemProvider
was never a happy fit in any Swift app, never mind SwiftUI apps, so it’s great to see a significantly smarter approach come in – Transferable
is one to watch, for sure.
Probably the biggest change in this release is a rather dramatic rethink of navigation in SwiftUI: NavigationView
has been soft deprecated, and is replaced by two new container views depending on what you’re trying to achieve.
The simpler option of the two is called NavigationStack
, and it’s the equivalent of using an old NavigationView
with the navigation style of .stack
. In fact, you can just replace one with the other and get exactly the same result:
struct ContentView: View {
var body: some View {
NavigationStack {
List(1..<50) { i in
NavigationLink {
Text("Detail \(i)")
} label: {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationTitle("Navigation")
}
}
}
However, what the new NavigationStack
allows is for us to separate the navigation destination from the navigation link – to attach that as a separate modifier, like this:
struct ContentView: View {
var body: some View {
NavigationStack {
List(1..<50) { i in
NavigationLink(value: i) {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Detail \(i)")
}
.navigationTitle("Navigation")
}
}
}
So, you can see the NavigationLink
is responsible for setting a value for whatever row number was tapped, and navigationDestination()
is responsible for reading that back out and displaying a detail view.
That might seem little different from what we had before, but this new approach to navigation makes it much easier to create deep links, to handle state restoration, and also to jump to arbitrary places in our navigation – perhaps to push several views at once, or to pop all our views and return to the root.
For example, we could pre-select some specific rows by creating a new @State
property, then pass that into the NavigationStack
as its path:
struct ContentView: View {
@State private var presentedNumbers = [1, 4, 8]
var body: some View {
NavigationStack(path: $presentedNumbers) {
List(1..<50) { i in
NavigationLink(value: i) {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Detail \(i)")
}
.navigationTitle("Navigation")
}
}
}
That approach works great when you have only integers, but part of the magic of NavigationStack
is that you can attach multiple navigation destinations to handle various types of data being selected. In this case, your path should be NavigationPath
rather than a specific array of data – this is type erased, so it’s able to handle a mix of data types depending on what you push onto the stack.
Here’s how that looks:
struct ContentView: View {
@State private var presentedNumbers = NavigationPath()
var body: some View {
NavigationStack(path: $presentedNumbers) {
NavigationLink(value: "Example") {
Text("Tap Me")
}
List(1..<50) { i in
NavigationLink(value: i) {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Detail \(i)")
}
.navigationDestination(for: String.self) { i in
Text("String Detail \(i)")
}
.navigationTitle("Navigation")
}
}
}
So, NavigationStack
is a great choice when you want views being pushed and popped onto a single view. If you’re looking for multi-column layouts, you should use NavigationSplitView
instead, which lets you mark exactly what should go where in your split layout.
For example, we can get a simple two-column layout with a list and detail view like this:
struct ContentView: View {
@State private var players = ["Dani", "Jamie", "Roy"]
@State private var selectedPlayer: String?
var body: some View {
NavigationSplitView {
List(players, id: \.self, selection: $selectedPlayer, rowContent: Text.init)
} detail: {
Text(selectedPlayer ?? "Please choose a player.")
}
}
}
You can also get a three-column layout by adding another trailing closure:
struct Team: Identifiable, Hashable {
let id = UUID()
var name: String
var players: [String]
}
struct ContentView: View {
@State private var teams = [Team(name: "AFC Richmond", players: ["Dani", "Jamie", "Row"])]
@State private var selectedTeam: Team?
@State private var selectedPlayer: String?
var body: some View {
NavigationSplitView {
List(teams, selection: $selectedTeam) { team in
Text(team.name).tag(team)
}
.navigationSplitViewColumnWidth(250)
} content: {
List(selectedTeam?.players ?? [], id: \.self, selection: $selectedPlayer) { player in
Text(player)
}
} detail: {
Text(selectedPlayer ?? "Please choose a player.")
}
.navigationSplitViewStyle(.prominentDetail)
}
}
I snuck in a couple of extra modifiers in there so you can see more of what NavigationSplitView
can do:
navigationSplitViewColumnWidth()
to specify that the sidebar should be exactly 250 points wide.navigationSplitViewStyle(.prominentDetail)
to tell SwiftUI to try to keep the detail view the same size as it displays the other views around it.I’ve managed to explore maybe two thirds of the new API, but there’s still more to explore – I’ll update this article soon.
What’s your favorite change in SwiftUI for iOS 16? What do you think SwiftUI is still missing to really work for your apps? Tweet me @twostraws and let me know!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.