Collection
Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
Earlier this year we devoted a series of episodes on the topic of “concise forms.” In them we explored how SwiftUI’s property wrappers make it super easy to build form-based UIs, mostly because there is very little code involved in establishing two-way bindings between your application’s state and various UI controls.
We contrasted this with how the Composable Architecture handles forms. Because the library adopts what is known as a “unidirectional data flow”, the only way to update state is to introduce explicit user actions for each UI control and then send them into the store. Unfortunately, for simple forms, this means introducing a lot more boilerplate than what we see in the vanilla SwiftUI version.
But, then we demonstrated two really interesting things. First, we showed that some of that boilerplate wasn’t necessary. By employing some advanced techniques in Swift, such as key paths and type erasure, we were able to make the Composable Architecture version of the form nearly as concise as the vanilla SwiftUI version.
And then we showed that once you start layering on advanced behavior onto the form, such as side effects, the brevity of the vanilla Swift version starts to break down. You are forced to leave the nice world of using syntax sugar for deriving bindings, and instead need to do things like construct bindings from scratch. On the other hand, the Composable Architecture really shines when adding this behavior because it’s perfectly situated for handling side effects.
Even though we accomplished something really nice by the end of that series of episodes, there is still room for improvement. One problem with our current solution is that it’s not particularly safe. If you make use of the tools we built in those episodes you essentially open up your entire state to mutation from the outside. This goes against the grain of one of the core tenets of the Composable Architecture, which is that mutations to state are only performed in the reducer when an action is sent. We are going to show how to fix this deficiency.
Further, we will make the binding tools we developed last time even more concise. In fact, it will compete with vanilla SwiftUI on a line-by-line basis, and in some ways it can be even more concise than vanilla SwiftUI.
So, let’s start by giving a quick overview of what we accomplished last time.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Update the effectful notification logic in the vanilla SwiftUI view model to use async/await. As of this episode, Apple does not provide async/await APIs for the UserNotifications framework, so you will need to write your own helpers using tools like
withUnsafeContinuation
andwithUnsafeThrowingContinuation
.Solution
First we need to introduce some helpers on
UNNotificationCenter
:extension UNUserNotificationCenter { var notificationSettings: UNNotificationSettings { get async { await withUnsafeContinuation { continuation in self.getNotificationSettings { notificationSettings in continuation.resume(with: .success(notificationSettings)) } } } } func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool { try await withUnsafeThrowingContinuation { continuation in self.requestAuthorization(options: options) { granted, error in if let error = error { continuation.resume(with: .failure(error)) } else { continuation.resume(with: .success(granted)) } } } } }
With these in place, we can simplify
attemptToggleSendNotifications
:@MainActor func attemptToggleSendNotifications(isOn: Bool) async { guard isOn else { self.sendNotifications = false return } let settings = await UNUserNotificationCenter.current().notificationSettings guard settings.authorizationStatus != .denied else { self.alert = .init(title: "You need to enable permissions from iOS settings") return } withAnimation { self.sendNotifications = true } let granted = (try? await UNUserNotificationCenter.current().requestAuthorization(options: .alert)) ?? false if !granted { withAnimation { self.sendNotifications = false } } else { UIApplication.shared.registerForRemoteNotifications() } }
We:
- Upgrade the method to be
async
- Use
@MainActor
to eliminate theDispatchQueue.main.async
calls - Make calls to the async helpers we just defined and eliminate a lot of nesting
Finally, in the view we can spin off a
Task
:Toggle( "Send notifications", isOn: Binding( get: { self.viewModel.sendNotifications }, set: { isOn in Task { await self.viewModel.attemptToggleSendNotifications(isOn: isOn) } } ) )
- Upgrade the method to be
Update the live
NotificationsClient
, a dependency used by the Composable Architecture version of the application, to use these new async helpers withEffect.task
Solution
extension UserNotificationsClient { static let live = Self( getNotificationSettings: { .task { .init(rawValue: await UNUserNotificationCenter.current().notificationSettings) } }, registerForRemoteNotifications: { .fireAndForget { UIApplication.shared.registerForRemoteNotifications() } }, requestAuthorization: { options in .task { try await UNUserNotificationCenter.current().requestAuthorization(options: options) } } ) }
References
Collection: Composable Architecture
Brandon Williams & Stephen CelisArchitecture is a tough problem and there’s no shortage of articles, videos and open source projects attempting to solve the problem once and for all. In this collection we systematically develop an architecture from first principles, with an eye on building something that is composable, modular, testable, and more.