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.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Loosen the
Hashable
constraint onFormAction
’s erasedValue
generic toEquatable
by implementing anAnyEquatable
type eraser, such thatAnyEquatable(myEquatableValue)
should compile.Solution
We can implement
AnyEquatable
using erasure in a similar way to how we did withFormAction
: 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 anEquatable
constraint instead of aHashable
constraint by holding the value in anAnyEquatable
instead ofAnyHashable
: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 onAlertState
and everything still compiles!
References
Combine Schedulers: Erasing Time
Brandon Williams & Stephen Celis • Monday Jun 15, 2020We 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-FreeA word game by us, written in the Composable Architecture.