-
Notifications
You must be signed in to change notification settings - Fork 76
EpoxyNavigationController
NavigationController
brings an easy-to-use declarative API to UINavigationController
making it clear what the current state of the navigation stack is, while also making it easy to update.
Imagine you have a flow of view controllers that you'd like to coordinate. Maybe you are working on a form or onboarding flow with multiple steps. In an imperative world, you would need to manually push and pop view controllers as needed when actions occur. This can quickly get hairy and lead to unexpected states and errors. NavigationController
solves this by having a central source of truth for the current state of the navigation stack.
As an example, let's assume we are building a form with 3 steps. We can keep track of which steps are on the navigation stack through a shared State
object, and simply return a NavigationModel
if it should be shown, or nil
if it should be hidden. Here's the code:
final class FormViewController: NavigationController {
override func viewDidLoad() {
super.viewDidLoad()
setStack(stack, animated: false)
}
// MARK: Private
private struct State {
// we don't need a showStep1 flag because it will always be on the stack
var showStep2 = false
var showStep3 = false
}
private enum DataIDs {
case step1
case step2
case step3
}
private var state = State() {
didSet {
setStack(stack, animated: true)
}
}
private var stack: [NavigationModel?] {
// Note that when this view loads, only step1 is non-nil which will have the result
// of our navigation stack only having one UIViewController.
// Order is important here - the order you return these in
// will determine the order they are pushed onto the stack
[
step1,
step2,
step3
]
}
private var step1: NavigationModel {
// we use the `root` `NavigationModel` here because `step1` will always be present in the stack
// acting as our UINavigationController's rootViewController
.root(dataID: DataIDs.step1) { [weak self] in
let viewController = Step1ViewController()
viewController.didTapNext = {
// setting this property on our state will automatically update the `state` instance variable
// which will cause the stack to be re-created and reset. This will have the effect of pushing on Step2ViewController.
self?.state.showStep2 = true
}
}
}
private var step2: NavigationModel? {
guard state.showStep2 else { return nil }
return NavigationModel(
dataID: DataIDs.step2,
makeViewController: { [weak self] in
let vc = Step2ViewController()
vc.didTapNext = {
self?.showStep3 = true
}
return vc
},
// if the user taps back (or we programmatically pop this VC) this closure will be called so we can
// keep our navigation stack state up-to-date
remove: { [weak self] in
self?.state.showStep2 = false
})
}
private var step3: NavigationModel? {
guard state.showStep3 else { return nil }
return NavigationModel(
dataID: DataIDs.step3,
makeViewController: { [weak self] in
let vc = Step3ViewController()
vc.didTapNext = {
// Handle dismissal of this flow, or navigate somewhere else
}
return vc
},
remove: { [weak self] in
self?.state.showStep3 = false
})
}
}
One issue with UINavigationController
as it is today is that you cannot nest them. If you try to push a UINavigationController
onto another UINavigationController's
navigation stack, your app will crash. Epoxy's NavigationController
solves this by allowing you to initialize it with an optional wrapNavigation
closure which returns a standard UIViewController
with the expectation that the provided UINavigationController
is a child of that UINavigationController
.
final class ComplexFormViewController: NavigationController {
init() {
super.init(wrapNavigation: { navigationController in
// wrap the navigationController in a `UIViewController` and return that view controller
})
}
}
Note that it's important to ensure that nested UINavigationControllers
hide their navigationBar
. You can use EpoxyBars
TopBarInstaller
to create custom navigation bars that live outside of the UINavigationController
as a substitute.
NavigationModel
has a few helpful callbacks you can set to respond to navigation lifecycle events. For example, if you wanted to log an event whenever a particular view controller becomes visible in the stack, you could do that like this:
private var step2: NavigationModel? {
guard state.showStep2 else { return nil }
return NavigationModel(
dataID: DataIDs.step2,
makeViewController: { ... },
remove: { ... })
.didShow { [weak self] viewController in
self?.logDidShowEvents(for: viewController)
}
}
There are 4 available callbacks you can utilize:
Callback | Discussion |
---|---|
didShow |
Invoked when the view controller becomes the top view controller on the navigation stack (the currently visible view controller) |
didHide |
Invoked when the view controller is no longer the top view controller on the navigation stack |
didAdd |
Invoked when the view controller is added to the navigation stack |
didRemove |
Invoked when the view controller is removed from the navigation stack |
- Overview
ItemModel
andItemModeling
- Using
EpoxyableView
CollectionViewController
CollectionView
- Handling selection
- Setting view delegates and closures
- Highlight and selection states
- Responding to view appear / disappear events
- Using
UICollectionViewFlowLayout
- Overview
GroupItem
andGroupItemModeling
- Composing groups
- Spacing
StaticGroupItem
GroupItem
withoutEpoxyableView
- Creating components inline
- Alignment
- Accessibility layouts
- Constrainable and ConstrainableContainer
- Accessing properties of underlying Constrainables