Collection
Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
So that’s the basics of building a moderately complex settings. We are leveraging the power of SwiftUI and the Form
view to get a ton done for us basically for free. It’s super easy to get a form up on the screen, and easy for changes to that form to be instantly reflected in a backing model.
However, we did uncover a few complexities that arise when dealing with something a little bit more realistic, and not just a simple demo. For one thing, we saw that if we need to react to a change in the model, such as wanting to truncate the display name to be at most 16 characters, then we had to tap into the didSet
of that field. However, it is very dangerous to make mutations in a didSet
. As we saw it can lead to infinite loops, and it can be very difficult to understand all the code paths that can lead to an infinite loop. It’s possible for the didSet
to call a method, which calls another method, which then mutates the property, and then bam… you’ve got an infinite loop on your hands.
Another complexity is that if you want to do something a little more complicated when the form changes, such as hook into notification permissions, then you are forced to leave the nice confines of ergonomic magic that SwiftUI provides for us. We needed to write a binding from scratch just so that we could call out to a view model method instead of mutating the model directly. Doing that wasn’t terrible, we just think it’s worth pointing out that working with more real world, complex examples you often can’t take advantage of SwiftUI’s nice features, and gotta get your hands dirty.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Write a test for the unhappy path of denying push permission.
Solution
It looks a lot like the happy path test, but we can make a few changes:
- Instead of introducing state for
didRegisterForRemoteNotifications
, we can leave that dependency as afatalError
instead. - When we receive an authorization response where granted is false, we need to assert that state flips the toggle back, as well.
func testNotifications_UnhappyPath_Deny() { let store = TestStore( initialState: SettingsState(), reducer: settingsReducer, environment: SettingsEnvironment( mainQueue: DispatchQueue.immediateScheduler.eraseToAnyScheduler(), userNotifications: UserNotificationsClient( getNotificationSettings: { .init(value: .init(authorizationStatus: .notDetermined)) }, registerForRemoteNotifications: { fatalError() }, requestAuthorization: { _ in .init(value: false) } ) ) ) store.assert( .send(.sendNotificationsChanged(true)), .receive(.notificationSettingsResponse(.init(authorizationStatus: .notDetermined))) { $0.sendNotifications = true }, .receive(.authorizationResponse(.success(false))) { $0.sendNotifications = false } ) }
- Instead of introducing state for
Write a test for tapping send notifications when authorization stats starts in a
denied
state. Try to get as much coverage for this flow as possible.Solution
Much like the previous test, but:
- We can now fatal error in
requestAuthorization
, as well. - We must update state with the alert.
- We should additionally send a
.dismissAlert
action to testnil
ing out this state.
func testNotifications_UnhappyPath_PreviouslyDenied() { let store = TestStore( initialState: SettingsState(), reducer: settingsReducer, environment: SettingsEnvironment( mainQueue: DispatchQueue.immediateScheduler.eraseToAnyScheduler(), userNotifications: UserNotificationsClient( getNotificationSettings: { .init(value: .init(authorizationStatus: .denied)) }, registerForRemoteNotifications: { fatalError() }, requestAuthorization: { _ in fatalError() } ) ) ) store.assert( .send(.sendNotificationsChanged(true)), .receive(.notificationSettingsResponse(.init(authorizationStatus: .denied))) { $0.alert = .init(title: "You need to enable permissions from iOS settings") }, .send(.dismissAlert) { $0.alert = nil } ) }
- We can now fatal error in
References
Collection: Dependencies
Brandon Williams & Stephen CelisDependencies can wreak havoc on a codebase. They increase compile times, are difficult to test, and put strain on build tools. Did you know that Core Location, Core Motion, Store Kit, and many other frameworks do not work in Xcode previews? Any feature touching these frameworks will not benefit from the awesome feedback cycle that previews afford us. This collection clearly defines dependencies and shows how to take back control in order to unleash some amazing benefits.