Public repo: For those of you who want to check out the code right away here is the repo: https://github.com/Lascorbe/SwiftUI-MVP-Coordinator.
Welcome back! This is the 3rd part of the series on creating an MVP+Coordinators app in SwiftUI. If you're looking for the 1st part, please go here: lascorbe.com/posts/2020-04-27-MVPCoordinators-SwiftUI-part1. If you're looking for the 2nd part instead, please go here: lascorbe.com/posts/2020-04-28-MVPCoordinators-SwiftUI-part2.
In part 1, we learned how to set up an entire screen with the MVP pattern, we created a base Coordinator
protocol, and implemented our first 2 coordinators. We saw how to wrap our view in a NavigationView
, and how to implement NavigationLink
so it doesn't depend on anything else in the view.
In part 2, we learned how to extract that NavigationLink
from our MasterView
creating a handy new NavigationButton
along the way. We saw how to set up our Coordinator
so we can return SwiftUI Views from the start()
function, and how to easily change presenting a view as a modal instead of in a navigation stack. And we took a look at how to present several views from the same view.
In this part, the 3rd one, we're going to reimplement our Coordinator
protocol to store the identifier, parent and children of the coordinators, so we can complete the implementation of the coodinator pattern. And we'll also deal with a bit of memory management so we don't create retain cycles. Are you ready? Let's go!
This is what we completed in part 1 and part 2:
// MARK: Coordinator
protocol Coordinator {
associatedtype U: View
func start() -> U
}
extension Coordinator {
func coordinate<T: Coordinator>(to coordinator: T) -> some View {
return coordinator.start()
}
}
// MARK: AppCoordinator
final class AppCoordinator: Coordinator {
private weak var window: UIWindow?
init(window: UIWindow) {
self.window = window
}
@discardableResult
func start() -> some View {
let coordinator = RootMasterCoordinator(window: window)
return coordinate(to: coordinator)
}
}
// MARK: MasterCoordinator
protocol MasterCoordinator: Coordinator {}
extension MasterCoordinator {
func presentDetailView(isPresented: Binding<Bool>) -> some View {
let coordinator = NavigationDetailCoordinator(isPresented: isPresented)
return coordinate(to: coordinator)
}
}
final class RootMasterCoordinator: MasterCoordinator {
private weak var window: UIWindow?
init(window: UIWindow?) {
self.window = window
}
func start() -> some View {
let view = MasterFactory.make(with: self)
let navigation = NavigationView { view }
let hosting = UIHostingController(rootView: navigation)
window?.rootViewController = hosting
window?.makeKeyAndVisible()
return EmptyView()
}
}
// MARK: DetailCoordinator
protocol DetailCoordinator: Coordinator {}
final class NavigationDetailCoordinator: DetailCoordinator {
private var isPresented: Binding<Bool>
init(isPresented: Binding<Bool>) {
self.isPresented = isPresented
}
func start() -> some View {
return NavigationLink(destination: EmptyView(), isActive: isPresented) {
EmptyView()
}
}
}
// MARK: Factory
enum MasterFactory {
static func make<C: MasterCoordinator>(with coordinator: C) -> some View {
let presenter = MasterPresenter(coordinator: coordinator)
let view = MasterView(presenter: presenter)
return view
}
}
// MARK: MVP
struct MasterViewModel {
let date: Date
}
protocol MasterPresenting: ObservableObject {
associatedtype U: View
var viewModel: MasterViewModel { get }
func onButtonPressed(isPresented: Binding<Bool>) -> U
}
final class MasterPresenter<C: MasterCoordinator>: MasterPresenting {
@Published private(set) var viewModel: MasterViewModel
private let coordinator: C
init(coordinator: C) {
self.coordinator = coordinator
self.viewModel = MasterViewModel(date: Date())
}
func onButtonPressed(isPresented: Binding<Bool>) -> some View {
return coordinator.presentDetailView(isPresented: isPresented)
}
}
struct MasterView<T: MasterPresenting>: View {
@ObservedObject var presenter: T
var body: some View {
NavigationButton(contentView: Text("\(presenter.viewModel.date, formatter: dateFormatter)"),
navigationView: { isPresented in
self.presenter.onButtonPressed(isPresented: isPresented)
})
}
}
struct NavigationButton<CV: View, NV: View>: View {
@State private var isPresented = false
var contentView: CV
var navigationView: (Binding<Bool>) -> NV
var body: some View {
Button(action: {
self.isPresented = true
}) {
contentView
.background(
navigationView($isPresented)
)
}
}
}
Coordinator
In the previous parts, we created our Coordinator
protocol where we set up the start()
and coordinate(:)
functions. Now, we're going to extend the functionality of this protocol, and we're going to start by adding a parent
property to have a reference to the coordinator's parent.
How can we store this? I have to confess I used a trick, I used the Objective-C runtime to store them, creating a Mixin. Let's go to the Coordinator
protocol:
protocol Coordinator: AssociatedObject { // notice AssociatedObject conformance
associatedtype U: View
associatedtype P: Coordinator
func start() -> U
}
extension Coordinator {
// parent must be `weak` so we don't create a retain cycle
private(set) weak var parent: P? {
get { associatedObject(for: &parentKey) }
set { setAssociatedObject(newValue, for: &parentKey, policy: .weak) }
}
}
extension Coordinator {
func coordinate<T: Coordinator>(to coordinator: T) -> some View {
coordinator.parent = self as? T.P
return coordinator.start()
}
}
private var parentKey: UInt8 = 0
What's a mixin? Well, my friend Luis Recuenco explains it better than me on this blog post, which I recommend you to read. In that post you can find the implementation of AssociatedObject
, but here's a direct link to its implementation in my project if you want to take a look (I just copied Luis' implementation).
Long story short, mixins are a way of leveraging on composition instead of subclassing. Right now we want to create a parent
property in our Coordinator
protocol, but it'd be great if we don't have to define that property on every coordinator class that conforms to Coordinator
. To do that, we could you use subclassing, create a base coordinator class and make all the coordinators subclasses, but I prefer to achieve polymorphic behavior and code reuse by composition instead of inheritance if I can.
Now that we have stored properties in the protocol extension, we can save the parent of every coordinator, leveraging again in the power of generics.
This last change to the Coordinator
protocol impacts the coordinators' implementation, and we have to make small changes to them in order to conform to Coordinator
:
final class AppCoordinator: Coordinator {
// this is the root Coordinator so we can just point the parent to itself
typealias P = AppCoordinator
{...}
@discardableResult
func start() -> some View {
let coordinator = RootMasterCoordinator<AppCoordinator>(window: window)
return coordinate(to: coordinator)
}
}
protocol MasterCoordinator: Coordinator {}
extension MasterCoordinator: Coordinator {
func presentDetailView(isPresented: Binding<Bool>) -> some View {
let coordinator = NavigationDetailCoordinator<Self>(isPresented: isPresented)
return coordinate(to: coordinator)
}
}
final class RootMasterCoordinator<P: Coordinator>: MasterCoordinator {
{...}
}
final class NavigationDetailCoordinator<P: Coordinator>: DetailCoordinator {
{...}
}
That's it! We set AppCoordinator
parent to itself, since it won't have parent, then we set RootMasterCoordinator
's parent as AppCoordinator
with <AppCoordinator>
on the constructor. Then on the rest of our Coordinators we can use <Self>
to set the parent type, if the function is in a protocol extension, otherwise just use the parent's type directly. And we have to add <P: Coordinator>
to the coordinator implementations so we define their parents on construction as well (when calling their init).
Now we have to store all the children of the coordinator, but first we have to add a way to identify each coordinator instance individually. Let's do that then! Back to our Coordinator
protocol:
protocol Coordinator: AssociatedObject {
{..}
}
extension Coordinator {
private(set) var identifier: UUID {
get {
guard let identifier: UUID = associatedObject(for: &identifierKey) else {
self.identifier = UUID()
return self.identifier
}
return identifier
}
set { setAssociatedObject(newValue, for: &identifierKey) }
}
private(set) weak var parent: P? {
get { associatedObject(for: &parentKey) }
set { setAssociatedObject(newValue, for: &parentKey, policy: .weak) }
}
{...}
}
private var identifierKey: UInt8 = 0
private var parentKey: UInt8 = 0
Relying again on the power of the AssociatedObject
protocol, aka the Objective-C runtime, we created a new stored property to lazily create the identifier
on get
.
Now we can add a way to store the children of the coordinator:
protocol Coordinator: AssociatedObject {
{..}
}
extension Coordinator {
private(set) var identifier: UUID {
{...}
}
private(set) var children: [UUID: Any] {
get {
guard let children: [UUID: Any] = associatedObject(for: &childrenKey) else {
self.children = [UUID: Any]()
return self.children
}
return children
}
set { setAssociatedObject(newValue, for: &childrenKey) }
}
private(set) weak var parent: P? {
{...}
}
private func store<T: Coordinator>(child coordinator: T) {
children[coordinator.identifier] = coordinator
}
private func free<T: Coordinator>(child coordinator: T) {
children.removeValue(forKey: coordinator.identifier)
}
{...}
}
private var identifierKey: UInt8 = 0
private var childrenKey: UInt8 = 0
private var parentKey: UInt8 = 0
We've added the property children
, and the functions store
and free
to manage adding and removing children.
Great! We have learned how to implement stored properies in protocol extensions creating a mixin, using the power of the Objective-C runtime. In the next section, we'll see what we have to do to manage memory correctly so we don't create retain cycles.
This is what we've done so far:
// MARK: Coordinator
protocol Coordinator: AssociatedObject {
associatedtype U: View
associatedtype P: Coordinator
func start() -> U
}
extension Coordinator {
private(set) var identifier: UUID {
get {
guard let identifier: UUID = associatedObject(for: &identifierKey) else {
self.identifier = UUID()
return self.identifier
}
return identifier
}
set { setAssociatedObject(newValue, for: &identifierKey) }
}
private(set) var children: [UUID: Any] {
get {
guard let children: [UUID: Any] = associatedObject(for: &childrenKey) else {
self.children = [UUID: Any]()
return self.children
}
return children
}
set { setAssociatedObject(newValue, for: &childrenKey) }
}
private func store<T: Coordinator>(child coordinator: T) {
children[coordinator.identifier] = coordinator
}
private func free<T: Coordinator>(child coordinator: T) {
children.removeValue(forKey: coordinator.identifier)
}
private(set) weak var parent: P? {
get { associatedObject(for: &parentKey) }
set { setAssociatedObject(newValue, for: &parentKey, policy: .weak) }
}
func coordinate<T: Coordinator>(to coordinator: T) -> some View {
coordinator.parent = self as? T.P
return coordinator.start()
}
}
private var identifierKey: UInt8 = 0
private var childrenKey: UInt8 = 0
private var parentKey: UInt8 = 0
// MARK: AppCoordinator
final class AppCoordinator: Coordinator {
typealias P = AppCoordinator
{...}
@discardableResult
func start() -> some View {
let coordinator = RootMasterCoordinator<AppCoordinator>(window: window)
return coordinate(to: coordinator)
}
}
// MARK: MasterCoordinator
protocol MasterCoordinator: Coordinator {}
extension MasterCoordinator {
func presentDetailView(isPresented: Binding<Bool>) -> some View {
let coordinator = NavigationDetailCoordinator<Self>(isPresented: isPresented)
return coordinate(to: coordinator)
}
}
final class RootMasterCoordinator<P: Coordinator>: MasterCoordinator {
{...}
}
// MARK: DetailCoordinator
protocol DetailCoordinator: Coordinator {}
final class NavigationDetailCoordinator<P: Coordinator>: DetailCoordinator {
{...}
}
// MARK: Factory
{...}
// MARK: MVP
{...}
Now that we have all the properties we need in our Coordinator
protocol, we have to manage how to actually add and remove the children of the coordinators.
Let's go to again to our Coordinator
protocol to modify the coordinate
function:
extension Coordinator {
{...}
private func store<T: Coordinator>(child coordinator: T) {
children[coordinator.identifier] = coordinator
}
func coordinate<T: Coordinator>(to coordinator: T) -> some View {
store(child: coordinator) // hi! I'm a new line
coordinator.parent = self as? T.P
return coordinator.start()
}
}
We've added 1 line, a call to store
to save the coordinator as a child.
Ok, but now we're retaining the coordinators we coordinatee to from here, and we are retaining the coordinators somewhere else too, in the presenters. We have to fix that!
Let's go to the presenter and make sure we don't have 2 strong references to the same object:
final class MasterPresenter<C: MasterCoordinator>: MasterPresenting {
private(set) weak var coordinator: C?
{...}
}
We switched our coordinator
property from private let
to private(set) weak var
, so we have a weak reference to the coordinator from our presenter and a strong refence to the coordinator from its parent (in children
).
Neat! We just avoided a bullet (aka retain cycle).
Now we need a way to free that memory whenever we have to release the coordinator, and we're going to implement another function usually found in the coordinator pattern, stop
.
Let's go back to Coordinator
:
extension Coordinator {
{...}
private func free<T: Coordinator>(child coordinator: T) {
children.removeValue(forKey: coordinator.identifier)
}
func stop() {
children.removeAll()
parent?.free(child: self)
}
}
With the stop
function we remove all the children, and with free
, we ask the parent to remove this child.
We have everything in place, now we just have to see where we're going to call this stop
function. But we have a problem, SwiftUI is not like UIKit, we do not have delegate methods to know when a view was released, like UINavigationControllerDelegate
.
So instead of waiting for the view to notify the coordinator it dissappeared, we're going to rely on the presenter to know when we have to release a coordinator:
final class MasterPresenter<C: MasterCoordinator>: MasterPresenting {
{...}
deinit {
coordinator?.stop()
}
{...}
}
Whenever SwiftUI decides to drop the view and it's linked presenter, we're going to tell its coordinator to also release itself.
Great! Looks good but... sadly, I have a but. There's another problem, do you think we're going to remember to call coordinator?.stop()
on the deinit
of every presenter we create? Exactly, me neither.
So we have 2 ways to solve this, one it's to add a rule to our linter (like SwiftLint) to alert us whenever it doesn't find the call to stop
in the presenter's deinit
. Another solution is to create a base presenter class and make all the presenters inherit from that one.
In my case, I choose the 2nd one:
class Presenter<C: Coordinator> {
private(set) weak var coordinator: C?
init(coordinator: C) {
self.coordinator = coordinator
}
deinit {
coordinator?.stop()
}
}
final class MasterPresenter<C: MasterCoordinator>: Presenter<C>, MasterPresenting {
@Published private(set) var viewModel: MasterViewModel
override init(coordinator: C) {
self.viewModel = MasterViewModel(date: Date())
super.init(coordinator: coordinator)
}
func onButtonPressed(isPresented: Binding<Bool>) -> some View {
return coordinator?.presentDetailView(isPresented: isPresented)
}
}
We created a new Presenter
base class, from which all our presenters will inherit now. As you can see, MasterPresenter
is now a subclass, and we don't have to worry about the coordinator not being released anymore.
Perfect! Now all the puzzle it's completed! We added helper functions to Coordinator
to manage adding and removing children, we implemented the stop
method to release the coordinator. We learned how to avoid a retain cycle. And we created a new presenter base class to deal with the coordinator lifecycle.
As I said before, I prefer composition over inheritance, but you have to choose the option that fits better your needs, in programming there no silver bullets, so be open to everything!
This is all what we've done in these 2 parts:
// You can put this in a playground and run it!
import SwiftUI
protocol Coordinator: AssociatedObject {
associatedtype U: View
associatedtype P: Coordinator
func start() -> U
func stop()
}
extension Coordinator {
private(set) var identifier: UUID {
get {
guard let identifier: UUID = associatedObject(for: &identifierKey) else {
self.identifier = UUID()
return self.identifier
}
return identifier
}
set { setAssociatedObject(newValue, for: &identifierKey) }
}
private(set) var children: [UUID: Any] {
get {
guard let children: [UUID: Any] = associatedObject(for: &childrenKey) else {
self.children = [UUID: Any]()
return self.children
}
return children
}
set { setAssociatedObject(newValue, for: &childrenKey) }
}
private func store<T: Coordinator>(child coordinator: T) {
children[coordinator.identifier] = coordinator
}
private func free<T: Coordinator>(child coordinator: T) {
children.removeValue(forKey: coordinator.identifier)
}
private(set) weak var parent: P? {
get { associatedObject(for: &parentKey) }
set { setAssociatedObject(newValue, for: &parentKey, policy: .weak) }
}
func coordinate<T: Coordinator>(to coordinator: T) -> some View {
store(child: coordinator)
coordinator.parent = self as? T.P
return coordinator.start()
}
func stop() {
children.removeAll()
parent?.free(child: self)
}
}
private var identifierKey: UInt8 = 0
private var childrenKey: UInt8 = 0
private var parentKey: UInt8 = 0
// MARK: AppCoordinator
final class AppCoordinator: Coordinator {
typealias P = AppCoordinator
private weak var window: UIWindow?
init(window: UIWindow) {
self.window = window
}
@discardableResult
func start() -> some View {
let coordinator = RootMasterCoordinator<AppCoordinator>(window: window)
return coordinate(to: coordinator)
}
}
// MARK: MasterCoordinator
protocol MasterCoordinator: Coordinator {}
extension MasterCoordinator {
func presentDetailView(isPresented: Binding<Bool>) -> some View {
let coordinator = NavigationDetailCoordinator<Self>(isPresented: isPresented)
return coordinate(to: coordinator)
}
}
final class RootMasterCoordinator<P: Coordinator>: MasterCoordinator {
private weak var window: UIWindow?
init(window: UIWindow?) {
self.window = window
}
func start() -> some View {
let view = MasterFactory.make(with: self)
let navigation = NavigationView { view }
let hosting = UIHostingController(rootView: navigation)
window?.rootViewController = hosting
window?.makeKeyAndVisible()
return EmptyView()
}
}
// MARK: DetailCoordinator
protocol DetailCoordinator: Coordinator {}
final class NavigationDetailCoordinator<P: Coordinator>: DetailCoordinator {
private var isPresented: Binding<Bool>
init(isPresented: Binding<Bool>) {
self.isPresented = isPresented
}
func start() -> some View {
return NavigationLink(destination: EmptyView(), isActive: isPresented) {
EmptyView()
}
}
}
// MARK: Factory
enum MasterFactory {
static func make<C: MasterCoordinator>(with coordinator: C) -> some View {
let presenter = MasterPresenter(coordinator: coordinator)
let view = MasterView(presenter: presenter)
return view
}
}
// MARK: Presenter
class Presenter<C: Coordinator> {
private(set) weak var coordinator: C?
init(coordinator: C) {
self.coordinator = coordinator
}
deinit {
coordinator?.stop()
}
}
// MARK: MVP
struct MasterViewModel {
let date: Date
}
protocol MasterPresenting: ObservableObject {
associatedtype U: View
var viewModel: MasterViewModel { get }
func onButtonPressed(isPresented: Binding<Bool>) -> U
}
final class MasterPresenter<C: MasterCoordinator>: Presenter<C>, MasterPresenting {
@Published private(set) var viewModel: MasterViewModel
override init(coordinator: C) {
self.viewModel = MasterViewModel(date: Date())
super.init(coordinator: coordinator)
}
func onButtonPressed(isPresented: Binding<Bool>) -> some View {
return coordinator?.presentDetailView(isPresented: isPresented)
}
}
struct MasterView<T: MasterPresenting>: View {
@ObservedObject var presenter: T
var body: some View {
NavigationButton(contentView: Text("\(presenter.viewModel.date, formatter: dateFormatter)"),
navigationView: { isPresented in
self.presenter.onButtonPressed(isPresented: isPresented)
})
}
}
// MARK: Helpers
struct NavigationButton<CV: View, NV: View>: View {
@State private var isPresented = false
var contentView: CV
var navigationView: (Binding<Bool>) -> NV
var body: some View {
Button(action: {
self.isPresented = true
}) {
contentView
.background(
navigationView($isPresented)
)
}
}
}
let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .medium
return dateFormatter
}()
protocol AssociatedObject: class {
func associatedObject<T>(for key: UnsafeRawPointer) -> T?
func setAssociatedObject<T>(
_ object: T,
for key: UnsafeRawPointer,
policy: AssociationPolicy
)
}
extension AssociatedObject {
func associatedObject<T>(for key: UnsafeRawPointer) -> T? {
return objc_getAssociatedObject(self, key) as? T
}
func setAssociatedObject<T>(
_ object: T,
for key: UnsafeRawPointer,
policy: AssociationPolicy = .strong
) {
return objc_setAssociatedObject(
self,
key,
object,
policy.objcPolicy
)
}
}
enum AssociationPolicy {
case strong
case copy
case weak
var objcPolicy: objc_AssociationPolicy {
switch self {
case .strong:
return .OBJC_ASSOCIATION_RETAIN_NONATOMIC
case .copy:
return .OBJC_ASSOCIATION_COPY_NONATOMIC
case .weak:
return .OBJC_ASSOCIATION_ASSIGN
}
}
}
We have learned how to implement stored properies in protocol extensions creating a mixin, using the power of the Objective-C runtime. We added helper functions to Coordinator
to manage adding and removing children, we implemented the stop
method to release the coordinator. We learned how to avoid a retain cycle. And we created a new presenter base class to deal with the coordinator lifecycle.
That's it! We've completed part 3 of this series. Now I invite you to take a look at whole project I created, where the final implementation is, with more classes and examples, and both ways of navigating, navigation stack and modals. There you can also take a look at AssociatedObject.swift
and see how we can create stored properties under the hood.
We've finished our experimental MVP+Coordinators SwiftUI project, but there're still a few considerations I wonder about:
NavigationLink
nor .sheet
. Hopefully, Apple will make that possible in the next SwiftUI version.All of them good candidates for an upcoming post ๐. I'll try to explore them but I can't promise anything, I dedicated a lot of time to these 3 blog posts and I can tell you writing is A LOT of work. So if you really like a writer/blogger, tell them! I'm sure they'll appreciate it immensely.
Last but not least, I'd like to give a big thank you to my friends Marin Todorov and Benedikt Terhechte for giving me early feedback about these blog posts ๐๐ปโโ๏ธ, you're the best! ๐ค
I hope you liked these part 1, part 2 and part 3 posts covering my experience trying to decouple the navigation in SwftUI.
Thank you for reading. Let me know what you think and share it with your friends!
Luis.