Concise Forms: Bye Bye Boilerplate

Episode #133 • Feb 1, 2021 • Subscriber-Only

The Composable Architecture makes it easy to layer complexity onto a form, but it just can’t match the brevity of vanilla SwiftUI…or can it!? We will overcome a Swift language limitation using key paths and type erasure to finally say “bye!” to boilerplate.

Bye Bye Boilerplate
Introduction
00:05
The problem: action overload
01:56
The solution: a type-erased form action
12:40
Eliminating reducer boilerplate
33:13
Impact on tests
39:30
Improving ergonomics
42:05
Next time: the point
56:09

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

Introduction

This is a pretty comprehensive test that would have been impossible to write with the way the view model version of the code is written. By doing just a bit of upfront work we can get a ton of code coverage.

With just a little more work we can also write a test for the unhappy path, where the user denies us permission to their notifications, but we will save that as an exercise for the viewer.

OK, so we have done a really comprehensive overview of how forms work in vanilla SwiftUI applications and in Composable Architecture applications. There really doesn’t seem to be a clear winner as far as conciseness goes. On the one hand SwiftUI handles very simple forms amazingly, reducing boilerplate and noise, but things get messy fast when you are handling more complex, real world scenarios. On the other hand the Composable Architecture comes with a decent amount of boilerplate for a very simple form, but then really shines as you start layering on the complexities, giving you a wonderful story for dependencies, side effects and testing.

So, this is a bit of a bummer. We love the Composable Architecture, but it’s things like this boilerplate problem which can turn away people from using it even when there are so many other benefits to be had.

Well, luckily for us the boilerplate problem can be solved. Using some of the more advanced features of Swift we can eliminate almost all of the boilerplate when dealing with simple form data, which will hopefully make the Composable Architecture solution more palatable for those worried about boilerplate.

So let’s take all of the work we’ve done with the Composable Architecture and copy it over and chip away at the problem of eliminating that boilerplate.

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. Loosen the Hashable constraint on FormAction’s erased Value generic to Equatable by implementing an AnyEquatable type eraser, such that AnyEquatable(myEquatableValue) should compile.

    Solution

    We can implement AnyEquatable using erasure in a similar way to how we did with FormAction: by erasing the given value and holding onto a function that captures the equatable conformance. Then, we can introduce a generic initializer that enforces things safely.

    struct AnyEquatable: Equatable {
      let value: Any
      let valueIsEqualTo: (Any) -> Bool
    
      init<Value>(_ value: Value) where Value: Equatable {
        self.value = value
        self.valueIsEqualTo = { $0 as? Value == value }
      }
    
      static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.valueIsEqualTo(rhs.value)
      }
    }
    

    Now we can update FormAction to be initialized with an Equatable constraint instead of a Hashable constraint by holding the value in an AnyEquatable instead of AnyHashable:

    struct FormAction<Root>: Equatable {
      let keyPath: PartialKeyPath<Root>
      let value: AnyEquatable
      let setter: (inout Root) -> Void
    
      init<Value>(
        _ keyPath: WritableKeyPath<Root, Value>,
        _ value: Value
      ) where Value: Equatable {
        self.keyPath = keyPath
        self.value = AnyEquatable(value)
        self.setter = { $0[keyPath: keyPath] = value }
      }
    
      static func set<Value>(
        _ keyPath: WritableKeyPath<Root, Value>,
        _ value: Value
      ) -> Self where Value: Equatable {
        self.init(keyPath, value)
      }
      …
    }
    

    This lets us update the view store binding helper, as well:

    extension ViewStore {
      func binding<Value>(
        keyPath: WritableKeyPath<State, Value>,
        send action: @escaping (FormAction<State>) -> Action
      ) -> Binding<Value> where Value: Equatable {
        self.binding(
          get: { $0[keyPath: keyPath] },
          send: { action(.init(keyPath, $0)) }
        )
      }
    }
    

    Now we can even drop the Hashable constraing on AlertState and everything still compiles!

References

Combine Schedulers: Erasing Time

Brandon Williams & Stephen Celis • Monday Jun 15, 2020

We took a deep dive into type erasers when we explored Combine’s Scheduler protocol, and showed that type erasure prevented generics from infecting every little type in our code.

We refactor our application’s code so that we can run it in production with a live dispatch queue for the scheduler, while allowing us to run it in tests with a test scheduler. If we do this naively we will find that generics infect many parts of our code, but luckily we can employ the technique of type erasure to make things much nicer.

isowords

Point-Free

A word game by us, written in the Composable Architecture.

Downloads