# Most Useful iOS 15 SwiftUI Features

## Introduction

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:

- RCurrency
- RChat
- LiveTutorial2021
- task-tracker-swiftui

## Prerequisites

- Xcode 13
- iOS 15
- Realm-Cocoa (varies by app, but 10.13.0+ is safe for them all)

## Lists

SwiftUI `List`s are pretty critical to data-based apps. I use `List`s 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.

### Custom Swipe Options

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 `TasksView`:

```swift
List {
   ForEach(tasks) { task in
       TaskView(task: task)
           .swipeActions(edge: .leading) {
               if task.statusEnum == .Open || task.statusEnum == .InProgress {
                   CompleteButton(task: task)
               }
               if task.statusEnum == .Open || task.statusEnum == .Complete {
                   InProgressButton(task: task)
               }
               if task.statusEnum == .InProgress || task.statusEnum == .Complete {
                   NotStartedButton(task: task)
               }
           }
           .swipeActions(edge: .trailing) {
               Button(role: .destructive, action: { $tasks.remove(task) }) {
                   Label("Delete", systemImage: "trash")
               }
           }
   }
}
```

The role of the delete button is set to `.destructive` which automatically sets the color to red.

For the other actions, I created custom buttons. For example, this is the code for `CompleteButton`:

```swift
struct CompleteButton: View {
   @ObservedRealmObject var task: Task

   var body: some View {
       Button(action: { $task.statusEnum.wrappedValue = .Complete }) {
           Label("Complete", systemImage: "checkmark")
       }
       .tint(.green)
   }
}
```

### Searchable Lists

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 `SymbolPickerView` view.

The view includes a state variable to store the `searchText` (the characters that the user has typed) and a `searchResults` computed value that uses it to filter the full list of symbols:

```swift
struct SymbolPickerView: View {
   ...
   @State private var searchText = ""
   ...
   var searchResults: Dictionary {
       if searchText.isEmpty {
           return Symbols.data.symbols
       } else {
           return Symbols.data.symbols.filter {
               $0.key.contains(searchText.uppercased()) || $0.value.contains(searchText)}
       }
   }
}
```

The `List` then loops over those `searchResults`. We add the `.searchable` modifier to add the search bar, and bind it to the `searchText` state variable:

```swift
List {
   ForEach(searchResults.sorted(by: <), id: \.key) { symbol in
       ...
   }
}
.searchable(text: $searchText)
```

This is the full view:

```swift
struct SymbolPickerView: View {
   @Environment(\.presentationMode) var presentationMode

   var action: (String) -> Void
   let existingSymbols: String]

   @State private var searchText = ""

   var body: some View {
       List {
           ForEach(searchResults.sorted(by: <), id: \.key) { symbol in
               Button(action: {
                   pickedSymbol(symbol.key)
               }) {
                   HStack {
                       Image(symbol.key.lowercased())
                       Text("\(symbol.key): \(symbol.value)")
                   }
                   .foregroundColor(existingSymbols.contains(symbol.key) ? .secondary : .primary)
               }
               .disabled(existingSymbols.contains(symbol.key))
           }
       }
       .searchable(text: $searchText)
       .navigationBarTitle("Pick Currency", displayMode: .inline)
   }

   private func pickedSymbol(_ symbol: String) {
       action(symbol)
       presentationMode.wrappedValue.dismiss()
   }

   var searchResults: Dictionary {
       if searchText.isEmpty {
           return Symbols.data.symbols
       } else {
           return Symbols.data.symbols.filter {
               $0.key.contains(searchText.uppercased()) || $0.value.contains(searchText)}
       }
   }
}
```

## Pull to Refresh

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:

