An architecture for scalable and testable apps
S (DI)
Service (Dependency Injection)
MVVM
Model View - View Model
C
(Flow) Coordinators
View
Controller
Model
View
Controller
Model
}
Massive
View
Controller
Is this a model, a view or a controller? 🤔
Anything that isn't "data" or "graphic" gets thrown into the controller...
Model-View Binding
Subview allocation
Data Fetching
Layout
Data transformation
Navigation Flow
Model Mutation
Device configuration
"But...I am just supposed to update the view based on the model..." - VC
😱
Model-View Binding
Subview allocation
Data Fetching
Layout
Data transformation
Navigation Flow
Model Mutation
Device configuration
User Input
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let object = dataSource(objectAtIndexPath: indexPath)
let nextVC = MyViewController()
nextVC.object = object
nextVC.someConfProperty = ...
nextVC.someFlag = true
self.navigationController.pushViewController(nextVC, animated: true)
}
El classico...
👌
😒
💩
protocol Coordinator: class {
var rootViewController: UIViewController { get }
func addChildCoordinator(_ child: Coordinator)
func removeChildCoordinator(_ child: Coordinator)
func removeAllChildCoordinators()
func start()
func stop()
}
✅ No present(:), dimiss(:), performSegue(:) calls in VCs any more
✅ VCs tell their coordinator what happened via delegation.
✅ Coordinator is responsible to inject services and configure VCs on initialization.
protocol Coordinator: class {
var rootViewController: UIViewController { get }
func addChildCoordinator(_ child: Coordinator)
func removeChildCoordinator(_ child: Coordinator)
func removeAllChildCoordinators()
func start()
func stop()
}
✅ No present(:), dimiss(:), performSegue(:) calls in VCs any more
✅ VCs tell their coordinator what happened via delegation.
✅ Coordinator is responsible to inject services and configure VCs on initialization.
class AppCoordinator: BaseCoordinator {
init() {
let libraryCoordinator = LibraryCoordinator()
let profileCoordinator = ProfileCoordinator()
let tabBarController = UITabBarController()
tabBarController.viewControllers = [
libraryCoordinator.rootViewController,
profileCoordinator.rootViewController
]
super.init(rootViewController: tabBarController)
addChildCoordinator(libraryCoordinator)
addChildCoordinator(profileCoordinator)
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator!
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
appCoordinator = AppCoordinator()
let window = UIWindow()
window.rootViewController = appCoordinator.start()
window.makeKeyAndVisible()
self.window = window
return true
}
}
App
Coordinator
Login
Coordinator
Profile
Coordinator
List
Coordinator
Register
Coordinator
Forgot Pass Coordinator
GetEmailVC
SubmitPinVC
ChangePasswordVC
delegation
✅ VCs are now isolated. They only present data.
✅ VCs are now reusable. They don't assume the context they are presented in. You can mix & match them in any way.
✅ Coordinators are objects fully in control. No more relying on viewDid*() & viewWill* methods
✅ You have a place to configure and style your higher-level objects like UITabBarController and UINavigationController
✅ More flexible decision making based on device and interface idioms
View/View Controller
View Model
Model
owns
updates
updates
owns
✅ VCs do not know anything about or how to fetch the model
✅ VCs do not mutate the model directly, but through the View Model
✅ VCs bind on the View Model's state (data binding)
Data binding
View Controllers need a way of knowing when something in their View Model changes so they can update the UI.
✅ Delegation
✅ RxSwift & the rest...
✅ Closures
✅ A simple generic object....
Generic for Data Binding
class Observable<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(_ listener: Listener?) {
self.listener = listener
}
func bindAndFire(_ listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}
}
Singletons are an ANTI-PATTERN. Singletons must DIE.
"Use a singleton when you need the ☀️, not a 🔦"
Instead isolate your dependencies into separate classes or structs and pass them (inject them) where necessary.
public class Services: HasPushNotificationsService, HasAPIService {
/// A service for interacting with the server API
let apiService: APIService
/// A service for managing push notifications related concepts
let pushNotificationsManager: PushNotificationsManager!
init(apiService: APIService,
pushNotificationsManager: PushNotificationsManager,
{
self.apiService = apiService
self.pushNotificationsManager = pushNotificationsManager
}
}
Be explicit about it!
protocol HasAPIService {
var apiService: APIService { get }
}
class ViewControllerViewModel {
typealias Dependencies = HasAPIService & HasPushNotificationsService
private var dependencies: Dependencies!
inject(dependencies: Dependencies) {
self.dependencies = dependencies
}
}
✅ Initialise an object to hold your dependencies/services
✅ In appDidFinishLaunching, create the App Coordinator and inject the dependencies/services to it.
✅ The App Coordinator has the logic on which coordinator takes the ball next.
✅ Each coordinator is responsible for creating and configuring each View Model for each View Controller it presents
✅ Each coordinator is responsible for initialising, configuring, presenting dismissing its View Controllers
✅ View Controllers take user input and ask their coordinator to take the lead (via delegation)
✅ View Controllers are passive. They only present/update data based on what VM tells them to do. They DO NOT handle navigation or mutate their models directly.