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. Here in Part 2, I’ll introduce the first step in how code can be architected to be better positioned to respond to change. In Part 3, I’ll complete the approach.
ViewController
sWhile the Contact app example is simple, it shows how business logic creeps into display logic and makes reuse difficult. The problem is that the ViewController
knows too much about the world around it. A ViewController
should be designed to be unaware of the world around it.
ViewController
should understand only how to make itself operate and go; it only controls itself.ViewController
needs something from the outside world to make it go, such as the data to display, those things (dependencies) must be provided (injected).ViewController
needs to communicate something back to the outside world, it should use a broadcast mechanism such as delegation.There is also the implication a ViewController
should only publicly expose an API that is truly public. Properties such as IBOutlet
s, delegates, and other data members, along with internal implementation functions should be declared private
. In fact, it’s good practice to default to private
and only open up access when it must (Principle of Least Privilege).
The ViewController
will be implemented in standard ways: in code, via storyboard, or whatever the project’s convention may be.
That which the ViewController
needs to work – dependencies, such as data, helpers, managers, etc. – must be injected at or as close to instantiation time as possible, preferably before the view is loaded (viewDidLoad()
is called). If the ViewController
is instantiated in code, the dependency injection could be performed via an overloaded initializer. If instantiating a ViewController
from a storyboard, a configure()
function is used to inject the dependencies, calling configure()
as soon as possible after instantiation. Even if injection at initialization is possible, adopting configure()
may be desirable to ensure a consistent pattern of ViewController
initialization throughout the app. That which the ViewController
needs to relay to the outside world – for example, the user tapped something – must be relayed to the outside world, preferably via delegation.
Here’s the Contacts code, reworked under this design:
/// Shows a single Person in detail. class PersonViewController: UIViewController { // Person is an implicitly unwrapped optional, because the public API contract for this // class requires a `Person` for the class to operate properly: note how `configure()` // takes a non-optional `Person`.. The use of an Implicity Unwrapped Optional simplifies and // enforces this contract. // // This is not a mandated part of this pattern; just something it enables, if appropriate // for your need. private var person: Person! func configure(person: Person) { self.person = person } override func viewDidLoad() { super.viewDidLoad() title = person.name // for example } } /// Delegate protocol for handling PersonsViewController actions protocol PersonsViewControllerDelegate: AnyObject { func didSelect(person: Person, in viewController: PersonsViewController) } /// Shows a master list of Persons. class PersonsViewController: UITableViewController { private let persons = [ Person(name: "Fred"), Person(name: "Barney"), Person(name: "Wilma"), Person(name: "Betty") ] private weak var delegate: PersonsViewControllerDelegate? func configure(delegate: PersonsViewControllerDelegate) { self.delegate = delegate } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedPerson = persons[indexPath.row] delegate?.didSelect(person: selectedPerson, in: self) } }
PersonsViewController
now supports a delegate. The delegate is provided to the PersonsViewController
by means of a configure()
function, when the PersonsViewController
is instantiated (see Part 3 of this series). Because the delegate
property is an implementation detail, it is declared private
. The public API for accessing (setting) the delegate is the configure()
function. Note the delegate argument is non-optional; the public API contract of this class is that a delegate is required (optionality andweak
ness is an implementation detail of delegate properties).
When the user taps a row, the delegate’s didSelect(person:in:)
function is invoked. How should didSelect(person:in:)
be implemented? However is appropriate for the context: showing the PersonViewController
in the first app tab, and showing a GroupsViewController
in the second app tab. This business logic is up to someone else to decide, not the PersonsViewController
. I’ll show how this comes together in Part 3.
Now the PersonsViewController
is more flexible and reusable in other contexts. Perhaps the next version of the app adds a third tab, showing a list of Persons and tapping shows their parents. We can quickly implement the third tab with the existing PersonsViewController
and merely have a new delegate implementation.
I did not create a delegate for the PersonViewController
because it has no need to communicate with outside code. If PersonViewController
would need to notify outside code of user action (e.g. it supports an Edit mode and the user tapped to edit), then a PersonViewControllerDelegate
would be appropriate to define.
Additionally, the data was simply declared as a private data member of PersonsViewController
. A better solution would be for the data to be injected in via the configure()
function. Perhaps as [Person]
, or perhaps a DataSource
type object that provided the [Person]
from a network request, from a Core Data store, or wherever the app stores its data.
It’s an intentional choice to prefer delegation over notification (Notification
/NotificationCenter
). In general, only one other thing cares to know about the changes to the ViewController
, so delegation provides clear and focused handling. Notification
is global and heavy-handed for our needs. It’s great for fire-and-forget operations, but inappropriate when receipt is required. As well, Notification permits others that may not be directly involved to tap into the event system of the View-Delegate, which is generally not desired.
This isn’t to say you cannot use Notification – you could if truly that was right. On the same token, if you find yourself needing a ViewController
to notify more than one other thing, I would 1. consider your design to ensure this is truly needed and/or there isn’t another way to solve it, 2. consider instead using a MulticastDelegate
type of approach, so it’s still delegation, just to explicit many objects (vs. NotificationCenter
‘s global broadcast).
Consider as well, instead of using delegation or notification, you could provide closures to be executed as handlers for these actions. On one project, I implemented this design using closures because it made sense for what needed to be handled. There’s no hard-and-fast approach: it’s about knowing the options, and when it is and is not appropriate to use each.
Typically, a ViewController
‘s communication with the outside world will be one-way: out-going messages; as such, return types will be Void
. However, it is within the pattern to allow delegate functions to return a value. Perhaps during a ViewController
‘s operation, someone outside must be queried then the ViewController
responds accordingly. It’s perfectly acceptable to support this sort of query via delegation. This is another reason why delegation can work better than notifications. Note: this lends to synchronous queries. If you find the ViewController
needs an async mechanisms via delegation, it may be worth considering another route (including accepting the complexity).
We’re not done yet! In Part 3, I’ll show how to make all of this… flow.
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...