A blog exploring advanced programming topics in Swift.

Macro Bonanza: SwiftUI Navigation

Wednesday Nov 15, 2023

Preamble

To celebrate the release of Swift macros we releasing updates to 4 of our popular libraries to greatly simplify and enhance their abilities: CasePaths, ComposableArchitecture, SwiftUINavigation, and Dependencies. Each day this week we will detail how macros have allowed us to massively simplify one of these libraries, and increase their powers.

Today we are releasing version 1.1 of our popular library, SwiftUINavigation, which is a collection of tools that help you better model navigation using enums. This release does not introduce a macro to the library itself, but it does heavily make use of the new @CasePathable macro that we discussed earlier this week. We can now greatly simplify how you interact with SwiftUI navigation view modifiers while still modeling your domains as concisely as possible with enums.

Join us for a quick overview of the new tools, and be sure to update to version 1.1 of the library to take advantage of these tools.

Better domain modeling tools

The SwiftUINavigation library provides tools that allow you to drive navigation in your features using a single enum. This makes it possible to prove at compile time that only a single destination can be active at a time, helping reduce the complexity of your features.

For example, if you have an observable model for a meeting that is capable of showing an edit feature in a sheet, drilling down to a record meeting feature, or showing an alert, then an optimal way to design this domain is the following:

@Observable
class MeetingDetailModel {
  var destination: Destination?

  enum Destination {
    case alert(AlertState<AlertAction>)
    case edit(EditMeetingModel)
    case record(RecordMeetingModel)
  }

  …
}

The single piece of optional destination state determines whether or not we are currently navigated to a particular feature.

This can be powerful, but unfortunately vanilla SwiftUI does not provide the tools to drive navigation off of such a domain. Its tools, such as the sheet, alert and navigationDestination view modifiers, are tuned for bindings of booleans, and sometimes bindings of optionals.

Well, our SwiftUINavigation library tries to fill the gap, with the help of our CasePaths library, by providing view modifiers that allow you to drive navigation from the Destination enum:

.alert(
  self.$model.destination,
  case: /MeetingDetailModel.Destination.alert
) { action in
  await self.model.alertButtonTapped(action)
}
.navigationDestination(
  unwrapping: self.$model.destination,
  case: /MeetingDetailModel.Destination.record
) { $model in
  RecordMeetingView(model: model)
}
.sheet(
  unwrapping: self.$model.destination,
  case: /MeetingDetailModel.Destination.edit
) { $model in
  EditMeetingView(model: model)
}

These are custom view modifiers that ship with the SwiftUINavigation library that allow you to drive navigation from an optional enum value. You first specify a binding to the optional enum value, and then you specify a case path to isolate the case you care about for the navigation.

This works incredibly well, but it also a bit verbose.

Navigation with dynamic case lookup

Thanks to the new @CasePathable macro provided by our CasePaths library, we can greatly simplify the above view modifiers. We can start by annotating the Destination enum with the macro:

@CasePathable
enum Destination {
  case alert(AlertState<AlertAction>)
  case edit(EditMeetingModel)
  case record(RecordMeetingModel)
}

Just that one line of additional code gives us the ability to perform dot-chaining syntax onto the $model.destination binding for each case of the enum. This allows us to derive bindings that can be handed to the SwiftUI view modifiers, which massively simplifies the code we saw above:

-.alert(
-  self.$model.destination,
-  case: /MeetingDetailModel.Destination.alert
-) { action in
+.alert(self.$model.destination.alert) { action in
   await self.model.alertButtonTapped(action)
 }
-.navigationDestination(
-  unwrapping: self.$model.destination,
-  case: /MeetingDetailModel.Destination.record
-) { $model in
+.navigationDestination(item: self.$model.destination.record) { model in
   RecordMeetingView(model: model)
 }
-.sheet(
-  unwrapping: self.$model.destination,
-  case: /MeetingDetailModel.Destination.edit
-) { $model in
+.sheet(item: self.$model.destination.edit) { model in  
   EditMeetingView(model: model)
 }

There’s no need to deal with explicit case paths or the / prefix operator for constructing case paths. It’s simpler and more fluent Swift code. We are even now using the vanilla SwiftUI view modifiers navigationDestination(item:) and sheet(item:). There is no need for a custom view modifier anymore.

Get started today

Update your dependency on SwiftUINavigation to version 1.1 today to start taking advantage of the new @CasePathable macro, and more. Tomorrow we will discuss how these new case path tools have massively improved our Composable Architecture library.


Subscribe to Point-Free

👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series covering advanced programming topics in Swift. Consider subscribing today!