![Animation showing currencies being refreshed when the screen is dragged dowm

We allow the user to trigger the refresh by adding a `.refreshable` modifier and action (`refreshAll`) to the list of currencies in `CurrencyListContainerView`:

```swift
List {
   ForEach(userSymbols.symbols, id: \.self) { symbol in
       CurrencyRowContainerView(baseSymbol: userSymbols.baseSymbol,
                                   baseAmount: $baseAmount,
                                   symbol: symbol,
                                   refreshNeeded: refreshNeeded)
           .listRowSeparator(.hidden)
   }
   .onDelete(perform: deleteSymbol)
}
.refreshable{ refreshAll() }
```

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 `List` item. Not a big feature, but every little bit helps in letting us use native SwiftUI to get the exact design we want.

## Text
### Markdown

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 `Text` view. If you pass a literal link to a `Text` view, then it's automatically rendered correctly:

```swift
struct MarkDownTest: View {
   var body: some View {
       Text("Let's see some **bold**, *italics* and some ***bold italic text***. ~~Strike that~~. We can even include a link.")
   }
}
```

But, it doesn't work out of the box for string constants or variables (e.g., data read from Realm):

```swift
struct MarkDownTest: View {
   let myString = "Let's see some **bold**, *italics* and some ***bold italic text***. ~~Strike that~~. We can even include a link."

   var body: some View {
       Text(myString)
   }
}
```

The issue is that the version of `Text` that renders markdown expects to be passed an `AttributedString`. I created this simple `Markdown` view to handle this for us:

```swift
struct MarkDown: View {
   let text: String

   @State private var formattedText: AttributedString?

   var body: some View {
       Group {
           if let formattedText = formattedText {
               Text(formattedText)
           } else {
               Text(text)
           }
       }
       .onAppear(perform: formatText)
   }

   private func formatText() {
       do {
           try formattedText = AttributedString(markdown: text)
       } catch {
           print("Couldn't convert this from markdown: \(text)")
       }
   }
}
```

I updated the `ChatBubbleView` in RChat to use the `Markdown` view:

```swift
if chatMessage.text != "" {
   MarkDown(text: chatMessage.text)
   .padding(Dimensions.padding)
}
```

RChat now supports markdown in user messages:

### Dates

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 `Date.formatted` syntax. 

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 `TextDate` view to perform this conditional formatting:

```swift
struct TextDate: View {
   let date: Date

   private var isLessThanOneMinute: Bool { date.timeIntervalSinceNow > -60 }
   private var isLessThanOneDay: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 }
   private var isLessThanOneWeek: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 * 7}
   private var isLessThanOneYear: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 * 365}

   var body: some View {
       if isLessThanOneMinute {
           Text(date.formatted(.dateTime.hour().minute().second()))
       } else {
           if isLessThanOneDay {
               Text(date.formatted(.dateTime.hour().minute()))
           } else {
               if isLessThanOneWeek {
                   Text(date.formatted(.dateTime.weekday(.wide).hour().minute()))
               } else {
                   if isLessThanOneYear {
                       Text(date.formatted(.dateTime.month().day()))
                   } else {
                       Text(date.formatted(.dateTime.year().month().day()))
                   }
               }
           }
       }
   }
}
```

This preview code lets me test it's working in the Xcode Canvas preview:

```swift
struct TextDate_Previews: PreviewProvider {
   static var previews: some View {
       VStack {
           TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24 * 365)) // 1 year ago
           TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24 * 7))   // 1 week ago
           TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24))       // 1 day ago
           TextDate(date: Date(timeIntervalSinceNow: -60 * 60))            // 1 hour ago
           TextDate(date: Date(timeIntervalSinceNow: -60))                 // 1 minute ago
           TextDate(date: Date())                                          // Now
       }
   }
}
```

We can then use `TextDate` in RChat's `ChatBubbleView` to add context-sensitive date and time information:

```swift
TextDate(date: chatMessage.timestamp)
   .font(.caption)
```

## Keyboards

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...

### Submit Labels

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 `.submitLabel`) modifier to the input field. You pass the modifier one of these values:

- `done`
- `go`
- `send`
- `join`
- `route`
- `search`
- `return`
- `next`
- `continue`

I decided to use these labels to improve the login flow for the LiveTutorial2021 app. In `LoginView`, I added a `submitLabel` to both the "email address" and "password" `TextFields`:

```swift
TextField("email address", text: $email)
   .submitLabel(.next)
SecureField("password", text: $password)
   .onSubmit(userAction)
   .submitLabel(.go)
```

Note the `.onSubmit(userAction)` modifier on the password field. If the user taps "go" (or hits return on an external keyboard), then the `userAction` function is called. `userAction` either registers or logs in the user, depending on whether "Register new user” is checked.

### Focus

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 ~~"return"~~ "next", focus should move to the "password" field. When the user taps "go," the app logs them in.

You can use the new `FocusState` SwiftUI 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 `enum` option instead.

In `LoginView`, I define the `Field` enumeration type to represent whether the username (email address) or password is in focus. I then create the `focussedField` `@FocusState` variable to store the value using the `Field` type:

```swift
enum Field: Hashable {
   case username
   case password
}

@FocusState private var focussedField: Field?
```

I use the `.focussed` modifier to bind `focussedField` to the two fields:

```swift
TextField("email address", text: $email)
   .focused($focussedField, equals: .username)
   ...
SecureField("password", text: $password)
    .focused($focussedField, equals: .password)
   ...
```

It's a two-way binding. If the user selects the email field, then `focussedField` is set to `.username`. If the code sets `focussedField` to `.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:

