Nobody is really smart enough to program computers.
Steve McConnell (Code Complete 2.0)
We believe we should compose software from components that are:
- accurately named,
- simple,
- small,
- responsible for one thing,
- reusable.
- Active Components (ex. Interaction, Navigation, Mediator etc.)
- Passive Components (ex. Renderable)
- Views (ex. UILabel)
Renderable (noun)
Something (information) that view is able to show.
Renderable is a struct that you pass to a View which should show its contents.
- Passive,
- immutable struct,
- often converted from model or models,
struct BannerRenderable {
let message: String
}
- always look the same if ordered to render the same renderable,
- have code answering to one question: "how should I look?",
- have no state,
- accept infinite number of renderables in random order.
Step 1 - Create Renderable:
struct BannerRenderable {
let message: String
init(message: String) {
self.message = message
}
}
Step 2 - Expose View ability in Protocol:
protocol BannerRendering: AnyObject {
func render(_ renderable: BannerRenderable)
}
Step 2 - Implement View:
final class BannerView: UIView {
@IBOutlet fileprivate var bannerLabel: UILabel!
}
extension BannerView: BannerRendering {
func render(_ renderable: BannerRenderable) {
bannerLabel.text = renderable.message
}
}
Interaction (noun)
Component answering the question: What should I do in reaction to user's action.
Interaction is a final class that ViewController or View owns, which performs actions in reaction to user input - gestures, text input, device movement, geographical location change etc.
- Active component (has a lifecycle),
- final class,
- can use other components for data retrieval,
- should be literally between two actions, the one causing and the one caused,
- can call navigation methods when other ViewController should be presented,
- can call render methods on weakly held views (seen via protocol).
π RULE: There is no good reason for inheritance of custom classes, ever.
- Can have many Interactions for separate functions,
- should OWN Interaction and see it via protocol,
- subviews of VC could own separate Interaction but itβs not mandatory for simple VC.
Step 1 - Create Interaction protocols:
protocol PodBayDoorsInteracting: AnyObject {
func use(_ banner: BannerRendering)
func didTapMainButton()
}
Step 2 - Add Interaction implementation:
π RULE: Do not tell Interaction what to do, tell what happened, it should decide what should happen next.
final class PodBayDoorsInteraction {
fileprivate let killDave = true
fileprivate weak var banner: BannerRendering?
}
extension PodBayDoorsInteraction: PodBayDoorsInteracting {
private enum Strings {
static let halsAnswer = "I know you and Frank were planning to disconnect me, and that is something I cannot allow to happen."
}
func didTapMainButton() {
guard let banner = banner else { fatalError() }
if killDave { // Just to show the business logic is resolved here.
banner.render(BannerRenderable(message: Strings.halsAnswer))
} else {
// Open doors. Not implemented :P
}
}
func use(_ banner: BannerRendering) {
self.banner = banner
}
}
Step 3 - Use it in ViewController (via nib):
final class DiscoveryOneViewController: UIViewController {
fileprivate let podBayInteraction: PodBayDoorsInteracting
@IBOutlet private var bannerView: BannerRendering!
init(podBayInteraction: PodBayDoorsInteracting) {
self.podBayInteraction = podBayInteraction
super.init(nibName: nil, bundle: nil)
}
override func awakeFromNib() {
super.awakeFromNib()
podBayInteraction.use(bannerView)
}
@IBAction private func didTapMainButton() {
podBayInteraction.didTapMainButton()
}
}
Assembler (noun)
Assembler is gathering dependencies and injecting them into a single instantiated component.
Its purpose is to assemble dependencies for one class or struct.
- an enum,
- has only one method called
assemble()
. - can call ONLY ONE initializer directly,
- will call other Assemblers to instantiate dependencies.
Step 1 - Create Interaction protocols:
π RULE: When assembled component is dependent on other instances, you should use it's Assemblers, do not initialize more than one class directly.
protocol PodBayDoorsInteractionAssembling {
func assemble() -> PodBayDoorsInteracting
}
struct PodBayDoorsInteractionAssembler {
func assemble() -> PodBayDoorsInteracting {
return PodBayDoorsInteraction()
}
}
protocol DiscoveryOneAssembling {
func assemble() -> UIViewController
}
struct DiscoveryOneAssembler: DiscoveryOneAssembling {
let interactionAssembler: PodBayDoorsInteractionAssembling
func assemble() -> UIViewController {
return DiscoveryOneViewController(interaction: interactionAssembler.assemble())
}
}
let interactionAssembler = PodBayDoorsInteractionAssembler()
let viewControllerAssembler = DiscoveryOneAssembler(interactionAssembler: interactionAssembler)
let viewController = viewControllerAssembler.assemble()
Navigation (noun)
Component responsible for presenting views (here: View Controllers).
Interaction may need to open another View Controller in response to user's action, it has to use Navigation component to do that.
- A class,
- owned by Interaction,
- can push or open views modally,
- will use Assemblers to create ViewControllers to show.
π RULE: We should never pass classes directly, always via protocol or Adapter.
NavigationControlling.swift
protocol NavigationControlling: AnyObject {
func pushViewController(_ viewController: UIViewController, animated: Bool)
}
// `NavigationControlling` is a protocol convering (some) methods of `UINavigationController`
extension UINavigationController: NavigationControlling { }
Step 1 - Create Navigating protocol:
π RULE: When A uses B but A is not an owner of B, pass B via use(_)
method, not in init
.
PodBayNavigation.swift
protocol DiscoveryOneNavigating: AnyObject {
func use(_ navigationController: NavigationControlling)
func presentDiscoveryOneInterface()
}
Step 2 - Implement Navigation:
DiscoveryOneNavigation.swift
final class DiscoveryOneNavigation: DiscoveryOneNavigating {
private let discoveryAssembler: DiscoveryOneAssembling
private weak var navigationController: NavigationControlling?
init(navigationController: NavigationControlling, discoveryAssembler: DiscoveryOneAssembling) {
self.navigationController = navigationController
self.discoveryAssembler = discoveryAssembler
}
func use(_ navigationController: NavigationControlling) {
self.navigationController = navigationController
}
func presentDiscoveryOneInterface() {
let discovery = discoveryAssembler.assemble()
navigationController.pushViewController(discovery, animated: true)
}
}