8.
Architecture: Elements, Part 2
Written by René Cacheaux
In Chapter 7, you learned about Elements and how to design user interface and interaction responder elements. In this chapter, you’ll take a deep dive into two more elements: observer and use case.
Note: The example Koober Xcode project for this chapter is the same as Chapter 7’s Xcode project. To see this chapter’s material in Koober, open the Xcode project that is located in Chapter 7’s project directory.
Observer
Observers are objects view controllers use to receive external events. You can think of these events as input signals to view controllers. Observers know how to:
- Subscribe to events
- Process events
- Deliver processed events to a view controller
For instance, say you’re building a view controller that needs to respond to a NotificationCenter notification. An observer would know how to subscribe to the notification, how to pull out the relevant information from the user info dictionary and would know what view controller method to call. The view controller would then perform some work in response to the processed notification. You might be thinking, but wait, adding and removing observers from NotificationCenter is really easy. Why not leave this code in view controllers? Hang tight, you’ll read about the benefits soon.
Note: Observers allow you to decouple view controllers from event technologies such as
NotificationCenter, target-action, etc.Combinealso allows you to decouple view controllers from event technologies. As you read this section you might be wondering why not just useCombine? UsingCombineadds boilerplate code to your view controllers making them a bit harder to read. You can use the Observer pattern alongsideCombineto both decouple view controllers from event technologies and to make view controllers light and easy to read. UsingCombinedirectly inside view controllers is also a valid approach. This decision comes down to reading preference.
Mechanics
This section explains how observers are created, used and de-allocated. If this section is a bit fuzzy, don’t worry. You’ll see code examples of all these concepts further down.
Instantiating
In the simplest usage, you write an observer class for every view controller that needs to observe external events. Observers are initialized with references to the systems that emit events. This is so an observer can subscribe to events when a view controller wants to start observing.
Providing
Observers are created outside view controllers; i.e., observers are provided to their respective view controller. Observers are provided to view controllers either via a view controller’s initializer or by setting a view controller property. At this point, a view controller has a reference to its observer.
Observers hold references to the systems the view controller wants to observe, such as RxSwift Observables and Combine Publishers. During this phase, observers have not subscribed to any events.
During setup, observers need to be given a delegate. Observers call methods on their delegates every time they process a new event. Delegates, which are typically view controllers, are of type EventResponder. EventResponder is a protocol that you write specifically for each view controller. EventResponder protocols have all the methods that a view controller implements to respond to different events from different systems. For example, you might have a method for when the keyboard is dismissed.
Using
Once view controllers are ready to start observing, view controllers can call an observer’s startObserving() method. During this method, observers subscribe to all the events that a view controller needs to observe. At this point, observers are live. They are accepting, processing and delivering events to their view controller.
View controllers can call an observer’s stopObserving() method whenever they need to stop events from arriving. You might do this when a view controller is no longer visible but still alive in memory. If you need to start and stop observing different events at different times you can break up an observer into multiple observers. You’ll see an example of this in the variation and advanced usage section.
Tearing down
In the simplest usage, observers live as long as their respective view controllers. Observer and view controller lifetimes should match. To guarantee the lifetime, make sure that a view controller is the only object holding onto an observer. Also, observers need to hold a weak reference to their event responder; i.e., view controller, to avoid retain cycles.
As a best practice, view controllers should call stopObserving() before being de-allocated by ARC. However, you can build a nice safeguard inside observers by calling stopObserving() when the weak reference to an observer’s event responder; i.e., view controller, nils out. You can do this in a willSet or didSet property observer closure. You’ll see this safeguard in the example code ahead.
Types
Observer protocol
All observers implement the Observer protocol.
Note: If this name collides with a pre-existing type you can rename it to something similar.
View controllers should type annotate their observer property with this Observer protocol type as opposed to the observer’s concrete class type. This is so you don’t have to provide a real observer when unit testing view controllers. This is what the protocol looks like:
protocol Observer {
func startObserving()
func stopObserving()
}
startObserving() and stopObserving() are the only two methods that a view controller needs to call on any observer. View controllers use these methods to start observing and stop observing events.
Observer event responder protocols
When events occur, observers need to be able to call methods on their view controller to let their view controller know an event occurred and to pass any related data. In order to do this, observers hold a weak reference to their view controller.
The type of the weak reference could be the concrete view controller type; however, that gives observers access to call all visible view controller methods. Instead, you can define an EventResponder protocol.
You then declare conformance to this protocol by an observer’s view controller. This protocol includes all the methods that an observer can call. Because observers need to hold a weak reference of this type, this protocol type can only be conformed to by class types. Here’s an example:
protocol ObserverForSignInEventResponder: AnyObject {
func received(newErrorMessage errorMessage: ErrorMessage)
func received(newViewState viewState: SignInViewState)
func keyboardWillHide()
func keyboardWillChangeFrame(keyboardEndFrame: CGRect)
}
Notice how the events can come from different systems. For instance, in the example above, the first half of the methods are associated with Combine Publisher subscriptions and the second half of the methods are associated with NotificationCenter notifications. This is nice because view controllers no longer need to deal with different event technologies. This is also nice because the pattern removes subscription boilerplate code from view controllers and therefore makes view controllers much easier to read.
Note: The following code examples subscribe to keyboard notifications using
NotificationCenterAPIs. Alternatively, you can subscribe to keyboard events usingCombine. Either way, the Observer pattern does not change and that’s nice because the pattern is resilient to framework choice.
Also, in the example above, notice how the keyboard event methods do not pass the info dictionary from NotificationCenter notifications. Observers know how to pull out the relevant information. This is really nice because related view controllers no longer need to know how to fish for data that’s inside an info dictionary. Also, when unit testing, you won’t have to worry about creating an info dictionary. You just need to call the view controller’s event responder methods with test data. And, if later in time, events need to come from a different system — e.g., you switch from CoreData to SQLite — you won’t need to change any view controllers. You’ll just need to update observers.
Observer classes
Observer classes conform to the Observer protocol. As mentioned before, they hold a weak reference to their EventResponder, which is usually a view controller. Observer classes know how to subscribe to events, process events and call methods on an EventResponder. You implement one observer class for each view controller that needs to observe external events. Here’s an example skeleton implementation:
class ObserverForSignIn: Observer {
// MARK: - Properties
weak var eventResponder: ObserverForSignInEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
// MARK: - Methods
func startObserving() {
// Subscribe to events here.
// ...
}
func stopObserving() {
// Unsubscribe to events here.
// ...
}
}
Remember the safeguard you read about earlier? It’s implemented here, in the example above. Whenever the eventResponder weak reference nils out, the property observer calls stopObserving(). This makes sure the observer unsubscribes from events when the related view controller is de-allocated.
Example
In this section, you’ll walk through a complete example so you can see how all the different types and objects work together. The example is from Koober’s sign-in screen.
Note: To ease readability, some of the example code in this section has been simplified from the code in the example Xcode project.
Koober’s sign-in screen is implemented by SignInViewController. This view controller benefits from using an observer because it needs to observe several different events from different systems.
SignInViewController needs to observe the following events:
-
Sign-in view state: A
CombinePublisherprovides the controller’sUIViewstate. In order to reload the view, the controller needs to know when the view state changes. When the controller sees a new state, the controller passes the state object to its rootUIViewso the view can update itself. -
Error messages: The
SignInViewControllerneeds to be able to present aUIAlertControllerwhenever an error, such as an incorrect password, occurs. The error messages come from aCombinePublisher. -
Keyboard events: The sign-in screen needs to accommodate the keyboard for short screens found on iPhones such as the iPhone SE. In order to do this, the controller needs to observe keyboard notifications from
NotificationCenter.
By delegating event subscription to an observer, the view controller decouples itself from technologies such as RxSwift, Combine and NotificationCenter.
This makes the view controller’s code more robust, cleaner and easier to test. Now that you’re familiar with the events SignInViewController needs to observe, it’s time to walk through the code.
The first step to building an observer for a view controller is to design an EventResponder protocol with all the event handling methods. The view controller should implement these methods. The view controller’s observer calls into one of these methods when an event occurs. Here’s the SignInViewController’s EventResponder protocol you saw earlier:
protocol ObserverForSignInEventResponder: AnyObject {
func received(newErrorMessage errorMessage: ErrorMessage)
func received(newViewState viewState: SignInViewState)
func keyboardWillHide()
func keyboardWillChangeFrame(keyboardEndFrame: CGRect)
}
This is the exact same protocol. When designing EventResponder protocols, avoid including any details about the event systems. For instance, the example above avoids any Combine types and avoids concepts from NotificationCenter such as user info dictionaries. The goal is to design a really clean protocol that depends on as little as possible. This is important because the point of EventResponder protocols is to decouple view controllers from event systems.
So that’s the event responder. Once you’ve designed your view controller’s EventResponder protocol, you can implement the protocol methods in the view controller similar to the following:
extension SignInViewController:
ObserverForSignInEventResponder {
func received(newErrorMessage errorMessage: ErrorMessage) {
// ...
}
func received(newViewState viewState: SignInViewState) {
// ...
}
func keyboardWillHide() {
// ...
}
func keyboardWillChangeFrame(keyboardEndFrame: CGRect) {
// ...
}
}
Don’t worry too much about how these methods are implemented. The view controller responds to these events just like any other view controller. The important point is the view controller no longer needs to know how to receive events from specific technologies and systems.
With the EventResponder protocol designed and with the protocol implemented in the view controller, the next step is to look at the SignInViewController’s observer class, ObserverForSignIn:
// 1
class ObserverForSignIn: Observer {
// MARK: - Properties
// 2
weak var eventResponder: ObserverForSignInEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
// 3
let signInState: AnyPublisher<SignInViewControllerState,
Never>
var errorStateSubscription: AnyCancellable?
var viewStateSubscription: AnyCancellable?
// 4
private var isObserving: Bool {
if isObservingState && isObservingKeyboard {
return true
} else {
return false
}
}
private var isObservingState: Bool {
if errorStateSubscription != nil
&& viewStateSubscription != nil {
return true
} else {
return false
}
}
private var isObservingKeyboard = false
// MARK: - Methods
// 5
init(signInState: AnyPublisher<SignInViewControllerState,
Never>) {
self.signInState = signInState
}
// 6
func startObserving() {
assert(self.eventResponder != nil)
guard let _ = self.eventResponder else {
return
}
if isObserving {
return
}
subscribeToErrorMessages()
subscribeToSignInViewState()
startObservingKeyboardNotifications()
}
// 7
func stopObserving() {
unsubscribeFromSignInViewState()
unsubscribeFromErrorMessages()
stopObservingNotificationCenterNotifications()
}
func subscribeToSignInViewState() {
viewStateSubscription =
signInState
.receive(on: DispatchQueue.main)
.map { $0.viewState }
.removeDuplicates()
.sink { [weak self] viewState in
self?.received(newViewState: viewState)
}
}
func received(newViewState: SignInViewState) {
// 8
eventResponder?.received(newViewState: newViewState)
}
func unsubscribeFromSignInViewState() {
viewStateSubscription = nil
}
func subscribeToErrorMessages() {
errorStateSubscription =
signInState
.receive(on: DispatchQueue.main)
.map { $0.errorsToPresent.first }
.compactMap { $0 }
.removeDuplicates()
.sink { [weak self] errorMessage in
self?.received(newErrorMessage: errorMessage)
}
}
func received(newErrorMessage errorMessage: ErrorMessage) {
// 8
eventResponder?.received(newErrorMessage: errorMessage)
}
func unsubscribeFromErrorMessages() {
errorStateSubscription = nil
}
func startObservingKeyboardNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter
.addObserver(
self,
selector: #selector(
handle(keyboardWillHideNotification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
notificationCenter
.addObserver(
self,
selector: #selector(
handle(keyboardWillChangeFrameNotification:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
isObservingKeyboard = true
}
@objc func handle(
keyboardWillHideNotification notification: Notification
) {
assert(notification.name ==
UIResponder.keyboardWillHideNotification)
// 8
eventResponder?.keyboardWillHide()
}
@objc func handle(
keyboardWillChangeFrameNotification
notification: Notification) {
assert(notification.name ==
UIResponder.keyboardWillChangeFrameNotification)
guard let userInfo = notification.userInfo else {
return
}
guard let keyboardEndFrameUserInfo =
userInfo[UIResponder.keyboardFrameEndUserInfoKey] else {
return
}
guard let keyboardEndFrame =
keyboardEndFrameUserInfo as? NSValue else {
return
}
// 8
eventResponder?
.keyboardWillChangeFrame(
keyboardEndFrame: keyboardEndFrame.cgRectValue)
}
func stopObservingNotificationCenterNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
isObservingKeyboard = false
}
}
Here are some things worth highlighting in the example above:
- The class conforms to the
Observerprotocol you saw earlier in this chapter. Recall that this protocol allows the associated view controller to start and stop observation. - This is the stored property that holds a weak reference to the observer’s associated view controller. The property’s
willSetclosure ensures that this observer unsubscribes from event subscriptions whenever the event responder; i.e., the view controller, is de-allocated. The property is type annotated with theObserverForSignInEventResponderprotocol type. The protocol restricts which view controller methods the observer can call. If you feel comfortable exposing all the view controller methods you can forgo designingEventResponderprotocols and simply use view controller concrete types in observer implementations. Though, if you need to unit test the observer, theEventResponderprotocol is helpful because you won’t have to instantiate a real view controller. You can provide a fake implementation to the observer. - Because view controllers should have control over when observation starts and stops, observer classes cannot subscribe to events immediately during initialization. For this reason, observer classes have to hold onto references to the systems they observe. In this case, this observer observes events coming from a
CombinePublisher. This observer implementation has to hold onto thePublisherin order to subscribe and unsubscribe from thePublisherat later points in time. Some systems, likeNotificationCenter, provide global default singletons for adding observers. In these cases, the observer class does not need to hold on to anything. - The
isObservingcomputed property helps avoid accidentally subscribing to the same event stream more than once.startObserving()checks this value before subscribing to any events. - Observers should be initialized with the systems they need to observe. In this example, the observer is initialized with a
CombinePublisher. - This is the
Observerprotocol’sstartObserving()method implementation. This specific implementation makes sure that the observer has a reference to the event responder; i.e., the view controller. Then, the method subscribes to two different data models from the sameCombinePublisher. Finally, the method starts listening to two different keyboard notifications fromNotificationCenter. - This is the
Observerprotocol’sstopObserving()method implementation. In this example, the method unsubscribes from theCombinePublisherand removes itself as aNotificationCenterobserver. - These lines of code point out where the observer is making calls to the view controller via the event responder protocol. Notice how the observer processes the data coming from both the
CombinePublisherand processes the data coming fromNotificationCenterbefore calling the view controller. The observer is a great place to hide away any logic that is specific to event systems, likeNotificationCenternotification objects.
The last step in putting this pattern to practice is to add code to the view controller to start and stop observation.
Observers are provided to view controllers instead of view controllers instantiating their observers. This is because a view controller shouldn’t need to know how to get a hold of the event system objects needed to initialize an observer.
View controllers should have an observer property to hold onto their observer object. View controllers can then call startObserving() and stopObserving() at appropriate points in time.
Here’s how this works in SignInViewController:
public class SignInViewController: NiblessViewController {
// MARK: - Properties
// Observers
var observer: Observer
// User interface
let userInterface: SignInUserInterfaceView
// Factories
let signInUseCaseFactory: SignInUseCaseFactory
let makeFinishedPresentingErrorUseCase:
FinishedPresentingErrorUseCaseFactory
// MARK: - Methods
init(userInterface: SignInUserInterfaceView,
observer: Observer,
signInUseCaseFactory: SignInUseCaseFactory,
finishedPresentingErrorUseCaseFactory:
@escaping FinishedPresentingErrorUseCaseFactory
) {
self.userInterface = userInterface
self.observer = observer
self.signInUseCaseFactory = signInUseCaseFactory
self.makeFinishedPresentingErrorUseCase =
finishedPresentingErrorUseCaseFactory
super.init()
}
public override func loadView() {
view = userInterface
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
observer.startObserving()
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
observer.stopObserving()
}
// ...
}
extension SignInViewController:
ObserverForSignInEventResponder {
func received(newErrorMessage errorMessage: ErrorMessage) {
// ...
}
func received(newViewState viewState: SignInViewState) {
// ...
}
func keyboardWillHide() {
// ...
}
func keyboardWillChangeFrame(keyboardEndFrame: CGRect) {
// ...
}
}
// ...
This code is fairly straightforward. All of the icky details about Combine and NotificationCenter are no longer in this view controller. Because the code is so easy to read, there’s no need to walk through it step by step. The most important point is that the SignInViewController does not know about the observer’s concrete class type. Notice how the observer property is type annotated with Observer rather than ObserverForSignIn. This allows you to use a fake Observer implementation when unit testing SignInViewController.
The view controller from above receives its observer via its initializer. Therefore, the observer needs to be created outside of the view controller. In Koober, observers are created in dependency containers. For this example, ObserverForSignIn is created and injected into SignInViewController in KooberOnboardingDependencyContainer:
public class KooberOnboardingDependencyContainer {
// ...
func makeSignInViewController() -> SignInViewController {
// User interface element
let userInterface = SignInRootView()
// Observer element
// 1
let statePublisher =
makeSignInViewControllerStatePublisher()
let observer =
ObserverForSignIn(signInState: statePublisher)
// ....
let signInViewController =
SignInViewController(
userInterface: userInterface,
// 2
observer: observer,
// ...
)
// Wire responders
userInterface.ixResponder = signInViewController
//3
observer.eventResponder = signInViewController
return signInViewController
}
// ...
}
Here are the steps in makeSignInViewController that create and inject ObserverForSignIn:
- The observer is created with a
CombinePublisher. Subscriptions to thePublishercarry all the state updates needed bySignInViewController. - The observer is injected into a new
SignInViewController. Recall thatSignInViewControllerinitializer’s parameter forobserveris type annotated withObserverrather thanObserverForSignIn. - Every observer needs an
eventResponder. Event responder protocols are almost always implemented by view controllers. In this case, thesignInViewControlleris set as theobserver’seventResponder.
That’s all the code needed to build, create and use observers in any codebase. This section covers the basics. There are many other ways to design observers. You’ll learn about all the variations and advanced usages next.
Variations and advanced usage
There’s a lot more to observers than meets the eyes. In this section, you’ll explore more ways to implement observers.
Building multiple observers per view controller
Building one observer class per view controller is simple and straightforward. However, in certain situations, you might prefer to break a single observer into multiple observer classes.
Sometimes, you might need to start and stop observing different systems at different times. For example, you might want to stop observing UI related events when a view controller goes off the screen while continuing to observe non-UI related events. To do this, you’ll need to build multiple observers. If you don’t need to start and stop observing at different times, you still might want to build multiple observers. A single observer class might be very long. In these cases it’s nice to build a separate observer for separate event systems.
To illustrate this pattern, the following code examples demonstrate how to break up the ObserverForSignIn from the previous section into two observers: SignInViewControllerStateObserver and SignInKeyboardObserver.
Note: The example Xcode project has a different variation of the observer element. The project does not include
SignInKeyboardObserver. However, the entire implementation forSignInKeyboardObserveris available below.
First, the ObserverForSignInEventResponder needs to be separated into these two protocols:
protocol SignInKeyboardObserverEventResponder: AnyObject {
func keyboardWillHide()
func keyboardWillChangeFrame(keyboardEndFrame: CGRect)
}
protocol SignInStateObserverEventResponder: AnyObject {
func received(newErrorMessage errorMessage: ErrorMessage)
func received(newViewState viewState: SignInViewState)
}
These protocols are nicer than the single ObserverForSignInEventResponder because each protocol is only responsible for a single kind of event. SignInKeyboardObserverEventResponder handles keyboard events and SignInStateObserverEventResponder handles state change events.
With the event responders figured out, the next step is to look at separate observer implementations: SignInKeyboardObserver and SignInStateObserver.
class SignInKeyboardObserver: Observer {
// MARK: - Properties
weak var eventResponder:
SignInKeyboardObserverEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
private var isObserving = false
// MARK: - Methods
func startObserving() {
assert(self.eventResponder != nil)
guard let _ = self.eventResponder else {
return
}
if isObserving {
return
}
startObservingKeyboardNotifications()
}
func stopObserving() {
stopObservingNotificationCenterNotifications()
}
func startObservingKeyboardNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter
.addObserver(
self,
selector: #selector(
handle(keyboardWillHideNotification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
notificationCenter
.addObserver(
self,
selector: #selector(
handle(keyboardWillChangeFrameNotification:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
isObserving = true
}
@objc func handle(
keyboardWillHideNotification notification: Notification
) {
assert(notification.name ==
UIResponder.keyboardWillHideNotification)
eventResponder?.keyboardWillHide()
}
@objc func handle(
keyboardWillChangeFrameNotification
notification: Notification
) {
assert(notification.name ==
UIResponder.keyboardWillChangeFrameNotification)
guard let userInfo = notification.userInfo else {
return
}
guard let keyboardEndFrameUserInfo =
userInfo[UIResponder.keyboardFrameEndUserInfoKey] else {
return
}
guard let keyboardEndFrame =
keyboardEndFrameUserInfo as? NSValue else {
return
}
eventResponder?
.keyboardWillChangeFrame(
keyboardEndFrame: keyboardEndFrame.cgRectValue)
}
func stopObservingNotificationCenterNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
isObserving = false
}
}
class SignInStateObserver: Observer {
// MARK: - Properties
weak var eventResponder: SignInStateObserverEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
let signInState: AnyPublisher<SignInViewControllerState,
Never>
var errorStateSubscription: AnyCancellable?
var viewStateSubscription: AnyCancellable?
private var isObserving: Bool {
if errorStateSubscription != nil
&& viewStateSubscription != nil {
return true
} else {
return false
}
}
// MARK: - Methods
init(signInState: AnyPublisher<SignInViewControllerState,
Never>) {
self.signInState = signInState
}
func startObserving() {
assert(self.eventResponder != nil)
guard let _ = self.eventResponder else {
return
}
if isObserving {
return
}
subscribeToErrorMessages()
subscribeToSignInViewState()
}
func stopObserving() {
unsubscribeFromSignInViewState()
unsubscribeFromErrorMessages()
}
func subscribeToSignInViewState() {
viewStateSubscription =
signInState
.receive(on: DispatchQueue.main)
.map { $0.viewState }
.removeDuplicates()
.sink { [weak self] viewState in
self?.received(newViewState: viewState)
}
}
func received(newViewState: SignInViewState) {
eventResponder?.received(newViewState: newViewState)
}
func unsubscribeFromSignInViewState() {
viewStateSubscription = nil
}
func subscribeToErrorMessages() {
errorStateSubscription =
signInState
.receive(on: DispatchQueue.main)
.map { $0.errorsToPresent.first }
.compactMap { $0 }
.removeDuplicates()
.sink { [weak self] errorMessage in
self?.received(newErrorMessage: errorMessage)
}
}
func received(newErrorMessage errorMessage: ErrorMessage) {
eventResponder?.received(newErrorMessage: errorMessage)
}
func unsubscribeFromErrorMessages() {
errorStateSubscription = nil
}
}
The implementation for these observers comes straight from ObserverForSignIn. Because each observer only deals with a single event system such as Combine or NotificationCenter, these observer classes are much easier to read than the single ObserverForSignIn from the previous section. This is a nice plus to breaking observers down into multiple classes.
Now, it’s time to look at how the SignInViewController receives and uses the two observer instances:
class SignInViewController: NiblessViewController {
// MARK: - Properties
// 2
var stateObserver: Observer
var keyboardObserver: Observer
let userInterface: SignInUserInterfaceView
// MARK: - Methods
// 1
init(userInterface: SignInUserInterfaceView,
stateObserver: Observer,
keyboardObserver: Observer) {
self.userInterface = userInterface
self.stateObserver = stateObserver
self.keyboardObserver = keyboardObserver
super.init()
}
override func loadView() {
view = userInterface
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 3
stateObserver.startObserving()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 3
keyboardObserver.startObserving()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 4
stateObserver.stopObserving()
keyboardObserver.stopObserving()
}
// ...
}
extension SignInViewController:
SignInStateObserverEventResponder {
func received(newErrorMessage errorMessage: ErrorMessage) {
// ...
}
func received(newViewState viewState: SignInViewState) {
// ...
}
}
extension SignInViewController:
SignInKeyboardObserverEventResponder {
func keyboardWillHide() {
// ...
}
func keyboardWillChangeFrame(keyboardEndFrame: CGRect) {
// ...
}
}
// ...
This implementation of SignInViewController is pretty much the same as before, except, this version manages two observer instances instead of one. Here are some quick things to look at:
-
The view controller’s initializer takes two observers. One for observing the keyboard and another for observing changes to the view controller and view state. Notice how, same as before, both parameters are type annotated with the
Observerprotocol instead of their concrete types. -
The view controller needs two properties to hold each observer instance.
-
This is the main difference from the last implementation. Because the observation is built using separate observers, this view controller can now start observing state changes and keyboard events in different view controller lifecycle methods.
-
Both observers are stopped during
viewWillDisappear(_:).
Alright, that’s most of the example.
The last thing to look at is how KooberOnboardingDependencyContainer injects SignInViewController with the two observers:
class KooberOnboardingDependencyContainer {
// ...
func makeSignInViewController() -> SignInViewController {
// User interface element
let userInterface = SignInRootView()
// Observer elements
// 1
let statePublisher =
makeSignInViewControllerStatePublisher()
let stateObserver =
SignInStateObserver(signInState: statePublisher)
let keyboardObserver = SignInKeyboardObserver()
// 2
let signInViewController =
SignInViewController(
userInterface: userInterface,
stateObserver: stateObserver,
keyboardObserver: keyboardObserver)
// Wire responders
userInterface.ixResponder = signInViewController
// 3
stateObserver.eventResponder = signInViewController
keyboardObserver.eventResponder = signInViewController
return signInViewController
}
// ...
}
The main difference in this version of the dependency container is that the factory method needs to create, inject and wire two observers instead of one.
Some quick highlights:
- Both observers are created.
- The observers are injected into a new
SignInViewController. - Each observer needs a reference to an event responder.
SignInViewControllerconforms to both event responder protocols, therefore both observers are given thesignInViewControlleras the event responder.
That’s it! Breaking up a single view controller observer into single responsibility observers is a bit more work, but you get a cleaner and easier-to-read codebase. Now that you’ve seen these smaller single responsibility observers, you might be wondering if you could build an observer that can be reused by multiple view controllers. That’s next.
Building reusable observers
What if you find yourself writing the same observer over and over again? Many of the systems that generate events in Cocoa Touch are general in nature.
The code you write to subscribe and respond to these events is virtually identical no matter what view controller you’re building. For these cases, you can write a general purpose observer that you can re-use in any view controller. Observing keyboard events is a perfect example. You’ll see a sample implementation of a general purpose keyboard observer next.
The first step is to design an event responder protocol that any view controller could conform to in order to respond to keyboard events. There’s a problem though, Cocoa Touch doesn’t have a keyboard user info data type. So how can a protocol be designed for methods such as keyboardWillChangeFrame?
The easiest thing to do would be to just pass along the user info dictionary to view controllers, but one of the goals of the observer pattern is to remove this kind of responsibility and complexity away from view controllers. You can accomplish this removal of responsibility by designing a custom data type to carry notification values. First, you’ll explore this custom KeyboardUserInfo struct type:
struct KeyboardUserInfo {
// MARK: - Properties
let animationCurve: UIView.AnimationCurve
let animationDuration: Double
let isLocal: Bool
let beginFrame: CGRect
let endFrame: CGRect
let animationCurveKey =
UIResponder.keyboardAnimationCurveUserInfoKey
let animationDurationKey =
UIResponder.keyboardAnimationDurationUserInfoKey
let isLocalKey = UIResponder.keyboardIsLocalUserInfoKey
let frameBeginKey = UIResponder.keyboardFrameBeginUserInfoKey
let frameEndKey = UIResponder.keyboardFrameEndUserInfoKey
// MARK: - Methods
init?(_ notification: Notification) {
guard let userInfo = notification.userInfo else {
return nil
}
// Animation curve.
guard let animationCurveUserInfo =
userInfo[animationCurveKey],
let animationCurveRaw =
animationCurveUserInfo as? Int,
let animationCurve =
UIView.AnimationCurve(rawValue: animationCurveRaw)
else {
return nil
}
self.animationCurve = animationCurve
// Animation duration.
guard let animationDurationUserInfo =
userInfo[animationDurationKey],
let animationDuration =
animationDurationUserInfo as? Double
else {
return nil
}
self.animationDuration = animationDuration
// Is local.
guard let isLocalUserInfo = userInfo[isLocalKey],
let isLocal = isLocalUserInfo as? Bool else {
return nil
}
self.isLocal = isLocal
// Begin frame.
guard let beginFrameUserInfo = userInfo[frameBeginKey],
let beginFrame = beginFrameUserInfo as? CGRect else {
return nil
}
self.beginFrame = beginFrame
// End frame.
guard let endFrameUserInfo = userInfo[frameEndKey],
let endFrame = endFrameUserInfo as? CGRect else {
return nil
}
self.endFrame = endFrame
}
}
KeyboardUserInfo is a pure data type that’s instantiated with a Notification object. During initialization, KeyboardUserInfo pulls all the values out of the notification’s user info dictionary and sets those values on its own properties. Because the user info dictionary could be nil and because the dictionary could have a missing key-value pair, the initializer is fail-able. The reason this data type exists is to design an event responder protocol for keyboard events. What does this event provider protocol look like?
protocol KeyboardObserverEventResponder: AnyObject {
func keyboardWillShow(_ userInfo: KeyboardUserInfo)
func keyboardDidShow(_ userInfo: KeyboardUserInfo)
func keyboardWillHide(_ userInfo: KeyboardUserInfo)
func keyboardDidHide(_ userInfo: KeyboardUserInfo)
func keyboardWillChangeFrame(_ userInfo: KeyboardUserInfo)
func keyboardDidChangeFrame(_ userInfo: KeyboardUserInfo)
}
The protocol has a method for every kind of keyboard notification that Cocoa Touch defines. This is pretty neat, but you typically only need to write code for some of these methods. Every one of these methods is required. You wouldn’t want to implement every single one of these methods in every view controller. To solve this problem, we could make this an @objc protocol and make the methods optional, or we can write a protocol extension with empty methods. The second option is more native to Swift. So lets look at the second option. Here’s what the protocol extension looks like:
extension KeyboardObserverEventResponder {
func keyboardWillShow(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
func keyboardDidShow(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
func keyboardWillHide(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
func keyboardDidHide(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
func keyboardWillChangeFrame(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
func keyboardDidChangeFrame(_ userInfo: KeyboardUserInfo) {
// No-op.
// This default implementation allows this protocol method
// to be optional.
}
}
With this extension, any object can conform to KeyboardObserverEventResponder without having to implement all the required methods. Awesome! That’s the event responder, next is the observer implementation:
class KeyboardObserver: Observer {
// MARK: - Properties
weak var eventResponder: KeyboardObserverEventResponder? {
didSet {
if eventResponder == nil {
stopObserving()
}
}
}
private var isObserving = false
// MARK: - Methods
func startObserving() {
if isObserving == true {
return
}
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(keyboardDidShow),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(keyboardDidHide),
name: UIResponder.keyboardDidHideNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(keyboardWillChangeFrame),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(keyboardDidChangeFrame),
name: UIResponder.keyboardDidChangeFrameNotification,
object: nil
)
isObserving = true
}
func stopObserving() {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
isObserving = false
}
@objc func keyboardWillShow(notification: Notification) {
// 1
assert(notification.name ==
UIResponder.keyboardWillShowNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardWillShow(userInfo)
}
@objc func keyboardDidShow(notification: Notification) {
// 1
assert(notification.name ==
UIResponder.keyboardDidShowNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardDidShow(userInfo)
}
@objc func keyboardWillHide(notification: Notification) {
// 1
assert(notification.name ==
UIResponder.keyboardWillHideNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardWillHide(userInfo)
}
@objc func keyboardDidHide(notification: Notification) {
// 1
assert(notification.name ==
UIResponder.keyboardDidHideNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardDidHide(userInfo)
}
@objc func keyboardWillChangeFrame(
notification: Notification
) {
// 1
assert(notification.name ==
UIResponder.keyboardWillChangeFrameNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardWillChangeFrame(userInfo)
}
@objc func keyboardDidChangeFrame(
notification: Notification
) {
// 1
assert(notification.name ==
UIResponder.keyboardDidChangeFrameNotification)
// 2
guard let userInfo = KeyboardUserInfo(notification) else {
assertionFailure()
return
}
// 3
eventResponder?.keyboardDidChangeFrame(userInfo)
}
}
This observer is implemented exactly the same way as all the other observers you’ve seen so far. What’s new here is how the observer responds to keyboard notifications in a general way. All the notification response methods follow this pattern:
- Each response method knows how to process a particular kind of keyboard notification. So, first, the method ensures that the
NotificationCenternotification passed in is of the expected kind. - Then, each method tries to create a
KeyboardUserInfowith the notification object. BecauseKeyboardUserInfo’s initializer is fail-able, the method needs to be able to handle initialization errors. You can handle an error in many different ways. In this example, ifKeyboardUserInfo’s initializer fails, the method crashes on debug builds and returns in release builds as an error here would be unlikely. - The event responder is called with the
KeyboardUserInfoobject.
You can instantiate, inject and wire this observer as you’ve seen in previous examples. The only difference is that this observer is not designed for a specific view controller; i.e., you can instantiate multiple instances for use by different view controllers.
Note: Using this
KeyboardObserverimplementation results in a tiny bit more Objective-C method calling overhead. This is becauseKeyboardObserversubscribes to every kind of keyboard notification even if the associated view controller only implements one of theKeyboardObserverEventRespondermethods. In most cases this is probably negligible. However, this is something to know and measure.
This reusable observer pattern works for most cases. However, in rare performance sensitive situations, you might need to implement single instance, multicast observers. You’ll learn more about this in the next section.
Building multicast observers
In the case that you have a large number of view controllers on-screen, which are all listening to the same events from the same reusable observer class, your app could have a large number of observer instances all listening to the exact same notifications or events.
In rare performance sensitive environments, this could be an issue. To solve this issue, you can implement more sophisticated re-usable observers by implementing the multicast pattern.
Walking through an implementation of a multicast observer is out of scope for this book. However, you can easily find many examples of multicast objects online by searching for ‘multicast delegate Swift.’ The gist is that multicast observers are instantiated once. They subscribe once to notifications or events and allow for multiple event responder delegates. This is more efficient than the previous examples you’ve seen because all the notifications or events are only processed once by one observer as opposed to having several observer instances all subscribing and processing the same notifications or events. If you take this route, make sure to keep an eye out for memory management issues.
Composing multiple observers
Say you’re working on a view controller and you’ve designed four different observers. You plan on calling startObserving and stopObserving on all four observers at the same time. Creating four observer properties in the view controller and calling these methods can be inconvenient.
There’s got to be a better way. The good news is that the Observer protocol lends itself to composition nicely. Here’s a sample implementation of an observer composition class:
class ObserverComposition: Observer {
// MARK: - Properties
let observers: [Observer]
// MARK: - Methods
init(observers: Observer...) {
self.observers = observers
}
func startObserving() {
observers.forEach {
$0.startObserving()
}
}
func stopObserving() {
observers.forEach {
$0.stopObserving()
}
}
}
Really simple, right? Notice how this implementation is itself an Observer. Also, notice how this observer does not manage any event responders. When using this pattern you need to wire the event responder to each individual observer, but not the composition. Shortly, you’ll see an example of how to create a composition and how to wire the event responders.
You can use this implementation any time a view controller needs to manage a large number of observers. This pattern only works for observers that start and stop observing at the same time.
OK. What about instantiating a composition of observers? Here’s an example:
class KooberOnboardingDependencyContainer {
// ...
func makeSignInViewController() -> SignInViewController {
// User interface element
let userInterface = SignInRootView()
// Observer elements
// 1
let statePublisher =
makeSignInViewControllerStatePublisher()
let stateObserver =
SignInViewControllerStateObserver(state: statePublisher)
let keyboardObserver = KeyboardObserver()
// 2
let composedObservers =
ObserverComposition(stateObserver, keyboardObserver)
// 3
let signInViewController =
SignInViewController(
userInterface: userInterface,
observer: composedObservers
)
// Wire responders
userInterface.ixResponder = signInViewController
// 4
stateObserver.eventResponder = signInViewController
keyboardObserver.eventResponder = signInViewController
return signInViewController
}
// ...
}
Walking through the code step by step:
- The observers are created.
- The observers are packaged into a composition.
- The composition is injected into the view controller.
- The individual observers are given the
signInViewControlleras an event responder.
This really simplifies things for the view controller since there’s now only one Observer to manage. The view controller has no idea the observer its given is a composition. All the view controller knows is that it, the view controller, needs to conform to multiple event responder protocols. So that’s observer composition. Next is a slight twist on wiring event responders to observers.
Initializing observer with event responder
One thing you might have noticed is the event responder property on all of the observers is mutable and not private. If, in your code, you’re following the dependency container factory method patterns shown in the examples, this isn’t a huge problem because view controllers don’t have access to observer’s event responder properties. However, you don’t have to use the dependency container pattern in order to use this Observer pattern.
So, if you find yourself in this situation and are worried about the event responder being changed unexpectedly, here’s a different approach that you might like better:
class KeyboardObserver: Observer {
// MARK: - Properties
private weak var eventResponder:
KeyboardObserverEventResponder?
private var isObserving = false
// MARK: - Methods
init(eventResponder: KeyboardObserverEventResponder) {
self.eventResponder = eventResponder
}
func startObserving() {
if isObserving {
return
}
// ...
isObserving = true
}
func stopObserving() {
// ...
isObserving = false
}
// ...
}
Like all software engineering decisions, there’s a tradeoff to this approach. This approach guarantees that the event responder cannot be changed by another object. That’s the benefit.
On the flip side, the view controller becomes a bit more complicated and messy. That’s because you need to give the observer’s initializer an event responder. The event responder, in most cases, is the view controller. It’s a Catch-22 because the view controller’s initializer wants the observer. You can’t create the observer without the view controller. The only way around this is to remove the observer parameter from the view controller’s initializer and to make the view controller’s observer property mutable and optional:
class SignInViewController: NiblessViewController {
// MARK: - Properties
let userInterface: SignInUserInterfaceView
var observer: Observer? // < Look here.
// MARK: - Methods
init(userInterface: SignInUserInterfaceView) {
self.userInterface = userInterface
super.init()
}
override func loadView() {
view = userInterface
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
observer?.startObserving()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
observer?.stopObserving()
}
// ...
}
Because this adds a bit of complexity, I tend to prefer allowing the event responder to be mutable in observers while not allowing view controllers to know the concrete Observer type, so that the view controller can’t change the observer’s event responder. The best thing to do is to try out both variations and see which one works best for your codebase.
That wraps up all the Observer variations and advanced usages. You’re now ready to go into your codebase and try some of these techniques out. Keep reading if you want to understand the benefits of this pattern and to learn how Josh and I ended up using this pattern.
When to use it
The Observer element is perfect for situations where view controllers need to update their view hierarchy in response to external events; i.e., events not emitted by the view controller’s own view hierarchy. If you’re taking a unidirectional approach, all of your view controllers probably need an observer to listen for view state changes.
This is true even if your view controllers are simply observing a Core Data query to update their user interfaces.
Note: If you find yourself performing side effects, such as networking or persistence, in your event responder methods, consider moving the side effect triggering logic outside your content view controllers and into higher level objects such as a container view controllers or any application scoped object. Performing side effects in event responder methods is typically an indication that view controllers are performing work that they don’t need to be responsible for.
Why use this element?
Observers help keep your view controllers small and light. They remove a lot of technology-specific boilerplate from your view controllers. This ends up making your view controllers much easier to read and reason about. Using observers, any developer can read a view controller without having to know specifics of NotificationCenter, Combine, ReSwift store subscriptions, etc. Anyone reading a view controller can clearly and obviously see what all external events come into the view controller by inspecting the event responder methods.
Additionally, the Observer element allows you to refactor where signals are coming from without having to change view controller code.
The Observer element helps teams parallelize work by allowing one person to work on the observation logic while the other person works on the view controller response to events.
Not only that, Observers make your view controllers easier to unit test. Your tests can simply make direct method calls to the event responder methods implemented by the view controller without having to go through NotificationCenter, Combine, RxSwift, etc.
Your tests can do this by either injecting a fake Observer implementation and passing calls to the view controller through the fake observer, or, by injecting a no-op Observer and calling view controller methods directly.
The Observer element is a nice and easy pattern to apply. It helps clean your code without needing to read another book or know any advanced techniques. Give it a try and let us know how it goes.
Origin
The observer element isn’t a new idea. It’s one of the patterns explained in the famous 1994 Gang of Four book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides.
Josh and I started using this pattern back when Objective-C was the only iOS language and back when you had to make sure you unsubscribed your NotificationCenter notifications before view controllers were deallocated.
Every one of our teammates would be super nervous every time we added a new notification subscription into a view controller because we could easily crash the app if we forgot to unsubscribe.
So we thought, why not place all this logic in another class so that the view controller only needs to call unsubscribe once and so that we could easily unsubscribe all pertinent events? We also needed observers for listening to changes in our data model for rendering updates to our views. We were building a collection view for a chat app that was wired to a realtime network socket. We needed an object between the view controller and the network, to manage back pressure.
Before building an observer, we were overloading UICollectionView with too many animations. Building an observer helped us control when data changes were sent to the collection view.
After implementing a couple of view controller specific observers, we quickly realized all the other benefits associated with using observers. And so, it became a part of Elements early on.
So that’s the Observer element. It can be used by itself or in conjunction with any other element. Next, you’ll read all about the UseCase element and how use cases can also help keep view controllers stay nice and light.
Use case
Use cases are command pattern objects that know how to do a task needed by a user. Use cases know:
- What objects are needed to perform each step in a user task.
- What steps are needed to complete a user task.
- How to coordinate amongst object dependencies to complete a user task.
- How to manage asynchronous nature of I/O steps in a user task.
Use cases encapsulate all the object dependencies and all the orchestration amongst object dependencies. For example, a use case knows what objects are needed to perform networking and persistence tasks for a specific user task, such as liking a post, signing in, navigating to a screen, etc.
Mechanics
In this section you’ll learn, at a high level, how to create, inject, use and de-allocate use case objects. This section is pure theory. If it’s a bit fuzzy, don’t worry, you’ll walk through many different code examples further ahead. The theory will help you hit the ground running when reading through the code examples.
Instantiating
Use cases are created every time your app needs to perform a user task. For instance, a Twitter app would create a new LikeTweetUseCase instance every time a user taps on a tweet’s Like button. Use cases are usually created and started by view controllers in response to a user’s interaction with the UI. However, you can create and start a use cases in response to any system event as well; i.e., use cases aren’t just for responding to UI events.
In the simplest usage, use cases can be created with four different kinds of objects:
-
Input data: Input data is data needed to perform the user task implemented by a use case. For example, if a use case signs in users, the use case would be created with a username object and a password object. In the previous Twitter example, the
LikeTweetUseCasewould be created with the ID of the tweet liked by the user. -
Side-effect subsystem objects: These objects perform some sort of I/O such as networking or persistence. Side-effect objects allow use cases to change state in the outside world; i.e., outside the use case and outside of the object starting the use case.
-
Pure business logic objects: Within a use case, you might need to do some pure business logic such as user input validation. These pure business logic objects perform deterministic tasks that do not change outside state.
-
Progress closures: The pattern behind the simplest usage of use cases is an imperative, bi-directional, approach. In this usage, the object starting a use case, usually a view controller, might want to know when the use case starts, when progress is made, when the use case completes its task and/or whether the task was completed successfully. You can design use case initializers to take closures that can be called to signal use case start, progress and completion.
Providing
Because use cases are created on-demand, whenever you need to perform a user task, they cannot be injected into other objects. Say you’re building a view controller for a settings screen. The view controller needs to be able to create a new use case every time a user toggles a setting. So the view controller can’t be injected with a single use case instance, because the view controller might need to create more than one instance.
The solution could be as easy as letting view controllers call use case initializers to create new use case instances. There’s a problem though. Use case initializers need side-effect subsystem objects that view controllers might not have.
One easy solution is to inject these side-effect subsystem objects into view controllers. That way, view controllers can pass those objects into use case initializers. This works, but in practice this solution bloats view controller initializers. If a view controller needs to be able to create upwards of three use cases, the view controller’s initializer now needs to have parameters for all the different dependencies needed by all of the use cases.
In reality, the view controller doesn’t depend on these objects. The use cases depend on these objects. Rather than inject the dependencies into a view controller you can inject view controllers with use case factories. That’s next.
A use case factory knows how to create a type of use case. You inject a factory into any object that needs to instantiate a use case. You inject one factory for each type of use case needed to be created.
A factory can either be a closure or an object. To create a use case with a factory, you invoke the closure or a method with the input data and progress closures needed by the use case. Basically, you invoke the factory with everything except the use case’s object dependencies, such as side-effect subsystem objects and pure business logic objects.
You can inject view controllers with use case factories. Then, view controllers can invoke the factory whenever they need to start a user task. This approach solves the problems with injecting view controllers with all the use cases’ dependencies.
If you’re using use cases and following the dependency injection pattern from Chapter 4, “Objects & Their Dependencies,” you might already have all the use case factories you need. In the example section below, you’ll see how to use dependency injection containers as use case factories.
Using
Use cases are super easy to use. Once you’ve created a use case, you just need to start it. It’s similar to how you create and resume URLSessionDataTasks.
If you provide progress closures, use cases will call the progress closures during execution. So, if you need to for example start an activity indicator when a use case starts, you can place the activity indicator start logic inside an onStart closure that you provide to a use case factory.
Tearing down
This part is a bit more complicated. Ideally, use cases are created when needed and deallocated when completed. The easiest way to accomplish this is to have view controllers, or whatever objects are starting use cases, hold each use case instance in an optional stored property. When a use case finishes, the view controller can nil out the property.
In the code examples below, you’ll notice that use cases are not held by a view controller. The use cases are created and then started. It looks like ARC should deallocate the use cases.
However, the use cases remain in memory until they finish running. The use cases remain allocated because the use case examples are held in memory by promises. The use cases are implemented using PromiseKit promises.
This is important because you can use whatever asynchrony technology you prefer, such as completion closures, Combine Futures, RxSwift Singles, etc., to coordinate work inside a use case. So, the technology you use might require a different approach to managing the object lifetimes of use cases.
Types
It’s time to transition from theory to code. To get started, this section covers the main types you’ll declare in order to build, create and use use case objects.
Use case protocol
Use cases are represented by a very simple protocol:
protocol UseCase {
func start()
}
The protocol has a single start method. start starts all the work to be done by the use case.
You might be wondering why you would need a protocol for just a single method. The use case protocol comes in handy when writing unit tests. The protocol allows you to swap out a real use case implementation with a fake implementation while running unit tests. The protocol also allows you to hide concrete use case types from view controllers, or from any other objects needing to start use cases.
Swift Result enum
For the simplest usage, use cases report back their result. Result is a great way to report success or failure in a clean manner. Result is included starting with Swift 5:
// A value that represents either a success or a failure, including an
// associated value in each case.
public enum Result<Success, Failure> where Failure : Error {
// A success, storing a `Success` value.
case success(Success)
// A failure, storing a `Failure` value.
case failure(Failure)
// ...
}
Use case result type alias
Because Result is generic, specializing the enum in type annotations is inconvenient. Especially in closure types because the type signature becomes really long. typealiases, like the following one, help keep lines of code short:
typealias SignInUseCaseResult = Result<UserSession,
ErrorMessage>
To follow this pattern, declare a UseCaseResult typealias for each use case class. The one above is used by SignInUseCase in the example you’ll see next.
Use case classes
This is a skeleton of an example use case implementation used to sign users in to Koober:
// 1
class SignInUseCase: UseCase {
// MARK: - Properties
// 2
// Input data
let username: String
let password: Secret
// 3
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
let dataStore: UserSessionDataStore
// 4
// Progress closures
let onStart: () -> Void
let onComplete: (SignInUseCaseResult) -> Void
// MARK: - Methods
// 5
init(
username: String,
password: String,
remoteAPI: AuthRemoteAPI,
dataStore: UserSessionDataStore,
onStart: (() -> Void)? = nil,
onComplete: ((SignInUseCaseResult) -> Void)? = nil
) {
// Input data
self.username = username
self.password = password
// Side-effect subsystems
self.remoteAPI = remoteAPI
self.dataStore = dataStore
// Progress closures
self.onStart = onStart ?? {}
self.onComplete = onComplete ?? { result in }
}
// 6
func start() {
assert(Thread.isMainThread)
onStart()
// Do some work and call onComplete when finished.
// ...
}
}
Here are the different parts to the implementation above:
- Use cases are classes. They should use reference semantics because each instance represents a specific run of the use case. Also, use case classes should conform to the
UseCaseprotocol. - These stored properties hold the input data provided in the use case’s initializer.
- These stored properties hold side-effect subsystem objects. It’s a good practice to type annotate these kinds of objects using protocol types. You generally don’t want to have to change a use case’s implementation as a result of a side-effect object implementation change, such as a networking stack change.
- Here are the progress closures the use case uses to report progress. In this example, the use case can run a closure when the use case starts and when the use case completes. If you have a long running use case, you can add another closure for reporting on-going progress. Notice how the
onCompleteclosure uses theSignInUseCaseResulttypealiasfrom before. - The initializer has parameters for all the data and objects needed to run the use case. As described before, use case initializers are best called by use case factories. Also worth noting is the optional types on the progress closure parameters. These are optional as a convenience to the object starting the use case. Sometimes, these objects don’t need to do any work in a progress closure. So it’s nice not to require them.
- This is an implementation of the
startmethod from theUseCaseprotocol. You’ll see the full implementation of this method soon. This code example is meant to illustrate the anatomy of a use case. Beforestartbegins any work it performs a threading check and then calls theonStartclosure. Use cases, like this one, can be required to be used from the main thread in order to simplify the threading model. Don’t worry, all the work that the use case does is off the main thread. This example uses the main thread simply as a coordination queue for passing data into and out of side-effect subsystems. More on the threading model later.
Each use case should represent some piece of work that a user could explain. You should be able to design a use case for every user story in your product backlog. It’s very tempting to create use cases for very small technical type of tasks.
In practice, those small technical tasks are best modeled through compose-able asynchronous methods. When you keep your use cases focused on user tasks, use cases become very easy to reason about and very easy to talk about with other people.
Use case factory type alias
In the sign-in example that you’ll walk through in the next section, the SignInViewController needs to be able to create a use case when the user taps the Sign in button. SignInViewController creates a use case using a use case factory closure. The closure’s type is too long to use inline. This use case factory typealias solves the closure type length problem:
typealias SignInUseCaseFactory =
(
String,
Secret,
@escaping () -> Void,
@escaping (SignInUseCaseResult) -> Void
) -> UseCase
Notice how the factory closure has parameters for everything the use case needs except for side-effect subsystem object dependencies. This makes it much easier for SignInViewController to create a new use case because SignInViewController doesn’t need to know how to get a reference to the side-effect subsystem object dependencies.
The only thing about this that isn’t great is the fact that the closure parameters can’t have labels. It’s not obvious what object should go in which closure parameter. You’ll see an alternative that solves this problem in the variations and advanced usage section.
Note: The
UseCaseprotocol is the return type in the factory signature as opposed to the concreteSignInUseCaseclass type. This is so the object that starts theSignInUseCasedoesn’t have access to anything other than thestart()method. This lets you refactor use cases without needing to worry about breaking other code. ReturningUseCasealso lets you inject a fake implementation during unit testing, if necessary.
Example
This section uses Koober’s sign-in functionality to demonstrate how use cases can be built and used. Koober’s SignInViewController needs a use case that’s capable of trying to sign a user into Koober with a username and password. In Koober, SignInUseCase implements the logic needed by SignInViewController. When a user taps the Sign In button on the sign-in screen, SignInViewController starts a SignInUseCase.
Note: To ease readability, some of the example code in this section has been simplified from the code in the example Xcode project.
As you’ve seen before, to report use case completion using a Result, you can declare a use case result typealias for each use case. This helps shorten closure type signatures. Here’s SignInUseCase’s SignInUseCaseResult typealias:
typealias SignInUseCaseResult = Result<UserSession,
ErrorMessage>
This is the exact same typealias from before. And here’s the complete implementation of SignInUseCase:
class SignInUseCase: UseCase {
// MARK: - Properties
// Input data
let username: String
let password: Secret
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
let dataStore: UserSessionDataStore
// Progress closures
let onStart: () -> Void
let onComplete: (SignInUseCaseResult) -> Void
// MARK: - Methods
init(
username: String,
password: String,
remoteAPI: AuthRemoteAPI,
dataStore: UserSessionDataStore,
onStart: (() -> Void)? = nil,
onComplete: ((SignInUseCaseResult) -> Void)? = nil
) {
// Input data
self.username = username
self.password = password
// Side-effect subsystems
self.remoteAPI = remoteAPI
self.dataStore = dataStore
// Progress closures
self.onStart = onStart ?? {}
self.onComplete = onComplete ?? { result in }
}
public func start() {
assert(Thread.isMainThread)
onStart()
// 1
firstly {
// 2
self.remoteAPI.signIn(username: username,
password: password)
}.then { userSession in
// 3
self.dataStore.save(userSession: userSession)
}.done { userSession in
// 4
self.onComplete(.success(userSession))
}.catch { error in
// 5
let errorMessage =
ErrorMessage(title: "Sign In Failed",
message: """
Could not sign in.
Please try again.
""")
self.onComplete(.failure(errorMessage))
}
}
}
This implementation uses PromiseKit promises to coordinate asynchronous work. To coordinate work inside use cases, you can use whatever async technology you prefer. I like using promises inside use cases because promise chains are really easy to follow and because the default promise threading behavior works great for use cases.
Here’s a step-by-step explanation of the promise chain above:
-
firstlymarks the beginning of a promise chain.firstlyis completely optional. It exists to line up the code nicely, so that the chain is easy to read. Thefirstlyclosure is expected to return aPromise. - Going to the cloud is the first async I/O task. In this step, Koober calls its remote API to check if the username and password provided by the user are valid credentials. If the credentials are good, the remote API responds with an auth token. The auth token gets bundled into a
UserSessionobject. ThesignInmethod returns aPromise<UserSession>. ThesignInmethod is called from the main queue. The real implementation ofsignInis expected to perform networking work asynchronously, off the main queue. If this operation fails,signInreturns a rejected promise and the promise chain short circuits to thecatchclosure. - If
signIncompletes successfully, execution returns to the main queue. The nextthenclosure is run. In this step, theUserSessionreturned from theremoteAPIis persisted into the user sessiondataStore. Because this step also performs I/O work, the API to save the user session is asynchronous; i.e., the method returns a promise. Just like theremoteAPI, thedataStoreis expected to do its work on another queue.
The promise returned by the dataStore carries over the UserSession from the remoteAPI so the promise chain can continue to thread the result all the way to the last promise chain step. If this step fails, the promise chain jumps to the catch closure.
4. If all goes well, the done closure is called. In this last step, the use case’s onComplete closure is called with a successful Result carrying the UserSession object. This completes the promise chain execution, and therefore, completes the use case execution. At this point, the promise chain releases the reference to self; i.e., the use case. ARC then de-allocates this use case object.
5. If anything goes wrong, the catch closure is called. In this step, an error is created and the use case’s onComplete closure is called with a failed Result carrying the error. This completes the promise chain execution. The use case will then be de-allocated by ARC.
Note: The promise chain closures capture a strong reference to
self; i.e., the use case object. You might be wondering if there’s a retain cycle, here. The promise chain holds onto the use case. The use case is not held by any other object. The promise chain is also not held by any other object. So it’s safe to capture a strong reference toself. This strong reference is what keeps the use case alive while the use case runs. If the reference wasweak, the use case would have to be held by another object, like the view controller, in order to stay allocated.
Notice how this use case doesn’t know how to do anything specific. It delegates all work to other objects. This is by design. To be effective, use cases should be lightweight objects that coordinate work amongst different abstractions. This allows you to re-use individual promise chain steps in other use cases.
Once you build several use cases in your own projects, you might be tempted to compose or chain use cases together. In theory this sounds great, but in practice it adds unnecessary complexity. Instead of trying to chain use cases together, identify the steps that need to be used in multiple use cases. Compose those steps into a single method call and then call this method from multiple use cases.
Regardless of the async technology you chose to use to coordinate work, you can follow the same threading pattern used inside SignInUseCase. The idea is to use a serial queue to coordinate async work. start() should begin by creating a serial queue. Then start() starts its first async task from the serial queue. The task runs on another queue. The result of the async task is returned back on the serial queue. Once back on the serial queue, the result from the async task is given to the next async task. So on and so forth, until all the work is finished.
SignInUseCase uses the main queue as its serial synchronization queue. This is OK because the coordination work is not CPU intensive. It’s not likely to stall the main thread. However, you can use whatever serial queue to coordinate work inside a use case. Having a standard threading pattern, like the one used here, removes a lot of the complexity surrounding asynchrony. It also makes everyone’s code much easier to reason about. This pattern might not work for every single-use case, however, it should work for the majority of use cases that are normally needed by cloud-connected mobile apps.
So that’s the use case implementation. Now, it’s time to walk through the code needed to create instances of this use case.
The use case factory typealias is the first place to look:
typealias SignInUseCaseFactory =
(
String, // username
Secret, // password
@escaping () -> Void, // onStart
@escaping (SignInUseCaseResult) -> Void // onComplete
) -> UseCase
This is the exact same typealias you saw before. You’ll need to define one of these for every use case you build. This typalias is completely optional. The typealias simply helps shorten type signatures.
Alright, it’s time for the fun part. How do view controllers create instances of use cases without calling use case initializers?
Remember, the use case initializer has parameters for side-effect subsystem object dependencies that the view controller, or whatever object needs to start a use case, really shouldn’t need to have. Because each instance of a use case represents one invocation of the user’s task, you might need to instantiate multiple instances of the use case. For example, in the sign-in screen, say the user enters their username and password incorrectly.
When the user taps the Sign in button, a new SignInUseCase should be created. The use case fails and the error is reported to the user. The user corrects a typo and taps the Sign in button again. A new SignInUseCase should be created. For this reason, you can’t simply inject a single use case object. So in order to create a use case, an object needs to be injected with a factory that the object can use to create new instances of use cases.
With that in mind, here are the relevant parts of SignInViewController:
class SignInViewController: NiblessViewController {
// MARK: - Properties
// 1
let makeSignInUseCase: SignInUseCaseFactory
let userInterface: SignInUserInterfaceView
// MARK: - Methods
// 2
init(
userInterface: SignInUserInterfaceView,
signInUseCaseFactory: @escaping SignInUseCaseFactory
) {
self.userInterface = userInterface
self.makeSignInUseCase = signInUseCaseFactory
super.init()
}
public override func loadView() {
view = userInterface
}
// ...
}
extension SignInViewController: SignInIxResponder {
// 3
func signIn(email: String, password: Secret) {
// 4
let onStart = {
// Update UI to indicate use case has started,
// such as starting an activity indicator.
// ...
}
let onComplete: (SignInUseCaseResult) -> Void = { result in
// Process result from running use case by
// for example, stopping activity indicator
// and presenting error if necessary.
// ...
}
// 5
let useCase = makeSignInUseCase(email,
password,
onStart,
onComplete)
// 6
useCase.start()
}
// ...
}
Here are the steps used by SignInViewController to create and use SignInUseCases:
- This stored property holds onto the use case factory closure. This closure is injected into the view controller through the view controller’s initializer. Here’s where the use case factory
typealiascomes in handy. Without thetypealiasthis declaration would look like:let makeSignInUseCase: (String, Secret, @escaping () -> Void, @escaping (SignInUseCaseResult) -> Void) -> UseCase. - Here’s the view controller’s initializer. The use case factory closure is provided to the view controller, here.
- This is the method called by the UI when the user taps the Sign in button. This is where a new
SignInUseCaseneeds to be created and started. - The first step inside
signInis to create the progress closures. - Then, the view controller uses the use case factory closure to create a new
SignInUseCaseusing the username and password entered by the user along with the progress closures created in the last step. - Finally, the view controller starts the use case. When the use case finishes, the use case calls the
onCompleteclosure created previously. The view controller can use theonCompleteclosure to know when the use case finishes and to know whether the use case run was successful or not.
This removes a ton of complexity from SignInViewController. If SignInViewController were to have more user interactions to process, the overall complexity of SignInViewController would be spread out to various use cases. In other words, all the complexity would be broken down into several use case objects as opposed to having moved all of the view controller’s complexity into a single object, such as a view model. The great thing about use cases is that you can use them in practically any architecture pattern. For instance, you could create and start use cases inside MVVM view models.
The last thing to look at is how KooberOnboardingDependencyContainer injects the sign-in use case factory closure into a SignInViewController:
class KooberOnboardingDependencyContainer {
// ...
// 1
func makeSignInUseCase(
username: String,
password: Secret,
onStart: @escaping () -> Void,
onComplete: @escaping (SignInUseCaseResult) -> Void
) -> UseCase {
// 2
let authRemoteAPI = self.makeAuthRemoteAPI()
let userSessionDataStore =
self.userSessionDataStore
// 3
let useCase = SignInUseCase(
username: username,
password: password,
remoteAPI: authRemoteAPI,
dataStore: userSessionDataStore,
onStart: onStart,
onComplete: onComplete)
// 4
return useCase
}
// 5
func makeSignInViewController() -> SignInViewController {
// User interface element
let userInterface = SignInRootView()
// Use case element
// 6
let signInUseCaseFactory = self.makeSignInUseCase
// 7
let signInViewController =
SignInViewController(
userInterface: userInterface,
stateObserver: stateObserver,
keyboardObserver: keyboardObserver,
signInUseCaseFactory: signInUseCaseFactory)
// Wire responders
userInterface.ixResponder = signInViewController
return signInViewController
}
// ...
}
There are two main pieces to this. One is the factory method that knows how to create a new SignInUseCase using the use case’s initializer. The second piece is the SignInViewController factory that injects the first method as the sign-in use case factory into a new SignInViewController.
Here are the details, step by step:
- This is the
SignInUseCasefactory method inside the dependency container. It accepts all the objects needed by the use case except for the side-effect subsystem objects. The side-effect subsystem objects are available inside the dependency container. - This is how the factory creates or gets a hold of the side-effect subsystem objects needed by the sign-in use case. The factory uses the dependency container to create a new
authRemoteAPI. Then the factory grabs the shareduserSessionDataStoreheld by the dependency container. - Then, the factory uses the arguments passed in alongside the side-effect subsystem objects from the dependency container in order to call the use case’s initializer to instantiate a new use case.
- Finally, the factory returns a new sign-in use case.
- This is the
SignInViewControllerfactory used to create a new view controller when a user navigates to the sign-in screen. - This step gets a reference to the sign in use case factory method. Note that this is a reference to a method, not an object. Remember how the sign-in use case factory parameter type from
SignInViewController‘s initializer is a closure type? The closure type represented by theSignInUseCaseFactorytypealias. Even though the parameter is a closure type, a method reference can be passed in as an argument as long as the method’s signature matches the closure’s signature. - In this step, the dependency container’s sign-in use case factory method,
makeSignInUseCase, is injected into a newSignInViewController. The use case factory method is injected so that the view controller can invoke this method whenever the view controller needs to create a new use case. With this approach, the view controller can create a sign-in use case without needing to know how to create anauthRemoteAPIand how to get a hold of a shareduserSessionDataStore. Cool!
OK, so that’s how you design, build, create and use use cases. Now that you know the basics you can take a look at the next section to see if there’s any variation of this pattern that you’d like to try.
Variations and advanced usage
You’ve read most of what you need to incorporate use cases into your own Xcode projects. However, there are some subtle variations that you might prefer to use. This section walks through using protocols instead of closure types for use case factories, designing unidirectional use cases and designing cancelable use cases.
Using use case factory protocols instead of closures
One of the big drawbacks with the use case factory closure type is that the parameters aren’t labeled:
typealias SignInUseCaseFactory =
(
String, // username
Secret, // password
@escaping () -> Void, // onStart
@escaping (SignInUseCaseResult) -> Void // onComplete
) -> UseCase
Comments are needed to indicate what should go in each parameter. Instead of using a closure type, you can declare a use case factory protocol. This is a bit more work and adds more types to your codebase so you might not like this approach. It really comes down to preference. I like this approach because it makes it easier for someone else to create the use cases you’ve designed. At the factory call site, other developers may not know all the parameters needed by the closure type.
Here’s what a use case factory protocol looks like:
protocol SignInUseCaseFactory {
func makeSignInUseCase(
username: String,
password: Secret,
onStart: @escaping () -> Void,
onComplete: @escaping (SignInUseCaseResult) -> Void
) -> UseCase
}
The protocol is a simple single factory method protocol. The factory method signature is exactly the same as the closure’s signature in the typealias. The only difference is that the parameters are labeled.
How does this change the view controller? Not much. Take a look:
class SignInViewController: NiblessViewController {
// MARK: - Properties
let signInUseCaseFactory: SignInUseCaseFactory
let userInterface: SignInUserInterfaceView
// MARK: - Methods
init(
userInterface: SignInUserInterfaceView,
signInUseCaseFactory: SignInUseCaseFactory
) {
self.userInterface = userInterface
self.signInUseCaseFactory = signInUseCaseFactory
super.init()
}
override func loadView() {
view = userInterface
}
// ...
}
extension SignInViewController: SignInIxResponder {
func signIn(email: String, password: Secret) {
let onStart = {
// Update UI to indicate use case has started,
// such as starting an activity indicator.
}
let onComplete: (SignInUseCaseResult) -> Void = { result in
// Process result from running use case by
// for example, stopping activity indicator
// and presenting error if necessary.
}
let useCase =
signInUseCaseFactory.makeSignInUseCase(
username: email,
password: password,
onStart: onStart,
onComplete: onComplete
)
useCase.start()
}
}
// ...
The only difference is that the view controller calls a method on the factory as opposed to just invoking the factory itself. Notice how in this version of the view controller, the arguments to the factory method are labeled. This is much easier to write. It’s a bit more verbose to read though. That’s one of the tradeoffs.
Instead of the factory closure typealias, this example uses a protocol. So, what object conforms to this factory protocol? Here’s the dependency container:
class KooberOnboardingDependencyContainer {
// ...
func makeSignInUseCase(
username: String,
password: Secret,
onStart: @escaping () -> Void,
onComplete: @escaping (SignInUseCaseResult) -> Void
) -> UseCase {
// Factory method implementation.
// ...
}
// ...
}
If you cross reference the protocol with this code above, you’ll notice that makeSignInUseCase matches the protocol method exactly. KooberOnboardingDependencyContainer already conforms to the factory protocol. Easy!
The only thing that is needed is a protocol conformance declaration:
extension KooberOnboardingDependencyContainer:
SignInUseCaseFactory {}
With the conformance declared, the makeSignInViewController factory method can inject the KooberOnboardingDependencyContainer into a new SignInViewController as a SignInUseCaseFactory:
class KooberOnboardingDependencyContainer {
// ...
func makeSignInViewController() -> SignInViewController {
// User interface element
let userInterface = SignInRootView()
let signInViewController =
SignInViewController(
userInterface: userInterface,
signInUseCaseFactory: self // < Look here.
)
// Wire responders
userInterface.ixResponder = signInViewController
return signInViewController
}
// ...
}
The main difference in this code, compared to the previous example, is that the dependency container itself is injected into the view controller as opposed to injecting the dependency container’s makeSignInUseCase method.
Both the typealias and protocol approach do the exact same thing. Try both of them out and see what feels best.
Providing use case completion closure on start
In the main example, the sign-in use case’s onComplete closure was provided to the use case during initialization of the use case. You might have thought that looked a bit strange.
Instead, why not provide the completion closure in the use case’s start method?
This does look nicer:
class SignInViewController: NiblessViewController {
// ...
}
extension SignInViewController: SignInIxResponder {
func signIn(email: String, password: Secret) {
let useCase = makeSignInUseCase(email,
password,
onStart,
onComplete)
useCase.start() { result in
// Process result from running use case by
// for example, stopping activity indicator
// and presenting error if necessary.
// ...
}
}
// ...
}
In order to take this approach, you’ll need a different UseCase protocol:
protocol UseCase {
associatedtype Success
associatedtype Failure: Error
func start(
onComplete: (Result<Success, Failure>) -> Void)
}
Yikes! Now, you have to deal with the infamous associatedtype. The associated types are needed because the Result type is generic. Each use case implementation can have different Success and Failure types. Because this version of the UseCase protocol has associated type requirements, the code below does not compile:
class KooberOnboardingDependencyContainer {
// ...
// ! Does not compile. Compiler error:
// Protocol 'UseCase' can only be used as a generic constraint
// because it has Self or associated type requirements
func makeSignInUseCase(
username: String,
password: Secret,
onStart: @escaping () -> Void,
onComplete: @escaping (SignInUseCaseResult) -> Void
) -> UseCase { // < The problem is here, with the return type.
// ...
}
// ...
}
It’s not impossible to take this approach. You’ll need to implement a type erased AnyUseCase to be able to type things as any kind of use case. This adds a whole lot of complexity with not a lot in return. Walking through a type erased AnyUseCase type is beyond the scope of this book. If you’d like to learn more, search for ‘Swift associatedtype type erasure.’
Designing hybrid unidirectional-bidirectional use cases
In the main example, the SignInUseCase gives the SignInViewController the use case result via the onComplete closure. What if another object also needs to know the result? The SignInViewController could start communicating with other objects by passing the result around. However, this isn’t great because object data flow becomes very hard to follow.
This approach of passing objects around can also result in inconsistent state. Because of this, it’s common for iOS view controllers to listen for data changes in database(s).
If your view controllers are listening to database changes you might prefer to design your use cases like this:
typealias SignInUseCaseResult = Result<Void,
ErrorMessage>
class SignInUseCase: UseCase {
// MARK: - Properties
// Input data
let username: String
let password: Secret
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
let dataStore: UserSessionDataStore
// Progress closures
let onStart: () -> Void
let onComplete: (SignInUseCaseResult) -> Void
// MARK: - Methods
init(
username: String,
password: String,
remoteAPI: AuthRemoteAPI,
dataStore: UserSessionDataStore,
onStart: (() -> Void)? = nil,
onComplete: ((SignInUseCaseResult) -> Void)? = nil
) {
// Input data
self.username = username
self.password = password
// Side-effect subsystems
self.remoteAPI = remoteAPI
self.dataStore = dataStore
// Progress closures
self.onStart = onStart ?? {}
self.onComplete = onComplete ?? { result in }
}
func start() {
assert(Thread.isMainThread)
onStart()
firstly {
self.remoteAPI.signIn(username: username,
password: password)
}.then { userSession in
self.dataStore.save(userSession: userSession)
}.done { userSession in
self.onComplete(.success(())) // < Look here.
}.catch { error in
let errorMessage =
ErrorMessage(title: "Sign In Failed",
message: """
Could not sign in.
Please try again.
""")
self.onComplete(.failure(errorMessage))
}
}
}
The difference here is that the Result type no longer carries a value on success. The UserSession is saved in the dataStore. This implementation assumes that objects are listening to the dataStore to know when a user has signed in and to get access to the user’s UserSession. This isn’t purely unidirectional because the use case still returns a result to the view controller, or whatever object is starting this use case.
There’s still some form of bidirectional communication. Typically, the state representing the progress of a use case is only needed by a single view controller. In most instances, having a private back and forth between a view controller and a use case works well. Or you might be going all-in on unidirectional data flow. The next two sections demonstrate unidirectional use case examples.
Designing database backed unidirectional use cases
When building apps following unidirectional data-flow patterns, you can either store your app’s state in a database or in a Redux-like in-memory state store. This section demonstrates what use cases look like if you’re using a database to store your app state.
Here’s a unidirectional version of SignInUseCase:
class SignInUseCase: UseCase {
// MARK: - Properties
// Input data
let username: String
let password: Secret
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
let dataStore: UserSessionDataStore
// MARK: - Methods
init(
username: String,
password: String,
remoteAPI: AuthRemoteAPI,
dataStore: UserSessionDataStore
) {
// Input data
self.username = username
self.password = password
// Side-effect subsystems
self.remoteAPI = remoteAPI
self.dataStore = dataStore
}
func start() {
assert(Thread.isMainThread)
firstly {
// 1
self.dataStore.save(signingIn: true)
}.then { _ in
self.remoteAPI.signIn(username: username,
password: password)
}.done { userSession in
// 2
self.dataStore.save(userSession: userSession,
signingIn: false)
}.catch { error in
let errorMessage =
ErrorMessage(title: "Sign In Failed",
message: """
Could not sign in.
Please try again.
""")
// 3
firstly {
self.dataStore.save(signInError: errorMessage,
signingIn: false)
}.catch { error in
assertionFailure("\(error)")
}
}
}
}
The first thing to note is that all the progress closures are gone. The use case result typealias is no longer needed. Unidirectional use cases are much simpler.
The other thing to note is how there’s more database tasks in this use case:
- This first step updates the state in the database to signal that the user is signing in. A view controller might be listening to the database and using the observation in order to control an activity indicator.
- If all goes well, the user’s
UserSessionis stored in the database and the signing in state is set tofalsein the database. A navigation controller could be listening for user session changes in the database and automatically take the user out of the sign-in screen and into the app when a new user session is saved. - If something goes wrong, the error and signing in state are saved in the database. This part is a bit odd because you have to do I/O when an error occurs and because this requires a new promise chain. If there’s something wrong with the database, there’s not much you can do other than crash debug builds with an
assertionFailure. If you can recover from database errors you would place that logic in the secondcatchclosure.
The drawback here is having to deal with more asynchrony than before. Another option is to use a Redux-like state store. That example is next.
Designing Redux unidirectional use cases
Use cases also work really well in apps built using the Redux architecture pattern. Here’s another version of SignInUseCase that could be used inside Chapter 6’s example project:
Note: Check out Chapter 6, “Architecture: Redux,” if you want to follow this example and you’re not familiar with Redux.
class SignInUseCase: UseCase {
// MARK: - Properties
// Input data
let username: String
let password: Secret
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
// Redux action dispatcher
let actionDispatcher: ActionDispatcher
// MARK: - Methods
init(
username: String,
password: String,
remoteAPI: AuthRemoteAPI,
actionDispatcher: ActionDispatcher
) {
// Input data
self.username = username
self.password = password
// Side-effect subsystems
self.remoteAPI = remoteAPI
self.actionDispatcher = actionDispatcher
}
func start() {
assert(Thread.isMainThread)
// 1
let action = SignInActions.SigningIn()
actionDispatcher.dispatch(action)
firstly {
self.remoteAPI.signIn(username: username,
password: password)
}.done { userSession in
// 2
let action =
SignInActions.SignedIn(userSession: userSession)
self.actionDispatcher.dispatch(action)
}.catch { error in
let errorMessage =
ErrorMessage(title: "Sign In Failed",
message: """
Could not sign in.
Please try again.
""")
// 3
let action =
SignInActions.SignInFailed(errorMessage: errorMessage)
self.actionDispatcher.dispatch(action)
}
}
}
As in the previous unidirectional database use case example, all the progress closures are gone. The dataStore is also gone. In Chapter 6, “Architecture: Redux,” the dataStore listens to the Redux store to persist the user’s UserSession. Therefore, the dataStore isn’t needed by the use case. And finally, there’s a new dependency, the actionDispatcher. The actionDispatcher is used to dispatch Redux actions to the Redux store.
One thing you’ll notice when building use cases alongside Redux is that use cases tend to dispatch several actions. For example, in the example code above:
- An action is dispatched to signal that the app is attempting to sign in a user. The progress closures are replaced with actions that represent the progress through the use case.
- Once the user’s credentials successfully authenticate with the
remoteAPI, an action is dispatched carrying the newUserSession. - If something goes wrong, an error action is dispatched carrying the error message.
When first applying use cases to a Redux codebase, it’s tempting to design a use case for every Redux action. However, use cases are much less granular than Redux actions. Design your use cases based on the work a view controller needs to do as opposed to the state events Redux needs to update the app’s state.
This use case pattern solves one of the more difficult challenges with Redux, mixing async side-effect I/O with actions. You don’t have to deal with middleware. And even better, with this pattern, view controllers don’t even know the app is built using Redux. All the view controller knows is what kind of use case to create and run in response to what user interaction.
That wraps up all the unidirectional variations. You might have noticed that so far, none of the use cases can be cancelled. The next section demonstrates how to build cancelable use cases you can build when you’d like your users to be able to cancel an ongoing use case.
Note: The Elements version of the Koober Xcode project example that comes with this chapter uses the Redux unidirectional version of use cases. Use cases replace the
UserInteractionsobjects from the Redux version of Koober.
Designing cancelable use cases
By adding some additional types, you can take what you’ve learn so far and add cancelation to any use case. The first type to look at is the Cancelable protocol:
protocol Cancelable {
func cancel()
}
You’ll need to declare this protocol yourself since it’s not part of Swift. Just like the UseCase protocol, this protocol is very simple. It’s just a single method that can be called by, for example, a view controller to cancel on-going work. The cancel method could have just been added to UseCase, but then every single use case has to be cancelable. Many use cases shouldn’t cancelable. So instead of adding cancel to UseCase, you can declare the Cancelable protocol from above.
Next is the CancelableUseCase typealias:
typealias CancelableUseCase = Cancelable & UseCase
This typealias is a convenience for type annotating constants and variables that conform to Cancelable and that conform to UseCase. This allows a view controller to declare a use case factory, such as the one below, that returns a cancelable use case:
typealias SearchDropoffLocationsUseCaseFactory =
(
String, // query
Location // pickupLocation
) -> CancelableUseCase
Any view controller that’s injected with this factory can create, start and cancel a SearchDropoffLocationsUseCase.
Here’s SearchDropoffLocationsUseCase’s implementation:
class SearchDropoffLocationsUseCase: CancelableUseCase {
// MARK: - Properties
let query: String
let pickupLocation: Location
let actionDispatcher: ActionDispatcher
let remoteAPI: NewRideRemoteAPI
// 1
var cancelled = false
// MARK: - Methods
init(query: String,
pickupLocation: Location,
actionDispatcher: ActionDispatcher,
remoteAPI: NewRideRemoteAPI) {
self.query = query
self.pickupLocation = pickupLocation
self.actionDispatcher = actionDispatcher
self.remoteAPI = remoteAPI
}
// 2
func cancel() {
assert(Thread.isMainThread)
cancelled = true
}
func start() {
assert(Thread.isMainThread)
// 3
guard !cancelled else {
return
}
firstly {
remoteAPI.getLocationSearchResults(
query: query,
pickupLocation: pickupLocation
)
}.done { results in
// 4
guard self.cancelled == false else {
return
}
let action = ReceivedSearchResultsAction(results: results)
self.actionDispatcher.dispatch(action: action)
}.catch { error in
let errorMessage =
ErrorMessage(title: "Error Searching",
message: """
Could not run location search.
Please try again.
""")
let action =
SignedInErrorOccuredAction(errorMessage: errorMessage)
self.actionDispatcher.dispatch(action: action)
}
}
}
Here’s a walkthrough of all the additional logic added above to implement a cancelable use case:
- The use case needs this boolean stored property to hold the cancelation state. The use case is created in the not-canceled state.
- This implements the
cancelmethod from theCancelableprotocol. To avoid any issues with mutating state with concurrency, this method first checks that it’s running on the main thread. It then changes the state of the use case to canceled. This allows the rest of the use case to inspect and check whether the use case has been canceled. - One of the first things that
startdoes is abort if the use case has been canceled. This would be very rare. It could happen if the use case was created but not started right after. - Once the networking completes, the
doneclosure first checks to see if the use case has been canceled. If so, it exits early without dispatching any actions. This form of cancellation doesn’t stop any work in progress. It abandons the processing of the result. If a use case is performing a long-lived networking task, you might want to stop the networking as soon as the use case’scancelmethod is called. You can do this by storing the promise in a property and canceling the promise chain. To learn more about canceling promises, visitPromiseKit’s GitHub repo.
And that’s how you can incorporate cancelation into use cases. That takes care of demonstrating all the variations and advanced usages of use cases.
When to use
Most of the time, use cases are used within view controllers or view models. Use cases typically run as a response to a user’s interaction with your app’s UI. However, sometimes you need to do some work in response to some system event, such as a location notification. You can use use cases for these situations as well.
Why use this element?
The use case pattern is one of the most versatile patterns I’ve used in iOS app development. Use cases fit into nearly all architecture patterns. And, they come with a lot of benefits.
Breaking up your app’s main chunks of work into use cases allows you to re-use logic in any view controller. For example, say you’re building a social networking app and you’re building a LikePostUseCase for responding to a user liking a post. If you need to add the like-post button into multiple view controllers, you can easily re-use the LikePostUseCase to run the logic behind the button.
In most architecture patterns, work is organized by screen rather than by use case. The logic behind any one button then gets tied to the logic for the screen that the button is in. It’s much harder to re-use the button’s logic when, all of the sudden, you need to add the button to another screen. This situation is very common in MVC and MVVM architecture patterns. The good news is you can incorporate use cases to both patterns. If you’ve ever gone through a massive app re-design you know how valuable this flexibility can be. In addition, with use cases, you can solve the massive view controller problem without moving the problem somewhere else like a massive view model.
Breaking up your app’s main chunks of work into use cases also allows you to build some pretty cool functional tests. If you need to test a particular sequence of user actions, you can write an entire test suite without needing any UI objects. In the test suite you can instantiate and run a sequence of use cases. And because use cases are named after user tasks, these tests are super easy to read.
Use cases also come in handy when writing unit tests. Say you need to ensure that a piece of work is started in response to a specific notification. You can harness a unit test with a fake UseCase implementation that exposes a property that allows you to assert whether the start method was called. Then to test the behavior you can emit the notification and assert that the use case was started by whatever object is under test.
Also, the use case pattern is relatively simple. It’s easy to teach and it’s easy to put into practice. Incorporating use cases doesn’t require you to re-architect an entire app. You end up with a simple and effective threading strategy for most common mobile app I/O tasks. Use cases also help make dependency management easier. View controllers don’t need to get references to things like databases and networking objects.
When using use cases, you’ll find that you won’t need to change view controller code that often anymore. Usually when we are changing code, we are changing how some feature works as opposed to changing what features are in an app. For instance, if you’re changing your app to use a new cloud API or a new database, you’ll end up working mostly in use cases and side-effect subsystems.
Just like other elements, use cases allow you to parallelize development work amongst team members. If a view controller needs three use cases, a different developer can build each use case.
Last but not least, use cases help you communicate your work with all your team members across all disciplines. For example, you can create tasks, that everyone understands, in a backlog for each use case. I’ve seen this communication benefit pop up several times. Just recently, I was in a project retrospective where our product manager referenced use cases. He suggested that we could have built a first version, of whatever library we were building, by focusing on shipping one use case first. Teamwork becomes way more productive and enjoyable when everyone understands the work that’s happening.
Origin
I first came across code that looked like use cases when reading Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin. The use case pattern in Elements was inspired by the transaction pattern presented in the book’s Payroll case study.
Josh and I have evolved the pattern quite a bit since we started using it five years ago as of this writing. We first used NSOperations to run what we called Actions. While this pattern worked, it was very cumbersome. For every use case you had to implement an Action class and a NSOperation subclass. We then simplified the pattern by placing all the use case logic inside each NSOperation.
If you’d like to see this pattern, you can watch the App Architecture tutorial I gave at RWDevCon 2016. At the time, we were using NSOperation because we could chain operations together and we thought it would be handy to chain use cases together. The more we used the pattern though, the more we realized we never needed to chain use cases. NSOperation was just more complexity that we didn’t need. So in 2017, we decided to drop NSOperation and model use cases using the simple UseCase protocol you saw here.
If you’d like to see this version of the pattern, you can watch the Advanced App Architecture workshop Josh and I gave at RWDevCon 2017. In 2017 and 2018, we were learning how to build iOS apps using the Redux unidirectional pattern. We ended up evolving the pattern to its current form for use in unidirectional architectures. If you’d like to learn more about advanced unidirectional techniques using use cases you can watch my RWDevCon 2018 tutorial, Advanced Unidirectional Architecture.
The idea behind object-oriented use cases has been around for a while. To get a glimpse of the early thoughts, you can read Ivar Jacobson’s book, Object Oriented Software Engineering: A Use Case Driven Approach, published in 1992.
Pros and cons of Elements
Pros of Elements
-
You can incorporate any one of the elements without needing to refactor an entire app.
-
The individual elements are simple and intuitive. They are easy to learn, teach and practice.
-
Elements are only needed to be built if needed. You won’t have a bunch of boilerplate code. You won’t have any empty proxy classes either. For example, if a view controller doesn’t need to do any user initiated work, you don’t have to build any use cases. If a view controller doesn’t need to observe anything, you don’t need to implement an
Observerclass. -
You can easily distribute the development workload across your team. Different team members can build different elements in parallel.
-
Elements can be used alongside many other architecture patterns.
-
Elements helps you unit test a large portion of your codebase including view controllers, views, observers, etc. This is because every element is represented by a protocol. This allows you to use fake implementations of different Elements at runtime during unit tests.
Cons of Elements
- Elements makes use of many different protocols. You might feel like you’re working with too many protocols. This is especially true in the dependency container code. If this is the case, the protocols are all optional. Feel free to exclusively use concrete versions. Just know that you might lose some unit testing benefits.
- Elements breaks logic down into fairly small pieces. You can end up with lots of classes. It can be difficult to navigate an Xcode project if the files aren’t organized well.
- While most of the Elements evolved from existing ideas and techniques, Elements as a whole is new and other developers might not be familiar with the patterns. As of this writing, this book is the only source of information about Elements.
Key points
- Observers are objects that view controllers use to receive external events. You can think of these events as input signals to view controllers.
- The
Observerelement is perfect for situations where view controllers need to update their view hierarchy in response to external events; i.e., events not emitted by the view controller’s own view hierarchy. -
Observers help keep your view controllers small and light. They remove a lot of technology specific boilerplate from your view controllers. - Use cases are command pattern objects that know how to do a task needed by a user.
-
UseCases fit into nearly all architecture patterns — and they come with a lot of benefits. - Most of the time, use cases are used within view controllers or view models. Use cases typically run as a response to a user’s interaction with your app’s UI.