```swift
VStack(spacing: 16) {
   ...
}
.onAppear {
   DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
       focussedField = .username
...
   }
}
```

The final step is to shift focus to the password field when the user hits the "next" key in the username field:

```swift
TextField("email address", text: $email)
   .onSubmit { focussedField = .password }
   ...
```

This is the complete body from `LoginView`:

```swift
var body: some View {
   VStack(spacing: 16) {
       Spacer()
       TextField("email address", text: $email)
           .focused($focussedField, equals: .username)
           .submitLabel(.next)
           .onSubmit { focussedField = .password }
       SecureField("password", text: $password)
           .focused($focussedField, equals: .password)
           .onSubmit(userAction)
           .submitLabel(.go)
       Button(action: { newUser.toggle() }) {
           HStack {
               Image(systemName: newUser ? "checkmark.square" : "square")
               Text("Register new user")
               Spacer()
           }
       }
       Button(action: userAction) {
           Text(newUser ? "Register new user" : "Log in")
       }
       .buttonStyle(.borderedProminent)
       .controlSize(.large)
       Spacer()
   }
   .onAppear {
       DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
           focussedField = .username
       }
   }
   .padding()
}
```

## Buttons
### Formatting

Previously, I've created custom SwiftUI views to make buttons look like…. buttons.

Things get simpler in iOS 15.

In `LoginView`, I added two new modifiers to my register/login button:

```swift
Button(action: userAction) {
   Text(newUser ? "Register new user" : "Log in")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
```

Before making this change, I experimented with other button styles:

### Confirmation

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 `LogoutButton` view:

```swift
struct LogoutButton: View {
   ...
   @State private var isConfirming = false

   var body: some View {
       Button("Logout") { isConfirming = true }
       .confirmationDialog("Are you sure want to logout",
                           isPresented: $isConfirming) {
           Button(action: logout) {
               Text("Confirm Logout")
           }
           Button("Cancel", role: .cancel) {}
       }
   }
   ...
}
```

These are the changes I made:

- Added a new state variable (`isConfirming`)
- Changed the logout button's action from calling the `logout` function to setting `isConfirming` to `true`
- Added the `confirmationDialog`-9ibgk) modifier to the button, providing three things:
    - The dialog title (I didn't override the `titleVisibility` option and so the system decides whether this should be shown)
    - A binding to `isConfirming` that 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

## Material

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 `OpaqueProgressView` to use Material to blur the content that's behind the overlay. To get this effect, I update the background modifier for the `VStack`:

```swift
var body: some View {
   VStack {
       if let message = message {
           ProgressView(message)
       } else {
           ProgressView()
       }
   }
   .padding(Dimensions.padding)
   .background(.ultraThinMaterial,
               in: RoundedRectangle(cornerRadius: Dimensions.cornerRadius))
}
```

The result looks like this:

## Developer Tools

Finally, there are a couple of enhancements that are helpful during your development phase.

### Landscape Previews

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: `.preferredColorScheme(.dark)`

A glaring omission was that there was no way to preview landscape mode. That's fixed in iOS 15 with the addition of the `.previewInterfaceOrientation`) modifier. 

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:

```swift
struct CurrencyRow_Previews: PreviewProvider {
   static var previews: some View {
       Group {
           List {
               CurrencyRowView(value: 3.23, symbol: "USD", baseValue: .constant(1.0))
               CurrencyRowView(value: 1.0, symbol: "GBP", baseValue: .constant(10.0))
           }
           List {
               CurrencyRowView(value: 3.23, symbol: "USD", baseValue: .constant(1.0))
               CurrencyRowView(value: 1.0, symbol: "GBP", baseValue: .constant(10.0))
           }
           .preferredColorScheme(.dark)
           .previewInterfaceOrientation(.landscapeLeft)
       }
   }
}
```

### Self._printChanges

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 `ContentView` for the LiveChat app:

```swift
struct ContentView: View {
   @State private var username = ""

   var body: some View {
       print(Self._printChanges())
       return NavigationView {
           Group {
               if app.currentUser == nil {
                   LoginView(username: $username)
               } else {
                   ChatRoomsView(username: username)
               }
           }
           .navigationBarTitle(username, displayMode: .inline)
           .navigationBarItems(trailing: app.currentUser != nil ? LogoutButton(username: $username) : nil) }
    }
}
```

If I log in and check the Xcode console, I can see that it's the update to `username` that triggered the refresh (rather than `app.currentUser`):

```swift
ContentView: _username changed.
```

There can be a lot of these messages, and so remember to turn them off before going into production.

## Conclusion

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:

- `AsyncImage` is 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.
- The `task`/) view 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.