Most Useful iOS 15 SwiftUI Features
Andrew MorganPublished Sep 27, 2021 • Updated Sep 01, 2022
Rate this tutorial
I'm all-in on using SwiftUI to build iOS apps. I find it so much simpler than wrangling with storyboards and UIKit. Unfortunately, there are still occasions when SwiftUI doesn't let you do what you need—forcing you to break out into UIKit.
That's why I always focus on Apple's SwiftUI enhancements at each year's WWDC. And, each year I'm rewarded with a few more enhancements that make SwiftUI more powerful and easy to work with. For example, iOS14 made it much easier to work with Apple Maps.
WWDC 2021 was no exception, introducing a raft of SwiftUI enhancements that were coming in iOS 15/ SwiftUI 3 / Xcode 13. As iOS 15 has now been released, it feels like a good time to cover the features that I've found the most useful.
I've revisited some of my existing iOS apps to see how I could exploit the new iOS 15 SwiftUI features to improve the user experience and/or simplify my code base. This article steps through the features I found most interesting/useful, and how I tested them out on my apps. These are the apps/branches that I worked with:
- Xcode 13
- iOS 15
- Realm-Cocoa (varies by app, but 10.13.0+ is safe for them all)
Lists are pretty critical to data-based apps. I use
Lists in almost every iOS app I build, typically to represent objects stored in Realm. That's why I always go there first when seeing what's new.
We've all used mobile apps where you swipe an item to the left for one action, and to the right for another. SwiftUI had a glaring omission—the only supported action was to swipe left to delete an item.
This was a massive pain.
This limitation meant that my task-tracker-swiftui app had a cumbersome UI. You had to click on a task to expose a sheet that let you click on your preferred action.
With iOS 15, I can replace that popup sheet with swipe actions:
The swipe actions are implemented in
The role of the delete button is set to
.destructivewhich automatically sets the color to red.
For the other actions, I created custom buttons. For example, this is the code for
When you're presented with a long list of options, it helps the user if you offer a way to filter the results.
RCurrency lets the user choose between 150 different currencies. Forcing the user to scroll through the whole list wouldn't make for a good experience. A search bar lets them quickly jump to the items they care about:
The selection of the currency is implemented in the
The view includes a state variable to store the
searchText(the characters that the user has typed) and a
searchResultscomputed value that uses it to filter the full list of symbols:
Listthen loops over those
searchResults. We add the
.searchablemodifier to add the search bar, and bind it to the
This is the full view:
We've all used this feature in iOS apps. You're impatiently waiting on an important email, and so you drag your thumb down the page to get the app to check the server.
This feature isn't always helpful for apps that use Realm and Atlas Device Sync. When the Atlas cloud data changes, the local realm is updated, and your SwiftUI view automatically refreshes to show the new data.
However, the feature is useful for the RCurrency app. I can use it to refresh all of the locally-stored exchange rates with fresh data from the API:
We allow the user to trigger the refresh by adding a
.refreshablemodifier and action (
refreshAll) to the list of currencies in
In that code snippet, you can see that I added the
.listRowSeparator(.hidden)modifier to the
List. This is another iOS 15 feature that hides the line that would otherwise be displayed between each
Listitem. Not a big feature, but every little bit helps in letting us use native SwiftUI to get the exact design we want.
I'm a big fan of Markdown. Markdown lets you write formatted text (including tables, links, and images) without taking your hands off the keyboard. I added this post to our CMS in markdown.
iOS 15 allows you to render markdown text within a
Textview. If you pass a literal link to a
Textview, then it's automatically rendered correctly:
But, it doesn't work out of the box for string constants or variables (e.g., data read from Realm):
The issue is that the version of
Textthat renders markdown expects to be passed an
AttributedString. I created this simple
Markdownview to handle this for us:
I updated the
ChatBubbleViewin RChat to use the
RChat now supports markdown in user messages:
We all know that working with dates can be a pain. At least in iOS 15 we get some nice new functionality to control how we display dates and times. We use the new
In RChat, I want the date/time information included in a chat bubble to depend on how recently the message was sent. If a message was sent less than a minute ago, then I care about the time to the nearest second. If it were sent a day ago, then I want to see the day of the week plus the hour and minutes. And so on.
I created a
TextDateview to perform this conditional formatting:
This preview code lets me test it's working in the Xcode Canvas preview:
We can then use
ChatBubbleViewto add context-sensitive date and time information:
Customizing keyboards and form input was a real pain in the early days of SwiftUI—take a look at the work we did for the WildAid O-FISH app if you don't believe me. Thankfully, iOS 15 has shown some love in this area. There are a couple of features that I could see an immediate use for...
It's now trivial to rename the on-screen keyboard's "return" key. It sounds trivial, but it can give the user a big hint about what will happen if they press it.
To rename the return key, add a
.submitLabelmodifier to the input field. You pass the modifier one of these values:
I decided to use these labels to improve the login flow for the LiveTutorial2021 app. In
LoginView, I added a
submitLabelto both the "email address" and "password"
.onSubmit(userAction)modifier on the password field. If the user taps "go" (or hits return on an external keyboard), then the
userActionfunction is called.
userActioneither registers or logs in the user, depending on whether "Register new user” is checked.
It can be tedious to have to click between different fields on a form. iOS 15 makes it simple to automate that shifting focus.
Sticking with LiveTutorial2021, I want the "email address" field to be selected when the view opens. When the user types their address and hits "next", focus should move to the "password" field. When the user taps "go," the app logs them in.
You can use the new
FocusStateSwiftUI property wrapper to create variables to represent the placement of focus in the view. It can be a boolean to flag whether the associated field is in focus. In our login view, we have two fields that we need to switch focus between and so we use the
LoginView, I define the
Fieldenumeration type to represent whether the username (email address) or password is in focus. I then create the
@FocusStatevariable to store the value using the
I use the
.focussedmodifier to bind
focussedFieldto the two fields:
It's a two-way binding. If the user selects the email field, then
focussedFieldis set to
.username. If the code sets
.password, then focus switches to the password field.
This next step feels like a hack, but I've not found a better solution yet. When the view is loaded, the code waits half a second before setting focus to the username field. Without the delay, the focus isn't set:
The final step is to shift focus to the password field when the user hits the "next" key in the username field:
This is the complete body from
Previously, I've created custom SwiftUI views to make buttons look like…. buttons.
Things get simpler in iOS 15.
LoginView, I added two new modifiers to my register/login button:
Before making this change, I experimented with other button styles:
It's very easy to accidentally tap the "Logout" button, and so I wanted to add this confirmation dialog:
Again, iOS 15 makes this simple.
This is the modified version of the
These are the changes I made:
- Added a new state variable (
- Changed the logout button's action from calling the
logoutfunction to setting
- Added the
confirmationDialogmodifier to the button, providing three things:
- The dialog title (I didn't override the
titleVisibilityoption and so the system decides whether this should be shown)
- A binding to
isConfirmingthat controls whether the dialog is shown or not
- A view containing the contents of the dialog:
- A button to logout the user
- A cancel button
I'm no designer, and this is blurring the edges of what changes I consider worth adding.
The RChat app may have to wait a moment while the backend MongoDB Atlas App Services application confirms that the user has been authenticated and logged in. I superimpose a progress view while that's happening:
To make it look a bit more professional, I can update
OpaqueProgressViewto use Material to blur the content that's behind the overlay. To get this effect, I update the background modifier for the
The result looks like this:
Finally, there are a couple of enhancements that are helpful during your development phase.
I'm a big fan of Xcode's "Canvas" previews. Previews let you see what your view will look like. Previews update in more or less real time as you make code changes. You can even display multiple previews at once for example:
- For different devices:
.previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Max"))
- For dark mode:
A glaring omission was that there was no way to preview landscape mode. That's fixed in iOS 15 with the addition of the
For example, this code will show two devices in the preview. The first will be in portrait mode. The second will be in landscape and dark mode:
SwiftUI is very smart at automatically refreshing views when associated state changes. But sometimes, it can be hard to figure out exactly why a view is or isn't being updated.
iOS 15 adds a way to print out what pieces of state data have triggered each refresh for a view. Simply call
Self._printChanges()from the body of your view. For example, I updated
ContentViewfor the LiveChat app:
If I log in and check the Xcode console, I can see that it's the update to
usernamethat triggered the refresh (rather than
There can be a lot of these messages, and so remember to turn them off before going into production.
SwiftUI is developing at pace. With each iOS release, there is less and less reason to not use it for all/some of your mobile app.
This post describes how to use some of the iOS 15 SwiftUI features that caught my attention. I focussed on the features that I could see would instantly benefit my most recent mobile apps. In this article, I've shown how those apps could be updated to use these features.
There are lots of features that I didn't include here. A couple of notable omissions are:
AsyncImageis going to make it far easier to work with images that are stored in the cloud. I didn't need it for any of my current apps, but I've no doubt that I'll be using it in a project soon.
taskview modifier is going to have a significant effect on how people run asynchronous code when a view is loaded. I plan to cover this in a future article that takes a more general look at how to handle concurrency with Realm.
- Adding a toolbar to your keyboards (e.g., to let the user switch between input fields).
If you have any questions or comments on this post (or anything else Realm-related), then please raise them on our community forum. To keep up with the latest Realm news, follow @realm on Twitter.