Advance iOS App Architecture PDF
Advance iOS App Architecture PDF
Notice of Rights
All rights reserved. No part of this book or corresponding materials (such as text,
images, or source code) may be reproduced or distributed by any means without prior
written permission of the copyright owner.
Notice of Liability
This book and all corresponding materials (such as source code) are provided on an “as
is” basis, without warranty of any kind, express of implied, including but not limited to
the warranties of merchantability, fitness for a particular purpose, and
noninfringement. In no event shall the authors or copyright holders be liable for any
claim, damages or other liability, whether in action of contract, tort or otherwise,
arising from, out of or in connection with the software or the use of other dealing in the
software.
Trademarks
All trademarks and registered trademarks appearing in this book are the property of
their own respective owners.
raywenderlich.com 2
Advanced iOS App Architecture
Dedications
"To my beautiful wife Lauren, to my fun-loving angel Zara, to my
soon-to-arrive son René Jr., to my parents who have given me
everything, and, last but not least, to my furry pals Paco and Charlie.
I love you all."
— René Cacheaux
— Josh Berlin
raywenderlich.com 3
Advanced iOS App Architecture
raywenderlich.com 4
Advanced iOS App Architecture
Darren Ferguson is the final pass editor for this book. He's an
experienced software developer and works for M.C. Dean, Inc, a
systems integration provider from North Virginia. When he's not
coding, you'll find him enjoying EPL Football, traveling as much as
possible and spending time with his wife and daughter. Find Darren
on Twitter at @darren102.
raywenderlich.com 5
Advanced iOS App Architecture
raywenderlich.com 6
Advanced iOS App Architecture
raywenderlich.com 7
Advanced iOS App Architecture
raywenderlich.com 8
Advanced iOS App Architecture
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
raywenderlich.com 9
W What You Need
• Swift 5: all projects have been written to work with Swift 5 in Xcode.
• Xcode 10.2 or later. You'll need Xcode 10.2 or later to open and run the example
apps included in this book.
If you haven't installed the latest version of macOS or Xcode, be sure to do that before
continuing with the book. The code covered in this book depends on Swift 5 and Xcode
10.2.
This book provides the building blocks for developers who wish to broaden their
horizons and learn how architectures can help them build robust and maintainable
applications and SDKs.
The only prerequisites for this book are an intermediate understanding of Swift and iOS
development. If you’ve worked through our classic beginner books — Swift Apprentice
https://store.raywenderlich.com/products/swift-apprentice and iOS Apprentice https://
store.raywenderlich.com/products/ios-apprentice — or have similar development
experience, you’re ready to read this book.
As you work through the book, you’ll be taken through a deep dive into different
architectures for a fictional app named Koober. Each chapter will explain the theory
behind each of the architectures first. The second half of the chapters will guide you
through how the Koober application utilized the architecture and show you how the
architecture was used within the application.
raywenderlich.com 10
L Book License
By purchasing Advanced iOS App Architecture, you have the following license:
• You are allowed to use and/or modify the source code in Advanced iOS App
Architecture in as many apps as you want, with no attribution required.
• You are allowed to use and/or modify all art, images and designs that are included in
Advanced iOS App Architecture in as many apps as you want, but must include this
attribution line somewhere inside your app: “Artwork/images/designs: from
Advanced iOS App Architecture, available at www.raywenderlich.com”.
• The source code included in Advanced iOS App Architecturer is for your personal use
only. You are NOT allowed to distribute or sell the source code in Advanced iOS App
Architecture without prior authorization.
• This book is for your personal use only. You are NOT allowed to sell this book
without prior authorization, or distribute it to friends, coworkers or students; they
would need to purchase their own copies.
All materials provided with this book are provided on an “as is” basis, without warranty
of any kind, express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose and noninfringement. In no event shall
the authors or copyright holders be liable for any claim, damages or other liability,
whether in an action of contract, tort or otherwise, arising from, out of or in connection
with the software or the use or other dealings in the software.
All trademarks and registered trademarks appearing in this guide are the properties of
their respective owners.
raywenderlich.com 11
B Book Source Code &
Forums
This book comes with the source code for the example apps for each chapter. These
resources are shipped with the digital edition you downloaded from
store.raywenderlich.com.
We’ve also set up an official forum for the book at forums.raywenderlich.com. This is a
great place to ask questions about the book or to submit any errors you may find.
raywenderlich.com 12
A About the Cover
Although pufferfish look like the cutest little squeezable stress balls in the sea, they’re
nothing you would ever want to cuddle up next to. Most pufferfish contain toxins to
protect themselves from predators; one pufferfish alone can contain enough toxin to
kill 30 humans — with no known antidote. James Bond knows this all too well.
But we didn’t feature the pufferfish because learning about advanced architecture is
toxic; rather, pufferfish are one of nature’s great architects. During spawning season,
using nothing but their fins, male pufferfish create stunning geometric patterns in the
sand on the seabed as potential nesting sites for females.
These structures can be up to seven feet in diameter and take the males anywhere from
seven to nine days to build. It’s not certain if the structure serves to attract the females,
or if the females are simply drawn to the soft sand the male stirred up in the process.
So when you’re getting frustrated with Xcode or Swift, just be thankful that you have
better tools to architect with than a pair of fins!
For an extra-cool view on how the male pufferfish architects his “software,” check out
this video: https://www.youtube.com/watch?v=p1PID91sEW8.
raywenderlich.com 13
1 Chapter 1: Welcome
Welcome to Advanced iOS App Architecture. The main goal of this book is to thoroughly
explain and show how to apply popular iOS app architectures, one by one. We can’t wait
for you to explore the architectures covered in the following chapters.
We absolutely love this topic. We are super passionate about architecture because
architecture unlocks the ability for teams to grow and go quickly. Now, more than ever,
it’s very important to understand and apply good software architecture practices in our
projects as apps are getting more complex and as development teams are pressured to
deliver faster results despite constantly changing requirements.
Chapter 5 through Chapter 7 are architecture chapters; in other words, they explore
one architecture at a time. Each architecture chapter begins with a little history
followed by a detailed theory walkthrough. The second half of each architecture chapter
focuses on applying the theory to iOS app development. Each architecture chapter ends
by covering the pros and cons of that architecture. Feel free to read these latter
chapters in any order.
There are many architectures not covered in this book because we wanted to go deep
instead of broad; however, we plan to add more architectures in future editions of this
book.
raywenderlich.com 14
Advanced iOS App Architecture Chapter 1: Welcome
If you’re new to Swift, check out the raywenderlich.com Swift Apprentice book; for a
refresher on design patterns, check out the raywenderlich.com Design Patterns by
Tutorials.
If you aren’t sure which architectures you’d like to explore, we recommend reading the
theory section of all the architecture chapters first in order to identify which
architectures fit your needs the most. Then you can take a deep dive by reading the iOS
app portion of the chapters you found most compelling.
Our hope is that, after reading this book, you will be able to apply different app
architectures to different projects in a way that will unleash your team’s ability to build
quickly and soundly. Happy architecting!
raywenderlich.com 15
2 Chapter 2: Which
Architecture Is Right for Me?
By René Cacheaux
You might be wondering: Which architecture pattern is right for me? Honestly, there's
no perfect universal app architecture. An architecture that works best for one project
might not work best for your project. There are many different aspects to consider
when establishing an architecture for you and your team to follow. This chapter guides
you through the process of finding the best architecture for your project.
There's a lot that goes into shaping your app's codebase into a cohesive and effective
architecture. Knowing where to start can especially be overwhelming. Every single file
in your app's codebase plays a part in your app's architecture. There's no shortage of
architecture patterns. Unfortunately, most patterns only scratch the surface and leave
you to figure out the fine details. In addition, many patterns are similar to one another
and have only minor differences here and there.
All of this makes architecture hard to put into practice. Fortunately, there are pragmatic
steps you can take to ensure your architecture is effective:
5. Draw a line in the sand and define your app's baseline architecture.
6. Look back and determine if your architecture is effectively addressing the problems
you want to solve.
raywenderlich.com 16
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Notice how selecting an architecture pattern isn't the first item on the list. The reality
is that selecting an architecture pattern is less important than understanding the the
problems you're trying to solve using architectural patterns. Taking the time to
understand the problems you want to solve allows you to focus on the few aspects of
architecture that really make a difference. While many problems will be specific to your
project, there are several general problems you can solve through good architecture.
The next several sections cover these general problems in detail.
raywenderlich.com 17
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
You can solve these problems by applying architecture concepts. All of these problems
have common root causes. Walking through some of the root causes will help set the
stage for studying each of these problems in detail.
Understanding these root causes is important when creating a plan for boosting team
velocity and strengthening code quality. So what exactly are these root causes, and how
do you know if they've made it into your codebase? That's next.
Without properly encapsulating your code, your interdependencies can run rampant!
The more you tightly couple parts of your codebase, the more likely something
unexpectedly breaks when making code changes. This is further complicated by large
teams with multiple developers because everyone needs to fully understand the
interdependencies, which on very large teams may be an impossible task.
Large types
Large types are classes, structs, protocols and enums that have long public interfaces
due to having many public methods and/or properties. They also have very long
implementations, often hundreds or even thousands of lines of code.
raywenderlich.com 18
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Adding code to an existing type is much easier than coming up with a new type. When
creating a new type, you have to think about so many things: What should the type be
responsible for? How long should instances of this type live for? Should any existing
code be moved to this type? What if this new type needs access to state held by another
type?
Designing object-oriented systems takes time. When you're under pressure to deliver a
feature making this tradeoff can be difficult. The opportunity cost is hard to see. The
thing is, many problems are caused by large types – problems that will slow you down
and problems that will affect your code's quality. You'll read about examples of these
consequences in the following sections. For now, just know that breaking large types
into smaller types is a great way to improve your codebase's architecture.
Now that you're familiar with the root causes, you're ready to dig into the problems that
can cause slow team velocity and fragile code quality.
Note: As you read the upcoming sections, keep in mind that highly
interdependent code and large types are just the common root causes. You'll see
these common root causes in almost all the problem sections below. However,
you'll also read about other, problem-specific, root causes.
With that in mind, it's time to examine the problems associated with team velocity and
code quality.
raywenderlich.com 19
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
600 line view controllers are very difficult to understand. If all you need to know is how
a button functions, fishing through 600 lines of view controller code will take a lot of
valuable time. A good architecture breaks large chunks of code into small, modular
pieces that are easy to read and understand. The more an architecture encourages
locally encapsulated behavior and state, the easier the code will be to read. Think about
the current app you're working on. If a new team member joins your team tomorrow
and needs to understand a single view controller, what percentage of the app's overall
codebase will that developer need to understand? This is a good gauge to use when
evaluating how much your architecture is helping improve your code's readability.
Unfortunately, most architecture patterns don't emphasize this point enough. The good
news is that this practice can be applied to pretty much any architecture pattern. So
this is more of a universal aspect of architecture.
How many global variables does your codebase have, and how many objects are
instantiated directly in another object?
The more your objects directly depend on each other and the more your objects depend
on global state, the less information a developer will have when reading a single file.
This makes it incredibly difficult to know how a change in a file might affect code living
in another file. This forces developers to Command-click into tons of files in order to
piece together the control flow. This takes a lot of time. Similar to class size, carefully
managing dependencies is unfortunately not emphasized enough by popular
architecture patterns. Carefully managing dependencies is a universal aspect that can
be applied to any architecture pattern. In fact, we apply this aspect to every
architecture code example that ships with this book. We also dedicated a whole chapter
to this. You can read more about managing dependencies in Chapter 4.
How differently are your view controllers implemented across your app's
codebase?
Developers, including your future self, will spend a lot of time figuring things out if
different features are implemented using different architecture patterns. Human brains
are amazing at identifying patterns. You can take advantage of this ability by ensuring
your codebase follows similar architecture patterns throughout. Having a consistent
structure drastically reduces the cognitive overhead required to understand code. In
turn, developers will feel more comfortable changing and improving older parts of an
app's codebase because they'll understand the common patterns.
raywenderlich.com 20
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Those are just a few ways in which architecture impacts code readability. Improving
your code's readability by applying architecture patterns and concepts will help you and
your team boost productivity and prevent accidental bugs from creeping into your app.
The main architectural cause for this problem is highly interdependent code. Say that
you're fixing a bug in a content view controller. This view controller manages the
display and animation of an activity indicator. The activity indicator should stop
animating when the view controller finishes loading. However, the indicator keeps
animating forever. To fix this, you to toggle the indicator off by stopping the animation.
You do so by adding code to the content view controller that turns the animation off.
The fix then gets shipped to users. Before long, you discover a new bug. The indicator
stops animating, but it starts animating again soon thereafter. As it turns out, the
indicator is a public property that is also being managed by a container view controller.
On completion of some work, the container view controller was incorrectly turning the
indicator's animation on, when it should have been turning it off...! Ultimately, the
problem here is that the control of the indicator is not encapsulated within the content
view controller. There's an interdependency between the container view controller, the
content view controller and the activity indicator.
You can't see the effects of code changes easily when you're working in a codebase
that's highly interdependent. Ideally, you should be able to easily reason about how the
current file you're editing is connected to the rest of your codebase. The best way to do
this is to limit object dependencies and to make the required dependencies obvious
and visible.
This situation really slows teams down because any time any feature is built or any bug
is fixed there's a chance for something to go wrong. If something does go wrong, it
might be all hands on deck to figure out the root cause. In a really fragile codebase, the
raywenderlich.com 21
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
change-break-fix cycle can snowball out of control. You end up spending more time
fixing issues than improving your app. Not only is this a team velocity problem, it is
also a code quality problem. The chances of shipping a bug to users is much higher
when the connections between code are hard to see and understand. Code that's hard to
understand leads to code that easily breaks when changed. All to say, a good modular
architecture can help you avoid accidentally introducing bugs when making changes.
For example, you might notice many crash reports arriving due to a race condition
associated with some mutable state. This kind of crash can take days to diagnose and
fix. Enough of these kinds of issues can really grind a team to a halt. Some architecture
patterns and concepts attempt to address these kinds of issues by designing constraints
that act as guard rails to help teams avoid the most common pitfalls. They help you stay
out of trouble. Therefore, if you find yourself working in a fairly complex environment,
try establishing architecture patterns as a means to manage complexity. The more you
can make your app behave in deterministic ways, the less likely users are to experience
strange bugs and the less time you and your team will have to spend chasing these
strange bugs.
Large types can prevent your code from being reusable. For example, a huge 2,000-line
class is unlikely to be reusable because you might only need part of the class. The part
you need might be tightly coupled with the rest of the class, making the part you need
impossible to use without the rest of the class. Types that are smaller and that have less
responsibility are more likely to be reusable.
Writing code takes more time if you can't re-use any code. If you're solving complex UI
problems that are applicable in many different ways, it makes sense to spend time to
refactor your code to be reusable. Making code reusable not only helps you build new
raywenderlich.com 22
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
things quicker, it helps you make modifications to existing behaviors. But what if you
don't need to re-use most of your code? For example, you probably don't instantiate
most of your view controllers from more than one place. This is important: Reusability
is not just about being able to re-use code. It's also about being able to move code
around when making changes to your app. The more reusable everything is, the easier it
is to shuffle code around without needing to do risky refactors.
Also, a codebase with code that's not reusable can result in code quality problems. For
instance, say you have field validation logic in several screens where users enter
information. The validation error UI logic is duplicated in each screen's view controller.
Because similar logic is duplicated, perhaps each screen displays validation errors
slightly differently, resulting in an inconsistent user experience. If someone discovers a
bug, you’ll have to find all the view controllers that show validation errors. You might
miss one instance and end up continuing to ship the same bug...! Ultimately, making
code reusable allows you to ship a consistent user experience and allows you to tweak
your app's behavior easily.
Updating the types in your code to be easily replaceable really speeds up team velocity
because it allows multiple people to work on multiple parts of a codebase at the same
time.
Ideally, a codebase has small enough units that each person on a team can write code in
a separate file while building a feature. Otherwise, you'll run into issues such as merge
conflicts that can take a long time to resolve. For example, if your app's main screen is
completely implemented in a single view controller, the developer building the UI's
layout will probably conflict with the person building the network refresh. It's amazing
raywenderlich.com 23
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
when you can meet as a team and self-organize around different aspects of building a
feature. Someone can build the layout, someone else can build the networking,
someone else can build the caching, someone else can build the validation logic and so
on and so forth. Good architecture enables you to do this.
If multiple developers are building the same feature, having small types is not enough
because the code that one developer is building might depend on unwritten code from
other developers. While developers could hardcode fake data while they wait on other
developers, they could move even faster if they agreed on APIs upfront. You can design
and write protocols for those dependencies so developers can call into systems that are
not built yet. This allows developers to write unit tests and even complete
implementations without needing to do a large integration once all systems are built.
Also, this guarantees that UI code does not depend on implementation details of
networking, caching, etc.
Back in the day, apps were built by one- to five-person teams. Today, many apps are
built by twenty or more iOS developers. Some are even built by more than a hundred
developers!
Companies who hire lots of developers are looking for ways to maximize the
productivity of large development groups by organizing developers into cross-
functional feature teams. Many folks call this the "squad model."
The squad model was popularized by Spotify. As your team and company grows, there
comes a point where coordinating among teams takes a lot of time. The more
dependent one team's code is on another team's code, the more these teams will have
to depend on each other in order to ship. Because of this dependency, developers will
start stepping on each other's toes. This is where architecture comes into the picture.
The trick is to design an app architecture that allows developers to build features in
isolation. An architecture that loosely couples each feature into separate Swift
modules. This gives each squad a container that the squad can use to build their feature
however they need. In turn, squads can ship features much faster because this kind of
architecture gives squads the autonomy they need to move fast.
To summarize, you'll be able to build features much faster if your app's architecture
allows your team to easily parallelize work by loosely coupling layers and features that
make up your codebase.
raywenderlich.com 24
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Swift language designers made a big decision when designing the Swift language. They
decided against using header files. They did this to reduce duplication and to make the
language easier to learn. Because Swift does not use header files, the Swift compiler has
to read all of the Swift files that make up a Swift module when compiling each file. This
means when you change a single file, the Swift compiler might need to parse through
all the Swift files in the module. If you have lots of Swift files in a single app target, i.e.
one Swift module, recompiling your app can take a while, even when you make a small
change. How this works in detail is out of scope for this book, but know that the Xcode
build system is getting smarter every year. Starting with Xcode 10, Xcode has the ability
to do some incremental compilation.
Despite these improvements, breaking your app into several Swift modules can speed
up your build times. This is because the Xcode build system doesn't have to recompile
modules for which Swift files have not changed. In addition, breaking your app into
multiple modules results in smaller modules, i.e., modules with fewer Swift files. This
means the Xcode build system has to do less work to build each module. Architectures
that enable multi-module apps help you to speed up build times so you can spend less
time waiting for your app to run.
raywenderlich.com 25
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Speeding up your local build times is great, but if that's not good enough, you can speed
up your build times even more by using a different build system that can use a
distributed build cache. Build systems, such as Google's Bazel, let you cache, download
and re-use Swift modules that were compiled on someone else's machine. Imagine
building a pull request branch you just pulled only to find the app's .ipa downloaded
and installed onto your simulator without any source needing to be compiled. All
because one of your co-workers already built the code found in this pull request branch.
Wouldn't that be amazing? What's better than a zero-compile-time build?
These build time benefits are only possible when you have an architecture that allows
for multi-module Swift codebases.
raywenderlich.com 26
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Here are some problems that can be solved with architecture in order to increase your
code's agility:
You can be locked into technology when your higher-level types, such as view
controllers, are tightly coupled to lower-level system implementations. This happens
when the higher-level code makes calls into implementation specific types as opposed
to making calls to protocol types. For instance, if your view controllers were making
direct calls to NSURLConnection, pre iOS 7, then you probably had to go into every view
controller and update your code to use NSURLSession. If you have a very large codebase,
you might wait until the last minute possible to migrate because of the effort involved.
This is just one of many possible ways your higher-level code can be tightly coupled to
lower-level systems.
You can also be locked into a technology when your higher-level types depend on
specific data formats. You typically need to work with data formats when
communicating with servers and when persisting information. The server
communications data format situation is the trickiest because you probably don't
control the server backend. The team that builds and maintains the backend app
servers might come knocking on your door one day asking to use a different data format
or even a different networking paradigm such as GraphQL.
raywenderlich.com 27
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Say your app servers are sending JSON today and say your view controllers are
deserializing and serializing JSON. If your server team decides to use a different format,
such as Protocol Buffers, you might need to reimplement every single view controller!
While the previous example is somewhat straight forward, data format issues can be a
bit more nuanced though. For instance, the chat app from one of my previous projects
needed to relay chat messages from chat servers to the app's UI. Chat messages were
encoded using XML. We knew not to pass XML straight to the view controllers. That
wasn't enough. The structure of the chat messages were defined by yet another
standard called XMPP. We could have easily modeled the struct that carries chat
messages in a way that mirrors the XMPP spec. We decided to model the struct based
on the appearance properties of chat messages so that our view controllers would not
be tightly coupled to the chat server technology. We didn't want to be locked into
XMPP.
These are just a few ways in which your architecture can either lock you into
technologies or give you the freedom to easily switch to new technologies.
The database is the classic example. Have you ever been in a CoreData versus Realm
discussion? A lot of the time, these discussions happen before a single line of code is
written. The problem is, these database technologies add a lot of complexity. And, if
you make this decision early on, chances are good that you’ll be locked into one of
these technologies. The thing is, you probably don't have all the information you need
to make this decision at the beginning of a project. In one of my previous projects we
decided to design DataStore protocols and use NSCoding to serialize Objective-C objects
to disk. We did this as a temporary measure until we got time to incorporate CoreData.
It turned out we didn't even need CoreData! The simplest solution was good enough.
We ended up shipping the app to millions of users and never had any issues with
persistence.
raywenderlich.com 28
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Now, we could have just as easily needed a database like CoreData. The point is that
you can architect to allow your team to build significant portions of your app without
needing to make big, upfront decisions.
Most of the patterns are very similar to each other. This section provides a guide to help
you figure out what order to use when exploring existing architecture patterns. As you
read the following paragraphs, keep in mind that this book covers three of the
architectures mentioned. You'll read about why in the following section on putting
patterns into practice.
Since UIKit is designed with Model View Controller (MVC) in mind, any pattern other
than MVC will need to be retrofitted into UIKit. Therefore, when surveying patterns,
MVC is a great place to start.
Once you've looked at MVC, the next place to look are any of the MV- patterns, such as
MVVM and MVP. The notable exception is MVI; you'll see why in a bit. MV- patterns are
the next natural place to look because these patterns are so similar to MVC. They have
models and views, so they map easily to most of UIKit's MVC structure. With non-MVC
MV- architectures, you'll have to figure out how to connect view controllers to their
equivalent types in whatever MV- pattern you're using. For instance, you'll have to
figure out how to map view models to view controllers when using MVVM. You can read
more about MVVM in Chapter 5.
raywenderlich.com 29
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Clean Architecture and Ports & Adapters is a good place to look next. These
concepts by themselves are very high level and abstract. You'll need to do a lot of
reading and thinking in order to apply Clean Architecture and Ports & Adapters to iOS
app development. If you have time, I recommend you go down this route before
jumping into any of the specific patterns derived from these concepts.
A deep understanding will help you tweak any of the derived patterns. If you want to
explore the iOS architecture patterns derived from Clean Architecture and Ports &
Adapters, check out VIPER and RIBs. RIBs is Uber's take on VIPER. Clean Architecture
and Ports & Adapters patterns fit really well into apps that have a lot of local business
logic. If your app is presentation heavy and doesn't have a whole lot of local business
logic, these patterns might not work well for you.
One of the common properties of all these patterns is the components they define are
all interconnected. They're fairly inflexible, i.e., they're not designed to be mixed. You
might feel like you have to take one pattern over the other. This is why Josh, my co-
author, and I decided to come up with another approach that we call Elements.
This gives you a bird's eye view of some patterns you can use to shape your app's
architecture. This is by no means a comprehensive list. New patterns pop up all the
time, so it's worth looking around to see if you can find something else that works best
for you.
raywenderlich.com 30
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Selecting a pattern
Once you become familiar with the patterns that look promising, you'll want to decide
which pattern(s) to use. Choosing a pattern is not easy because we tend to feel a strong
connection with one pattern or another. In all honesty, which pattern you select is less
important than how you put the pattern to practice.
I've seen really well-architected MVC iOS apps, and I've seen very poorly architected
MVVM iOS apps and vice versa. Yes, view models can be as massive as view controllers.
The reality is, many patterns don't go deep into each of their layers. For example, what
exactly does a model look like in any of the MV- patterns? There's really not a single
way to design a model layer in MV- patterns. It's very open-ended. Not only that, most
patterns were not designed with mobile apps in mind — let alone today's complex iOS
environment. Therefore, most patterns only scratch the surface. Because of this,
selecting the "right" pattern will not automatically result in a well-architected
codebase.
Hopefully, this gives you a sense of freedom! The next time you find yourself in a hotly
debated discussion about architecture, remember that the patterns themselves aren't
that important. I'm not suggesting patterns are not important at all, I'm just saying that
choosing a particular pattern isn't the single most important decision. You can have a
well architected app using any of the patterns.
The best way to decide which pattern to use is to try a couple of the patterns in your
codebase. This will give you the best information about how well a pattern will meet
your needs. When trying different patterns, don't be afraid to experiment a bit with
each. Also, search the internet to see what other people's experience has been with the
patterns you're considering.
In addition, don't forget to consider the human aspects. How big is your team? How
experienced are your teammates? What patterns are your teammates most familiar
with? How tight are your deadlines? And of course, consider the technical aspects such
as, what constraints are you looking to design into your codebase? If you're short on
time, MVC is probably your best bet when it comes to iOS app development because
you won't have to spend time figuring how to incorporate a foreign pattern into UIKit's
MVC structure.
raywenderlich.com 31
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
Are there any "gotchas" to watch out for while trying out different patterns? Absolutely!
Here are some questions based on some of the pain points I've experienced when trying
different patterns:
• Do you end up with a lot of boilerplate code? If so, does the boilerplate at least make
the code easier to understand?
• Do you end up with a lot of empty files that only proxy method calls to other objects?
These aren't necessarily bad things. They're just things to think about as you survey and
compare different patterns. Also, don't feel like you have to pick one pattern over
another. Even though these patterns were not designed to be mixed and matched,
there's no reason you can't combine them. For example, if you really like unidirectional
pattens, but your codebase is built using MVVM, you could easily layer MVVM over
something like Redux. Just have your view models dispatch actions and have your view
models listening to the Redux store... In case that didn’t make any sense — no worries!
You learn all about MVVM and Redux in Chapters 5 and 6, respectively. All this to say,
architecture is more of an art than science. Go experiment, learn and be creative.
There's no right way to do it. Just remember, there are many good ways to architect and
there are many not-so-great ways to architect — but not a single right way.
You might be wondering why we cover so few patterns. Remember, how you apply a
pattern is more important than which pattern you pick. We wanted to take you on a
couple of deep dives into applying patterns. And, we didn't want to shy away from the
kind of complexities you'd find in a real app. Instead of scratching the surface with lots
of patterns, we opted to focus on a few. We also wanted to cover material not found in
other architecture books. For example, many patterns leave out important aspects of
app architecture, such as navigation. Most pattern chapters in this book cover
navigation, regardless of the pattern's main focus.
raywenderlich.com 32
Advanced iOS App Architecture Chapter 2: Which Architecture Is Right for Me?
You also might be wondering why we selected these three specific patterns. We wanted
to cover one pattern from each heritage. For example, MVVM comes from the MV- set
of patterns. Redux is a unidirectional pattern. And Elements is rooted in Clean
Architecture plus Ports & Adapters. We plan on adding a couple more patterns in future
editions of the book. Let us know in the book’s forum if there’s a particular pattern
you’d like to us cover!
In the following chapters, you'll read about the specifics of each pattern in depth.
However, you might be wondering what general things to look for when putting any
pattern into practice. Here are some to keep in your back pocket:
• Loosely coupled parts: Whether you're using MVC, MVVM, Redux, VIPER, etc. make
sure your code is broken down into small loosely coupled parts.
• Cohesive types: Make sure your types exhibit high cohesion, i.e., the properties and
methods that make up each type belong together. If you have small types that have
very focused responsibilities, your types probably exhibit high cohesion.
• Multi-module apps: Make sure your app is broken down into several Swift modules.
These are the aspects of architecture that make a real difference. We demonstrate all of
these aspects in the chapters ahead and in the companion example code.
Key points
• There's no such thing as a perfect universal app architecture.
• You can use architecture to boost your team's velocity, to strengthen your code's
quality and to increase your code's agility.
• Selecting the "right" architecture pattern won't guarantee your codebase will be well-
architected. Which pattern you select is less important than how you put the pattern
to practice.
raywenderlich.com 33
3 Chapter 3: Example App:
Koober
By René Cacheaux
This chapter introduces Koober, the example app used throughout this book. You’ll
explore all the screens that comprise the app and how they work together. At the end of
this chapter, you’ll take a quick tour of the Xcode project and source code. A
reimplementation of Koober accompanies every chapter so that you can compare and
contrast different architectures. The material in this book assumes that you have a
good understanding of the example app, so make sure to read this chapter before diving
into any of the following chapters.
Koober
Imagine a world in which animals are human-like. They speak different languages, live
in different countries, go to Mermaidbucks for coffee and so on. In this animal
kingdom, smartphones have just hit the market for the first time. All the developers
around the world race to build the next big app.
The kangaroo taxi industry in Australia is prime for disruption. Riders are tired of
paying in cash and having to physically walk to the street to hail a kangaroo. Plus, some
of the kangaroos have been hopping too high, making their passengers sick. Riders have
no way to give feedback. A team of developers in Sydney noticed this and decided to
launch a new startup company to build Koober, the next big ride-hailing app.
raywenderlich.com 34
Advanced iOS App Architecture Chapter 3: Example App: Koober
Launching
You’ll see the launch screen when you first open the app. This screen comes and goes
really quickly, so you might not see it. The app is determining if a user is signed in at
the time this screen is presented.
Welcome
If a user is not signed in, the app transitions to the welcome screen.
raywenderlich.com 35
Advanced iOS App Architecture Chapter 3: Example App: Koober
Signing up
From the welcome screen, you can navigate to the sign-up screen to create a new user
account.
Requesting a ride
Once you’re signed in, you’re taken to the pick-me-up app flow. First, the app
determines your current physical location. The location is used as the ride’s pick-up
location. Koober's user location system always returns Sydney, Australia as your
current location so that you don't have to give your actual location.
raywenderlich.com 36
Advanced iOS App Architecture Chapter 3: Example App: Koober
Then, you’re presented with a map that’s annotated with your pick-up location. At the
top of the screen, there’s a Where to? button for navigating to the drop-off location
picker.
The drop-off location picker is pre-seeded with locations. You can also perform a search
using the UISearchBar.
raywenderlich.com 37
Advanced iOS App Architecture Chapter 3: Example App: Koober
Once you pick a drop-off location, you’re taken back to the map screen wherein the
drop-off location is annotated and the ride-option picker is presented at the bottom of
the screen.
You use the ride-option picker to pick a ride option. A ride option specifies which kind
of animal will pick you up. Some animals can carry more riders than others. For
example, in Sydney, wallabies, wallaroos and kangaroos want to give rides for Koober.
Wallabies, wallaroos and kangaroos are considered different kinds of kangaroos. The
three kinds of kangaroos range from smallest to largest in size, respectively.
This means that wallaroos can cary more weight than wallabies, and they are more
expensive to ride.
raywenderlich.com 38
Advanced iOS App Architecture Chapter 3: Example App: Koober
As soon as you pick a ride option, you can confirm your new ride request. That’s how
you request a ride with Koober.
raywenderlich.com 39
Advanced iOS App Architecture Chapter 3: Example App: Koober
Signing out
You can sign out of the app from the profile screen.
Signing in
After you sign out, the app presents the welcome screen wherein you can navigate to
the sign-in screen. You can always sign in with [email protected] and password.
raywenderlich.com 40
Advanced iOS App Architecture Chapter 3: Example App: Koober
Why Koober?
When planning this book, we wanted to make sure that the example code would be
applicable to real projects. We heard from the community that most architecture books
oversimplify examples, leaving readers to figure out the real-world application of the
theory. So we decided to build an entire app with the complexities of a real app to use
as the basis for our examples. We really liked ride hailing because ride-hailing apps
have all the complexity of a real-world app without an explosion of screens and UI to
build.
If you try out any of this book’s techniques in your current projects, let us know how it
goes! We’d love to hear from you in the book’s forum.
Launch sequence
When launching Koober for the very first time, you’ll see two screens: the launch screen
and the welcome screen.
raywenderlich.com 41
Advanced iOS App Architecture Chapter 3: Example App: Koober
When the app starts up, the MainViewController is installed. MainViewController loads
by presenting the LaunchViewController as a child view controller. When the launch
screen is presented, the MainViewController and LaunchViewController make up the
View Controller hierarchy.
The LaunchViewController then determines whether a user is signed in or not. The first
time you run Koober, a user will not be signed in, so the MainViewController will
navigate from the LaunchViewController to the OnboardingViewController.
raywenderlich.com 42
Advanced iOS App Architecture Chapter 3: Example App: Koober
A single Xcode project contains all the source for Koober, and the source is organized
into several targets.
• Koober_iOS: This Cocoa Touch Framework contains all the UI code specific to the
Koober iOS app, such as view controllers and views.
• KooberUIKit: This Cocoa Touch Framework contains code that depends on UIKit
and that could be used on other UIKit platforms such as tvOS.
• KooberKit: This last Cocoa Touch Framework contains code that does not depend on
UIKit. Therefore, this framework can be used in any Apple platform.
Presenting MainViewController
To get started tracing the launch sequence in code, inside Xcode’s Project navigator,
open Koober/AppDelegate.swift. On the first line of
application(didFinishLaunchingWithOptions:), the MainViewController is
instantiated by the injectionContainer.
On line 46, the MainViewController is set as the window’s root view controller. So that’s
how the MainViewController makes its way to the screen.
raywenderlich.com 43
Advanced iOS App Architecture Chapter 3: Example App: Koober
On line 36, the view model’s viewSubject is initialized with a .launching MainView
enum case.
Return to Koober_iOS/iOSApp/MainViewController.swift.
Whenever a new value is emitted, the subscription to the view model calls the
present(view:) method on line 69. Because the first value emitted is .launching, the
presentLaunching method is called inside present(view:), right after
MainViewController loads. The presentLaunching method on line 94 adds the
LaunchViewController as a child to MainViewController, presenting the launch screen.
You can find the loadUserSession method on line 58. Once this method finishes
querying for a user session, goToNextScreen(userSession:) is called.
raywenderlich.com 44
Advanced iOS App Architecture Chapter 3: Example App: Koober
When you first run Koober, there won’t be a signed-in user so the
goToNextScreen(userSession:) method will be called with nil. When
goToNextScreen(userSession:) determines that a user is not signed in, on line 84 and
85, the notSignedInResponder’s notSignedIn method is called.
On line 42, inside notSignedIn, notice how MainViewModel is emitting a new view enum
case value, .onboarding, to the viewSubject. This is telling MainViewController to
transition from it’s current presentation to presenting OnboardingViewController. The
transition happens in MainViewController’s presentOnboarding method.
Open Koober_iOS/iOSApp/Onboarding/OnboardingViewController.swift.
Alright! Now that you’re familiar with Koober’s source code, you’ll have no problem
following along with the example code in rest of the chapters.
raywenderlich.com 45
Advanced iOS App Architecture Chapter 3: Example App: Koober
Key points
• Koober, a kangaroo ride-hailing app, is the example app used throughout this book.
• The Koober Xcode project consists of four targets: Koober, Koober_iOS, KooberUIKit
and KooberKit.
raywenderlich.com 46
4 Chapter 4: Objects & Their
Dependencies
By René Cacheaux & Josh Berlin
Ready to dig deep into object-oriented programming? Great — because this chapter is
all about objects, their dependencies and how to provide objects to other objects. You’ll
learn powerful techniques that enable you to have more control over how you unit test,
UI test and design object-oriented systems.
Designing how objects are decomposed into smaller objects, and how to thread them
together, is a fundamental architectural technique. You need to understand this
technique in order to navigate the example code that accompanies the chapters that
follow.
In this chapter, you’ll first learn the benefits of managing object dependencies. Then,
you’ll take a quick look at common dependency patterns. Finally, you’ll spend the rest
of the chapter taking a deep dive into Dependency Injection, one of the common
dependency patterns. Before diving into the theory, you’ll walk through the goals that
object dependency management techniques seek to achieve so that you can understand
what you can expect to get out of the practices covered in this chapter.
• Testability: Deterministic unit and UI tests, i.e., tests that don’t rely on things you
can’t control, such as the network.
raywenderlich.com 47
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
• Deferability: Having the ability to defer big decisions such as selecting a database
technology.
• Parallel work streams: Being able to have multiple developers work independently
on the same feature at the same time without stepping on each others toes.
• Minimizing object lifetimes: For any given app, the less state a developer has to
manage at once, the more predictably an app behaves. Therefore, you want to have
the least amount of objects in-memory at once.
Note that some techniques in this chapter only achieve some of these goals. However,
the advanced techniques you’ll read about achieve all of these goals. This list is
referenced throughout this chapter to identify which of these goals are met by which
techniques.
Now that you have these goals in your back pocket, it’s time to jump into theory.
raywenderlich.com 48
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Typically, when app developers use the term dependency, they are talking about
libraries. However, in this chapter, a dependency is an object that another object
depends on in order to do some work.
A dependency can also depend on other objects. These other objects are called
transitive dependencies.
raywenderlich.com 49
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
The reason an object goes under construction is to be used by yet another object — the
consumer.
All together, you have a consumer that needs the object-under-construction that
depends on dependencies that depend on transitive dependencies and so on.
raywenderlich.com 50
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
While reading about dependency patterns in the following sections, you’ll see the terms
outside and inside. Outside refers to code that exists outside the object-under-
construction. Inside refers to the code that exists inside the object-under-construction.
As you’ll see, this distinction is architecturally significant.
That’s the terminology you’ll need to know to follow along this deep and winding
object-dependency journey. Reviewing when and how dependencies are created will
help you understand why dependencies exist in the first place, so that’s next.
Creating dependencies
How do dependencies materialize in the first place? Here’s a couple of common
scenarios.
raywenderlich.com 51
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
How you do this refactoring has a direct impact on how many of the outlined goals you
can achieve. There are three fundamental considerations that will help you achieve the
goals.
Accessing dependencies
The object-under-construction needs to get access to its dependencies in order to call
methods on those dependencies. Here are the ways an object-under-construction can
get a hold of its dependencies.
raywenderlich.com 52
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Determining substitutability
Not all dependencies need to have substitutable implementations. For example, you
probably don’t need to substitute the implementation of a dependency that has no side
effects, i.e. it only contains pure business logic. However, if the dependency writes
something to disk, makes a network call, sends analytic events, navigates the user to
another screen, etc. then you probably want to substitute the dependency’s
implementation during development or during testing.
Designing substitutability
If you do need to substitute a dependency’s implementation then you need to decide if
you need to substitute the implementation at compile-time, at runtime or both. To
illustrate, you’ll probably need runtime substitutability when you need to provide a
different experience to different users for A/B testing. On the other hand, for testing,
developers typically rely on compile-time substitutability.
You have the goals, the vocabulary and the main considerations — what’s next? You
might be wondering why there’s so much talk about testing in an architecture book.
That’s the next stop on this journey.
raywenderlich.com 53
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
techniques are not a silver bullet. It’s also possible to design a poor architecture while
using these techniques. This chapter is just one of many puzzle pieces you must master
in order to design great software.
You’re now ready to learn how to take control of your objects’ dependencies. There are
several patterns you can use to design objects and their dependencies. This chapter
focuses on one of those patterns, but it’s worth taking a quick look at all the different
patterns first.
Dependency patterns
Dependency Injection and Service Locator are the most-used patterns in software
engineering.
• Dependency Injection: This is the pattern you’ll learn all about in this chapter. The
basic idea of the pattern is to provide all dependencies outside the object-under-
construction. More on this later.
• Service Locator: A Service Locator is an object that can create dependencies and
hold onto dependencies. You provide the object-under-construction with a Service
Locator. Whenever the object-under-construction needs a dependency, the object-
under-construction can simply ask the Service Locator to create or provide the
dependency. This pattern is easier to use than Dependency Injection but results in
more work when harnessing automated tests. Many developers use this pattern to
successfully achieve the goals outlined in this chapter.
Here are other patterns you can use that have been created by the Swift community:
• Protocol Extension: This pattern uses Swift’s protocol extensions to allow the
object-under-construction to get access to its dependencies. To learn more about
this pattern, see Daniel Hall’s article, A Swift-y Approach to Dependency Injection.
Now, the stage is set. You’ve got everything you need. It’s time to take a deep dive into
the world of Dependency Injection.
raywenderlich.com 54
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Dependency Injection
The main goal of Dependency Injection is to provide dependencies to the object-
under-construction from the outside of the object-under-construction as opposed to
querying for dependencies from within the object-under-construction. Dependencies
are “injected” into the object-under-construction.
From here, you can learn about the history behind Dependency Injection or you can
skip ahead and jump into the details.
History
Dependency injection, or DI, is not a new concept. Ask any Android developer if they
are familiar with DI, and they will likely tell you DI is essential to building well-
architected apps. Dependency injection is also heavily used when building Java backend
applications. So, it’s no surprise that Java developers take advantage of the design
pattern when moving to Android.
DI is intimately built into the core of some popular frameworks like AngularJS. The
official AngularJS documentation has an entire section on the topic. The authors of the
documents stressing that DI is “pervasive throughout Angular.”
The object-oriented theory behind DI has been around for a while. DI is based on the
Dependency Inversion Principle, also known as Inversion of Control. According to
Martin Fowler, Inversion of Control was first written about in 1988 in a paper titled
Designing Reusable Classes by Johnson and Foote. Arguably, the concept was
popularized by Robert Martin’s paper, Object Oriented Design Quality Metrics: An
Analysis of Dependencies published in 1994. These papers are worth a read if you want
to dig deep into the roots of object-oriented design.
raywenderlich.com 55
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
The term Dependency Injection was coined by Fowler in his January 14, 2004, post
titled, Inversion of Control Containers and the Dependency Injection Pattern. The
increasing popularity of Agile and Test-Driven Development motivated developers to
find ways to easily test object-oriented code. As a result, to meet testability needs,
developers invented Inversion of Control and, more specifically, DI. As you’ll see, using
Dependency Injection is key to building testable and maintainable iOS apps.
Types of injection
There are three types of injection:
raywenderlich.com 56
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Circular dependencies
Sometimes, two objects are so closely related to each other that they need to depend on
one another. For this case to work when using Dependency Injection, you have to use
property or method injection in one of the two objects that are in the circular
dependency. That’s because you cannot initialize both objects with each other; you
have to create one first and then create the second object with the first object via
initializer injection, then set a property on the first object with the second. Also,
remember to avoid retain cycles by making one reference weak or unowned.
Say you have a dependency class that stores and retrieves data from a database.
Injecting this database object, i.e., dependency, into a view controller does not give you
control over how the database object behaves during a test. This is true because the
view controller depends on a specific implementation of the database dependency that
cannot be substituted at runtime. In order to control this dependency, the view
controller should be able to accept a different implementation of the database object so
that a fake implementation, which you control, can be injected during a test.
Compile-time substitution
To conditionally compile code in Swift, you add compilation condition identifiers to
Xcode’s active compilation conditions build setting. Once you add custom identifiers
to the active compilation condition’s build setting, you use the identifiers in #if and
raywenderlich.com 57
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
You can use conditional compilation to change the dependency implementation that
you want for a specific build configuration. For example, if you want to use a fake
remote API implementation during tests:
• Change your target scheme’s Test scheme action’s build configuration to the Test
build configuration created in the previous step.
• Add a TEST identifier to your target’s active compilation conditions build setting
for the Test build configuration.
• Find the line of code wherein the consumer is creating a real remote API instance.
• Write an #if TEST compilation directive and, under the if statement, instantiate a
fake remote API.
• Write an #else compilation directive and instantiate a real remote API under the
else.
• Write an #endif compilation directive on the next line to close the conditional
compilation block.
When you run the Test action in Xcode, to run unit and UI tests, the Swift compiler will
compile the code that instantiates a fake remote API. When you run any other build
action, such as Run, the Swift compiler will compile the code that instantiates a real
remote API. Cool! Say goodbye to those flakey tests that try to make real network calls.
Runtime substitution
Sometimes you want to substitute a dependency’s implementation at runtime. For
instance, if you want to run different logic for your beta testers who are using
Testflight, you’ll need to use runtime substitution since the build that Testflight uses is
the exact same build distributed to end users via the App Store. Therefore, you can’t
use compile-time substitution for this situation. The Testflight use case is just one
example.
raywenderlich.com 58
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
This is useful when you’re developing an app in Xcode. This is neat because you don’t
need to recompile the app to change dependency implementations. Simply grab the
launch arguments from UserDefaults and wrap your dependency instantiations with if
statements that check launch argument values. You can use this trick during
development or even during a continuous integration test.
• Factories: Here, you begin to centralize initialization logic. This approach is also
fairly simple and is designed to help you learn the fundamentals.
• Single container: This approach packages all the initialization logic together into
one container. Since there’s state involved, it’s a bit more difficult to put into
practice than the previous two approaches.
• Container hierarchy: One of the problems with centralizing all the initialization
logic is you end up with one massive class. You can break a single container down
into a hierarchy of containers. That’s what this approach is all about.
On-demand approach
This approach is designed for learning DI and for using DI in very simple situations. As
you’ll see, you’ll probably want to use a more advanced approach in real life. In the on-
demand approach, whenever a consumer needs a new object-under-construction, the
consumer creates or finds the dependencies needed by the object-under-construction
at the time the consumer instantiates the object-under-construction. In other words, the
consumer is responsible for gathering all dependencies and is responsible for providing
those dependencies to the object-under-construction via the initializer, a stored-
property or a method.
raywenderlich.com 59
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
In this case, because the consumer is initializing all the dependencies, the consumer
needs to know which concrete implementation to use when initializing a dependency.
As long as the object-under-construction uses protocol types for its dependencies, the
object-under-construction won’t know what concrete implementation the consumer
used to create the dependencies, and that’s what you want.
These are the mechanics to the on-demand approach. What are the pros and cons?
• Your code is testable because you can substitute nondeterministic side effect
dependencies with deterministic fake implementations.
• You can defer decisions. For example, you can use an in-memory data store
implementation while you decide on a database technology. Changing from the in-
memory implementation to the database implementation is easy because you can
find all the in-memory instantiations and replace them with the database
instantiations. This can be a bit tedious, so this is also a con that’s addressed in more
advanced approaches.
raywenderlich.com 60
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
• Your team can work on the same feature at the same time because one developer can
build an object-under-construction while another builds the dependencies. The
developer building the object-under-construction can use fake implementations of
the dependencies while the other developer builds the real implementations of the
dependencies.
• Consumers need to know how to build the entire dependency graph for an object-
under-construction. Dependencies can also have dependencies and so on. The
consumer might have to instantiate a lot of dependencies. This is not ideal because
multiple consumers using the same object-under-construction class will have to
duplicate the dependency graph instantiation logic.
These cons can be addressed by taking a factories approach. You’ll learn this approach
next.
Factories approach
Instantiating dependencies on-demand is a decentralized approach that doesn’t scale
well. That’s because you’ll end up writing a lot of duplicate dependency instantiation
logic as your dependency graph gets larger and more complex. The factories approach
is all about centralizing dependency instantiation.
This approach works for ephemeral dependencies, i.e., dependencies that can be
instantiated at the same time as the object-under-construction. This approach does not
address managing long-lived dependencies such as singletons.
To take the factories approach, you create a factories class. What does a factories class
look like?
Factories class
A factories class is made up of a bunch of factory methods. Some of the methods create
dependencies and some of the methods create objects-under-construction. Also, a
factories class has no state, i.e., the class should not have any stored properties.
raywenderlich.com 61
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
One goal of creating a factories class is to make it possible for consumers to create
objects-under-construction without having to know how to build dependency graphs
required to instantiate objects-under-construction. This makes it super easy for any
part of your code to get a hold of any object needed regardless of how much the object
in question is broken down into smaller objects.
Next, you’ll learn how to design the different kinds of factory methods that make up a
factories class.
raywenderlich.com 62
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
This centralizes the dependency resolution so that you only have once place in your
codebase that knows how to resolve the UserProfileDataStore dependency. This is
awesome because you can change what kind of data store your entire app uses by
changing one line of code.
Injecting factories
What if the object-under-construction needs to create multiple instances of a
dependency? What if the object-under-construction is a view controller that needs to
create a dependency every time a user presses a button or types a character into a text
field?
raywenderlich.com 63
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Your first instinct might be to simply create an instance of the factories class within the
object-under-construction.
The object-under-construction would then have access to every single factory method.
While this is a very simple approach, the problem is that the object-under-construction
becomes harder to unit test. That’s because all dependencies are no longer injected
from the outside. With this approach, you’d need to work with the factories class in
order to substitute real implementations with fake implementations.
You can give this power to objects-under-construction from the outside using one of
two Swift features: closures or protocols.
Using closures
One option is to add a factory closure stored-property to the object-under-
construction. Here are the steps:
• Go to the factories class and find the factory method that creates the object-under-
construction.
raywenderlich.com 64
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Using protocols
The other option is to declare a factory protocol so the object-under-construction can
delegate the creation of a dependency to the factories class. Here are the steps:
• Declare a new factory protocol that contains a single method for the dependency
that the object-under-construction needs to create.
• The factories class will already conform to this protocol because the dependency
factory method in the protocol should match the implemented factory method in the
factories class. Simply declare conformance in the factories class.
• Add a stored-property and initializer parameter of the factory protocol type to the
object-under-construction. This allows you to inject the factories object into the
object-under-construction; however, the object-under-construction will only see the
single factory method defined in the protocol. The object-under-construction does
not know it is injected with the factories object because the protocol restricts the
object-under-construction’s view.
The object-under-construction now has the power to create new dependency instances
whenever, while not having access to all the factories in the factories class. You get the
same benefits with this approach as the closure approach. This decision comes down to
style preference.
That’s all there is to injecting factories. It’s time to take a quick look at when and how
to create instances of the factories class.
raywenderlich.com 65
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
You’ll learn about this next, after checking out the pros and cons to this factories
approach.
• Consumers are more resilient to change because they no longer need to know how to
build dependency graphs. That’s one less responsibility for all consumers. This helps
your team work in parallel because code is more loosely coupled.
• Code is generally easier to read because all of the initialization boilerplate is moved
out of the classes that do interesting work.
• This approach only works for ephemeral objects. Longer-lived objects need to be
held somewhere. Ideally, all dependencies should be centrally managed regardless of
lifespan. You’ll learn how to do this in the next section.
In practice, a factories class is not enough. You’ll most likely need to convert the
factories class into a container class.
This factories section is here in order to help you take small steps because there’s so
much to learn about DI.
raywenderlich.com 66
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
When refactoring a codebase to use DI, feel free to take this factories approach to get a
feel for the pattern. As you’ll see in the next section, you can easily update your code to
go from this factories approach to the container approach.
Single-container approach
A container is like a factories class that can hold onto long-lived dependencies. A
container is a stateful version of a factories class.
What are some examples of long-lived dependencies? A data store is a perfect example.
A data store is a container for data that is needed to render screens. Since this data can
probably change, you want a single copy of this data. Therefore, you don’t want to
create a new data store instance every time an object needs a data store. You probably
want a single instance to live as long as the app’s process, i.e., you need a singleton. To
keep a data store instance alive, you need an object to hold onto this singleton so that
ARC doesn’t de-allocate the data store.
Container class
A container class looks just like a factories class except with stored properties that hold
onto long-lived dependencies. You can either initialize constant stored properties
during the container’s initialization or you can create the properties lazily if the
properties use a lot of resources. However, lazy properties have to be variables so
constant properties are better by default.
Having long-lived dependencies co-located with factories changes how factories access
these long-lived dependencies. You’ll explore this more, next.
raywenderlich.com 67
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
All factory methods can be invoked without any inputs. The fact that these methods
can have zero parameters is super powerful. You take a dependency with a complex
initializer such as init(remoteAPI: UserRemoteAPI, dataStore: UserDataStore) and
reduce it down to a factory method, such as makeProfileViewModel().
The above is true except for runtime value parameters. Since runtime values are
provided outside of the container, and because runtime values are not long-lived
dependencies, factory methods in a container are still much easier to invoke. As you’ll
see later, this comes in handy when injecting factories.
Just like dependency factory methods from above, these factory methods also don’t
need to have parameters for long-lived dependencies. This is a huge benefit for
consumers because consumers don’t have to manage anything in order to create
objects-under-construction. They simply invoke the empty argument factory method or
provide runtime values.
Easy peasy. At this point, you probably want to know when and how to create a
container.
raywenderlich.com 68
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Going from learning how to build a factories class to learning how to build a single
container is not a huge leap. However, the theory behind containers gets interesting
when you need to break a single container into a container hierarchy. You’ll learn about
this next, after going through the pros and cons of the single-container approach.
• You can change an object’s dependency graph without having to change code outside
the container class.
raywenderlich.com 69
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
You’ll also notice a lot of optional conditional unwrapping. Most apps have many
dependencies that need to know about the currently signed-in user to do things like
authenticate HTTP requests.
If all the reusable dependencies live as long as an app lives, the container logic will
need to handle optional cases because the user can be signed out while the app is
running.
Based on the container design thus far, there’s nothing stopping any consumer from
asking the container for dependencies that require the user to be signed in.
Ideally, consumers would only have access to these reusable dependencies when a user
is signed in. This is just one of many examples of optional case handling that sneaks
into your singe-dependency container.
In this section, you’ll learn how to use advanced DI techniques address these
undesirable qualities.
Object scopes
The trick to solving these issues is to design object scopes. To do this, think about at
what point in time dependencies should be created and destroyed. Every object has a
lifetime. You want to explicitly design when objects come and go. For example, objects
in a user scope are created when a user signs in and are destroyed when a user signs
out. Objects in a view controller scope are created when the view controller loads and
are destroyed when the view controller is de-allocated.
• App scope: Traditional singletons fall under this scope. Objects in the app scope are
created when the app launches and are destroyed when the app is killed. Typical
dependencies you find in this scope include authentication stores, analytics trackers,
logging systems, etc.
• User scope: User scope objects are created when a user signs in, and they’re
destroyed when a user signs out. Some apps allow users to sign in to multiple
accounts. In this case, the app could have multiple user scopes alive at the same
raywenderlich.com 70
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
time. Most dependencies, such as remote API’s and data stores, are usually found in
this scope. This scope also typically contains more specific versions of dependencies
found in the app scope. For instance, the app scope could have an anonymous
analytics tracker while a user scope could have a user specific analytics tracker.
Scopes are very powerful because they help convert a bunch of mutable state into
immutable state. For that reason, you can go even further with scopes by designing
shorter lived scopes. Here are a couple of examples.
• Feature scope: Objects in a feature scope are created when the user navigates to a
feature and are destroyed when the user navigates away. Feature scopes are handy
when a feature needs to share data amongst many objects that make up the feature.
For example, in Koober, the pick-me-up feature needs to know the user’s current
location. The user’s current location is fetched once and then is not retrieved again;
the current location is immutable from the pick-me-up feature’s point of view.
Many different view controllers and objects with business logic need to utilize the
current location value in order to function.
Imagine having to pass this value around from object to object. By creating a feature
scope, the current location can be injected into all of these objects.
The objects don’t need to worry about how to get the value. As far as the objects in the
feature are concerned, the location value is immutable even though the user can still
ask for a new ride, and a new current location will be fetched. This works because, every
time a user starts a new ride, an entirely new object graph is created with the current
static location value.
Once you’ve designed the scopes that you need, and once you’ve identified which
dependencies should live in which scopes, the next step is to break up the single
container into a container hierarchy.
Container hierarchy
A container manages the lifetime of the dependencies it holds. Because of this, each
scope maps to a container. A user scope would have a user-scoped container. The user-
scoped container is created when the user signs in and so forth. This is how the
dependencies that are in the user scope are all created and destroyed at the same time,
because the scoped container owns these objects.
raywenderlich.com 71
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
For every scope you design, you create a container class. When you do this, you’ll notice
that scoped containers will want to have access to factory methods and stored
properties from other containers. To do this, you build a container hierarchy.
The app scoped container is always the root container. If you think about how the
hierarchy maps to length of object lifetimes, the rule makes a lot of sense. Parent
containers live longer than child containers. If the parent was allowed to ask for a
dependency from a child container, the child container might no longer be alive. So
that’s the rationale for the rule.
The container hierarchy is an object graph itself; therefore, you can use initializer
injection to provide child containers with parent containers.
As with all DI conventions, this sounds more complicated than it really is. The child
container’s initializer needs to have a parameter for the parent container. The child
container can then hold a reference to the parent container in a stored-property. This
gives the child access to all the factory methods and stored properties in the parent
container.
As an example, say you have a UserProfileViewModel. This view model needs a Logger
in order to log events. Logger needs to live as long as the app is alive because you want
to be able to log messages regardless of whether or not a user is signed in. So the logger
goes into an AppDependencyContainer.
raywenderlich.com 72
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Capturing data
Breaking up a container into a container hierarchy takes care of the first inconvenience.
What about the second inconvenience — the one about handling optionals?
Besides managing the lifetime of dependencies, a container can also capture data
model values. This is extremely helpful if the data model value is immutable for the
lifetime of the container. Capturing data in a container is a way to convert mutable
values into immutable values. This makes the code inside a container more
deterministic because the logic does not have to consider a change in the captured
value.
This is important and really neat because a user container cannot exist without a user
session. This takes away the optional case handling related to signed-in user.
Moving forward with the example, inside UserContainer, say you have a factory method
for creating a UserProfileRemoteAPI. The remote API needs the user session in order to
function. That’s easy — the remote API factory method can access the user session
stored-property.
Remember the factory and the stored-property are both inside the same UserContainer
class. The days of having to check if there’s a signed-in user all over a codebase are
gone!
• By capturing values in a scope, you can convert mutable values into an immutable
values.
• Container classes are shorter when you divide container classes into scoped
container classes.
raywenderlich.com 73
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
• Even when containers are broken up into scoped containers, complex apps might
still end up with really long container classes.
By this point, you’ve learned a lot about DI, and you’re on your way to mastering object-
oriented design. This is all the theory you’ll need to understand how Koober, the
example app, uses DI. In this book, you’ll encounter different versions of Koober that
are built using different architectures. DI is such a universal approach that every
version of Koober you’ll see uses the same DI pattern as DI can support all kinds of
different architectures.
Most of the theory you’ve read is applicable to iOS without any special considerations.
However, there are a couple of iOS-specific decisions that you’ll need make when using
DI in your iOS codebases. It’s time to go from theory to practice.
One of the first objects you typically instantiate is a root view controller. Koober’s root
view controller is the first object-under-construction example that you’ll explore. The
root view controller is the root of the object graph that you design when building iOS
apps. The ultimate goal is to learn how to use DI containers to construct this object
graph. In the rest of this chapter, you’ll take steps towards this ultimate goal.
raywenderlich.com 74
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
To set the stage, the following section walks you through the object graph required to
authenticate users in Koober.
The example code uses FakeAuthRemoteAPI so you don’t need to have a network
connection or a local server to use Koober. All implementations of AuthRemoteAPI do
not depend on any other objects.
raywenderlich.com 75
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
raywenderlich.com 76
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
This can happen during launch or when a user signs out. The object that implements
this protocol is responsible for navigating the user to the OnboardingViewController.
MainViewModel implements this protocol.
This can occur on launch or after a user successfully signs in or signs up.
MainViewModel implements this protocol.
• LaunchViewController: When Koober launches for the first time, Koober needs to
start up all the subsystems and needs to determine if a user is signed in. While
raywenderlich.com 77
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
raywenderlich.com 78
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
raywenderlich.com 79
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
raywenderlich.com 80
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
To illustrate, MainViewController presents and dismisses the launch screen, the on-
boarding screens and the signed-in screens. MainViewController depends on view
controller factory methods in order to create LaunchViewControllers,
OnboardingViewControllers and SignedInViewControllers.
raywenderlich.com 81
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Now that you’re familiar with Koober’s top-level object graph, you’ll see how to use the
on-demand approach to build this graph.
raywenderlich.com 82
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
As you can see, decomposing large objects into single-responsibility objects results in
deep object graphs. For this reason, you’ll find the on-demand approach is not very
practical for real-world apps that have large and deep object graphs.
The on-demand approach is good for teaching DI and for using Dependency Injection
in small apps. Nevertheless, it’s worth taking a look at how to build
MainViewController’s dependency graph using the on-demand approach as a stepping
stone to learning the factories approach.
raywenderlich.com 83
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
A free global constant is a good place to hold this object since it needs to live as long as
the app is running. Here’s how to set this up:
let userSessionCoder =
UserSessionPropertyListCoder()
let userSessionDataStore =
KeychainUserSessionDataStore(
userSessionCoder: userSessionCoder)
let authRemoteAPI =
FakeAuthRemoteAPI()
return KooberUserSessionRepository(
dataStore: userSessionDataStore,
remoteAPI: authRemoteAPI)
}()
#if USER_SESSION_DATASTORE_FILEBASED
let userSessionDataStore =
FileUserSessionDataStore()
#else
raywenderlich.com 84
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
let userSessionCoder =
UserSessionPropertyListCoder()
let userSessionDataStore =
KeychainUserSessionDataStore(
userSessionCoder: userSessionCoder)
#endif
let authRemoteAPI =
FakeAuthRemoteAPI()
return KooberUserSessionRepository(
dataStore: userSessionDataStore,
remoteAPI: authRemoteAPI)
}()
Same as before, if you could create more than one UserSessionDataStore, you would
have to duplicate the conditional compilation if you want to use the same
UserSessionDataStore implementation across your codebase. This is inconvenient and
undesirable. Substitution is much more powerful when used alongside the factories and
containers approach.
That wraps up creating the UserSessionDataStore. The example will use this code to
create a MainViewController in the next section.
Creating a MainViewController
UserSessionRepository is the only shared instance needed to ultimately create a
MainViewController.
Now that you’ve seen how to set up a shared instance dependency, it’s time to go into
application(_:didFinishLaunchingWithOptions:) to see how the MainViewController
is created and installed:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let launchViewModel =
LaunchViewModel(
raywenderlich.com 85
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
userSessionRepository: GlobalUserSessionRepository,
notSignedInResponder: mainViewModel,
signedInResponder: mainViewModel)
let launchViewController =
LaunchViewController(viewModel: launchViewModel)
let mainViewController =
MainViewController(
viewModel: mainViewModel,
launchViewController: launchViewController)
window.frame = UIScreen.main.bounds
window.makeKeyAndVisible()
window.rootViewController = mainViewController
return true
}
So far, you’ve seen how to use the on-demand approach to build the root view
controller’s object graph. The app delegate isn’t the only place an app needs to create
new objects. Next, you’ll visit the MainViewController’s implementation to see how the
on-demand approach works when a parent view controller needs to create a new
instance of a child view controller.
Using global references in this way is not ideal. Later in this chapter, you’ll see how to
use a container instead of global references to store shared instances.
raywenderlich.com 86
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
let welcomeViewModel =
WelcomeViewModel(goToSignUpNavigator: onboardingViewModel,
goToSignInNavigator: onboardingViewModel)
let welcomeViewController =
WelcomeViewController(viewModel: welcomeViewModel)
let signInViewModel =
SignInViewModel(
userSessionRepository: GlobalUserSessionRepository,
signedInResponder: self.viewModel)
let signInViewController =
SignInViewController(viewModel: signInViewModel)
let signUpViewModel =
SignUpViewModel(
userSessionRepository: GlobalUserSessionRepository,
signedInResponder: self.viewModel)
let signUpViewController =
SignUpViewController(viewModel: signUpViewModel)
let onboardingViewController =
OnboardingViewController(
viewModel: onboardingViewModel,
welcomeViewController: welcomeViewController,
signInViewController: signInViewController,
signUpViewController: signUpViewController)
raywenderlich.com 87
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
In this section, you’ll learn how to create the same objects from the last section using a
factories class named KooberObjectFactories.
class KooberObjectFactories {
#else
let coder = makeUserSessionCoder()
raywenderlich.com 88
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
This example takes all the code that was in the GlobalUserSessionRepository’s
declaration from the previous on-demand approach example and distributes object
initializations into factory methods, one for each dependency.
One really nice thing about factory methods is that they can hide implementation
substitutions. For example, look at makeUserSessionDataStore() in the above code. The
caller of this method has no idea they may get a FileUserSessionDataStore or a
KeychainUserSessionDataStore.
This is great because this gives you the flexibility to change which data store to use by
changing one method without needing to change any of the calling code.
Now the factories class is set up, take a look below at how
GlobalUserSessionRepository is declared:
let objectFactories =
KooberObjectFactories()
let userSessionRepository =
objectFactories.makeUserSessionRepository()
return userSessionRepository
}()
This is a lot less code than the same declaration you saw in the on-demand approach
example. The factories approach moves a ton of boilerplate code away from object
usage sites into the centralized factories class. This helps you and other developers read
code because you don’t have to reason about how object graphs are assembled. If you
need to see how an object is constructed, the factories class is always a Command-click
away.
Alright, that’s how the global shared UserSessionRepository is created using the
factories approach. Next, you’ll see how this code is used to create a
MainViewController.
raywenderlich.com 89
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Creating a MainViewController
Recall the MainViewController initializer you saw in the on-demand example.
This is a simplified initializer; the one in Koober is more complex. The initializer used
in Koober needs a couple of factory closures because MainViewController needs to be
able to create view controllers after it’s created. Here’s the initializer used in Koober:
init(viewModel: MainViewModel,
launchViewController: LaunchViewController,
// Closure that creates an OnboardingViewController
onboardingViewControllerFactory:
@escaping () -> OnboardingViewController,
// Closure that creates a SignedInViewController
signedInViewControllerFactory:
@escaping (UserSession) -> SignedInViewController)
Considering that this adds quite a bit of complexity, you’ll first walk through a factories
example that uses the same simple initializer from the on-demand approach, and then
you’ll explore the code necessary to use the more complex initializer.
Since MainViewController needs a MainViewModel, you’ll first look at how the factories
approach creates a MainViewModel. The following code adds a MainViewModel factory
method to KooberObjectFactories:
class KooberObjectFactories {
...
There’s not much to say about this code; it adds a simple factory method. You’ll see
where it’s used, next.
Since MainViewModel is stateful, the factory setup should only create one MainViewModel
instance. For this reason, you need a global constant such as the one below:
raywenderlich.com 90
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
return mainViewModel
}()
Now that KooberObjectFactories can create and access a shared MainViewModel, it’s
time to give KooberObjectFactories the ability to create a MainViewController:
class KooberObjectFactories {
...
return MainViewController(
viewModel: mainViewModel,
launchViewController: launchViewController)
}
func makeLaunchViewController(
userSessionRepository: UserSessionRepository,
notSignedInResponder: NotSignedInResponder,
signedInResponder: SignedInResponder)
-> LaunchViewController {
// 2
func makeLaunchViewModel(
userSessionRepository: UserSessionRepository,
notSignedInResponder: NotSignedInResponder,
raywenderlich.com 91
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
return LaunchViewModel(
userSessionRepository: userSessionRepository,
notSignedInResponder: notSignedInResponder,
signedInResponder: signedInResponder)
}
}
2. This is another factory method that needs to have dependencies passed in from the
outside. LaunchViewModel needs objects that conform to NotSignedInResponder and
SignedInResponder. You and I know that MainViewModel conforms to this, but
KooberObjectFactories does not know because KooberObjectFactories does not
manage long-lived dependencies and MainViewModel is a long-lived dependency.
Therefore, KooberObjectFactories cannot create a NotSignedInResponder nor a
SignedInResponder.
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let mainViewController =
objectFactories.makeMainViewController(
viewModel: sharedMainViewModel,
userSessionRepository: sharedUserSessionRepository)
window.frame = UIScreen.main.bounds
window.makeKeyAndVisible()
window.rootViewController = mainViewController
raywenderlich.com 92
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
return true
}
The above code gets shared instances needed to create a MainViewController, creates a
factories class instance, and finally creates a MainViewController using the factories
class instance.
Remember, a factories class is stateless. You can instantiate the class whenever you
need to invoke a factory method. Just remember that doing this inside any object other
than the app delegate can make your objects harder to unit test.
Great! You now know how to design a simple factories class. What about that complex
initializer you saw earlier? What code would need to be added to this example? This
example is going to go from using the following MainViewController initializer:
If you recall Koober’s real MainViewController initializer you saw earlier, you’ll notice
this new version isn’t exactly the same. This version is missing the last factory closure
parameter. That’s because walking through adding the last factory closure parameter
would take forever.
raywenderlich.com 93
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
The good new: If you understand how the version above works, you’ll understand how
the real version works as well!
The next part of this example demonstrates how to apply the theory about injecting
factories into an object-under-construction. Here’s the first bit of code:
class KooberObjectFactories {
...
raywenderlich.com 94
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
func makeMainViewController(
viewModel: MainViewModel,
userSessionRepository: UserSessionRepository)
-> MainViewController {
return MainViewController(
viewModel: mainViewModel,
launchViewController: launchViewController,
// New factory closure argument:
onboardingViewControllerFactory:
onboardingViewControllerFactory)
}
...
}
The above code modifies MainViewController’s factory method to account for the
factory injected version of MainViewController’s initializer. Notice how the example
adds a closure constant named onboardingViewControllerFactory. This factory closure
is injected into MainViewController via initialization.
class KooberObjectFactories {
...
...
raywenderlich.com 95
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
func makeOnboardingViewController(
userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder)
-> OnboardingViewController {
return OnboardingViewController(
viewModel: onboardingViewModel,
welcomeViewController: welcomeViewController,
signInViewController: signInViewController,
signUpViewController: signUpViewController)
}
func makeWelcomeViewController(
goToSignUpNavigator: GoToSignUpNavigator,
goToSignInNavigator: GoToSignInNavigator)
-> WelcomeViewController {
func makeWelcomeViewModel(
goToSignUpNavigator: GoToSignUpNavigator,
goToSignInNavigator: GoToSignInNavigator)
-> WelcomeViewModel {
return WelcomeViewModel(
goToSignUpNavigator: goToSignUpNavigator,
goToSignInNavigator: goToSignInNavigator)
}
func makeSignInViewController(
userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder)
-> SignInViewController {
raywenderlich.com 96
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
func makeSignInViewModel(
userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder)
-> SignInViewModel {
return SignInViewModel(
userSessionRepository: userSessionRepository,
signedInResponder: signedInResponder)
}
func makeSignUpViewController(
userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder)
-> SignUpViewController {
func makeSignUpViewModel(
userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder)
-> SignUpViewModel {
return SignUpViewModel(
userSessionRepository: userSessionRepository,
signedInResponder: signedInResponder)
}
}
Wow — OnboardingViewController has quite a dependency graph. The above code was
previously in MainViewController’s presentOnboarding() method in the on-demand
version of this example. The complexity of assembling OnboardingViewController’s
object graph has now moved outside of MainViewController. This allows
MainViewController to focus on being a great view controller.
class KooberObjectFactories {
raywenderlich.com 97
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
...
func makeMainViewController(
viewModel: MainViewModel,
userSessionRepository: UserSessionRepository)
-> MainViewController {
return MainViewController(
viewModel: mainViewModel,
launchViewController: launchViewController,
onboardingViewControllerFactory:
onboardingViewControllerFactory)
}
...
...
}
raywenderlich.com 98
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
That’s how you inject factories! There was a lot of changes necessary in order to give
MainViewController the power to create OnboardingViewControllers. Take a look at
how application(_:didFinishLaunchingWithOptions:) needs to change to account for
all this:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let mainViewController =
objectFactories.makeMainViewController(
viewModel: sharedMainViewModel,
userSessionRepository: sharedUserSessionRepository)
window.frame = UIScreen.main.bounds
window.makeKeyAndVisible()
window.rootViewController = mainViewController
return true
}
Wait a second... that’s right: nothing changed. You’ve now witnessed the awesome
power of DI. Also, remember that very long presentOnboarding() method from the on-
boarding example?
You’re starting to become a DI guru. But wait — there’s more. The one problem with
KooberObjectFactories is you have to create global constants for long-lived
dependencies. You probably don’t want these objects just hanging out in global space.
To solve this, you’ll see how you can upgrade KooberObjectFactories to a
KooberAppDependencyContainer in the next section.
raywenderlich.com 99
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
The first order of business is to create and store the shared UserSessionRepository:
class KooberAppDependencyContainer {
// MARK: - Properties
// 1
let sharedUserSessionRepository: UserSessionRepository
// MARK: - Methods
init() {
// 2
func makeUserSessionRepository() -> UserSessionRepository {
let dataStore = makeUserSessionDataStore()
let remoteAPI = makeAuthRemoteAPI()
return KooberUserSessionRepository(dataStore: dataStore,
remoteAPI: remoteAPI)
}
#else
let coder = makeUserSessionCoder()
return KeychainUserSessionDataStore(
userSessionCoder: coder)
#endif
}
// 3
self.sharedUserSessionRepository =
makeUserSessionRepository()
}
}
raywenderlich.com 100
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
1. This declares a constant stored property. This property holds onto the shared
UserSessionRepository instance that should be used when creating an object-
under-construction that depends on a UserSessionRepository.
2. Notice how these factory methods are inside the container’s initializer. These
factory methods cannot be instance methods because Swift does not allow an
initializer to call a method on self until all stored properties are initialized. In this
case, you need these methods to initialize a stored property.
The example above gives the container the ability to fully create and store a shared
UserSessionRepository. Next, you’ll look at how to give the container the ability to
create a MainViewController.
class KooberAppDependencyContainer {
// MARK: - Properties
let sharedUserSessionRepository: UserSessionRepository
// 1
let sharedMainViewModel: MainViewModel
// MARK: - Methods
init() {
func makeUserSessionRepository() -> UserSessionRepository {
let dataStore = makeUserSessionDataStore()
let remoteAPI = makeAuthRemoteAPI()
return KooberUserSessionRepository(dataStore: dataStore,
remoteAPI: remoteAPI)
}
#else
let coder = makeUserSessionCoder()
return KeychainUserSessionDataStore(
userSessionCoder: coder)
raywenderlich.com 101
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
#endif
}
// 2
// Because `MainViewModel` is a concrete type
// and because `MainViewModel`’s initializer has
// no parameters, you don’t need this inline
// factory method, you can also initialize the
// `sharedMainViewModel` property on the
// declaration line like this:
// `let sharedMainViewModel = MainViewModel()`.
// Which option to use is a style preference.
func makeMainViewModel() -> MainViewModel {
return MainViewModel()
}
self.sharedUserSessionRepository =
makeUserSessionRepository()
// 3
self.sharedMainViewModel =
makeMainViewModel()
}
}
1. This line adds a constant stored property to hold onto a shared MainViewModel. The
container will use this instance any time an object-under-construction needs a
MainViewModel.
2. This block adds a new inlined MainViewModel factory method to init. One neat
thing is this design guarantees that another MainViewModel won’t be accidentally
created because this factory method is inaccessible outside init.
class KooberAppDependencyContainer {
// MARK: - Properties
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
// 1
raywenderlich.com 102
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
// MARK: - Methods
init() {
...
}
// 2
// On-boarding (signed-out)
// Factories needed to create an OnboardingViewController.
func makeOnboardingViewController()
-> OnboardingViewController {
// 3
self.sharedOnboardingViewModel = makeOnboardingViewModel()
// 4
return OnboardingViewController(
viewModel: self.sharedOnboardingViewModel!,
welcomeViewController: welcomeViewController,
signInViewController: signInViewController,
signUpViewController: signUpViewController)
}
raywenderlich.com 103
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
Notice how the factory methods don’t have parameters anymore! That’s because
factory methods in a container can use other factory methods to create ephemeral
dependencies and because factory methods in a container can access the container’s
properties to get long-lived dependencies. Containers have everything they need to
assemble entire dependency graphs.
Here are some additional things to note about the above code:
class KooberAppDependencyContainer {
// MARK: - Properties
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
var sharedOnboardingViewModel: OnboardingViewModel?
raywenderlich.com 104
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
// MARK: - Methods
init() {
...
}
// On-boarding (signed-out)
// Factories needed to create an OnboardingViewController.
...
// Main
// Factories needed to create a MainViewController.
There’s nothing too surprising, here. The above code adds two factory methods: one to
create a LaunchViewModel, which is then used to create a LaunchViewController in the
other factory method.
All the setup is complete. The only thing missing is a factory method that can create a
MainViewController:
class KooberAppDependencyContainer {
// MARK: - Properties
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
var sharedOnboardingViewModel: OnboardingViewModel?
// MARK: - Methods
init() {
...
}
// On-boarding (signed-out)
// Factories needed to create an OnboardingViewController.
...
// Main
// Factories needed to create a MainViewController.
raywenderlich.com 105
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
// 2
let onboardingViewControllerFactory = {
return self.makeOnboardingViewController()
}
// 3
return MainViewController(
viewModel: self.sharedMainViewModel,
launchViewController: launchViewController,
onboardingViewControllerFactory:
onboardingViewControllerFactory)
}
...
}
2. Look how simple the OnboardingViewController factory closure is, now that the
OnboardingViewController factory method takes no arguments.
3. This is what you’ve been waiting for: the line that creates the MainViewController.
This line uses a long-lived dependency, a newly created dependency and a factory
closure to create a MainViewController. All the big concepts, wrapped up into a
single line.
OK, the container is setup and ready to build Koober’s object graph. It’s time for the
very final step — making a MainViewController and its entire graph when Koober
launches:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
// 1
let appContainer = KooberAppDependencyContainer()
let window = UIWindow()
// MARK: - Methods
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 2
let mainVC = appContainer.makeMainViewController()
window.frame = UIScreen.main.bounds
window.makeKeyAndVisible()
raywenderlich.com 106
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
window.rootViewController = mainVC
return true
}
}
Look how simple this is. It only takes two steps to create Koober’s entire dependency
graph. With the above code, you:
1. Create the app container and store it in a constant inside the app delegate. Creating
this container is easy because the initializer doesn’t have any parameters.
Remember, you should only create one instance of a container because containers
are stateful unlike a factories class.
2. Create the root object, in this case a MainViewController, by invoking the root
object’s factory method on the container. This single line creates and sets up
everything Koober needs in order to run. All dependencies are provided from the
outside.
The really neat thing about all this is that all of the classes inside Koober have no idea
about the dependency containers. It’s not like using DI will introduce a bunch of things
into your existing code that you might want to get rid of later.
What a journey it’s been. You’ve seen all big three approaches used in practice. You’re
almost at the finish line! The only pesky thing that needs addressing is the optional
sharedOnboardingViewModel. Don’t you hate it when you find yourself needing to force
unwrap something? I know I do. In the next section, you’ll see how to address this issue
by separating the on-boarding factory logic into a separate scoped container.
class KooberAppDependencyContainer {
// MARK: - Properties
// Long-lived dependencies
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
// MARK: - Methods
raywenderlich.com 107
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
init() {
func makeUserSessionRepository() -> UserSessionRepository {
let dataStore = makeUserSessionDataStore()
let remoteAPI = makeAuthRemoteAPI()
return KooberUserSessionRepository(dataStore: dataStore,
remoteAPI: remoteAPI)
}
#else
let coder = makeUserSessionCoder()
return KeychainUserSessionDataStore(
userSessionCoder: coder)
#endif
}
self.sharedUserSessionRepository =
makeUserSessionRepository()
self.sharedMainViewModel =
makeMainViewModel()
}
// Main
// Factories needed to create a MainViewController.
let onboardingViewControllerFactory = {
return self.makeOnboardingViewController()
}
return MainViewController(
viewModel: self.sharedMainViewModel,
launchViewController: launchViewController,
onboardingViewControllerFactory:
onboardingViewControllerFactory)
}
// Launching
raywenderlich.com 108
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
// On-boarding (signed-out)
// Factories needed to create an OnboardingViewController.
func makeOnboardingViewController()
-> OnboardingViewController {
The above code is the exact same KooberAppDependencyContainer from before except
without all the on-boarding factory methods barring the primary
OnboardingViewController factory method, makeOnboardingViewController().
This method will use the child on-boarding dependency container in order to create an
OnboardingViewController. You’ll see the implementation of this method at the end of
this example once you’ve explored the child on-boarding container’s class.
Next, you’ll explore a new container class that represents the on-boarding scope.
Koober transitions into the on-boarding scope when Koober determines a user is not
signed in. This could occur at launch or when a user signs out.
class KooberOnboardingDependencyContainer {
// MARK: - Properties
// 1
// From parent container
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
// 2
// Long-lived dependencies
let sharedOnboardingViewModel: OnboardingViewModel
// MARK: - Methods
// 3
init(appDependencyContainer: KooberAppDependencyContainer) {
// 4
func makeOnboardingViewModel() -> OnboardingViewModel {
return OnboardingViewModel()
}
raywenderlich.com 109
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
// 5
self.sharedUserSessionRepository =
appDependencyContainer.sharedUserSessionRepository
self.sharedMainViewModel =
appDependencyContainer.sharedMainViewModel
// 6
self.sharedOnboardingViewModel =
makeOnboardingViewModel()
}
// 7
// On-boarding (signed-out)
// Factories needed to create an OnboardingViewController.
func makeOnboardingViewController()
-> OnboardingViewController {
return OnboardingViewController(
viewModel: self.sharedOnboardingViewModel,
welcomeViewController: welcomeViewController,
signInViewController: signInViewController,
signUpViewController: signUpViewController)
}
raywenderlich.com 110
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
1. These two long-lived dependencies are held by the app dependency container.
Instead of holding onto the app dependency container, this example holds onto the
long-lived dependencies themselves. This is so the factory methods in this on-
boarding container can have easy access to the long-lived dependencies without
needing to know how to fish for the dependencies out of the app dependency
container.
3. This is the container’s initializer. Notice how the app dependency container is
required in order to create this on-boarding container. That’s because the objects,
that this container creates, need long-lived dependencies held by the app
dependency container. The app dependency container is the on-boarding
container’s parent container.
5. These lines find the long-lived dependencies held by the parent app dependency
container and uses those dependencies to set corresponding properties on this child
container. The properties are needed so that the on-boarding dependency container
can hold onto these long-lived dependencies. Holding dependencies from a parent
container is OK because parent containers outlive child containers. There’s no
chance this child container is holding onto something for longer than it should.
6. The shared OnboardingViewModel is created here, using the inlined factory method.
7. Here are all the factory methods that used to be in the app dependency container.
The only difference here is that sharedOnboardingViewModel is no longer forced
unwrapped.
raywenderlich.com 111
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
There’s one step left. Recall that MainViewController’s factory method needs to be able
to create a new OnboardingViewController inside the factory closure that gets injected
into MainViewController.
class KooberAppDependencyContainer {
// MARK: - Properties
let sharedUserSessionRepository: UserSessionRepository
let sharedMainViewModel: MainViewModel
// MARK: - Methods
init() {
...
}
...
func makeOnboardingViewController()
-> OnboardingViewController {
// 1
let onboardingDependencyContainer =
KooberOnboardingDependencyContainer(
appDependencyContainer: self)
// 2
return onboardingDependencyContainer
.makeOnboardingViewController()
}
}
1. First, you need to create the child on-boarding dependency container using self,
the parent app dependency container.
2. Finally, you use the child container to create and return a new
OnboardingViewController.
raywenderlich.com 112
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
OK — the container hierarchy is set up and ready to build Koober’s object graph. It’s
time for the very final step — making a MainViewController and its entire graph when
Koober launches:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
let appContainer = KooberAppDependencyContainer()
let window = UIWindow()
// MARK: - Methods
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window.frame = UIScreen.main.bounds
window.makeKeyAndVisible()
window.rootViewController = mainVC
return true
}
}
Yup, this code hasn’t changed at all. Refactoring the single container into a container
hierarchy did not affect the consuming code. Cool, right? And that wraps up going
through Koober’s use of DI!
Congratulations; you made it to the end! By practicing all the techniques you saw in
this chapter, you’ll become a DI master in no time. Everything you learned in this
chapter is the foundation needed to design well-architected object-oriented software.
That’s right — you’ll even be able to use these techniques outside of mobile
development. Taking the time to solidify your comfort level with DI will pay off big
time. Make sure you have a good understanding of DI before moving on to the next
chapters so that you can easily navigate the sample codebases.
Key points
• The iOS SDK is object oriented; therefore, you use object-oriented techniques to
design well-architected iOS apps.
• There are many beneficial goals including testability and maintainability that can
be achieved by managing object dependencies.
raywenderlich.com 113
Advanced iOS App Architecture Chapter 4: Objects & Their Dependencies
• Dependency Injection (DI) is all about providing dependencies from the outside of
objects.
• There are three types of DI: Initializer, property and method injection.
• You saw how to apply DI four ways: On-demand, Factories, Single Container and
Container Hierarchy.
• When applying the DI pattern, your goal is to construct an app’s entire object graph
upfront, starting with a root object, typically the root view controller.
• Dependency Injection in .NET by Mark Seemann. Although this book uses .NET for
example code, the material is applicable to any object-oriented language. This is
probably the most thorough treatment of DI available.
If you’re interested in learning how to apply DI using a library, check out Gemma
Barlow’s tutorial on Swinject: Swinject Tutorial for iOS: Getting Started.
Which chapter should you read next? Since the next chapters do not build upon each
other, it’s really up to you which chapter you dive into next. Enjoy!
raywenderlich.com 114
5 Chapter 5: Architecture:
MVVM
By Josh Berlin & René Cacheaux
Model-View-ViewModel (MVVM) is the new trend in the iOS community, but its roots
date back to the early 2000s at Microsoft. Yes, you read that correctly! Microsoft.
Microsoft architects introduced MVVM to simplify design and development using
Extensible Application Markup Language (XAML) platforms, such as Silverlight.
Prior to MVVM, designers would drag and drop user interface components to create
views, and developers would write code for each view specifically. This resulted in the
tight coupling between views and business logic — changing one typically required
changing the other. Designers lost freedom due to this workflow: They became hesitant
to change view layouts because, doing so, often required massive code rewrites.
Microsoft specifically introduced MVVM to decouple views and business logic. This
alleviated pain points for designers: They could now change the user interface, and
developers wouldn‘t have to change too much code.
Fast forward to iOS today, and you‘ll find that iOS designers usually don‘t modify Xcode
storyboards or auto layout constraints directly. Rather, they create designs using
graphical editors such as Adobe Photoshop. They hand these designs to developers,
who, in turn, create both the views and code. Thereby, the goals of MVVM are different
for iOS.
MVVM isn‘t intended to allow designers to create views via Xcode directly. Rather, iOS
developers use MVVM to decouple views from models. But the benefits are the same:
iOS designers can freely change the user interface, and iOS developers won‘t need to
change much business logic code.
raywenderlich.com 115
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
What is it?
MVVM is a “reactive” architecture. The view reacts to changes on the view model, and
the view model updates its state based on data from the model.
• The model layer contains data access objects and validation logic. It knows how to
read and write data, and it notifies the view model when data changes.
• The view model layer contains the state of the view and has methods to handle user
interaction. It calls methods on the model layer to read and write data, and it notifies
the view when the model‘s data changes.
• The view layer styles and displays on-screen elements. It doesn‘t contain business
or validation logic. Instead, it binds its visual elements to properties on the view
model. It also receives user inputs and interaction, and it calls methods on the view
model in response.
As a result, the view layer and model layer are completely decoupled. The view layer
and model layer only communicate with the view model layer.
raywenderlich.com 116
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Model layer
The model layer is responsible for all create, read, update and delete (CRUD)
operations.
You can design the model layer in many different ways; yet, two of the most common
are push-and-pull and observe-and-push designs:
• Push-and-pull designs require consumers to ask for data and wait for the response,
which is the “pull” part. Consumers can also update model data and tell the model
layer to send it, which is the “push” part.
Repository pattern
Repositories contain data access objects that can call out to a server or read from disk.
raywenderlich.com 117
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The repository pattern provides a façade for networking, persistence and in-memory
caching. This façade creates, reads, updates and deletes data on disk and in the cloud.
The repository doesn‘t expose to consumers how it retrieves or stores the data.
When combined with MVVM, view models use the repository façade, instead of
performing these operations themselves. In turn, view models transform and expose
model data to views to display on-screen.
Repository structure
The repository provides a set of asynchronous CRUD methods. The underlying
implementations can be either stateless or stateful. Stateless implementations don‘t
keep data around after retrieving it, whereas stateful implementations save data for
later. The components are usually stateful and keep data in-memory for quick access.
Under the hood, the repository has multiple layers of data access. Each implementation
of a repository may implement all or only one of these layers:
• The cloud-remote-API layer makes calls to a server to read and update data. This
may make REST calls, get data from a socket connection or another means. The data
at this layer always comes from outside of the app.
• The persistent-store layer puts data in a local database. The database can be Core
Data, Realm or a Plist file on disk. The data at this layer always comes from the app.
The data gets persisted after the app closes.
• The in-memory-cache layer stores data in objects that stay around for the lifetime
of the repository. The cache doesn’t persist between app sessions. The in-memory
cache is useful for showing pre-fetched data before making a network call to the
cloud.
Example: KooberUserSessionRepository
In Koober, signing up or signing in creates a new session. The session contains the
current user‘s authentication token and metadata, such as name and avatar.
When a new user signs up, the KooberUserSessionRepository calls out to the Koober
Cloud REST API, creates a user session from the response, and finally saves the user
session to a persistent store.
raywenderlich.com 118
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
This all happens under the covers. KooberUserSessionRepository exposes none of the
internal implementation to its consumers. In particular, KooberUserSessionRepository
API exposes nothing about where the data comes from. The underlying implementation
could change to call out to a different REST API and store the data in-memory only. If it
did, the API would still stay the same and consumers wouldn‘t be impacted.
The sign-in API takes in an email and password, and it asynchronously returns a user
session object. The API’s caller only cares that they get the most up-to-date user
session. They don’t care whether or not the user session comes from an in-memory
store, a cloud API or a persistent store.
Repositories allow flexibility in your implementations, while keeping the user interface
layer stable. Repository implementations can change due to new project requirements.
The callers of the user repository methods never change, regardless of the
implementation. If your company asks you to switch from REST to Protocol Buffers, do
you panic or do you keep calm and refactor? The flexibility makes your app more stable,
less prone to bugs and requires less refactoring when implementations do inevitably
change.
View layer
A view is a user interface for a screen. In MVVM, the view layer reacts to state changes
through bindings to view model properties. It also notifies the view model of user
interaction, like button taps or text input updates.
The purpose of the view is to render the screen. It knows how to layout and style the
user interface elements, but doesn‘t know anything about business logic.
In MVVM, you use one-way data binding to bind the UI elements from the view to the
view model. This means the view model is the single source of truth. The view doesn’t
update until the view model changes its state.
The view layer contains a hierarchy of views. Each parent view knows about its children
and has access to their properties.
raywenderlich.com 119
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The view model knows how to handle user interactions, like button taps. User
interactions map to methods in the view model. The methods do some work, like
making an API call, and then change the state of the view model. The state update
causes the view to react.
The purpose of the view model is to decouple the view controller from the view. Ever
heard of the “massive view controller problem”? Does your view controller file seem to
scroll forever? View models are here to help. They are completely separate from the
view controller, and they know nothing about its implementation.
You can replace your entire view with a different layout without changing the view
model. View models give MVVM big wins in testability, since you can test them without
a user interface.
Kickstarter wrote its iOS app using view models. It has over 1,000 view model tests. On
its blog, Kickstarter writes, “We write these as a pure mapping of input signals to output
signals, and test them heavily, including tests for localization, accessibility, and event
tracking.” This idea of “pure mapping” is at the core of MVVM. View models take input
signals and produce output signals, providing a clear boundary between view models
and views.
Next, you‘ll learn about the structure of a view model in more depth.
• View State is stored in the view model. The state is made up of public observable
properties. Using RxCocoa, the user interface elements bind themselves to the
observables when the view model is created.
• Task Methods perform tasks in response to user interactions. The methods do some
work, such as calling a sign-in API, and then updating the view model‘s state. The
view knows if the state changes because the observables signal new data. You usually
mark task methods as @objc methods, because you have to target-action pair on a UI
Control.
• Dependencies are passed to the view model through initializer injection. Task
methods rely on the dependencies to communicate with other subsystems in the
app, such as a REST API or persistent store. View models know how to use the
dependencies, but have no knowledge of the underlying implementations.
View models sometimes use other view models to change state across the app. In this
case, other view models are injected using initializer injection. You‘ll cover how to
signal out to other view models in the code example sections.
raywenderlich.com 120
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
• The SignedInResponder handles a successful sign in. It signals out to switch app
state from onboarding to signed in.
• The emailInput and passwordInput observables bind to the text fields in the view.
They update each time you enter new text in the text fields.
• The errorMessages observable contains a list of strings. The view presents the error
each time you add a new string to the list.
The only task method in the sign-in view model is signIn(). The method asks the
UserSessionRespository to sign in using values in emailInput and passwordInput. If
sign in succeeds, the view model gives the SignedInResponder the new user session. If
the sign in fails, the view model adds the error to errorMessages and the view displays
the error.
In Koober, view controllers create the view and the view model inside loadView(). The
view controller creates the view model first and passes it to the view. Since Koober
creates view layouts in code, views can have a custom initializer.
If you use Interface Builder to create view controllers and views, view controllers would
contain implicitly unwrapped view model variables. Dependency containers would
inject view models into view controllers using property injection instead of initializer
injection. View controllers would pass the view model to the View inside viewDidLoad()
or awakeFromNib().
raywenderlich.com 121
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Container views
Each screen in Koober has a container view — a top-level view that contains other child
views. The container view‘s purpose is to build a complex screen out of modular views.
Instead of throwing all the user interface into one massive view, keep your views small,
focused and reusable.
Child views limit the responsibility of the top-level container view. The number of child
views needed depends on the screen‘s complexity. Each child view is reusable and
performs all its work independently.
Why not throw everything in one massive container view? What if you don’t need to
reuse the view anywhere else in the app? It doesn‘t matter if the view is reusable.
What‘s important is moving the coordination of view code out of the view model. This
lets you change the structure of the app without having to change code inside every
view model.
A view model shouldn‘t know how things work at a higher level. It shouldn‘t make
assumptions or tightly couple itself to coordination code. This makes view coordination
code easier to change and allows developers to work together without stepping on each
other‘s toes.
This allows one developer to work on a single screen while another developer works on
coordinator screens. They can even make changes in parallel, which is pretty cool!
raywenderlich.com 122
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The pick-me-up screen contains the meat of the Koober app — the map, the available
ride options, and pick-up and drop-off location selections. This is a ton of functionality.
If we wrote all this functionality in one view controller, the file would be massive.
raywenderlich.com 123
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Container views help us organize all the functionality and design each piece
independently.
The map and ride-option picker are child views that can live on their own. In Koober,
they only live in the pick-me-up screen, but they get the advantage modular view
design. One developer could build out the entire map screen while another builds the
ride-option picker. Then, the pick-me-up screen adds them to the view hierarchy in the
proper place.
When view models signal out, they communicate what occurred rather than what to do.
The application decides what to do. This provides flexibility — you can change how the
app responds without changing the view model.
• Closures: For signaling out, view models take a closure as an initializer argument.
The view model calls the closure when an event occurs.
• Protocols: Each output signal is modeled with a single method protocol. Other view
models that want to respond to outgoing signals must conform to the protocol. You
should use a single protocol per signal; otherwise, you force the conformer to place
all response logic into a single object.
raywenderlich.com 124
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Navigating
For the onboarding flow in Koober, tapping the Sign In button pushes the sign-in
screen onto the navigation stack.
In MVVM, the same flow is more complicated. Remember, the view calls task methods
on the view model to get work done — even navigation. The Sign In button tap tells the
view model what happened. Next, the view model circles back and tells the view to
navigate to the sign-in screen. This indirection is weird but, in pure MVVM, view
models handle user interaction.
In this section, you‘ll look at how to drive navigation between screens, manage view
state during navigation and manage scopes when navigating.
Model-driven navigation
In model-driven navigation, view models contain a view enum describing all possible
navigation states. The system observes this and navigates to the next screen when the
value changes.
Container views and container view models handle navigation for their children.
Children view models signal out to the container view model that handles navigation at
raywenderlich.com 125
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
• Collaborating view models signal to each other when a view enum value changes.
Child view models get injected with a higher level view model and call task methods
when navigation should occur.
• Shared observable view state holds a mutable Observable subject property with
the current view enum value. A dependency container injects child view models with
the Observable subject. Any child view model can push a new view enum value. The
container view model observes the value and navigates to the next screen when it
changes.
System-driven navigation
System-driven navigation is any navigation managed by the system. For example,
gestures that trigger scroll view page navigation, or tapping a Back button in a
navigation stack, automatically navigate the user to the previous screen.
In pure MVVM, you override all these gestures, and the view model handles the user
interaction. For most apps, this is overkill. A better option is to work with the system
and leverage what’s already designed for you by Apple. As soon as an MVVM
implementation causes friction with the built-in system paradigms, consider using an
MVVM implementation that combines model-driven navigation and system-driven
navigation.
Combination
You can use built-in, system-driven navigation to your advantage, while still
implementing an MVVM architecture. For example, you can use model-driven
navigation to move a navigation stack forwards and use system-driven navigation to
move backwards.
The onboarding view model switches between three states: welcome, sign in, and sign
up. Tapping the Sign In button on the welcome screen changes the view model state to
“sign in.” The onboarding view reacts to the change, and it pushes the sign-in screen
onto the navigation stack. Tapping the Back button on the sign-in screen‘s navigation
bar uses system-driven navigation to pop the screen from the stack.
Managing state
raywenderlich.com 126
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Some navigation schemes create new views when navigating, and other schemes hold
onto views and reuse them.
The system deallocates views each time the application dismisses them. When the
application creates them again, the view model populates the view with the correct
initial state. With this option, you don’t need to worry about state changes when the
view is offscreen.
In Koober, you get the user’s current location before showing the map screen.
raywenderlich.com 127
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
That operation might take a couple seconds, so you show the finding-your-location
screen while it‘s in progress.
Tab bars hold onto a list of view controllers that live in memory. Navigation controllers
reuse views when moving backwards in the stack.
Now that you know the core MVVM concepts, you’ll have much more fun in the code
example section.
raywenderlich.com 128
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The code example section covers three important real-world use cases for MVVM:
• In Building a view, you’ll learn how to create the Koober sign-in screen’s model
layer, view model layer, and view layer.
• In Composing views, you’ll learn how to request a Koober ride on the map screen.
You’ll learn how to build the ride option selector, the map screen, and how they
communicate with each other using view models.
• In Navigating, you’ll learn how to drive navigation with view models, and how the
map screen navigates between views. Also, you‘ll learn how the user’s profile info is
modally presented from the map screen.
Building a view
The sign-in screen allows you to authenticate with Koober. The initial state shows
placeholders for empty email and password fields. The Sign In button is always active,
raywenderlich.com 129
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
even when the text fields are empty. Tapping the button validates the email and
password, and shows an error if either field is empty or if the API call returns an error.
While the sign-in API request is in progress, the screen displays a spinner and disables
the user interface.
Note: Most of the code snippets are subsets of the full files. Feel free to open 05-
architecture-mvvm/final/KooberApp/KooberApp.xcodeproj while reading if
you’d like to follow along and check out the full source.
Model layer
The sign-in model layer does most of the authentication work. It authenticates with the
Koober server and persists the user session.
The sign-in model layer uses the repository pattern for accessing data — specifically,
the UserSessionRepository protocol:
UserSessionRepository has methods for reading the user session and authenticating a
user. For the sign-in screen, you‘ll call signIn(email: password:).
All the Repository methods return a promise with a UserSession object. You use
PromiseKit, a third-party framework, to create promises. A promise allows the caller to
return from the method immediately and expect either a success or failure. You‘ll learn
more about how to use promises in the View model layer section below.
raywenderlich.com 130
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
UserSession is a simple class that contains a profile and a user session. UserProfile
contains metadata about the user. RemoteUserSession contains an AuthToken, a
typealisased String.
That’s it for the model layer! The repository pattern is awesome because the actual
underlying implementation of the UserSessionRepository doesn’t matter to the callers
of the protocol methods.
init(userSessionRepository: UserSessionRepository,
signedInResponder: SignedInResponder) {
self.userSessionRepository = userSessionRepository
self.signedInResponder = signedInResponder
}
// Observables go here
// Task Methods go here
}
protocol SignedInResponder {
func signedIn(to userSession: UserSession)
}
• UserSessionRepository authenticates the user as you saw above in the Model layer
section.
raywenderlich.com 131
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
SignInViewModel contains the entire state of the sign-in view. The view binds its user
interface elements to the behavior subjects, and the view model updates them
internally.
The emailInput and passwordInput behavior subjects are bound to the view‘s email and
password fields. To sign in, they both must contain non-empty values.
raywenderlich.com 132
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The view model tells the user-session repository to sign in using the email and
password values. On success, the view model notifies the signed in responder of the
new user session object. On error, the view model updates the errorMessage behavior
subject with the error message.
Next, look at how the sign-in view model implements task methods:
@objc
public func signIn() {
indicateSigningIn()
let (email, password) = getEmailPassword()
userSessionRepository.signIn(
email: email,
password: password)
.done(signedInResponder.signedIn(to:))
.catch(indicateErrorSigningIn)
}
The sign-in view calls the signIn() task method when the user taps the Sign In button.
The method is marked @objc since the view adds a target / action pair to this selector
for its Sign In button.
func indicateSigningIn() {
emailInputEnabled.onNext(false)
passwordInputEnabled.onNext(false)
signInButtonEnabled.onNext(false)
signInActivityIndicatorAnimating.onNext(true)
}
The indicateSigningIn method disables all the user interface controls by setting their
enabled behavior subjects to false. The method also shows the spinner by updating the
signInActivityIndicatorAnimating behavior subject to true.
raywenderlich.com 133
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The view model doesn‘t care how the sign-in view reacts to these state changes. The
view can contain an UIActivityIndicatorView or custom spinner.
This is cool because the sign-in business logic is completely separate from the user
interface implementation.
Next, the method asks the UserSessionRepository to sign the user in using the value of
the emailInput and passwordInput behavior subjects. The signIn() immediately
method returns a promise containing either a valid UserSession or an Error.
In the success case, signedInResponder updates the app with the new UserSession.
In the error case, indicateErrorSigningIn() updates the view model state using the
generated Error.
raywenderlich.com 134
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
This method re-enables all the user interface controls by setting their enabled behavior
subjects to true.
raywenderlich.com 135
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
One more thing: You’ll notice, when looking at the view model files in the sample
project, that none of them import UIKit and are completely independent of UIKit. This
ensures you can test the view model logic without access to any UIKit elements.
View layer
We created all Koober root views in code instead of using storyboards. The kangaroos
made us do it! No, really, there‘s a valid reason for this. Root views get a view model
injected on initialization. Using storyboards, this would be impossible. Also, in-code
constraint creation is a lot easier these days. But that‘s a debate for another day.
SignInRootView is a UIView subclass that contains all the sign-in UI: email and
password text fields, the Sign In button and an activity-indicator spinner.
init(viewModelFactory: SignInViewModelFactory) {
self.viewModelFactory = viewModelFactory
raywenderlich.com 136
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
super.init()
}
protocol SignInViewModelFactory {
func makeSignInViewModel() -> SignInViewModel
}
View controllers don’t know how to create view models. View models have
dependencies that are outside of the view controller‘s scope. So you inject factories into
view controllers that know how to create view models.
Note: Creating view model factories is out of the scope for this chapter. But, if
you’d like to explore the code, the SignInViewModelFactory implementation is in
Koober_iOS/iOSApp/Onboarding/
KooberOnboardingDependencyContainer.swift.
That’s it for the view controller‘s responsibility in MVVM. The root view handles the
user interface updates, and the view controller manages the view‘s lifecycle.
raywenderlich.com 137
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
SignInRootView has a custom initializer that takes a frame and a view model. It uses the
injected SignInViewModel to bind its UI elements to observables in initialization.
// SignInRootView
func bindTextFieldsToViewModel() {
bindEmailField()
bindPasswordField()
}
func bindEmailField() {
emailField.rx.text
.asDriver()
.map { $0 ?? "" }
.drive(viewModel.emailInput)
.disposed(by: disposeBag)
}
func bindPasswordField() {
passwordField.rx.text
.asDriver()
.map { $0 ?? "" }
.drive(viewModel.passwordInput)
.disposed(by: disposeBag)
}
The bindEmailField() method binds the emailField text observable to the view
model‘s emailInput behavior subject. Anytime the user enters text in the email text
field, the emailInput value changes. The passwordField behaves the same way.
The bind functions use the text field‘s rx.text properties to drive the view model‘s
corresponding behavior subjects. Binding the text field to the subject means the subject
always contains a valid text value. The map function returns an empty string if the text
is nil to ensure the value is always valid.
Next, let‘s look at how the view binds the view model‘s subjects to its views.
// SignInRootView
func bindViewModelToViews() {
bindViewModelToEmailField()
bindViewModelToPasswordField()
bindViewModelToSignInButton()
bindViewModelToSignInActivityIndicator()
}
func bindViewModelToEmailField() {
viewModel
.emailInputEnabled
.asDriver(onErrorJustReturn: true)
.drive(emailField.rx.isEnabled)
.disposed(by: disposeBag)
}
raywenderlich.com 138
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The binding is pretty simple. The view binds the isEnabled flag on the emailField to
the view model‘s emailInputEnabled subject. When emailInputEnabled changes,
emailField enables or disables.
SignInRootView has one more thing remaining to complete its set up: Bind the Sign In
button action to the view model‘s sign-in task method:
func wireController() {
signInButton.addTarget(
viewModel,
action: #selector(SignInViewModel.signIn),
for: .touchUpInside)
}
Composing views
The pick-me-up screen is the heart of the Koober app. This is where the ‘roos hop
around and fulfill their ride-sharing destinies.
raywenderlich.com 139
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Here, you select where you want a Koober to take you and select your Koober ride type.
The map displays pins for your pick-up and drop-off locations, and the bottom
container shows the ride-option picker.
In this example, you‘ll look at the flow of selecting a ride option after you select a drop-
off location.
raywenderlich.com 140
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The children notify PickMeUpViewModel when the user interacts with their views. They
don‘t communicate directly with the parent container view.
Next, look at how RideOptionPickerViewController signals out when you select a new
ride option.
// MARK: Dependencies
let imageCache: ImageCache
let pickupLocation: Location
let viewModelFactory: RideOptionPickerViewModelFactory
init(pickupLocation: Location,
imageCache: ImageCache,
viewModelFactory: RideOptionPickerViewModelFactory) {
self.pickupLocation = pickupLocation
self.imageCache = imageCache
self.viewModelFactory = viewModelFactory
super.init()
}
}
raywenderlich.com 141
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
You’ll notice that pickupLocation is a constant. If it changes, the entire view controller
gets destroyed and created again:
That‘s it for the view controller. It creates its root view and loads the ride options at the
user‘s pick-up location. All the user interaction happens in the root view, which
communicates with the view model.
raywenderlich.com 142
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The segmented control configures each ride-option button ride to notify the
RideOptionPickerViewModel when the the ride option selection changes. Each button‘s
didSelectRideOption closure fires on tap, and it calls func select(rideOptionID:
RideOptionID) with the new id.
Since the view model gets injected into the view, it has no clue how the underlying
method implementation works. The segmented control only knows how to make calls
to the view model‘s select ride-option task method.
raywenderlich.com 143
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
protocol RideOptionDeterminedResponder {
func pickUpUser(in rideOptionID: RideOptionID)
}
Next, circle back to the PickUpViewModel and see how you use the responder to update
the pick-me-up screen.
raywenderlich.com 144
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
1. initial displays the map with an initial hardcoded pick-up location. Koober
currently supports one pick-up spot. The select ride-option picker is hidden in this
state.
3. selectRideOption displays the select ride-option picker. The Confirm Ride button is
initially hidden until you select a ride option.
4. confirmRequest displays the select ride-option picker with one option highlighted,
as well as the Confirm button.
enum PickMeUpRequestProgress {
case initial(pickupLocation: Location)
case waypointsDetermined(waypoints: NewRideWaypoints)
case rideRequestReady(rideRequest: NewRideRequest)
}
raywenderlich.com 145
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
PickMeUpViewModel makes the state transition as soon as the user selects one of the ride
options.
// Observables
public var view: Observable<PickMeUpView> {
return viewSubject.asObservable()
}
private let viewSubject: BehaviorSubject<PickMeUpView>
// Internal state
var progress: PickMeUpRequestProgress
// RideOptionDeterminedResponder implementation
func pickUpUser(in rideOptionID: RideOptionID) {
raywenderlich.com 146
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The View reacts to the state change, and it displays the Confirm button.
raywenderlich.com 147
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Navigating
This section is all about navigation. You‘ll learn different techniques for driving
navigation, how to manage initial view state on navigation and managing scopes when
transitioning from onboarding to signed in.
Driving navigation
Koober uses three main techniques for driving navigation:
• Model-driven navigation: View model state changes drive transitions in the user
interface.
raywenderlich.com 148
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Model-driven navigation
The transitions from the map to the drop-off selection screen and drop-off selection
screen back to the map use model-driven navigation.
In the initial state, the pick-me-up screen shows your pick-up location, but no drop-off
location. Tapping the Where to? button brings up a screen to select the drop-off
location.
After you select a location, the screen dismisses, and the pick-me-up screen shows the
selected drop-off location along with the ride-option picker.
The pick-me-up view model changes the current view state, and the view performs the
navigation. You might remember the PickMeUpView enum from the Composing views
section:
raywenderlich.com 149
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
The view model‘s pick-me-up view starts in the initial state, and switches to
selectDropoffLocation when the user taps the Where to? button.
// MARK: Dependencies
let viewModel: PickMeUpViewModel
func bindWhereToButtonToViewModel() {
whereToButton.addTarget(
viewModel,
action: #selector(
PickMeUpViewModel.
showSelectDropoffLocationView),
for: .touchUpInside)
}
}
The View binds the Where to? button to the view model‘s
showSelectDropoffLocationView() method.
class PickMeUpViewModel {
Next, let‘s look at how the view controller observes the state changes.
// MARK: Factories
raywenderlich.com 150
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
func presentDropoffLocationPicker() {
let viewController =
viewControllerFactory.
makeDropoffLocationPickerViewController()
present(viewController, animated: true)
}
}
That’s it! View models update the view‘s current state, and the view reacts to state
changes.
extension DropoffLocationPickerContentRootView:
raywenderlich.com 151
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
UITableViewDelegate {
func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
do {
let selectedLocation =
try searchResults.value()[indexPath.row]
viewModel.select(
dropoffLocation: selectedLocation)
} catch {
fatalError("Error reading value from search results subject.")
}
}
}
The pick-me-up view controller still needs to know to dismiss the select drop-off
location screen. To accomplish this, the DropoffLocationPickerViewModel needs to
notify the PickMeUpViewModel.
protocol DropoffLocationDeterminedResponder {
func dropOffUser(at location: Location)
}
raywenderlich.com 152
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Next, look at how the PickMeUpViewModel responds to the new drop-off location:
When the user selects a new drop-off location, PickMeUpViewModel updates viewSubject
state to .selectRideOption:
presentRideOptionPicker()
}
}
Finally, the view controller reacts to the state change by dismissing the select drop-off
location screen and showing the ride option picker.
raywenderlich.com 153
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Model-driven navigation decouples child view controllers from the navigation flow.
Children live on their own and signal user interactions by calling task methods on their
view models. The container view controller handles high-level navigation.
raywenderlich.com 154
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
System-driven navigation
Koober doesn‘t use pure system-driven navigation anywhere in the app. So leave
Koober land for a bit and take a look at a simple UITabBarController example.
tabBarController.selectedViewController = secondViewController
The UITabBarController contains two child view controllers. The tab bar controller sets
its select view controller to the second child.
The tab bar uses system-driven navigation to switch between its child view controllers.
The tab bar holds on to firstViewController and secondViewController for its entire
lifecycle. When selectedViewController changes, the tab bar handles transitions to the
correct view controller.
raywenderlich.com 155
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
Combination
The onboarding screen uses model-driven navigation from the welcome screen to the
sign-in screen, and it uses system-driven navigation backwards to the welcome screen.
NavigationAction tells the view whether or not it needs to be presented or whether it‘s
finished presenting. This allows the view to update the user interface when the
navigation transition completes.
OnboardingView has three possible states: welcome, sign in and sign up.
raywenderlich.com 156
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
func navigateToSignUp() {
_view.onNext(.present(view: .signup))
}
func navigateToSignIn() {
_view.onNext(.present(view: .signin))
}
func subscribe(
to observable: Observable<OnboardingNavigationAction>) {
observable
.distinctUntilChanged()
.subscribe(onNext: { [weak self] action in
guard let strongSelf = self else { return }
strongSelf.respond(to: action)
}).disposed(by: disposeBag)
}
func respond(
raywenderlich.com 157
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
to navigationAction: OnboardingNavigationAction) {
switch navigationAction {
case .present(let view):
present(view: view)
case .presented:
break
}
}
func presentSignIn() {
pushViewController(signInViewController, animated: true)
}
}
The onboarding view controller doesn’t handle the navigation backwards when the user
taps the Back button in the sign-in screen.
OnboardingView state still needs to update to .welcome after the sign-in screen gets
dismissed. Onboarding view controller updates the state using
UINavigationControllerDelegate methods:
extension OnboardingViewController:
UINavigationControllerDelegate {
viewModel.uiPresented(onboardingView: shownView)
raywenderlich.com 158
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
}
}
Onboarding view controller updates state anytime a view controller gets shown on the
navigation stack. The onboardingView(associatedWith: UIViewController) method
returns the view state depending on the view controller‘s type. After the backwards
transition back to the welcome screen, the view model view state is set back
to .welcome.
Managing state
When you navigate between screens, there are two ways to manage state:
• Create a new view each time the application presents a new screen.
The main app navigation that navigates from the getting-location screen to the pick-
me-up screen to the waiting-for-pick-up screen is an example of creating new views on
navigation.
raywenderlich.com 159
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
protocol SignedInViewControllerFactory {
func makeGettingUsersLocationViewController() ->
GettingUsersLocationViewController
// MARK: Factories
let viewControllerFactory: SignedInViewControllerFactory
raywenderlich.com 160
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
func presentWelcome() {
pushViewController(welcomeViewController,
animated: false)
}
func presentSignIn() {
pushViewController(signInViewController,
animated: true)
}
}
The onboarding view controller must ensure the welcome screen is in the correct state
when the navigation stack pops the sign-in screen.
raywenderlich.com 161
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
When the user signs in, you switch the scope from unauthenticated to authenticated. At
this point, you can destroy and deallocate all onboarding screens and create a new map
screen.
You can find the view controller files in Koober_iOS/iOSApp and Koober_iOS/iOSApp/
Onboarding:
raywenderlich.com 162
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
SignedInviewController contains the map, ride-option picker, and displays the user-
profile screen.
if let signedInViewController =
self.signedInViewController {
self.remove(
childViewController: signedInViewController)
self.signedInViewController = nil
}
}
self.onboardingViewController = onboardingViewController
}
}
In the onboarding flow, the signed-in screen doesn’t exist. That screen requires a valid
user session as a dependency — during the onboarding flow, no valid user session
exists.
let signedInViewControllerToPresent:
SignedInViewController
if let vc = self.signedInViewController {
signedInViewControllerToPresent = vc
} else {
signedInViewControllerToPresent =
viewControllerFactory.
makeSignedInViewController(
session: userSession)
self.signedInViewController =
signedInViewControllerToPresent
}
addFullScreen(childViewController:
signedInViewControllerToPresent)
if onboardingViewController?.
presentingViewController != nil {
onboardingViewController = nil
raywenderlich.com 163
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
dismiss(animated: true)
}
}
After the app switches from non-authenticated to authenticated scope, only the
SignedInViewController exists. For any UI that existed in the non-authenticated scope,
the application tears it down and deallocates it.
Key points
• The model layer reads and writes data to disk and tells the view model when data
has changed.
• The view model layer contains all the view layer‘s state and handles user
interactions. The view model listens for change in the model layer and updates its
state.
• The view layer reacts when view model state changes and tells the view model when
the user interacts with its components.
• Repositories are a façade for networking and persistence. View models use
repositories for data access instead of performing the actions themselves.
• The view layer and model layer are completely decoupled. They each only
communicate with the view model layer.
2. View and model are completely decoupled from each other. View model talks to the
view and model separately.
raywenderlich.com 164
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
3. MVVM helps parallelize developer workflow. One team member can build a view
while another team member builds the view model and model. Parallelizing tasks
gives your team’s productivity a nice boost.
4. While not inherently modular, MVVM does not get in the way of designing a
modular structure. You can build out modular UI components using container view
and child views, as long as your view models know how to communicate with each
other.
5. View models can be used across Apple platforms (iOS, tvOS, macOS, etc.) because
they don’t import UIKit. Especially if view models are granular.
Cons of MVVM
1. There are steep learning curve with RxSwift (compared to MVC.) New team
members need to learn RxSwift and how to properly use view models. Development
time may slow down at first, until new team members get up to speed.
3. Business logic is not reusable from different views, since business logic is inside
view specific view models.
5. It can be hard to trace and debug, because UI updates happen through binding
instead of method calls.
6. View models have properties for both UI state and dependencies. This means that
view models can be difficult to read, because state management is mixed with side
effects and dependencies.
raywenderlich.com 165
Advanced iOS App Architecture Chapter 5: Architecture: MVVM
1. Check out how the signed-in dependency container gets created in Koober_iOS/
iOSApp/SignedIn/KooberSignedInDependencyContainer.swift. The container
creates all the screens that require an authenticated user session.
2. Before Koober shows the map, you have to fetch the user‘s current location. Follow
the flow showing the Getting Your Location screen, before navigating to the map.
Check out:
• KooberKit/UILayer/SignedIn/GettingUsersLocation/
GettingUsersLocationViewModel.swift for the fetching location logic.
• Koober_iOS/iOSApp/SignedIn/GettingUsersLocation/
GettingUsersLocationRootView.swift for calling the view model‘s task method.
3. Look into how the drop-off-location picker search works. The drop-off-location
picker view controller contains a custom observable search UI controller, and it
binds the search input to the drop-off-location picker view model. The view model
fetches new locations using a repository, and it updates its search results state.
Check out:
• KooberKit/UILayer/SignedIn/PickMeUp/SelecDropoffLocation/
DropoffLocationPickerViewModel.swift for the view model.
• Koober_iOS/iOSApp/SignedIn/PickMeUp/SelectDropoffLocation/
DropoffLocationPickerContentViewController.swift for the view.
raywenderlich.com 166
6 Chapter 6: Architecture:
Redux
By Josh Berlin & René Cacheaux
History
At Facebook, some years ago, a bug in the desktop web app sparked a new architecture.
The app presented the unread count of messages from Messenger in several views at
once, not always presenting the same amount of unread messages. This could get out of
sync and report different numbers, so the app looked broken. Facebook needed a way to
guarantee data consistency and, out of this problem, a new unidirectional architecture
was born — Flux.
After Facebook moved to a Flux based architecture, views that showed the unread
message count got data from the same container. This new architecture fixed a lot of
these kinds of bugs.
Flux is a pattern, though, not a framework. In 2015, Dan Abramov and Andrew Clark
created Redux as a JavaScript implementation of a Flux inspired architecture. Since
then, others have created Redux implementations in languages such as Swift and
Kotlin.
raywenderlich.com 167
Advanced iOS App Architecture Chapter 6: Architecture: Redux
What is Redux?
Redux is an architecture in which all of your app’s state lives in one container. The only
way to change state is to create a new state based on the current state and a requested
change.
A Reducer changes the app’s state using the current state and an action.
Store
The Redux store contains all state that drives the app’s user interface. Think of the
store as a living snapshot of your app. Anytime its state changes, the user interface
updates to reflect the new state.
You might think storing everything in one place is insane — that’s a valid thought.
raywenderlich.com 168
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Instead of creating one massive file for the state, split it up into different sub-states.
Each screen cares about a part of the entire apps state, anyway. We’ll talk more about
keeping the store organized in the example code section of this chapter.
Types of state
A Store contains data that represents an app’s user interface (UI). Here are some
examples:
• View state determines which user elements to show, hide, enable, disable or
whether a spinner is animating.
• Navigation state determines which view to present to the user and which views are
currently presented.
• High-level state determines whether the user is signed in or signed out. Current
user profile metadata and authentication tokens could be contained in the high-level
state.
• Data from web services include things like responses from a REST API. The
response gets parsed into models and placed in the store. In Koober, the available
ride options displayed on the map live in the store.
• Formatted strings are strings that get transformed for display from raw model data
from an API.
The store is the source of truth for your app. All views get data from the same store, so
there’s no chance of two views displaying different data, as was happening during
Facebook’s bug.
Derived values
The store doesn’t contain larger files, such as images or videos. Instead, it contains file
URLs pointing to media on disk.
The entire store is in memory at all times. If your app has tons of video files or images
in the store instead of file references, iOS may crash your app to free up memory.
raywenderlich.com 169
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Sign-in screen
Onboarding displays a welcome screen where you can navigate to the sign-in or sign-up
screens. The app state determines which screen is currently shown to the user. When
the app state changes, the app presents a new screen to the user.
The Onboarding state shows the unauthenticated screens before the user logs in. The
Signed In state shows the authenticated screens after the user logs in.
1. Welcoming
2. Signing In
3. Signing Up
Welcoming displays the welcome screen, which has Sign In and Sign Up buttons. When
you tap the Sign In button, you set the app state to Signing In. When you tap the Sign
Up button, you set the app state to Signing Up.
At any moment, you can look at the state of the Redux store to determine what screen
the user interface is presenting.
• Launching loads any data that the app needs to function, like a previous user
session.
raywenderlich.com 170
Advanced iOS App Architecture Chapter 6: Architecture: Redux
• Onboarding displays the sign-in or sign-up screen so that the user can authenticate.
Once the “launching” state reads the user session, the app transitions to the “running”
state. Then, the user interface displays either the onboarding flow or the signed-in
screens. The Redux store always has an initial state and never has an invalid state.
Redux forces you to declare every possible state for your app.
If you don’t persist data between launches, that data must have an initial default in the
store. For example, Koober has ride options you can choose before requesting a ride:
Wallaby, Wallaroo and Kangaroo. These values can change, so they come from the
server. Before you download them, the initial state in the Redux store is an empty array.
The user interface should be able to gracefully handle this empty state.
Subscription
For a view to render, it subscribes to changes in the store. Each time state changes, the
view gets wholesale changes containing the entire state — there is no middle ground.
This is unlike MVVM, where you manipulate one property at a time.
raywenderlich.com 171
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Using Focused Observation, the view can subscribe to pieces of state that it’s
interested in, avoiding updates when any app state changes occur. The view still gets
the piece of state in one update.
Views need the current state from the store each time the view loads. On load, they
always have an empty state. After subscribing to the store, it fires an update and the
view re-renders.
There’s a short delay between when the app presents the view on screen and when the
subscription fires its first update. The duration is usually short enough where you don’t
notice the first update. But make sure all views can gracefully display an empty state.
Dispatching an action is the only way to change state in the Redux store. No sneaky
view can grab the store and make changes without the rest of the app finding out.
Redux works because actions change the store, and it notifies subscribers across the
app.
For example, in the Welcome screen, there are two buttons, Sign In and Sign Up. When
the Sign Up button gets tapped, you create and dispatch a Go to Sign Up action. The
store updates its state, and it notifies the OnboardingViewController. Then, the
OnboardingViewController pushes the sign-up screen onto the navigation stack.
Reducers describe possible state changes. Reducers are the step between dispatching
an action and changing the store’s state. After an action is dispatched, it travels
through a reducer. The only place the store’s state can mutate is in a reducer. Reducers
are free functions that take in the current store’s state along with an action describing a
state. They mutate a copy of the current state based on the action, and return the new
state. Reducer functions should not introduce side effects. They should not make API
calls or modify objects outside of their scope.
In addition to updating state based on actions, reducers can run business logic to
transform state. Date formatting logic lives in reducers to transform data for display.
For example, a reducer can transform a Date object to a presentable String.
Koober contains a lot of logic in reducers. It’s already enough trouble keeping view
controllers small. The last thing you need is a massive reducer file.
raywenderlich.com 172
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Redux recommends to split your reducers into sub-reducers. Sub-reducers help keep
your reducer logic focused and readable. Koober has sub-reducers for the onboarding
flow, the sign-up screen, the sign-in screen and so on.
Threading
In Redux, it’s important to run all the reducers on the same thread. It doesn’t have to be
the main thread, but the same serial queue.
If you run the reducers on multiple threads, the input state of the reducer could change
while it’s running on another thread. Redux is a synchronization point by design.
Note: In a complex app, reducers might take some time. In this case, it can be a
good idea to run reducers on another serial queue that’s not the main queue. Most
of the time, the main queue is fine.
Reducers should be pure functions, free of side effects. In Redux, you handle side
effects before dispatching actions and after the store updates.
For example, apps commonly make asynchronous API calls to a server and wait for a
response. In Redux, you never make these asynchronous API calls in reducer functions.
Instead, create multiple actions for different stages of your network request.
raywenderlich.com 173
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Before starting the network request, dispatch an In-progress action. The reducer
updates the state in the store to indicate the network request is in-progress. The view
updates its user interface to reflect the change by showing a spinner and disabling UI
elements as needed.
Next, make the network request. Once the API call completes, dispatch a Network
Request Succeeded or Network Request Failed action. The store updates its state, and
the view updates to show a success or failed message, and enables its UI elements. You
can also dispatch actions during the network requests to update percentage complete
state in the store.
Rendering updates
Redux is a “reactive” architecture. The word “reactive” is thrown around a lot these
days. In Redux, “reactive” means the view receives updated state via subscriptions, and
it “reacts” to the updates. Views never ask the store for the current state; they only
update when the store fires an update that data changed.
Diffing
Each time a view receives new state via subscription, it gets the whole state. The view
needs to figure out what changed and then properly render the update.
The simple solution is to reload the entire UI, although this might look clunky. Another
solution is to diff the new state with the current state of the UI and render necessary
updates.
Diffing helps avoid unnecessary changes. It also allows views to animate changes, since
you know exactly which user interface element changed.
UIKit sometimes won’t render unnecessary changes. You can test this by subclassing a
UIView, set a property to some test value, and check if the system calls draw rect or
needs display.
raywenderlich.com 174
Advanced iOS App Architecture Chapter 6: Architecture: Redux
• Onboarding displays the sign-in or sign-up screen when the user is not
authenticated.
• Signed-in displays the map screen after the user signs in.
Koober handles the transition from onboarding to signed-in in the main view — a
container view that can display the sign-in screen or the map screen.
2. The user enters an email and password and then taps the Sign In button.
3. The Sign-in view tells its user interaction object to sign the user in to Koober.
raywenderlich.com 175
Advanced iOS App Architecture Chapter 6: Architecture: Redux
4. The user interaction object asks its repository to make the sign-in API call.
5. Once the API call completes, the user interactions object dispatches a Signed-in
action containing the new user session.
6. The store notifies the main view to transition to Signed-in and display the map.
Example: Signing in
The sign-in screen contains a Username / Email text field, Password text field and a
Sign In button. Tapping the Sign In button signs you in using the username and
password inputs.
If a sign in succeeds, an action gets dispatched containing the new user session. If sign
in fails, an action gets dispatched containing the Error message to present.
The sign-in state contains four Boolean values and error messages:
raywenderlich.com 176
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The sign-in screen dispatches actions on user interaction, which updates the sign-in
state in the store. Redux broadcasts the new state and the sign-in screen reacts by
updating its user interface.
2. Sign-in failed to signal sign-in failed along with the error message.
3. Finished presenting error to signal the user has acknowledged the error.
Signing In Action is dispatched after the user enters a username and password, and
taps the Sign In button.
The signing-in action gets dispatched before you make the call to the Koober API to
sign in, and the reducer updates the store’s state.
raywenderlich.com 177
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Then, the store notifies the view that the state changed to signing in.
Sign-in failed action is dispatched after the Koober API returns a failed response from
the sign-in call. The state change includes the error message for the view to present.
raywenderlich.com 178
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Finished presenting error action is dispatched after the view dismisses the error
message. This state is important when you want to modify the user interface while the
error is displayed on screen. You might also want to present a second error only after
the user dismisses the first error.
Signed in action is dispatched after the Koober API returns a successful response. In
this state, the sign-in screen can transition to a success state which includes the valid
user session object.
After the user successfully signs in to Koober, the sign-in screen has no more
responsibilities. The main view transitions the app from the unauthenticated state to
the authenticated state, and shows the map screen.
Koober uses ReSwift since it’s the most established library. For more details about
ReSwift, check out the GitHub repo located at https://github.com/ReSwift/ReSwift.
Note: Most of the code snippets are subsets of the full files. Feel free to open 06-
architecture-redux/final/KooberApp/KooberApp.xcodeproj while reading if
you’d like to follow along and check out the full source.
raywenderlich.com 179
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Building a view
Before you can hop on a Kangaroo around Sydney, you have to sign in to Koober. You
sign in to the app in the sign-in screen, which contains an Email field, Password field
and a Sign In button.
Tapping the Sign In button makes an authentication call to the Koober API and signs
you in to the app.
View controller
The SignInViewController configures the SignInRootView and observes store state
changes.
...
// MARK: - Methods
init(state: Observable<SignInViewControllerState>,
userInteractions: SignInUserInteractions) {
self.state = state
self.userInteractions = userInteractions
super.init()
}
...
}
raywenderlich.com 180
Advanced iOS App Architecture Chapter 6: Architecture: Redux
You’ll notice there are no ReSwift dependencies in the view controller. You should
abstract ReSwift away from view controllers so that you can change libraries or
paradigms without needing to refractor view layer code. This section walks you through
how to do that.
public init() {}
}
The SignInViewState describes all states of the SignInRootView. The first three Boolean
values determine if user interactions are possible in the root view. The
signInActivityIndicatorAnimating value determines if the activity indicator is
spinning or hidden.
public init() {}
}
raywenderlich.com 181
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The SignInViewControllerState encapsulates the root view state and error handling.
The view controller gets its own state because it presents errors using view controller
presentation APIs.
The error messages are a collection in case there are multiple error messages to display
in succession.
The user can sign in by tapping the Sign In button after entering an email and
password. Once the user taps the button, the signIn(email:password:) method gets
called.
If signing in fails, the view controller displays an error on the screen. After the user
dismisses the error, or the error dismisses after a short period of time, the
finishedPresenting(_:) method gets called.
The sign-in view controller has no clue about the underlying implementations for these
methods. The view controller gets a concrete instance of SignInUserInteractions on
initialization.
App state
SignInViewControllerState describes the sign-in screen in isolation. But the state is
part of a larger state tree.
As you might remember from the Example: Onboarding to signed in section, Koober
has two high-level app states.
raywenderlich.com 182
Advanced iOS App Architecture Chapter 6: Architecture: Redux
In the Onboarding state, the user is unauthenticated, and Koober can present the sign-
up or sign-in screen.
In the Signed In state, the user has authenticated in the sign-up or sign-in flow.
AppRunningState describes the high level app states. Each state has its own sub-state
which contains extra information.
• Welcoming displays the welcome screen, which can navigate to the sign-up or sign-
in screen.
raywenderlich.com 183
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The signed-in state is the authenticated state, where you can request a Koober on the
map. The important piece of the state in the .signedIn app running state is the
UserSession.
The UserSession object contains information about the current authenticated user, like
the authentication token, name and avatar URL.
The signed-in state always has a valid user session — it’s a dependency of the .signedIn
state. If the user logs out, the user session gets destroyed, and the app switches back to
the .onboarding state.
Make sure your state enums with associated values behave properly when compared.
Swift 4.2 handles auto synthesizing Equatable and Hashable for most models.
Using RxSwift
Koober abstracts the ReSwift dependency from all user interface code, including
UIViewControllers and UIViews. This makes it easier to switch the Redux
implementation down the road, since none of the user interface code needs to change.
raywenderlich.com 184
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Koober still gets the benefits of ReSwift, though. It still dispatches actions and changes
state in pure reducer functions. The difference is RxSwift drives the user interface
updates instead of ReSwift store subscriptions.
RxSwift proxies
Instead of subscribing directly to the store, Koober turns ReSwift store subscriptions
into RxSwift observables. For this to work, a proxy forwards the ReSwift store
subscriber updates to RxSwift observers:
raywenderlich.com 185
Advanced iOS App Architecture Chapter 6: Architecture: Redux
First, the method creates a StoreSubscriberRxProxy that gets called whenever the
State changes. Next, a Disposable is created to handle unsubscribing from the store
when the RxSwift subscription is disposed. Then, makeObservable() creates and returns
the observable. The observable gets injected into view controllers and fires when the
store state changes.
raywenderlich.com 186
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The profile screen only needs to re-render when user profile data changes.
raywenderlich.com 187
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Let’s follow the code path for creating the user profile screen’s focused observable:
// MARK: - Properties
// State
let state: Observable<ProfileViewControllerState>
let disposeBag = DisposeBag()
// User Interactions
let userInteractions: ProfileUserInteractions
...
}
ReSwift allows you to subscribe to only a subset of the Store using the select()
method on the Subscription class. When you subscribe to the Redux Store without
using select(), you subscribe to the entire app’s state.
let stateObservable =
stateStore
.makeObservable() { subscription in
subscription.select(
self.signedInGetters.getProfileViewControllerState
)
}
return stateObservable
}
raywenderlich.com 188
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Scoped state
When using enums to model app state, views might be observing state that goes out of
scope. When an enum case changes, some part of the state tree goes away. For example,
in the pick-me-up flow, there’s an enum for the step of the ride request the user
engaged in. As the user moves through the cases, anything observing an associated
value in a changed case goes out of scope. In practice, you don’t ever want to observe
an out-of-scope state. Going out of scope means a view controller is living longer than
you designed it to live for.
The ability to detect when you go out of scope helps detect bugs. Scoping is necessary
because observables observe associated values in an enum case, and the observable has
to be able to handle when that case is no longer set.
You could make the observable data type optional, but then your view controller can
live across scopes. A view controller for one user could suddenly be sent data for
another user after logging out and in. Handling the optional case everywhere is also a
pain and makes the code less readable.
Once the state a view controller is observing goes out of scope, the observable
completes - no more events will flow through it:
raywenderlich.com 189
Advanced iOS App Architecture Chapter 6: Architecture: Redux
LaunchViewController handles the initial app launch. Since the operation to read the
user session is asynchronous, the launch screen displays until it completes.
raywenderlich.com 190
Advanced iOS App Architecture Chapter 6: Architecture: Redux
func launchApp() {
loadUserSession()
}
The user interactions object reads the persisted user session in loadUserSession() from
the injected UserSessionDataStore.
At the end of the method, the user interaction object asks the
UserSessionStatePersister to start persisting changes to the user session. When the
user signs in or signs up, the persister saves the user session to disk. When the user
signs out, the persister removes the user session from the data store.
The trick is the persister can’t start observing right away. The app needs to load the
initial state from disk first in ReduxLaunchingUserInteractions.
class ReduxUserSessionStatePersister:
UserSessionStatePersister {
let authenticationStateObservable:
Observable<AuthenticationState?>
init(reduxStore: Store<AppState>) {
raywenderlich.com 191
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Observables emit the current state when subscribing and we don’t want to persist what
is already the current state. The observable needs a .skip(1) to skip the first state
event:
func startPersistingStateChanges(
to userSessionDataStore: UserSessionDataStore
) {
authenticationStateObservable
.subscribe(onNext: { [weak self] authenticationState in
self?.on(
authenticationState: authenticationState,
with: userSessionDataStore
)
})
.disposed(by: disposeBag)
}
raywenderlich.com 192
Advanced iOS App Architecture Chapter 6: Architecture: Redux
1. The launch view controller calls launchApp() on its user interactions object.
2. Launch user interactions loads the user session from the data store.
3. After the load completes, the user interactions object tells the persister to start
saving user session changes to the data store.
5. Anytime the auth state changes, the persister saves or removes the user session
from the data store.
That’s it! The persister ensures the data store is always up to date so the user session is
ready to load on the next launch.
raywenderlich.com 193
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The only place actions get dispatched in Koober are in user interactions objects.
init(
actionDispatcher: ActionDispatcher,
remoteAPI: AuthRemoteAPI
) {
self.actionDispatcher = actionDispatcher
self.remoteAPI = remoteAPI
}
}
The action dispatcher is a protocol that exposes the ReSwift store’s dispatch action
method:
protocol ActionDispatcher {
func dispatch(_ action: Action)
}
This works, but you would have to inject the store into all your user interaction objects.
You don’t want to give them access to all the store’s methods.
raywenderlich.com 194
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Instead, user interactions object dispatch actions using the dispatcher like this:
First, the sign-in method calls indicateSignIn() which dispatches a SigningIn action.
This indicates the request is in progress and the user interface can show a spinner and
disable user interaction.
Next, the user interactions object signs in the user using the remote API.
raywenderlich.com 195
Advanced iOS App Architecture Chapter 6: Architecture: Redux
If the request succeeds, the user interactions object dispatches a SignedIn action
containing the new UserSession object. The app dismisses the sign-in screen and
transitions to the map. If the request fails, the user interactions object dispatches a
SignInFailed containing the error message. The user interface can display the error
message, hide the spinner and enable user interaction.
Rendering updates
Actions describe a state change. Let’s look at what makes up an action:
struct SignInActions {
// Internal
struct SigningIn: Action {}
// External
struct SignedIn: Action {
let userSession: UserSession
}
}
• The SigningIn action only describes a new app state, but doesn’t need any extra
data.
• The SignedIn action changes the app state to “Signed In” and contains a UserSession
object.
On its own, actions can’t change state in the store. They need to flow through a reducer
function first. A reducer takes in the current state and an action and returns a new
state.
extension Reducers {
static func signInReducer(
action: Action,
state: SignInViewControllerState?
) -> SignInViewControllerState {
var state = state ?? SignInViewControllerState()
switch action {
case _ as SignInActions.SigningIn:
raywenderlich.com 196
Advanced iOS App Architecture Chapter 6: Architecture: Redux
SignInLogic.indicateSigningIn(viewState: &state.viewState)
// Handle other cases here.
default:
break
}
return state
}
}
struct SignInLogic {
static func indicateSigningIn(
viewState: inout SignInViewState) {
viewState.emailInputEnabled = false
viewState.passwordInputEnabled = false
viewState.signInButtonEnabled = false
viewState.signInActivityIndicatorAnimating = true
}
}
The sign-in reducer takes an action and the current SignInViewControllerState and
returns a new SignInViewControllerState. If no SignInViewControllerState is
present, the reducer uses a default state.
Once the reducer returns, store updates its state. Then, the observable in the sign-in
view controller fires and the user interface updates.
The circle of Redux is complete! You’ve seen how a state change starts as an action,
gets dispatched to the store and flows through a reducer to complete the update.
Next, you’ll learn how views communicate with each other using the Redux store.
View controllers in a Redux architecture are naturally slim and focused. They fire
actions and then forget about them, and may or may not care about how those actions
affect the app’s state.
raywenderlich.com 197
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Pick-me-up screen
The PickMeUpViewController contains the meat of the Koober app. It displays the map,
the Where To? button and the ride-option picker.
It also transitions between multiple states when you are requesting a Koober:
For this example, let’s focus on the initial and selectDropoffLocation state.
Initially, the map displays a Where To? button and a preset pick-up location. Tapping
the button brings up the drop-off location-picker screen, which loads a list of possible
locations to visit in a Koober. After you select a location, the picker closes, and the map
displays the selected location.
Going over how the map displays the picker when you press the button:
raywenderlich.com 198
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The PickMeUpViewControllerState has data the view controller needs to display its user
interface. Each time the state updates, the view controller maps the PickMeUpState to
PickMeUpView state, which is easier to consume:
func presentDropoffLocationPicker() {
let viewController =
viewControllerFactory.
makeDropoffLocationPickerViewController()
present(viewController, animated: true)
}
This method gets called when the PickMeUpView state changes from initial to
selectDropoffLocation. The state transition starts in the pick-me-up root view.
// Dependencies
let userInteractions: PickMeUpUserInteractions
// User Interface
let whereToButton: UIButton
raywenderlich.com 199
Advanced iOS App Architecture Chapter 6: Architecture: Redux
// Called on initialization
func bindWhereToControl() {
whereToButton.addTarget(
self,
action: #selector(goToDropoffLocationPicker),
for: .touchUpInside)
}
@objc
func goToDropoffLocationPicker() {
userInteractions.goToDropoffLocationPicker()
}
}
The root view makes a call to the user interactions object when the user taps the Where
To? button.
func goToDropoffLocationPicker() {
let action = PickMeUpActions.GoToDropoffLocationPicker()
actionDispatcher.dispatch(action)
}
}
struct PickMeUpActions {
struct GoToDropoffLocationPicker: Action {}
}
Next, the action needs to flow through a reducer to update the store.
raywenderlich.com 200
Advanced iOS App Architecture Chapter 6: Architecture: Redux
searchResults: [],
currentSearchID: nil,
errorsToPresent: [])
state.state = .selectDropoffLocation(
initialDropoffLocationViewControllerState)
// Other actions handled here.
}
}
The last step in the process is for the PickMeUpViewController to handle the state
update:
// Called on viewDidLoad()
func observeState() {
state
.map {
(state: $0.state, sendingState: $0.sendingState)
}
.map (mapToView)
.distinctUntilChanged()
.subscribe(onNext: { [weak self] view in
self?.present(view)
})
.disposed(by: disposeBag)
}
raywenderlich.com 201
Advanced iOS App Architecture Chapter 6: Architecture: Redux
1. The pick-me-up view controller creates a root view with a user-interactions object.
4. The store runs the action through a reducer, which switches the state
to .selectDropoffLocation.
6. The pick-me-up view controller presents the select drop-off location picker screen.
The view layer reacts to state change and the tells the user-interactions object when
the user interaction happened. The view only presents or dismisses screens when the
store updates its state.
raywenderlich.com 202
Advanced iOS App Architecture Chapter 6: Architecture: Redux
• Wallabies are tiny but cheap, and you’ll get to your destination with extra cash in
hand!
• Wallaroos are reliable, and they get you to your destination on time, every time.
In the pick-me-up screen example above, you saw the state transition from .initial
to .selectDropoffLocation. After the user selects a drop-off location, the state
transitions to .selectRideOption:
raywenderlich.com 203
Advanced iOS App Architecture Chapter 6: Architecture: Redux
case .selectRideOption:
dropoffLocationSelected()
// Other cases handled here.
}
}
func dropoffLocationSelected() {
if let _ = presentedViewController as?
DropoffLocationPickerViewController {
dismiss(animated: true)
}
presentRideOptionPicker()
}
}
The pick-me-up view controller transitions to the next screen in present(_:). For the
.selectRideOption state, it calls dropoffLocationSelected() to dismiss the drop-off
location picker and to present the ride-option picker.
raywenderlich.com 204
Advanced iOS App Architecture Chapter 6: Architecture: Redux
The view controller creates its RideOptionSegmentedControl root view with its user
interactions object:
var viewState =
RideOptionSegmentedControlState() {
didSet {
if oldValue != viewState {
loadAndRecreateButtons(withSegments: viewState.segments)
} else {
update(withSegments: viewState.segments)
}
}
}
The segmented control renders the ride options segments using the
RideOptionSegmentedControlState.
The view controller state and the root view have their own states. Giving the root view
a more granular state helps keep them focused on user-interface specific state.
If you’d like to read through the entire ride option segment creation process, the full
code is at Koober_iOS/iOSApp/SignedIn/PickMeUp/SelectRideOption/
RideOptionSegmentedControl.swift.
raywenderlich.com 205
Advanced iOS App Architecture Chapter 6: Architecture: Redux
When you tap a ride option, the user-interactions object handles selecting a new ride
option ID.
class ReduxRideOptionPickerUserInteractions:
RideOptionPickerUserInteractions {
let actionDispatcher: ActionDispatcher
struct RideOptionPickerActions {
struct RideOptionSelected: Action {
let rideOptionID: RideOptionID
}
}
After the user interactions object dispatches the action, a reducer handles the state
change:
extension Reducers {
static func rideOptionPickerReducer(
action: Action,
state: RideOptionPickerViewControllerState?)
-> RideOptionPickerViewControllerState {
raywenderlich.com 206
Advanced iOS App Architecture Chapter 6: Architecture: Redux
segments[index].isSelected =
(segment.id == action.rideOptionID)
}
state.segmentedControlState.segments = segments
// Handle other actions.
default:
break
}
return state
}
}
For the RideOptionSelected action, the reducer finds the segment matching the
action’s rideOptionID, and it sets its isSelected flag to true. Then, it updates the
state’s list of ride-option segments.
Next, the store notifies the subscribers of the updated state, which causes the
Observable<RideOptionPickerViewControllerState> in the
RideOptionPickerViewController to fire:
// Called in viewDidLoad()
func observeState() {
state
.map { $0.segmentedControlState }
.distinctUntilChanged()
.subscribe(onNext: {
[weak self] segmentedControlState in
self?.rideOptionSegmentedControl.viewState =
segmentedControlState
})
.disposed(by: disposeBag)
}
}
Updating the viewState variable causes the segments to re-render, and the segmented
control highlights the selected ride option segment.
raywenderlich.com 207
Advanced iOS App Architecture Chapter 6: Architecture: Redux
1. Ride-option picker view controller creates a root view with a user interactions
object.
4. The store runs the action through a reducer, which updates the isSelected Boolean
on each of the ride option segments.
6. The ride option picker view controller re-renders the segmented control to show
the new ride-option selection.
That’s it! The ride-option segment view signals out to the store when a new ride option
is selected. The view waits for the store to finish updating its state and then re-renders.
raywenderlich.com 208
Advanced iOS App Architecture Chapter 6: Architecture: Redux
Key points
• Redux architecture keeps all your app’s state in a single store.
• An action describes a state change. The only way to change state is to dispatch an
action to the store.
• Reducers are pure functions that take an action and the current state, and they
return a modified state. The only place the state can change is in a reducer function.
• Focus your store subscriptions on pieces of the whole state using the select()
method so observables fire only when they need to.
2. Descriptive state changes are all contained in reducers. Any developer can read
through your reducer functions to understand all state changes in the app.
3. The store is the single source of truth for your entire app. If data changes in the
store, the change propagates to all subscribers.
4. Data consistency across screens is good for iPad apps and other apps that display
the same data in multiple places at the same time.
6. Redux architecture, overall, is easy to test. You can create a test case by putting the
app in any app state you want, dispatch an action and test that the state changed
correctly.
7. Redux can help with state restoration by initializing the store with persisted state.
raywenderlich.com 209
Advanced iOS App Architecture Chapter 6: Architecture: Redux
8. It’s easy to observe what’s going on in your app because all the state is centralized
to the store. You can easily record state changes for debugging.
11. Redux embraces value types. State can’t change from underneath you.
Cons of Redux
1. You need to touch multiple files to add new functionality.
3. Model layer knows about the view hierarchy and is sensitive to user-interface
changes.
4. Redux can use more memory than other architectures since the store is always in
memory.
5. You need to be careful with performance because of possible frequent deep copies
of the app state struct.
6. Dispatching actions can result in infinite loops if you dispatch actions in response
to state changes.
7. Data modeling is hard. Benefits of Redux depend on having a good data model.
8. It is designed to work with a declarative user interface framework like React. This
can be awkward to apply to UIKit because UIKit is imperative. This isn’t a blocker,
just that it’s not a natural fit. Maybe Apple will give us a declarative Swift-written
UIKit one day.
9. Since the entire app state is centralized, it’s possible to have reducers that depend
on each other. That removes modularity and encapsulation of a model / screen /
component’s state. So refactoring a component’s state type could cause complier
issue elsewhere and this is not good. You won’t run into this if you organize your
reducers to only know about a module’s state and no more. This is not constrained
by the architecture, though, so it depends on everyone being aware.
raywenderlich.com 210
Advanced iOS App Architecture Chapter 6: Architecture: Redux
1. Follow the path for making the new ride request in the pick-me-up screen. After
selecting a ride option, and confirming the ride request, the app transitions from
the sendingRideRequest(NewRideRequest) state to .final in the
PickMeUpViewController.
Start in Koober_iOS/iOSApp/SignedIn/PickMeUp/
PickMeUpViewController.swift for the user interface transitions.
2. The Koober app prints out information about every dispatched action in the
console.
raywenderlich.com 211
7 Chapter 7: Architecture:
Elements, Part 1
By Josh Berlin & René Cacheaux
This all started back in 2013 when we met working at Mutual Mobile, a mobile
development firm in Austin, Texas. The company asked us to fly to New York and work
with an iOS team at Google. It was an immediate, "Yes!" from both of us.
We weren’t sure what to expect. Google didn’t hire "iOS Engineers" at that time and
expected their engineers to switch programming languages depending on the project.
Most of the team had strong Java backgrounds and learned Objective-C specifically for
this project. They seriously impressed us with their knowledge of the iOS SDK and the
nuances of Objective-C. While we were able to teach them things like Core Animation,
they were able to teach us about software development as a whole.
Their Java background came with an ingrained idea that every project needed
dependency injection. At the time, and even today, the use of dependency injection in
iOS projects is rare. Apple doesn’t have a built-in framework to help manage
dependencies, which doesn’t help. To the Google engineers, this was absurd and they
decided to use a third-party framework called Objection to handle dependencies in the
project.
Injecting dependencies into each view controller allowed us to mock objects and write
unit tests. For this project, every change request required unit tests. No exceptions.
This was a departure from some of the more lax projects at Mutual Mobile. But it was a
good departure. Dependency injection and unit tests changed our view on iOS
development and sparked our interest in software architecture.
Back in Austin, one of the developers at Mutual Mobile started a brown bag group to
watch Robert Martin — better known as Uncle Bob — videos. His videos were full of
great insights about software architecture. We were already passionate about
architecture, but these video fueled the fire.
raywenderlich.com 212
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Uncle Bob took ideas from multiple architectures and broke them down to their core
objective: separation of concerns. He used this idea to create clean architecture, which
divides software into multiple layers including business rules and interface adapters.
At about the same time, we started working on a brand new greenfield iOS project for a
major American brand. We knew this project was going to be difficult, but we were
excited to use our new-found passion for architecture to help the team succeed. The
client asked us not only to build an awesome iOS app, but an SDK so other apps within
the company could reuse the app’s core functionality. René was the tech lead and knew
architecture would be key to the project’s success. He started diving deeper into Uncle
Bob and Micah Martin's book — Agile Principles, Patterns, and Practices in C# — to absorb
as much of the insights as possible.
The app’s core feature was a chat service. We were asked to use XMPP, Extensible
Messaging and Presence Protocol, but were told that we might have had to switch to a
different chat protocol in 3–6 months. Our first thought was, "Are you serious?" Our
next thought was the realization that, if and when we had to rewrite the chat layer, our
data storage and user interface layers shouldn’t need to be touched.
René decided the best way to separate the chat layer from the user interface was to
create two different projects: a "Core" SDK to hold the chat code and a user interface
layer, which was the actual Xcode project. This forced us to build the user interface
layer in a way that isn’t tied to a specific chat protocol, but gets handed immutable data
objects such as a chat group or chat message. The user interface layer didn't care if they
came from XMPP or MQTT, Message Queuing Telemetry Transport, or push
notifications.
The clear separation allowed engineers to work on the chat layer and the user interface
layer in parallel. We were able to make rapid changes to isolated layers of the project
without affecting other layers. The patterns developed and used throughout the app’s
source code could be applied to many other iOS projects. What resulted was Elements.
Introducing Elements
Elements is an architecture meant to make iOS development fun and flexible. Elements
organizes your codebase and makes your project easy for anyone to navigate. This
organization allows you to make changes to layers of your app without affecting
stability. A set of "Elements" make up the architecture. The cool thing is that you can
choose which pieces to use in your own apps — there’s an Element for every layer of
your app, from networking to the user interface.
raywenderlich.com 213
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Elements is our take on architecture, grabbing bits and pieces from industry best
practices. The theory is not completely original since it pulls from many sources based
on our experience. The set of Elements was created by mixing these best practices with
our ideas and has evolved over time as more architectures make their way into the iOS
world.
There shouldn’t be one and only one way to architect software. Software architecture is
an artwork with some science mixed in. For this reason, this chapter and the next is
made up of architectural elements that have worked well for us in the past in the hope
of inspiring you. Take what you like and change it if it makes sense. Use an Element as-
is or make it it your own.
There’s no such thing as one architecture to rule them all. Well-architected apps are
made up of modular components. Bits and pieces made up of different structures.
Elements is not a take it or leave it approach. Instead, Elements breaks up the different
kinds of logic that you typically find in mobile apps. You can take bits and pieces into
your own apps or come up with entirely new elements to fit your needs.
Note: We feel that it's important to emphasize that most of the concepts that
make up Elements are not new. Elements is a collection of existing best practices
that we've found most helpful when architecting iOS apps. We've taken these
practices and evolved them over time to best fit iOS development.
Elements is designed on top of some core underlying concepts. Let's take a look at these
underlying concepts.
raywenderlich.com 214
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
entities would be the blood running through their veins; they are the DNA of apps.
Entities are the only values that travel across architectural layers. By holding this true,
you end up building extremely flexible software: Software that doesn’t require you to
rewrite everything every time something changes — every time a product manager
comes up with a new feature, a UI designer wants to restyle a screen, a UX designer
wants to change a flow or a server engineer wants to change an API.
Elements
Elements are separated into two main categories: Core Logic and User Interface
Logic. Core Logic contains the app’s business logic such as retrieving data from an API
and caching data. User Interface Logic contains the presentation logic such as handling
user input and navigation. In this section, you can read a brief description of each
Element. Later in the chapter, four main Elements are covered in more detail.
raywenderlich.com 215
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Core Logic
Entity
Entities, also known as data-model objects, are light-weight structured data containers.
They don’t do anything other than store values. Entities flow throughout the app,
passed between architectural layers. They’re considered foundational to your app’s
architecture. They constitute a contract between different object interfaces. When
building an object’s interface, methods typically take in an entity object and perform
some task. Sometimes, the method returns a new or modified entity object. In Swift,
entities are best built as immutable structures.
Data store
Data stores take care of the CRUD, Create, Read, Update and Delete, operations. They
abstract away the underlying data storage mechanism you want to use. They can
implement Core Data, NSCoder and even remote data store network logic. The interface
into a data store exposes nothing about the underlying implementation. Data stores
take in and pump out entity objects, another reason entities are foundational to your
app.
Remote API
Remote APIs talk to the network. They can create the endpoint and handle the
response. Remote APIs know if you’re getting data from a cloud API like Firebase, a
custom server or even a hardcoded JSON file. View controllers don’t care where data
comes from. By moving these implementation details out of view controllers and into
remote APIs, view controller code becomes more readable. Remote APIs are normally
used to create and update data on a remote data store, but can make any type of
network call.
Use case
Use cases represent the user stories that make up your app. They have names that
everyone involved in the project can understand. If you were asked to describe what
your users can do with the app, you would be naming use cases. Use cases are the main
unit of work. Every time a user wants to do something, a use case is created and
executed. Use cases cleanly separate your app’s core logic from your app’s user interface
logic.
Think of it this way: After you build all the use cases, you should be able to build a
command line interface for your app using the use cases you’ve defined.
raywenderlich.com 216
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Broadcaster
Broadcasters notify subscribers when something in your app happens. Multiple objects
can subscribe to a single broadcaster. As an example, you could create a reusable
keyboard broadcaster that subscribes to the relevant system keyboard notifications.
Encapsulating this functionality removes the need for multiple objects to directly
subscribe to Notification Center notifications and instead conform to the broadcaster’s
protocol.
Observer
Observers are objects that receive external events. These events are input signals to
view controllers. Observers know how to subscribe to events, process events and deliver
processed events to view controllers. For example, a KeyboardObserver can process
keyboard-related notifications from Notification Center when the keyboard is shown or
hidden and knows which method to call on the view controller. You’ll learn about the
benefits of abstracting the Notification Center logic into an observer later in Chapter 8.
User interface
User interfaces are, well... user interfaces. These objects allow you to configure what is
rendered on the screen. Each view controller's view has a user interface protocol. They
expose methods such as enableSignInButton() or startEditingFirstName(). They
don’t however expose implementation details such as UIKit objects. These objects
express every possible change you can make to the user interface.
Interaction responder
User interface objects know when a user interacts with the device, taps a button or
enters some text, but do not know how to handle those events. That’s where the
interaction responder comes in. The user interface tells the interaction responder what
to do, and the interaction responder knows how to do it.
raywenderlich.com 217
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
That wraps up the introduction of Elements. In the next four sections, in this chapter
and the next, you'll take a deep dive into the four main elements: user interface,
interaction responder, observer and use case.
Note: To see examples of the other elements in action, take a look at the Elements
version of Koober's Xcode project that accompanies this chapter. If you'd like to
see other elements covered in a future edition of this book, let us know in the
book forum.
User interface
User interface objects describe the views in your app. They allow you to configure and
change what the user sees and interacts with. They usually expose methods such as
showSuccessMessage() or displayWidget(). They don’t however expose the guts of the
interface. For example, they don’t expose UILabels, UIButtons or UITextFields. User
interface objects are meant to express what the user interface is capable of doing — the
what instead of the how. The how is an implementation detail. If you do this well, you
should be able to implement a completely new design by reimplementing only the user
interface object.
Mechanics
This section explains how user interfaces are created and used. It’s meant to briefly
explain the concepts. You’ll see code examples later on.
Instantiating
You create a user interface protocol for each view. Usually each view controller’s root
view has a single user interface protocol. Concrete instances of user interfaces are
initialized with references to objects that handle user interactions. These are called
interaction responders. This is so the user interface can wire its controls to methods
that notify the interaction responder.
raywenderlich.com 218
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Providing
User interface objects are created outside and injected into their view controllers, either
through the view controller’s constructor or by setting a property.
In most cases, the user interface object is the view controller’s root view. In iOS, each
view controller already has a root UIView set as its view property by default. Usually you
want that view to conform to a user interface protocol. So, in the view controller’s
loadView() method, you set the injected user interface object to the view property.
Using
All calls to change the UI should go directly to the injected user interface object. No
calls should be made directly to the view property. The user interface protocol should
expose every possible change to the view.
Injecting the user interface object into the view controller means you can mock the
protocol object to write unit tests. You can verify that the correct user interface
methods get called at the right times. This wouldn’t work if you set your view to a
concrete instance of a UIView.
Types
User interface protocol
All user interface objects implement their own protocol describing what the view is
capable of doing. The protocol should not expose implementation details about how
the internals operate. It shouldn’t have any references to UIKit.
Next, check out the user interface protocol for the Sign-in view.
protocol SignInUserInterface {
func render(newState: SignInViewState)
func configureViewAfterLayout()
func moveContentForDismissedKeyboard()
func moveContent(forKeyboardFrame keyboardFrame: CGRect)
}
The protocol methods describe only what the view can do. render(newState:
SignInViewState) describes transitions between different states of the sign-in user
interface. configureViewAfterLayout() performs any extra configuration such as
scrolling after the view has finished laying out its subviews.
raywenderlich.com 219
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
The key takeaway here is that the user interface isn’t specific to a UIView. Any object
can conform to this protocol. You want the user interface protocol to describe every
possible configuration change a caller can make on the view, without exposing
implementation details. You’ll see this user interface in more detail below in the
example section.
One good example is a map user interface. Let’s say your company is deciding between
Apple Maps and Google Maps. In six months, you could completely remove Google
Maps and replace them with Apple Maps.
Instead of having the view controller hold on to a concrete instance of a Google map
view, GMSMapView, or an Apple map view, MKMapView, it should hold on to a
MapUserInterface that describes everything the map can do. That way, when you decide
to switch internals, the view controller doesn’t need to change, only the underlying
implementation of your MapUserInterface.
View controllers need a UIView to operate. As a compromise, you can create a typealias
that conforms to the user-interface protocol and is also a UIView. Here’s an example:
You can also mock SignInUserInterfaceView in tests and validate the right user-
interface protocol methods are called by the view controller.
raywenderlich.com 220
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
Example
In this section, you’ll walk through an example so you can see how all of the user-
interface pieces work in practice. The example is from Koober’s sign-in screen.
let stateObservable =
makeSignInViewControllerStateObservable()
let observer = ObserverForSignIn(state: stateObservable)
// 1
let userInterface = SignInRootView()
let signInViewController =
SignInViewController(
userInterface: userInterface,
signInStateObserver: observer
)
raywenderlich.com 221
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
observer.eventResponder = signInViewController
// 2
userInterface.ixResponder = signInViewController
return signInViewController
}
// ...
}
2. The dependency container method also sets the ixResponder to the view controller.
The main reason to do this here instead of in the view controller is you pass in a
SignInUserInterface and UIView object that doesn’t have a property setter for the
responder. The view controller can’t set the responder object internally, so it has to
be done at injection time on the concrete instance, SignInRootView. This responder
object gets notified when the user interacts with the view. Think of it like a
delegate. The view controller just needs to conform to the responder protocol and
implement the responder callback methods. You’ll see more about the responder in
the interaction responder section.
Creating the sign-in view controller’s dependencies in the container helps keep the
initializer clean:
// User interface
let userInterface: SignInUserInterfaceView
// ...
// MARK: - Methods
init(
userInterface: SignInUserInterfaceView,
signInStateObserver: Observer,
signInUseCaseFactory: SignInUseCaseFactory,
finishedPresentingErrorUseCaseFactory:
@escaping FinishedPresentingErrorUseCaseFactory
) {
self.userInterface = userInterface
self.signInStateObserver = signInStateObserver
self.signInUseCaseFactory = signInUseCaseFactory
raywenderlich.com 222
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
self.makeFinishedPresentingErrorUseCase =
finishedPresentingErrorUseCaseFactory
super.init()
}
// ...
}
In loadView, you set the view controller’s view property to the user-interface object.
Since userInterface is a UIView subclass, this call works fine.
protocol SignInUserInterface {
func render(newState: SignInViewState)
func configureViewAfterLayout()
func moveContentForDismissedKeyboard()
func moveContent(forKeyboardFrame keyboardFrame: CGRect)
}
// MARK: - Properties
public internal(set) var emailInputEnabled = true
public internal(set) var passwordInputEnabled = true
public internal(set) var signInButtonEnabled = true
public internal(set)
var signInActivityIndicatorAnimating = false
// MARK: - Methods
public init() {}
}
The render(newState:) method transitions between every possible state of the sign-in
user interface. SignInViewState contains flags that configure the user interface:
raywenderlich.com 223
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
The SignInViewState is pretty simple. The key takeaway is the state struct describes
every possible state of the view’s user interface. You could, of course, also accomplish
this by splitting out the state into single methods like enableEmailField() and
disableEmailField().
// ...
}
extension SignInViewController:
ObserverForSignInEventResponder
{
func received(newViewState viewState: SignInViewState) {
userInterface.render(newState: viewState)
}
func keyboardWillHide() {
userInterface.moveContentForDismissedKeyboard()
}
userInterface.moveContent(
forKeyboardFrame: convertedKeyboardEndFrame
raywenderlich.com 224
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
)
}
}
Another thing to note is there’s no layout code in the view controller. The user interface
abstracts all of its layout away from its owner and deals with updates when its notified
of a change. If you need to change the way the user interface handles keyboard updates
internally, you shouldn’t need to modify the view controller code.
// MARK: - Properties
let emailField: UITextField = {
let field = UITextField()
field.placeholder = "Email"
// Other field configuration here...
return field
}()
// MARK: - Methods
func render(newState: SignInViewState) {
emailField.isEnabled = newState.emailInputEnabled
passwordField.isEnabled = newState.passwordInputEnabled
signInButton.isEnabled = newState.signInButtonEnabled
switch newState.signInActivityIndicatorAnimating {
case true:
signInActivityIndicator.startAnimating()
case false:
signInActivityIndicator.stopAnimating()
}
}
raywenderlich.com 225
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
func moveContentForDismissedKeyboard() {
resetScrollViewContentInset()
}
The view contains all the UIKit elements needed to render the sign-in screen. The
emailField and passwordField are UITextFields and signInButton is a UIButton object,
all laid out by this view.
When the render(newState:) method gets called, the isEnabled fields are updated on
each of the elements based on state. The activity indicator also starts or stops
spinning.
The keyboard user-interface methods adjust the scroll view’s content inset based on
whether the keyboard is displayed or dismissed. This allows the input fields to stay
visible and not hide under the keyboard.
The SignInRootView code should be pretty familiar since it’s just a normal UIView. The
big difference is it must conform to the SignInUserInterface and handle those
methods properly. That’s it for the user-interface section. Next, you’ll see how the
interaction responder works.
Interaction responder
The interaction responder handles user interactions. When a user interacts with the
screen, tapping a button or performing a swipe gesture, the user interface notifies the
interaction responder and the interaction responder handles the interaction. User-
interface objects only know when the user performs an interaction. They don’t actually
know how to handle the interaction. User-interface objects don't know how to do any
non user-interface tasks by design.
The user interface tells the interaction responder what to do, not what happened. This
removes any user interface terms from the interaction responder. For example, the
responder might expose a method that says createPost() instead of
createPostButtonTapped().
raywenderlich.com 226
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
If you swap out the user interface element to sign in for something other than a button,
the interaction responder protocol stays the same.
Usually, a view controller is the user interface’s interaction responder, but you can
create a new object that handles user interactions and inject it into the view controller.
Either way works.
The standalone object is easier to test since you don’t have to create a view controller.
The view controller approach requires less code since you can just make the view
controller conform to the responder protocol. Koober uses the view controller
approach.
Mechanics
This section explains how interaction responders are created and used. It is meant to
briefly explain the concepts. You’ll see code examples further down.
Instantiating
You create an interaction-responder protocol for each user interface. Interaction
responders are then provided as a reference to the user interface. Since Koober view
controllers are the interaction responders, they get created in a dependency container.
Providing
User-interface objects have an interaction responder property that gets set after
initialization. This is so the user interface can wire its controls to methods that notify
the interaction responder on user interactions.
The view controller that conforms to the interaction responder protocol gets created
with a user interface object. After the view controller is initialized, it gets set as the
user interface’s interaction responder.
Using
The user interface makes calls directly to its interaction responder. The view controller
just needs to implement the right interaction responder methods.
raywenderlich.com 227
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
The above diagram shows how the view controller gets initialized with a user interface
and set as the interaction responder.
1. First, the dependency container creates the view controller and injects the user
interface object.
2. Then, the dependency container sets the view controller as the user interface's
interaction responder.
3. From then on, all user interactions are handled by the view controller, which
conforms to the interaction responder protocol.
Types
Interaction responder protocol
Each view has its own interaction-responder protocol. Any interaction the user can
perform on the view is described in the protocol. It doesn’t expose any details about
how the internals are implemented. The protocol should have no references to UIKit
and nothing about what kind of user interface element triggered the user interaction.
raywenderlich.com 228
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
The sign-in responder protocol is simple and contains a single method to sign in the
user. The only user interaction a user can perform in the sign-in screen is tapping the
Sign in button after entering an email and password.
1. The responder doesn’t expose any details about how the sign in was initiated, such
as tapping a sign-in button or tapping the next button on the keyboard. If you
redesign the user interface you shouldn’t have to update the interaction responder
protocol.
2. The user interface tells the interaction responder what to do, not what happened.
The method isn’t signInButtonTapped(), but rather signIn(), telling the responder
what action to perform next. The interaction responder doesn’t care about what
happened. It’s the user interface’s responsibility to convert button taps and
gestures into an actionable item for the interaction responder.
Example
In this section, you’ll walk through an example so you can see how the interaction
responder is used in practice. The example is from Koober’s sign-in screen.
raywenderlich.com 229
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
let stateObservable =
makeSignInViewControllerStateObservable()
let observer = ObserverForSignIn(state: stateObservable)
let signInUseCase = makeSignInUseCase()
let finishedPresentingErrorUseCaseFactory =
self.makeFinishedPresentingSignInErrorUseCase
let signInViewController =
SignInViewController(
userInterface: userInterface,
signInStateObserver: observer,
// 1
signInUseCase: signInUseCase,
finishedPresentingErrorUseCaseFactory:
finishedPresentingErrorUseCaseFactory
)
observer.eventResponder = signInViewController
// 2
userInterface.ixResponder = signInViewController
return signInViewController
}
// ...
}
1. The only interaction responder method signs the user in. The sign-in use case
actually performs this work. It's a dependency of the SignInViewController, so the
interaction responder method can run the use case when the user is ready to sign
in.
2. The user interface is the object that makes calls to the interaction responder. Since
the sign-in view controller gets set as the user interaction responder in the
dependency creation code, the view controller only has to implement the protocol
methods. Also, since SignInViewController only takes in a
SignInUserInterfaceView, which is a SignInUserInterface and a UIView, you have
to set the interaction responder on the concrete instance, SignInRootView. Only the
dependency creation code knows about the concrete type, so the dependency code
needs to configure the responder.
The user interface wires its components to methods that call the interaction responder.
In the SignInRootView, a sign in UIButton tap makes the call:
// MARK: - Properties
weak var ixResponder: SignInIxResponder?
raywenderlich.com 230
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
// ...
func wireController() {
signInButton.addTarget(
self,
action: #selector(signIn),
for: .touchUpInside
)
}
@objc
func signIn() {
ixResponder?.signIn(
email: emailField.text ?? "",
password: passwordField.text ?? ""
)
}
// ...
}
The root view configures the target / action pairs as soon as it moves to the window.
You could alternatively do this when the view is initialized. The signInButton is
configured to call the signIn() method whenever it's tapped. The signIn() method
calls the interaction responder's signIn() method with the current email and password
input. This is fairly straightforward code. The key here is the UIButton target / action
can be switched to a tap gesture recognizer on another component and the interaction
responder doesn't need to change.
useCase.start()
}
}
Normally, the interaction responder methods run a use case. That’s it. The bulk of the
work to make API calls and change the state of the user interface is handled inside the
use case. The interaction responder implementations are only responsible for knowing
which use cases to run and running them.
raywenderlich.com 231
Advanced iOS App Architecture Chapter 7: Architecture: Elements, Part 1
This may seem overly simple, but you want to remove as much code from the view
controller as possible. You’ll get a better understanding of what happens after start()
is called on the use case in the use case section.
That’s it for the interaction responder! There's still a lot of ground to cover with the
next two elements. In Part 2, you’ll see how to implement Observer and Use Case
elements. Before jumping into Part 2, feel free to take a break, stretch and grab a coffee
or your drink of choice.
Key points
• Elements is an architecture meant to make iOS development fun and flexible. A set
of "Elements" make up the architecture. The cool thing is you can choose which
pieces to use in your own applications.
• Elements is designed on top of some core underlying concepts: entities allow objects
to communicate, protocols make software flexible and encapsulation enables safe
change.
• Elements are separated into two main categories: Core Logic and User Interface
Logic.
• This edition of this book dives deep into the four main elements: user interface,
interaction responder, observer and use case.
• User-interface objects describe the views in your app. They allow you to configure
and change what the users sees and interacts with.
• Interaction responders handle user interactions. When a user interacts with the
screen the user interface notifies the interaction responder.
• Interaction responders allow you to safely change a view hierarchy without needing
to change any view controller code. This is because interaction responders express
what a user can do with a user interface as opposed to what specific view triggers
what task.
raywenderlich.com 232
8 Chapter 8: Architecture:
Elements, Part 2
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
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.
raywenderlich.com 233
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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. 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.
raywenderlich.com 234
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
raywenderlich.com 235
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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:
Notice how the events can come from different systems. For instance, in the example
above, the first half of the methods are associated with RxSwift Observable
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 makes view controllers easier to read.
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:
// MARK: - Properties
weak var eventResponder: ObserverForSignInEventResponder? {
willSet {
if newValue == nil {
stopObserving()
raywenderlich.com 236
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
}
}
}
// 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.
• Sign-in view state: An RxSwift Observable provides the controller's UIView state. 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 root
UIView so the view can update itself.
• 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.
raywenderlich.com 237
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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 RxSwift 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 keyboardWillHide() {
// ...
}
raywenderlich.com 238
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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: Observable<SignInViewControllerState>
var errorStateSubscription: Disposable?
var viewStateSubscription: Disposable?
let disposeBag = DisposeBag()
// 4
private var isObserving: Bool {
if isObservingState && isObservingKeyboard {
return true
} else {
return false
}
}
// MARK: - Methods
// 5
init(signInState: Observable<SignInViewControllerState>) {
self.signInState = signInState
}
// 6
raywenderlich.com 239
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
func startObserving() {
assert(self.eventResponder != nil)
if isObserving {
return
}
subscribeToErrorMessages()
subscribeToSignInViewState()
startObservingKeyboardNotifications()
}
// 7
func stopObserving() {
unsubscribeFromSignInViewState()
unsubscribeFromErrorMessages()
stopObservingNotificationCenterNotifications()
}
func subscribeToSignInViewState() {
viewStateSubscription =
signInState
.map { $0.viewState }
.distinctUntilChanged()
.subscribe(onNext: { [weak self] viewState in
self?.received(newViewState: viewState)
})
viewStateSubscription?.disposed(by: disposeBag)
}
func unsubscribeFromSignInViewState() {
viewStateSubscription?.dispose()
}
func subscribeToErrorMessages() {
errorStateSubscription =
signInState
.map { $0.errorsToPresent.first }
.ignoreNil()
.distinctUntilChanged()
.subscribe(onNext: { [weak self] errorMessage in
self?.received(newErrorMessage: errorMessage)
})
errorStateSubscription?.disposed(by: disposeBag)
}
raywenderlich.com 240
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 8
eventResponder?.received(newErrorMessage: errorMessage)
}
func unsubscribeFromErrorMessages() {
errorStateSubscription?.dispose()
}
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
}
// 8
eventResponder?.keyboardWillHide()
}
assert(notification.name ==
UIResponder.keyboardWillChangeFrameNotification)
raywenderlich.com 241
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 8
eventResponder?
.keyboardWillChangeFrame(
keyboardEndFrame: keyboardEndFrame.cgRectValue)
}
func stopObservingNotificationCenterNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
isObservingKeyboard = false
}
}
1. The class conforms to the Observable protocol you saw earlier in this chapter.
Recall that this protocol allows the associated view controller to start and stop
observation.
2. This is the stored property that holds a weak reference to the observer's associated
view controller. The property's willSet closure 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 the
ObserverForSignInEventResponder protocol 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 designing EventResponder protocols and
simply use view controller concrete types in observer implementations. Though, if
you need to unit test the observer, the EventResponder protocol is helpful because
you won't have to instantiate a real view controller. You can provide a fake
implementation to the observer.
3. 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 an RxSwift
Observable. This observer implementation has to hold onto the Observable in order
to subscribe and unsubscribe from the Observable at later points in time. Some
systems, like NotificationCenter, provide global default singletons for adding
observers. In these cases, the observer class does not need to hold on to anything.
raywenderlich.com 242
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
5. Observers should be initialized with the systems they need to observe. In this
example, the observer is initialized with an RxSwift Observable.
8. 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 RxSwift Observable and processes the data coming from
NotificationCenter before calling the view controller. The observer is a great place to
hide away any logic that is specific to event systems, like NotificationCenter
notification objects.
The last step in putting this pattern to practice is to add code to the view controller to
start and stop observation.
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.
// MARK: - Properties
// Observers
var observer: Observer
// User interface
let userInterface: SignInUserInterfaceView
raywenderlich.com 243
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 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()
}
// ...
}
extension SignInViewController:
ObserverForSignInEventResponder {
func keyboardWillHide() {
// ...
}
raywenderlich.com 244
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
}
}
// ...
This code is fairly straightforward. All of the icky details about RxSwift 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:
// 1
let signInStateObservable =
makeSignInViewControllerStateObservable()
let observer =
ObserverForSignIn(signInState: signInStateObservable)
// ....
let signInViewController =
SignInViewController(
userInterface: userInterface,
// 2
observer: observer,
// ...
)
userInterface.ixResponder = signInViewController
//3
observer.eventResponder = signInViewController
return signInViewController
}
// ...
}
raywenderlich.com 245
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
1. The observer is created with an RxSwift Observable. The Observable carries all the
state updates needed by SignInViewController.
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.
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.
raywenderlich.com 246
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
With the event responders figured out, the next step is to look at separate observer
implementations: SignInKeyboardObserver and SignInStateObserver.
// MARK: - Properties
weak var eventResponder:
SignInKeyboardObserverEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
// MARK: - Methods
func startObserving() {
assert(self.eventResponder != nil)
if isObserving {
return
}
startObservingKeyboardNotifications()
}
func stopObserving() {
stopObservingNotificationCenterNotifications()
}
raywenderlich.com 247
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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
}
eventResponder?.keyboardWillHide()
}
eventResponder?
.keyboardWillChangeFrame(
keyboardEndFrame: keyboardEndFrame.cgRectValue)
}
func stopObservingNotificationCenterNotifications() {
raywenderlich.com 248
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
isObserving = false
}
}
// MARK: - Properties
weak var eventResponder: SignInStateObserverEventResponder? {
willSet {
if newValue == nil {
stopObserving()
}
}
}
// MARK: - Methods
init(signInState: Observable<SignInViewControllerState>) {
self.signInState = signInState
}
func startObserving() {
assert(self.eventResponder != nil)
if isObserving {
return
}
subscribeToErrorMessages()
subscribeToSignInViewState()
}
func stopObserving() {
unsubscribeFromSignInViewState()
unsubscribeFromErrorMessages()
}
raywenderlich.com 249
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
func subscribeToSignInViewState() {
viewStateSubscription =
signInState
.map { $0.viewState }
.distinctUntilChanged()
.subscribe(onNext: { [weak self] viewState in
self?.received(newViewState: viewState)
})
viewStateSubscription?.disposed(by: disposeBag)
}
func unsubscribeFromSignInViewState() {
viewStateSubscription?.dispose()
}
func subscribeToErrorMessages() {
errorStateSubscription =
signInState
.map { $0.errorsToPresent.first }
.ignoreNil()
.distinctUntilChanged()
.subscribe(onNext: { [weak self] errorMessage in
self?.received(newErrorMessage: errorMessage)
})
errorStateSubscription?.disposed(by: disposeBag)
}
func unsubscribeFromErrorMessages() {
errorStateSubscription?.dispose()
}
}
Now, it's time to look at how the SignInViewController receives and uses the two
observer instances:
// MARK: - Properties
raywenderlich.com 250
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 2
var stateObserver: Observer
var keyboardObserver: Observer
// MARK: - Methods
// 1
init(userInterface: SignInUserInterfaceView,
stateObserver: Observer,
keyboardObserver: Observer) {
self.userInterface = userInterface
self.stateObserver = stateObserver
self.keyboardObserver = keyboardObserver
super.init()
}
// ...
}
extension SignInViewController:
SignInStateObserverEventResponder {
extension SignInViewController:
SignInKeyboardObserverEventResponder {
raywenderlich.com 251
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
func keyboardWillHide() {
// ...
}
// ...
1. 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 Observer
protocol instead of their concrete types.
2. The view controller needs two properties to hold each observer instance.
3. 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.
Alright, that's most of the example. The last thing to look at is how
KooberOnboardingDependencyContainer injects SignInViewController with the two
observers:
class KooberOnboardingDependencyContainer {
// ...
// 1
let signInStateObservable =
makeSignInViewControllerStateObservable()
let stateObserver =
SignInStateObserver(signInState: signInStateObservable)
let keyboardObserver = SignInKeyboardObserver()
// 2
let signInViewController =
SignInViewController(
userInterface: userInterface,
stateObserver: stateObserver,
keyboardObserver: keyboardObserver
raywenderlich.com 252
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
)
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:
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.
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?
raywenderlich.com 253
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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
raywenderlich.com 254
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
}
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
}
}
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.
}
raywenderlich.com 255
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// MARK: - Properties
weak var eventResponder: KeyboardObserverEventResponder? {
didSet {
if eventResponder == nil {
stopObserving()
}
}
}
// MARK: - Methods
func startObserving() {
if isObserving == true {
return
}
raywenderlich.com 256
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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
}
raywenderlich.com 257
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
assertionFailure()
return
}
// 3
eventResponder?.keyboardWillShow(userInfo)
}
raywenderlich.com 258
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
eventResponder?.keyboardWillChangeFrame(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:
2. Then, each method tries to create a KeyboardUserInfo with the notification object.
Because KeyboardUserInfo'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, if KeyboardUserInfo's initializer fails, the method crashes on debug builds
and returns in release builds as an error here would be extremely unlikely.
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.
raywenderlich.com 259
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
// MARK: - Properties
let observers: [Observer]
// MARK: - Methods
init(observers: Observer...) {
self.observers = observers
}
func startObserving() {
observers.forEach {
$0.startObserving()
}
raywenderlich.com 260
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
class KooberOnboardingDependencyContainer {
// ...
// 1
let stateObservable =
makeSignInViewControllerStateObservable()
let stateObserver =
SignInViewControllerStateObserver(state: stateObservable)
let keyboardObserver = KeyboardObserver()
// 2
let composedObservers =
ObserverComposition(stateObserver, keyboardObserver)
// 3
let signInViewController =
SignInViewController(
userInterface: userInterface,
observer: composedObservers
)
userInterface.ixResponder = signInViewController
// 4
stateObserver.eventResponder = signInViewController
keyboardObserver.eventResponder = signInViewController
return signInViewController
}
raywenderlich.com 261
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// ...
}
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.
// MARK: - Properties
private weak var eventResponder:
KeyboardObserverEventResponder?
private var isObserving = false
// MARK: - Methods
init(eventResponder: KeyboardObserverEventResponder) {
self.eventResponder = eventResponder
}
func startObserving() {
if isObserving == true {
return
}
// ...
isObserving = true
raywenderlich.com 262
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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:
// MARK: - Properties
let userInterface: SignInUserInterfaceView
var observer: Observer? // < Look here.
// MARK: - Methods
init(userInterface: SignInUserInterfaceView) {
self.userInterface = userInterface
super.init()
}
// ...
}
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
raywenderlich.com 263
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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.
raywenderlich.com 264
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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, 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.
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.
raywenderlich.com 265
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
Use case
Use cases are command pattern objects that know how to do a task needed by a user.
Use cases know:
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
LikeTweetUseCase would be created with the ID of the tweet liked by the user.
raywenderlich.com 266
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
• 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.
raywenderlich.com 267
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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, 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.
raywenderlich.com 268
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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.
// ...
}
raywenderlich.com 269
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
// 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()
raywenderlich.com 270
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// ...
}
}
1. 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 UseCase protocol.
2. These stored properties hold the input data provided in the use case's initializer.
3. 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.
4. 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 onComplete closure uses the
SignInUseCaseResult typealias from before.
5. 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.
6. This is an implementation of the start method from the UseCase protocol. You'll
see the full implementation of this method soon. This code example is meant to
illustrate the anatomy of a use case. Before start begins any work it performs a
threading check and then calls the onStart closure. 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.
raywenderlich.com 271
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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 UseCase protocol is the return type in the factory signature as opposed
to the concrete SignInUseCase class type. This is so the object that starts the
SignInUseCase doesn't have access to anything other than the start() method.
This lets you refactor use cases without needing to worry about breaking other
code. Returning UseCase also 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.
raywenderlich.com 272
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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:
This is the exact same typealias from before. And here's the complete implementation
of SignInUseCase:
// 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 }
}
// 1
firstly {
// 2
self.remoteAPI.signIn(username: username,
raywenderlich.com 273
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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))
}
}
}
2. 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 UserSession object. The signIn method
returns a Promise<UserSession>. The signIn method is called from the main queue.
The real implementation of signIn is expected to perform networking work
asynchronously, off the main queue. If this operation fails, signIn returns a rejected
promise and the promise chain short circuits to the catch closure.
3. If signIn completes successfully, execution returns to the main queue. The next
then closure is run. In this step, the UserSession returned from the remoteAPI is
persisted into the user session dataStore. 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 the remoteAPI, the dataStore is expected to do its work on another queue.
raywenderlich.com 274
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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 to self. This strong reference is what keeps the use case alive while the
use case runs. If the reference was weak, 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.
raywenderlich.com 275
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
// MARK: - Properties
raywenderlich.com 276
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 1
let makeSignInUseCase: SignInUseCaseFactory
// MARK: - Methods
// 2
init(
userInterface: SignInUserInterfaceView,
signInUseCaseFactory: @escaping SignInUseCaseFactory
) {
self.userInterface = userInterface
self.makeSignInUseCase = signInUseCaseFactory
super.init()
}
// ...
}
// 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()
}
// ...
}
raywenderlich.com 277
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
Here are the steps used by SignInViewController to create and use SignInUseCases:
1. 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 typealias comes in handy. Without the typealias this declaration
would look like: let makeSignInUseCase: (String, Secret, @escaping () ->
Void, @escaping (SignInUseCaseResult) -> Void) -> UseCase.
2. Here's the view controller's initializer. The use case factory closure is provided to
the view controller, here.
3. This is the method called by the UI when the user taps the Sign in button. This is
where a new SignInUseCase needs to be created and started.
5. Then, the view controller uses the use case factory closure to create a new
SignInUseCase using the username and password entered by the user along with the
progress closures created in the last step.
6. Finally, the view controller starts the use case. When the use case finishes, the use
case calls the onComplete closure created previously. The view controller can use the
onComplete closure to know when the use case finishes and to know whether the
use case run was successful or not.
class KooberOnboardingDependencyContainer {
// ...
// 1
func makeSignInUseCase(
username: String,
password: Secret,
onStart: @escaping () -> Void,
onComplete: @escaping (SignInUseCaseResult) -> Void
) -> UseCase {
// 2
raywenderlich.com 278
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 3
let useCase = SignInUseCase(username: username,
password: password,
remoteAPI: authRemoteAPI,
dataStore: userSessionDataStore,
onStart: onStart,
onComplete: onComplete)
// 4
return useCase
}
// 5
func makeSignInViewController() -> SignInViewController {
let userInterface = SignInRootView()
// 6
let signInUseCaseFactory = self.makeSignInUseCase
// 7
let signInViewController =
SignInViewController(
userInterface: userInterface,
stateObserver: stateObserver,
keyboardObserver: keyboardObserver,
signInUseCaseFactory: signInUseCaseFactory
)
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.
raywenderlich.com 279
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
2. 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 shared userSessionDataStore held
by the dependency container.
3. 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.
5. This is the SignInViewController factory used to create a new view controller when
a user navigates to the sign-in screen.
6. 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 the SignInUseCaseFactory typealias. 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.
7. In this step, the dependency container's sign-in use case factory method,
makeSignInUseCase, is injected into a new SignInViewController. 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 an authRemoteAPI and how to get a hold of a shared userSessionDataStore.
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.
raywenderlich.com 280
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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:
// MARK: - Properties
let signInUseCaseFactory: SignInUseCaseFactory
let userInterface: SignInUserInterfaceView
// MARK: - Methods
init(
userInterface: SignInUserInterfaceView,
signInUseCaseFactory: SignInUseCaseFactory
) {
self.userInterface = userInterface
self.signInUseCaseFactory = signInUseCaseFactory
super.init()
}
raywenderlich.com 281
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// ...
}
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.
// ...
}
// ...
}
raywenderlich.com 282
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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!
extension KooberOnboardingDependencyContainer:
SignInUseCaseFactory {}
class KooberOnboardingDependencyContainer {
// ...
let signInViewController =
SignInViewController(
userInterface: userInterface,
signInUseCaseFactory: self // < Look here.
)
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.
raywenderlich.com 283
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// ...
}
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 {
// ...
// ...
}
raywenderlich.com 284
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.'
If your view controllers are listening to database changes you might prefer to design
your use cases like this:
// 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
raywenderlich.com 285
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
raywenderlich.com 286
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 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)")
}
}
raywenderlich.com 287
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
}
}
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:
1. 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.
2. If all goes well, the user's UserSession is stored in the database and the signing in
state is set to false in 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.
3. 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 second catch closure.
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.
Note: Check out Chapter 6, "Architecture: Redux," if you want to follow this
example and you're not familiar with Redux.
// MARK: - Properties
// Input data
let username: String
let password: Secret
// Side-effect subsystems
let remoteAPI: AuthRemoteAPI
raywenderlich.com 288
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
// 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.
raywenderlich.com 289
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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:
1. 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.
2. Once the user's credentials successfully authenticate with the remoteAPI, an action
is dispatched carrying the new UserSession.
3. 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 UserInteractions objects from the Redux version of Koober.
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
raywenderlich.com 290
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
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 SearchLocationsUseCaseFactory =
(
String, // query
Location // pickupLocation
) -> CancelableUseCase
Any view controller that's injected with this factory can create, start and cancel a
SearchLocationsUseCase.
// 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
raywenderlich.com 291
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
firstly {
remoteAPI.getLocationSearchResults(
query: query,
pickupLocation: pickupLocation
)
}.done { results in
// 4
guard self.cancelled == false else {
return
}
Here's a walkthrough of all the additional logic added above to implement a cancelable
use case:
1. The use case needs this boolean stored property to hold the cancelation state. The
use case is created in the not-canceled state.
2. This implements the cancel method from the Cancelable protocol. 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.
3. One of the first things that start does 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.
4. Once the networking completes, the done closure 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
raywenderlich.com 292
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
stop the networking as soon as the use case's cancel method is called. You can do
this by storing the promise in a property and canceling the promise chain. To learn
more about canceling promises, visit PromiseKit'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.
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.
raywenderlich.com 293
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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
raywenderlich.com 294
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
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.
2. The individual elements are simple and intuitive. They are easy to learn, teach and
practice.
3. 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 Observer class.
4. You can easily distribute the development workload across your team. Different
team members can build different elements in parallel.
raywenderlich.com 295
Advanced iOS App Architecture Chapter 8: Architecture: Elements, Part 2
6. 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
1. 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.
2. 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.
3. 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 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.
• 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.
raywenderlich.com 296
C Conclusion
What a journey it’s been! From exploring why architecture matters to diving deep into
dependency injection, to comparing different architecture patterns, you’ve gotten a
taste of architecting iOS apps using advanced techniques and patterns.
By putting the book concepts to practice, your codebase will become easier to work in
and you’ll have more fun writing code. Not only that, you’ll be able to respond quickly
to changing requirements.
We hope the ideas you saw in this book inspire you to explore and try out different
architecture practices and maybe even inspire you to come up with some of your own!
If you’re hungry for more architecture related books we recommend reading Design
Patterns by Tutorials. Also, if you found some of the RxSwift code hard to follow, we
recommend checking out RxSwift: Reactive Programming with Swift.
If you have any questions or comments as you work through this book, please stop by
our forums at http://forums.raywenderlich.com and look for the particular forum
category for this book.
Thank you again for purchasing this book. Your continued support is what makes the
books, tutorials, videos and other things we do at raywenderlich.com possible. We truly
appreciate it!
Happy architecting!
raywenderlich.com 297