Bahn-X / swift-composable-navigator

An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind
MIT License
580 stars 25 forks source link

Add NavigationTree #50

Closed ohitsdaniel closed 3 years ago

ohitsdaniel commented 3 years ago

Resolves #49.

Problem

ResultBuilder are now officially introduced in Swift 5.4. Using PathBuilders.name feels a bit clunky especially comparing it to 'native' SwiftUI. The idea was to come up with a DSL-like language that would allow PathBuilder composition.

Solution

I wanted to give ResultBuilders a try and came up with a NavigationTree builder. A NavigationTree is composed of PathBuilders and exposes a 'builder' attribute. The builder attribute is marked with @NavigationTreeBuilder which takes care of PathBuilder composition. As PathBuilders are now always 're-initialized' when they're used, I had to move over to a more functional approach in which a PathBuilder builds a single PathUpdate into a View that makes sure to build the path's tail. Because of this approach, I had to ensure that .build(path:) is only called once per PathUpdate, as any PathBuilders with side effects like onDismiss would retrigger their side effects on each build.

As part of this PR, the Routed view gets renamed to NavigationNode. I was looking for a better name for Routed for quite a while now and I think NavigationNode is quite fitting: NavigationNode displays the content of a PathElement and builds its successor whenever the path changes. This also means that conditions in the PathBuilder are only evaluated whenever the path changes. We'll have to come up with Trigger system that allows to 'force-rebuild' the path when a condition changes. Or we might just wrap the outcome of conditional builders in another view that checks the condition and forces a rebuild when the condition value changes? We should address this in a separate issue.

github-actions[bot] commented 3 years ago

Current coverage for ComposableDeeplinking.framework is 100.00%

Files changed - -
DeeplinkHandler.swift 100.00% :white_check_mark:
DeeplinkParser+Prepending.swift 100.00% :white_check_mark:
DeeplinkParser.swift 100.00% :white_check_mark:

Current coverage for ComposableNavigator.framework is 97.27%

Files changed - -
PathBuilder+Conditional.swift 81.25% :white_check_mark:
Root.swift 83.02% :white_check_mark:
PathBuilder+Screen.swift 85.71% :white_check_mark:
Screen.swift 87.50% :white_check_mark:
PathBuilder+Wildcard.swift 89.02% :white_check_mark:
NavigationTree+Screen.swift 94.44% :white_check_mark:
NavigatorDatasource.swift 97.44% :white_check_mark:
NavigationNode.swift 98.96% :white_check_mark:
Generated.PathBuilder+AnyOf.swift 99.03% :white_check_mark:
NavigatorKeys.swift 100.00% :white_check_mark:
Navigator.swift 100.00% :white_check_mark:
NavigationTree+Conditional.swift 100.00% :white_check_mark:
PathComponentUpdate.swift 100.00% :white_check_mark:
_PathBuilder.swift 100.00% :white_check_mark:
NavigationTree+Empty.swift 100.00% :white_check_mark:
PathUpdate.swift 100.00% :white_check_mark:
Generated.NavigationTreeBuilder+AnyOf.swift 100.00% :white_check_mark:
NavigationTree.swift 100.00% :white_check_mark:
NavigationTree+AnyOf.swift 100.00% :white_check_mark:
NavigationTreeBuilder.swift 100.00% :white_check_mark:
PathBuilder+OnDismiss.swift 100.00% :white_check_mark:
IdentifiedScreen.swift 100.00% :white_check_mark:
Navigator+Debug.swift 100.00% :white_check_mark:
NavigationTree+Wildcard.swift 100.00% :white_check_mark:
UIKitOnAppear.swift 100.00% :white_check_mark:
Navigator+Testing.swift 100.00% :white_check_mark:
PathBuilder+Empty.swift 100.00% :white_check_mark:

Current coverage for ComposableNavigatorTCA.framework is 98.46%

Files changed - -
PathBuilder+IfLetStore.swift 96.30% :white_check_mark:
NavigationTree+IfLetStore.swift 100.00% :white_check_mark:
PathBuilder+OnDismiss+TCA.swift 100.00% :white_check_mark:

Powered by xcov

Generated by :no_entry_sign: Danger

ohitsdaniel commented 3 years ago

As Github Actions availability is degraded at the moment, I will focus on rewriting the documentation as part of this PR.

ohitsdaniel commented 3 years ago

Because of this approach, I had to ensure that .build(path:) is only called once per PathUpdate, as any PathBuilders with side effects like onDismiss would retrigger their side effects on each build.

Found a way around this limitation: onDismiss now wraps the content in OnDismissView and 'observes' the navigation path. Whenever the path element is no longer part of the navigation path, it triggers the perform closure. This means, that building the path element has no direct side effects but the side effects (i.e. calling the perform closure) are triggered from the view. This means, that we can call build(path: ) as much as we want. The OnDismissView is responsible of guaranteeing that the side-effect is performed only once.