- Notifications
You must be signed in to change notification settings - Fork7
Models UI navigation patterns using TCA
License
heinzl/swift-composable-navigation
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
The Composable Navigation is a Swift Package that builds on top ofThe Composable Architecture (TCA, for short). It models UI navigation patterns using TCA. The corresponding navigation views are implemented in UIKit.
The concept is inspired by the coordinator pattern as it allows a clean separation between individual screens and the logic that ties those screens together. In a TCA world, a coordinator is represented by a state composed of its child views' sub-states and the navigation state. A reducer would then be able to manage the navigation state similar as a coordinator would do by observing actions from child views and presenting/dismissing other screens.
ModalNavigation models state and actions of a commonly used modal view presentation.Views can be presented with a certain style and dismissed. TheModalNavigationHandler listens to state changes and presents the provided views accordingly. Any state changes are reflected by the handler using UIKit.
Setting the current navigation item to a different screen will result in dismissing the old screen and presenting the new screen. Even changes to only the presentation style are reflected accordingly.
It also supports automatic state updates for pull-to-dismiss for views presented as a sheet.
This example shows how a modal-navigation could be implemented using an enum:
structOnboarding:Reducer{enumScreen{case logincase register}structState:Equatable{varmodalNavigation= ModalNavigation<Screen>.State()...}enumAction:Equatable{case modalNavigation(ModalNavigation<Screen>.Action)...}varbody:someReducer<State,Action>{Scope(state: \.modalNavigation, action:/Action.modalNavigation){ModalNavigation<Screen>()}Reduce{ state, actioninswitch action{case.loginButtonPressed:return.send(.modalNavigation(.presentFullScreen(.login)))case.anotherAction:return.send(.modalNavigation(.dismiss))}return.none}}}
StackNavigation models state and actions of a stack-based scheme for navigating hierarchical content.Views can be pushed on the stack or popped from the stack. Even mutations to the whole stack can be performed. TheStackNavigationHandler listens to state changes and updates the view stack accordingly using UIKit.
It also supports automatic state updates for popping items via the leading-edge swipe gesture or the long press back-button menu.
This example shows how a series of text inputs could be implemented:
structRegister:Reducer{enumScreen{case emailcase firstNamecase lastName}structState:Equatable{varstackNavigation= StackNavigation<Screen>.State(items:[.email])...}enumAction:Equatable{case stackNavigation(StackNavigation<Screen>.Action)...}varbody:someReducer<State,Action>{Scope(state: \.stackNavigation, action:/Action.stackNavigation){StackNavigation<Screen>()}Reduce{ state, actioninswitch action{case.emailEntered:return.send(.stackNavigation(.pushItem(.firstName)))case.firstNameEntered:return.send(.stackNavigation(.pushItem(.lastName)))...}return.none}}}
TabNavigation models state and actions of a tab-based scheme for navigating multiple child views.The active navigation item can be changed by setting a new item. Even mutations to items array can be performed (e.g. changing the tab order). TheTabNavigationHandler listens to state changes and updates the selected view or tab order accordingly.
Example:
structRoot{enumScreen:CaseIterable{case homecase listcase settings}structState:Equatable{vartabNavigation= TabNavigation<Screen>.State( items:Screen.allCases, activeItem:.home)...}enumAction:Equatable{case tabNavigation(TabNavigation<Screen>.Action)...}varbody:someReducer<State,Action>{Scope(state: \.tabNavigation, action:/Action.tabNavigation){TabNavigation<Screen>()}Reduce{ state, actioninswitch action{case.goToSettings:return.send(.tabNavigation(.setActiveItem(.settings)))...}return.none}}}
TheViewProvider creates a view according to the given navigation item. It implementsViewProviding which requires the type to create aPresentable (e.g. a SwiftUI View or a UIViewController) for a given navigation item.
Navigation handler (likeStackNavigationHandler) expect aViewProvider. It is used to create new views. The navigation handler will reuse the already created view for.b if the stack of navigation items changes like this:[.a, .b, .c] ->[.x, .y, .b,]
structViewProvider:ViewProviding{letstore:Store<State,Action>func makePresentable(for navigationItem:Screen)->Presentable{switch navigationItem{case.a:returnViewA(store: store.scope(...))case.b:returnViewB(store: store.scope(...))}}}
A navigation container view can be integrated like any otherUIViewController in your app.
This is an example of aTabNavigationViewController in aSceneDelegate:
classSceneDelegate:UIResponder,UIWindowSceneDelegate{varwindow:UIWindow? lazyvarstore:Store<App.State,App.Action>=...func scene(_ scene:UIScene, willConnectTo session:UISceneSession, options connectionOptions:UIScene.ConnectionOptions){guardlet windowScene= sceneas?UIWindowSceneelse{return}letcontroller=TabNavigationViewController(store: store.scope(state: \.tabNavigation,action:App.Action.tabNavigation),viewProvider:App.ViewProvider(store: store))letwindow=UIWindow(windowScene: windowScene)window.rootViewController= controllerself.window= windowwindow.makeKeyAndVisible()}...}
You can use the corresponding "handlers" instead e.g. (ModalNavigationHandler) if you already have a custom view controller implementation.Make sure to callnavigationHandler.setup(with: viewController) similar to this:
classExistingViewController:UIViewController{letviewStore:ViewStore<ExistingViewShowcase.State,ExistingViewShowcase.Action>varcancellables:Set<AnyCancellable>=[]letnavigationHandler:ModalNavigationHandler<ExistingViewShowcase.ViewProvider>init(store:Store<ExistingViewShowcase.State,ExistingViewShowcase.Action>){self.viewStore=ViewStore(store, observe:{ $0})self.navigationHandler=ModalNavigationHandler(store: store.scope(state: \.modalNavigation,action:ExistingViewShowcase.Action.modalNavigation), viewProvider:ExistingViewShowcase.ViewProvider(store: store))super.init(nibName:nil, bundle:nil)self.navigationHandler.setup(with:self)}...}
StackNavigationHandler can be initialized with theignorePreviousViewControllers: Bool parameter. When this parameter is set totrue theStackNavigationHandler will ignore the view controllers that are already on the stack. This is particularly helpful when Composable Navigation is used on top of already existing code.
You can find multiple showcases in theExamples project.
The example app hosts multiple showcases (and UI tests), to run one of the showcase you need to changes the variableshowcase inSceneDelegate.swift.
...// 👉 Choose showcase 👈letshowcase:Showcase=.advanced...
This library is released under the MIT license. SeeLICENSE for details.
About
Models UI navigation patterns using TCA
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.