Giter VIP home page Giter VIP logo

swift-composable-navigation's Introduction

CI codecov

Composable Navigation

The Composable Navigation is a Swift Package that builds on top of The 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.

Features

Modal navigation

ModalNavigation models state and actions of a commonly used modal view presentation. Views can be presented with a certain style and dismissed. The ModalNavigationHandler 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:

struct Onboarding: Reducer {
    enum Screen {
        case login
        case register
    }

    struct State: Equatable {
        var modalNavigation = ModalNavigation<Screen>.State()
        ...
    }

    enum Action: Equatable {
        case modalNavigation(ModalNavigation<Screen>.Action)
        ...
    }
    
	var body: some Reducer<State, Action> {
		Scope(state: \.modalNavigation, action: /Action.modalNavigation) {
			ModalNavigation<Screen>()
		}
		Reduce { state, action in
			switch action {
			case .loginButtonPressed:
				return .send(.modalNavigation(.presentFullScreen(.login)))
			case .anotherAction:
				return .send(.modalNavigation(.dismiss))
			}
			return .none
		}
	}
}

Stack navigation

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. The StackNavigationHandler 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:

struct Register: Reducer {
    enum Screen {
        case email
        case firstName
        case lastName
    }

    struct State: Equatable {
        var stackNavigation = StackNavigation<Screen>.State(items: [.email])
        ...
    }

    enum Action: Equatable {
        case stackNavigation(StackNavigation<Screen>.Action)
        ...
    }
    
	var body: some Reducer<State, Action> {
		Scope(state: \.stackNavigation, action: /Action.stackNavigation) {
			StackNavigation<Screen>()
		}
		Reduce { state, action in
			switch action {
			case .emailEntered:
				return .send(.stackNavigation(.pushItem(.firstName)))
			case .firstNameEntered:
				return .send(.stackNavigation(.pushItem(.lastName)))
			...
			}
			return .none
		}
	}
}

Tab navigation

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). The TabNavigationHandler listens to state changes and updates the selected view or tab order accordingly.

Example:

struct Root {
    enum Screen: CaseIterable {
        case home
        case list
        case settings
    }

    struct State: Equatable {
        var tabNavigation = TabNavigation<Screen>.State(
            items: Screen.allCases,
            activeItem: .home
        )
        ...
    }

    enum Action: Equatable {
        case tabNavigation(TabNavigation<Screen>.Action)
        ...
    }
    
	var body: some Reducer<State, Action> {
		Scope(state: \.tabNavigation, action: /Action.tabNavigation) {
			TabNavigation<Screen>()
		}
		Reduce { state, action in
			switch action {
			case .goToSettings:
				return .send(.tabNavigation(.setActiveItem(.settings)))
			...
			}
			return .none
		}
	}
}

ViewProviding

The ViewProvider creates a view according to the given navigation item. It implements ViewProviding which requires the type to create a Presentable (e.g. a SwiftUI View or a UIViewController) for a given navigation item.

Navigation handler (like StackNavigationHandler) expect a ViewProvider. 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,]

struct ViewProvider: ViewProviding {
    let store: Store<State, Action>
    
    func makePresentable(for navigationItem: Screen) -> Presentable {
        switch navigationItem {
        case .a:
            return ViewA(store: store.scope( ... ))
        case .b:
            return ViewB(store: store.scope( ... ))
        }
    }
}

Usage

A navigation container view can be integrated like any other UIViewController in your app.

This is an example of a TabNavigationViewController in a SceneDelegate:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
	var window: UIWindow?

    lazy var store: Store<App.State, App.Action> = ...

	func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
		guard let windowScene = scene as? UIWindowScene else {
			return
		}
		
		let controller = TabNavigationViewController(
			store: store.scope(
				state: \.tabNavigation,
				action: App.Action.tabNavigation
			),
			viewProvider: App.ViewProvider(store: store)
		)

		let window = UIWindow(windowScene: windowScene)
		window.rootViewController = controller
		self.window = window
		window.makeKeyAndVisible()
	}

    ...
}

You can use the corresponding "handlers" instead e.g. (ModalNavigationHandler) if you already have a custom view controller implementation. Make sure to call navigationHandler.setup(with: viewController) similar to this:

class ExistingViewController: UIViewController {
	let viewStore: ViewStore<ExistingViewShowcase.State, ExistingViewShowcase.Action>
	var cancellables: Set<AnyCancellable> = []
	let navigationHandler: 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)
	}
	
	...
}

Existing UINavigationController

StackNavigationHandler can be initialized with the ignorePreviousViewControllers: Bool parameter. When this parameter is set to true the StackNavigationHandler 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.

Showcases

You can find multiple showcases in the Examples project.

The example app hosts multiple showcases (and UI tests), to run one of the showcase you need to changes the variable showcase in SceneDelegate.swift.

...
// ๐Ÿ‘‰ Choose showcase ๐Ÿ‘ˆ
let showcase: Showcase = .advanced
...

License

This library is released under the MIT license. See LICENSE for details.

swift-composable-navigation's People

Contributors

heinzl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

swift-composable-navigation's Issues

Support for WindowGroup

Hi guys
I checked your library and example and I saw StackNavigationViewController and TabNavigationViewController work with
SceneDelegate
like this
`func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
return
}

	// ๐Ÿ‘‰ Choose showcase ๐Ÿ‘ˆ
	let showcase: Showcase = .advanced
	
	let window = UIWindow(windowScene: windowScene)
	window.rootViewController = makeRootViewController(
		with: readShowcaseForUITest() ?? showcase
	)
	self.window = window
	window.makeKeyAndVisible()
}`

But with SwiftUI project, we will use WindowGroup instead.
How can I set StackNavigationViewController, TabNavigationViewController for WindowGroup without create new UIViewControllerRepresentable

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.