I like Swift, like many other object oriented programming languages. Swift allows you to represent real world objects that have some characteristics and can perform some action.
I tend to think of an app as a world where each object is a person. They work and communicate. If a person can’t do the work alone, he needs to ask for help. Take a project, for example, if the manager has to do all of the work by himself, he will go crazy. So there is a need to organise and delegate tasks, and for many persons to collaborate on the project: designer, tester, scrum master, developer. After the task is done, the manager needs to be informed.
This may not a good example. But at least you get the importance of communication and delegation in OOP. I was very intrigued by the word “architecture” when I started iOS programming. But after doing for a while, it all comes down to identifying and splitting responsibilities. This article tells a bit about MVC and simple Extract Classrefactoring to MVVM, and how to go further with Rx. You are free to create your own architecture, but whatever you do, consistence is key as to not confuse or surprise your teammates.
Model View Controller
Take a look at the architecture you know the best, MVC, short for Model View Controller. You always get one when you create a new iOS project. View is where you present your data, using UIView, UIButton, UILabel. Model is just a fancy word for data. It can be your entity, data from networking, object from database, or from cache. The controller is the thing that mediates between the model and the view.
UIViewController is the center of the universe
The problem with ViewController is that it tends to be huge. Apple puts it as the center of the universe, where it has lots of properties and responsibilities. There are many things that you can only do with a UIViewController. Things like interacting with storyboard, managing the view, configuring view rotation, state restoration. UIViewController is designed with lots of hooks for you to override and customize.
Take a look at the many sections in UIViewController documentation, you can’t do the following without a UIViewController.
func viewDidLoad()
var preferredStatusBarStyle: UIStatusBarStyle { get }
UITableViewDataSource
var presentationController: UIPresentationController? { get }
func childViewControllerForScreenEdgesDeferringSystemGestures() -> UIViewController?
func didMove(toParentViewController parent: UIViewController?)
var systemMinimumLayoutMargins: NSDirectionalEdgeInsets
var edgesForExtendedLayout: UIRectEdge
var previewActionItems: [UIPreviewActionItem]
var navigationItem: UINavigationItem
var shouldAutorotate: Bool
As your app grows, we need to add more code for other logic. Such as networking, data source, handling multiple delegates, presenting child view controllers. We can, of course, put all the stuff on the view controller, but that results in big view controller and improvement in your scrolling skill. This is where you lose the big picture of responsibilities because all stuff stays in the mega view controller. You tend to introduce code duplication and bugs are hard to fix because they are all over the places.
The same goes with Page in Windows Phone or Activity in Android. They are intended for a screen or partial screen of functionality. There are certain actions that can only be done through them like Page.OnNavigatedTo, Activity.onCreate.
The buzzwords of architecture
What do you do when the ViewController is doing a lot? You offset the work to other components. By the way, if you want another object to do the user input handling, you can use the Presenter. If the Presenter is doing too much, then it can offset the business logic to the Interactor. Also, there are more buzzwords that can be used.
let buzzWords = [
"Model", "View", "Controller", "Entity", "Router", "Clean", "Reactive",
"Presenter", "Interactor", "Megatron", "Coordinator", "Flow", "Manager"
]
let architecture = buzzWords.shuffled().takeRandom()
let acronym = architecture.makeAcronym()
After all the buzzwords are assembled, we get an architecture. There are many of them, ranging from simple extract class refactoring, embracing MVC or taking inspiration from Clean Code, Rx, EventBus or Redux. The choice depends on project, and some teams prefer one architecture over the other.
The pragmatic programmer
People have different opinion about what is good architecture. For me, it is about clear separation of concern, good communication pattern and being comfortable to use. Each component in the architecture should be identifiable and have a specific role. The communication must be clear so that we know which object is talking to each other. This together with good dependency injection will make testing easier.
Things that sound good in theory may not work well in practice. Separated domain objects are cool, protocol extensions are cool, multiple layers of abstraction are cool. But too many of them can be a problem.
If you read enough about design patterns, you know they all come down to these simple principles:
Encapsulate what varies: identify the aspects of your application that vary and separate them from what stays the same.
Program to an interface, not an implementation
Prefer composition over inheritance
If there’s one thing we should master, it’s composition. The key is to identify responsibility and compose them in a reasonable and consistent way. Consult with your team mates on what suits the most. Always writing code with the thought that you will also be the future maintainer. Then you will write it differently.
Don’t fight against the system
Some architectures introduce completely new paradigm. Some of them are cumbersome that there people write scripts to generate boiler plate code. It’s good that there are many approaches to a problem. But for me sometimes it feels they are fighting the system. Some tasks are made easy while some trivial tasks become extremely hard. We should not constrain ourselves to one architecture simply because it are trendy. Be pragmatic, not dogmatic.
In iOS, we should embrace MVC. UIViewController is not meant for a full screen of content. They can contain and be composed to split the functionalities. We can use Coordinator and FlowController to manage dependencies and handle flow. Containerfor state transition, embedded logic controller, part of full screen content. This embracing ViewController approach play nicely with MVC in iOS and is my preferable way of doing.
Model View ViewModel
Another good-enough way is to offload some of the tasks to another object, let’s call it ViewModel. The name does not matter, you can name it Reactor, Maestro, Dinosaur. The important thing is your team get an agreed name. ViewModel takes some tasks from the ViewController and reports when it’s done. There are communication patterns in Cocoa Touch such as delegate, closures to use.
ViewModel is self-contained, has no reference to UIKit, and has just input and output. We can put a lot of things into ViewModel like calculation, formatting, networking, business logic. Also, if you don’t like the ViewModel to become massive, you surely need to create some dedicated objects. ViewModel is the first step to get a slim ViewController.
Synchronously
Below is a very simple ViewModel, which formats data based on User model. This is done synchronously.
class ProfileController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ViewModel(user: user)
nameLabel.text = viewModel.name
birthdayLabel.text = viewModel.birthdayString
salaryLabel.text = viewModel.salary
piLabel.text = viewModel.millionthDigitOfPi
}
}
Asynchronously
We work with asynchronous API all the time. What if we want to show the user’s number of Facebook friends? For this to work we need to call Facebook API and this operation takes time. The ViewModel can report back via closure.
viewModel.getFacebookFriends { friends in
self.friendCountLabel.text = "\(friends.count)"
}
Internally, the ViewModel may offload the task to a dedicated Facebook API client object.
class ViewModel {
func getFacebookFriends(completion: [User] -> Void) {
let client = APIClient()
client.getFacebookFriends(for: user) { friends in
DispatchQueue.main.async {
completion(friends)
}
}
}
}
Jetpack in Android
Google introduced Android Architecture Component, now part of Jetpack, at Google IO 2017. It has ViewModel and LiveData, which is also a type of MVVM applied in Android. ViewModel survives through configuration changes, and notify results in terms of LiveData for Activity to consume.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
model.getUsers().observe(this, { users ->
// update UI
})
}
}
This is one of the reason I like ViewModel. If we follow ViewModel like this, then code structures between iOS and Android become similar. There should be no need for some random Javascript cross platform solutions. You learn the concept once, and apply it on both iOS and Android. I learn ViewModel, RxSwift on iOS and feel right at home when I work with RxJava and RxBinding on Android. The Kickstarter project also proves that this works well in their iOS and Android apps.
Binding
To encapsulate the closure, we can create a class called Binding, which can notify one or multiple listeners. It takes advantage of didSet, and the observable property becomes clear.
class Binding<T> {
var value: T {
didSet {
listener?(value)
}
}
private var listener: ((T) -> Void)?
init(value: T) {
self.value = value
}
func bind(_ closure: @escaping (T) -> Void) {
closure(value)
listener = closure
}
}
Here is how to use it inside ViewModel:
class ViewModel {
let friends = Binding<[User]>(value: [])
init() {
getFacebookFriends {
friends.value = $0
}
}
func getFacebookFriends(completion: ([User]) -> Void) {
// Do the work
}
}
When ever friends are fetched, or changed, the ViewController is updated accordingly. This is called reaction to changes.
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.bind { friends in
self.friendsCountLabel.text = "\(friends.count)"
}
}
You often see MVVM introduction with reactive frameworks, and it is for a reason. They offer many chaining operators and make reactive programming easier and more declarative.
RxSwift
Perhaps the most common reactive framework in Swift is RxSwift. The thing I like about it is that it follows ReactiveX pattern. So you will feel more familiar if you already use RxJava, RxJs, or RxKotlin.
Observable
RxSwift unifies sync and async operations through Observable. This is how you make one.
class ViewModel {
let friends: Observable<[User]>
init() {
let client = APIClient()
friends = Observable<[User]>.create({ subscriber in
client.getFacebookFriends(completion: { friends in
subscriber.onNext(friends)
subscriber.onCompleted()
})
return Disposables.create()
})
}
}
The power of RxSwift lies in its numerous operators, which help you chain Observables. Here you can call 2 network requests, wait for both of them to finish, then sum up the friends. This is very streamlined and saves you lots of time. Here you can just subscribe to the Observable, it will be triggered when the request completes:
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}
Input and output
One thing that is nice about ViewModel and Rx is that we can separate input and output using Observable, which gives a clear interface. Read more at Learning from Open Source: Input and output container.
Below it is clear that we fetch is an input, and friends is the viable output.
class ViewModel {
class Input {
let fetch = PublishSubject<()>()
}
class Output {
let friends: Driver<[User]>
}
let apiClient: APIClient
let input: Input
let output: Output
init(apiClient: APIClient) {
self.apiClient = apiClient
// Connect input and output
}
}
class ProfileViewController: BaseViewController<ProfileView> {
let viewModel: ProfileViewModelType
init(viewModel: ProfileViewModelType) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
// Input
viewModel.input.fetch.onNext(())
// Output
viewModel.output.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}
}
How reactive works
If you feel like Rx, it’s good to get an understanding of them after using some frameworks for a while. There are some concepts like Signal, SignalProducer, Observable, Promise, Future, Task, Job, Launcher, Async and some people can have a very distinction about them. Here I simply call it Signal, which is something that can signal values.
Monad
Signal and its Result are just monads, which are thing that can be mapped and chained.
Signal makes use of deferred execution callback closures. It can be pushed or pulled. Which is just how the Signal updates its value and the order the callbacks are called.
Execution callback closure means that we pass a function to another function. And the passed in function will get called when appropriated.
Sync vs Async
Monad can be in either sync or async mode. Sync is easier to understand, but async is somewhat you’re already familiar and used in practice.
Basically,
Sync: you get the returned value right away via return
Aync: you get the returned value via callback block
Here is an example of simple sync and async free functions:
// Sync
func sum(a: Int, b: Int) -> Int {
return a + b
}
// Async
func sum(a: Int, b: Int, completion: Int -> Void) {
// Assumed it is a very long task to get the result
let result = a + b
completion(result)
}
And how sync and async apply to Result type. Notice the async version, we get the transformed value in a completion closure instead of immediate return from function.
enum Result<T> {
case value(value: T)
case failure(error: Error)
// Sync
public func map<U>(f: (T) -> U) -> Result<U> {
switch self {
case let .value(value):
return .value(value: f(value))
case let .failure(error):
return .failure(error: error)
}
}
// Async
public func map<U>(f: @escaping ((T), (U) -> Void) -> Void) -> (((Result<U>) -> Void) -> Void) {
return { g in // g: Result<U> -> Void
switch self {
case let .value(value):
f(value) { transformedValue in // transformedValue: U
g(.value(value: transformedValue))
}
case let .failure(error):
g(.failure(error: error))
}
}
}
}
Push Signal
Given a chained signals like this:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
Push Signal, means that when the source signal A is sent an event, it propagates that event via callbacks. PushSignal is similar to PublishSubject in RxSwift.
Triggered by sending event to the source signal.
We must keep A as it keeps the others around
We subscribe the last D
We send event to the first A
A ‘s callback gets called, it it in turn calls callback of B with the result of A ‘s map, then B ‘s callback calls C ‘s callback with the result of B‘s flatMap, and so on.
It is similar to Promise A+, you can see my Swift implementation of Promise A+ in my Then framework. For now, here is the Swift 4 implementation of a simple PushSignal.
public final class PushSignal<T> {
var event: Result<T>?
var callbacks: [(Result<T>) -> Void] = []
let lockQueue = DispatchQueue(label: "Serial Queue")
func notify() {
guard let event = event else {
return
}
callbacks.forEach { callback in
callback(event)
}
}
func update(event: Result<T>) {
lockQueue.sync {
self.event = event
}
notify()
}
public func subscribe(f: @escaping (Result<T>) -> Void) -> Signal<T> {
// Callback
if let event = event {
f(event)
}
callbacks.append(f)
return self
}
public func map<U>(f: @escaping (T) -> U) -> Signal<U> {
let signal = Signal<U>()
_ = subscribe { event in
signal.update(event: event.map(f: f))
}
return signal
}
}
Below is how a PushSignal is used to transform a chain from string to its length, you should see 4, the length of the string “test” printed.
let signal = PushSignal<String>()
_ = signal.map { value in
return value.count
}.subscribe { event in
if case let .value(value) = event {
print(value)
} else {
print("error")
}
}
signal.update(event: .value(value: "test"))
Pull Signal
Given a chained signals like this:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
Pull Signal, sometimes called Future, means when we subscribe to the final signal D, it causes the previous signals to action:
Triggered by subscribing to the final signal D;
We must keep D as it keeps the others around;
We subscribe the last D;
D ‘s operation runs, and it cause C ‘s operation to runs, … then A ‘s operation runs. It is in A that the task is performed (like fetching network, retrieving database, file access, heavy computation, …) to get the result, and A ‘s completion gets called. Then A’s completion calls B ‘s completion with the result mapped by B ‘s map, … all the way to the subscriber ‘s completion block.
Here is a Swift 4 implementation of PullSignal. PullSignal is similar to Observablein RxSwift and SignalProducer in ReactiveSwift.
public struct PullSignal<T> {
let operation: ((Result<T>) -> Void) -> Void
public init(operation: @escaping ((Result<T>) -> Void) -> Void) {
self.operation = operation
}
public func start(completion: (Result<T>) -> Void) {
operation() { event in
completion(event)
}
}
public func map<U>(f: @escaping (T) -> U) -> PullSignal<U> {
return PullSignal<U> { completion in
self.start { event in
completion(event.map(f: f))
}
}
}
}
The chain is inactive until you call start at the last signal in the chain, which triggers operation flow all over to the first signal. Run this snippet and you should see 4, the length of the string “test” printed on the console.
let signal = PullSignal<String> { completion in
// There should be some long running operation here
completion(Result.value(value: "test"))
}
signal.map { value in
value.count
}.start { event in
if case let .value(value) = event {
print(value)
} else {
print("error")
}
}
I hope those snippets are simple enough to help you understand how signal works under the hood, and how to differentiate hot and cold signals. To get a fully working Signal framework, you need to implement more operations. Such as retry, rebounce, throttle, queue, flatten, filter, delay, combine and add support for UIKit like RxCocoa does. Find out how to implement in my Signal repo.
Where to go from here
Architecture is a very opinionated topic. Hopefully this article gives you some ideas to add into your decision points. MVC is dominant in iOS, MVVM is a good friend and Rx is a powerful tool. Here are some more interesting readings:
❤️ Support my apps ❤️
- Push Hero - pure Swift native macOS application to test push notifications
- PastePal - Pasteboard, note and shortcut manager
- Quick Check - smart todo manager
- Alias - App and file shortcut manager
- My other apps
❤️❤️😇😍🤘❤️❤️
Top comments (0)