Factory Method
Factory Method places object creation in a method that a specialized creator can override. The caller asks for a screen; the creator chooses which concrete screen to make.
When it helps in iOS
Use it when the same app flow has multiple implementations, such as a standard checkout and a business checkout, and each flow must create its own review screen. It is especially useful in an existing UIKit codebase with subclassed flow controllers. For a single choice with no need for subclassing, a small factory function or switch is simpler.
Example: choosing a checkout review screen
import UIKit
class CheckoutFlow {
// Subclasses decide which concrete screen this flow creates.
func makeReviewScreen() -> UIViewController {
StandardReviewViewController()
}
func start(in navigationController: UINavigationController) {
navigationController.pushViewController(makeReviewScreen(), animated: true)
}
}
final class BusinessCheckoutFlow: CheckoutFlow {
override func makeReviewScreen() -> UIViewController {
BusinessReviewViewController()
}
}
final class StandardReviewViewController: UIViewController {}
final class BusinessReviewViewController: UIViewController {}
let flow: CheckoutFlow = BusinessCheckoutFlow()
// In an app, call flow.start(in: navigationController).The start method contains the common navigation behavior. Only makeReviewScreen() varies. A caller can hold a CheckoutFlow without knowing the concrete review controller.
How to use it in a real app
Create the flow at the composition root, where the app knows whether the account is standard or business. Inject the required services into the controller inside makeReviewScreen(). Keep the factory method responsible for construction; validation and payment submission belong in feature services.
Watch for: A growing subclass tree is a sign that composition may be better. If the differences are data or one injected service, use one screen with injected configuration instead.
Trace the decision
- The app chooses a flow from account state.
start(in:)keeps navigation identical for every flow.- The factory method supplies the review controller that matches that flow.
This is a class-based extension point. For a new app without subclassed flows, injecting a screen-building closure or a factory protocol is usually easier to evolve. Test the returned controller type and its injected services separately from navigation. Do not reach into UIApplication.shared inside the factory method; receive the navigation controller and dependencies explicitly.
Understand the extension point
CheckoutFlow.start(in:) defines the invariant part of the process: ask for a review screen, then push it. makeReviewScreen() is the variable step. BusinessCheckoutFlow overrides only that step, so it inherits the common navigation logic while choosing its own controller. The caller can hold a CheckoutFlow and start either variant without branching on the concrete screen type.
From example to UIKit navigation
At the app boundary, inspect the account type and instantiate the appropriate flow. In a real factory method, pass payment and order services into the newly created controller so it is ready to work before it is pushed. Keep the view controller focused on UI and let the flow own routing. If a new review variant differs only by text or a single dependency, consider one controller with configuration rather than a subclassed flow.
Failure modes and verification
Test that each flow returns the expected screen and has all required dependencies. Test start(in:) with a navigation controller to confirm the returned screen is pushed. A deep subclass hierarchy makes future combinations hard to reason about; a protocol-based factory or closure can be a better Swift-style extension point. Factory Method chooses one product; Abstract Factory coordinates several related products.