Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
This article series explores a coding approach we use at Big Nerd Ranch that enables us to more easily respond to change. In Part 1, I presented the example of a simplified Contacts app and how it might traditionally be implemented, along with disqualifying three common approaches used to respond to change. In Part 2, I introduced the first step in how code can be architected to be better positioned to respond to change. Here in Part 3, I’ll complete the approach.
The approach presented in Part 2 is a good start, but where does PersonsViewControllerDelegate
get implemented? Exactly how and where the delegate protocol is implemented can vary, just like any delegate implementation in typical iOS/Cocoa development. But if we take a fundamental Model-View-Controller (MVC) approach, it would be common for the Controller to implement the delegate. But now we’re getting into terminology overlap, so we’ve taken a slightly different approach with Coordinators, specifically a FlowCoordinator
.
A FlowCoordinator
does what the name says: it coordinates flows within the app. The app has numerous stand-alone ViewController
s for each screen in the app, and the FlowCoordinator
stitches them together along with helping to manage not just the UI flow but also the data flow. Consider a login flow (LoginFlowCoordinator
): the login screen, which could flow to a “forgot password” screen or a sign-up screen, and finally landing on the main screen of the app after successful login. Or a Settings flow (SettingsFlowCoordinator
), which navigates the user in and out of the various settings screens and helping to manage the data flow of the settings. Let’s rework the “show persons and their detail” part of the app to use a FlowCoordinator
:
protocol PersonsViewControllerDelegate: AnyObject { func didSelect(person: Person, in viewController: PersonsViewController) } /// Shows a master list of Persons. class PersonsViewController: UITableViewController { private var persons: [Person] = [] private weak var delegate: PersonsViewControllerDelegate? func configure(persons: [Person], delegate: PersonsViewControllerDelegate) { self.persons = persons self.delegate = delegate } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedPerson = persons[indexPath.row] delegate?.didSelect(person: selectedPerson, in: self) } } // ----- protocol FlowCoordinatorDelegate: AnyObject { } protocol FlowCoordinator { associatedtype DelegateType var delegate: DelegateType? { get set } var rootViewController: UIViewController { get } } // ----- protocol ShowPersonsFlowCoordinatorDelegate: FlowCoordinatorDelegate { // nothing, yet. } class ShowPersonsFlowCoordinator: FlowCoordinator { weak var delegate: ShowPersonsFlowCoordinatorDelegate? var rootViewController: UIViewController { return navigationController } private var navigationController: UINavigationController! private let persons = [ Person(name: "Fred"), Person(name: "Barney"), Person(name: "Wilma"), Person(name: "Betty") ] init(delegate: ShowPersonsFlowCoordinatorDelegate) { self.delegate = delegate } func start() { let personsVC = PersonsViewController.instantiateFromStoryboard() personsVC.configure(persons: persons, delegate: self) navigationController = UINavigationController(rootViewController: personsVC) } } extension ShowPersonsFlowCoordinator: PersonsViewControllerDelegate { func didSelect(person: Person, in viewController: PersonsViewController) { let personVC = PersonViewController.instantiateFromStoryboard() personVC.configure(person: person) navigationController.pushViewController(personVC, animated: true) } }
A FlowCoordinator
protocol defines a typical base structure for a Flow Coordinator. It provides a means to get the rootViewController
, and also a delegate of its own. The FlowCoordinator
pattern does not demand a delegate, but experience has proven it a handy construct in the event the FlowCoordinator
needs to pass information out (e.g. back to its parent FlowCoordinator
).
ShowPersonsFlowCoordinator.start()
s by creating the initial ViewController
: a PersonsViewController
. It is of some debate if initial FlowCoordinator
state should be established within init()
or a separate function like start()
; there are pros and cons to each approach. You can see here we also now have the FlowCoordinator
owning the data source (the array of Person
s), which is a more correct setup. Then the data to display and delegate are injected into the PersonsViewController
immediately after instantiation and before the view loads. Now when a user views a PersonsViewCoordinator
and selects a Person
, its PersonsViewControllerDelegate
is invoked. As ShowPersonsFlowCoordinator
is the delegate, it implements the instantiation of and navigation (flow) to the PersonViewController
to show the Person
in detail.
To implement the other tab, create a ShowGroupsFlowCoordinator
. It start()
s by instantiating the PersonsViewController
, and the delegate didSelect
can push the GroupsViewController
. We’re done. We’ve made the PersonsViewController
have a single responsibility, unaware of its surroundings, with dependencies injected, messages and actions delegated. This creates a thoughtful architecture, delivering quicker, with less complication, and a more robust, reusable codebase.
Stepping back and looking at the application as a whole, there are additional improvements that can be made to help with factoring, flow, and coordination.
Too often, AppDelegate
gets overloaded with application-level tasks, instead of purely UIApplicationDelegate
tasks. Having an AppCoordinator
avoids the “massive App Delegate” problem, enables the AppDelegate
to remain focused on UIApplicationDelegate
-level matters, factoring application-specific handling into the Coordinator. If you’re adopting UIScene
/UISceneDelegate
, you can adopt a similar approach. The AppCoordinator
could own shared resources, such as data sources, as well as owning and establishing the top-level UI and Flows. It might be implemented like this:
@UIApplicationMain class AppDelegate: UIResponder { var window: UIWindow? = { UIWindow(frame: UIScreen.main.bounds) }() private lazy var appCoordinator: AppCoordinator = { AppCoordinator(window: self.window!) }() } extension AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { appCoordinator.start() return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } // ----- class AppCoordinator: FlowCoordinator { weak var delegate: FlowCoordinatorDelegate? // protocol conformance; the AppCoordinator is top-most and does not have a delegate. private let window: UIWindow var rootViewController: UIViewController { guard let rootVC = window.rootViewController else { fatalError("unable to obtain the window's rootViewController") } return rootVC } private var personDataSource: PersonDataSourceable! private var showPersonsFlowCoordinator: ShowPersonsFlowCoordinator! private var showGroupsFlowCoordinator: ShowGroupsFlowCoordinator! init(window: UIWindow) { self.delegate = nil // emphasize that we do not have a delegate self.window = window establish() } func start() { // Typically a FlowCoordinator will install their first ViewController here, but // since this is the app's coordinator, we need to ensure the root/initial UI is // established at a prior time. // // Still, having this here is useful for convention, as well as giving a clear // point of instantiation and "starting" the AppCoordinator, even if the implementation // is currently empty. Your implementation may have tasks to start. } private func establish() { establishLogging() loadConfiguration() personDataSource = PersonDataSource() // shared data resource showPersonsFlowCoordinator = ShowPersonsFlowCoordinator(dataSource: personDataSource, delegate: self) showPersonsFlowCoordinator.start() showGroupsFlowCoordinator: ShowGroupsFlowCoordinator(dataSource: personDataSource, delegate, self) showGroupsFlowCoordinator.start() // abbreviated code, for illustration. let tabBarController = UITabBarController(...) tabBarController.setViewControllers([showPersonsFlowCoordinator.rootViewController, showGroupsFlowCoordinator.rootViewController], animated: false) window.rootViewController = tabBarController window.makeKeyAndVisible() } } extension AppCoordinator: ShowPersonsFlowCoordinatorDelegate { } extension AppCoordinator: ShowGroupsFlowCoordinatorDelegate { }
Storyboard segues create tight couplings: in the storyboard file itself, in the prepare(for:sender:)
function since it must exist within the ViewController
being transitioned from. We are striving to create loose couplings with flexible routing. Thus, segues generally are avoided with this approach.
The use of dependency injection – that typically the Coordinator
might own a resource and then “pass the baton” in via configure()
and data out via delegation – all of this tends to avoid the use of singletons and the issues they can bring.
I’m not anti-singleton. They must be used carefully, as they can complicate unit testing and make modularity difficult.
That said, I have encountered times using this design where the baton passing was heavy-handed. Some nested child Coordinator
was the only thing that needed some resource, and that resource was owned somewhere at the top of the chain. Then all things in between had to be modified, just to pass the baton down. Such is the trade-off; and is more exception than rule.
This isn’t a perfect solution to all things (as you can see, there’s some variance and adaptability allowed). However, it’s a solution that has worked well for us across a great many projects at Big Nerd Ranch.
As development on Apple platforms evolves, due to technologies like Combine and SwiftUI, we’ll evolve our approaches to enable us to leverage new technology while maintaining strong foundational principles of software development.
Hopefully, it can work well for you and your projects.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...