Safer, Conciser Forms: Part 1

Episode #158 • Aug 30, 2021 • Subscriber-Only

Previously we explored how SwiftUI makes building forms a snap, and we contrasted it with the boilerplate introduced by the Composable Architecture. We employed a number of advanced tools to close the gap, but we can do better! We’ll start by using a property wrapper to make things much safer than before.

Part 1
Introduction
00:05
Concise forms recap
02:30
Making concise forms safer
17:50
Next time: eliminating more boilerplate
27:59

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.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. 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 and withUnsafeThrowingContinuation.

    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 the DispatchQueue.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) }
        }
      )
    )
    
  2. Update the live NotificationsClient, a dependency used by the Composable Architecture version of the application, to use these new async helpers with Effect.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 Celis

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

Downloads