Skip to content

NavigationTree

ohitsdaniel edited this page Apr 29, 2021 · 3 revisions

NavigationTree

NavigationTrees define all valid navigation paths in an application

public protocol NavigationTree: PathBuilder 

NavigationTrees compose PathBuilders into a NavigationTree. As NavigationTrees are PathBuilders themselves, you can compose multiple NavigationTrees into a bigger NavigationTree.

Example

struct AppNavigationTree: NavigationTree {
  let homeViewModel: HomeViewModel
  let detailViewModel: DetailViewModel
  let settingsViewModel: SettingsViewModel

   var builder: some PathBuilder {
    Screen(
      HomeScreen.self,
      content: { HomeView(viewModel: homeViewModel) },
      nesting: {
        DetailScreen.Builder(viewModel: detailViewModel),
        SettingsScreen.Builder(viewModel: settingsViewModel)
      }
    )
  }
}

struct DetailScreen: Screen {
  let presentationStyle: ScreenPresentationStyle = .push
  let viewModel: DetailViewModel

  struct Builder: NavigationTree {
    var builder: some PathBuilder {
      Screen(
        DetailScreen.self,
        content: { DetailView(viewModel: viewModel) }
      )
    }
  }
}

struct SettingsScreen: Screen {
  let presentationStyle: ScreenPresentationStyle = .sheet(allowsPush: true)
  let viewModel: SettingsViewModel

  struct Builder: NavigationTree {
    var builder: some PathBuilder {
      Screen(
        SettingsScreen.self,
        content: { SettingsView(viewModel: viewModel) }
      )
    }
  }
}

Control flow

NavigationTree use a NavigationTreeBuilder result builder to compose path builders.

NavigationTreeBuilder implements the necessary methods to enable Swift's built-in control flow methods, including if, if let and switch.

This means, that you can in/exclude parts of a navigation tree based on a condition.

struct Tree: NavigationTree {
  let user: User

  var builder: some PathBuilder {
     if user.isLoggedIn() {
       HomeScreen.Builder(store: homeStore)
     } else {
       LoginScreen.Builder(store: loginStore)
     }
  }
}

Multiple entrypoints

If your NavigationTree has multiple entrypoints, you can list them in the builder.

A path builder indicates that is responsible of building a path element by returning a valid view hierarchy.

When a path element is built, the path builders are invoked in the listed order. The view hierarchy built by the first matching path builder is returned. If none of the listed path builders are reponsible of building the path element, the path builder returns nil.

struct Tree: NavigationTree {
  let user: User

  var builder: some PathBuilder {
     EntrypointA.Builder(store: ...)
     EntrypointB.Builder(store: ...)
     EntrypointC.Builder(store: ...)
     // Will never get called, as the first path builder takes care of EntrypointA
     EntrypointA.Builder(store: ...)
  }
}

Inheritance

PathBuilder

Default Implementations

AnyOf(_:)

public func AnyOf<P: PathBuilder>(
    @NavigationTreeBuilder _ builder: () -> P
  ) -> P 

If(screen:else:)

public func If<S: Screen, IfBuilder: PathBuilder, Else: PathBuilder>(
    @NavigationTreeBuilder screen pathBuilder: @escaping (S) -> IfBuilder,
    @NavigationTreeBuilder else: () -> Else
  ) -> _PathBuilder<EitherAB<IfBuilder.Content, Else.Content>> 

If(screen:)

public func If<S: Screen, IfBuilder: PathBuilder>(
    @NavigationTreeBuilder screen pathBuilder: @escaping (S) -> IfBuilder
  ) -> _PathBuilder<EitherAB<IfBuilder.Content, Never>> 

Empty()

public func Empty() -> PathBuilders.EmptyBuilder 

Screen(onAppear:content:nesting:)

public func Screen<
    S: Screen,
    Content: View,
    Successor: PathBuilder
  >(
    onAppear: @escaping (Bool) -> Void = { _ in },
    @ViewBuilder content build: @escaping (S) -> Content,
    @NavigationTreeBuilder nesting: () -> Successor
  ) -> _PathBuilder<NavigationNode<Content, Successor.Content>>

Screen(onAppear:content:)

public func Screen<S: Screen, Content: View>(
    onAppear: @escaping (Bool) -> Void = { _ in },
    @ViewBuilder content build: @escaping (S) -> Content
  ) -> _PathBuilder<NavigationNode<Content, Never>> 

Screen(_:onAppear:content:nesting:)

public func Screen<
    S: Screen,
    Content: View,
    Successor: PathBuilder
  >(
    _ type: S.Type,
    onAppear: @escaping (Bool) -> Void = { _ in },
    @ViewBuilder content build: @escaping () -> Content,
    @NavigationTreeBuilder nesting: () -> Successor
  ) -> _PathBuilder<NavigationNode<Content, Successor.Content>> 

Screen(_:onAppear:content:)

public func Screen<S: Screen, Content: View>(
    _ type: S.Type,
    onAppear: @escaping (Bool) -> Void = { _ in },
    @ViewBuilder content build: @escaping () -> Content
  ) -> _PathBuilder<NavigationNode<Content, Never>> 

Wildcard(screen:pathBuilder:)

Convenience wrapper around PathBuilders.wildcard. Replaces any screen with a predefined one.

func Wildcard<
    S: Screen,
    ContentBuilder: PathBuilder
  >(
    screen: S,
    pathBuilder: ContentBuilder
  ) -> _PathBuilder<PathBuilders.WildcardView<ContentBuilder.Content, S>> 

Based on the example for the conditional PathBuilder, you might run into a situation in which your deeplink parser parses a navigation path that can only be handled by the homeScreenBuilder. This would lead to an empty application, which is unfortunate.

To mitigate this problem, you can combine a conditional PathBuilder with a wildcard PathBuilder:

.conditional(
    either: .wildcard(
        screen: HomeScreen(),
        pathBuilder: HomeScreen.Builder(store: homeStore)
    ),
    or: wildcard(
        screen: LoginScreen(),
        loginScreen(store: loginStore)
   ),
    basedOn: { user.isLoggedIn }
)

This is example basically states: Whatever path I get, the first element should be a defined screen.

⚠️ Warning ⚠️

If you use a wildcard PathBuilder in as part of an anyOf PathBuilder, make sure it is the last one in the list. If it isn't, it will swallow all screens and the PathBuilders listed after the wildcard will be ignored.

Parameters

  • screen: The screen that replaces the current path element.
  • pathBuilder: The PathBuilder used to build the altered path.

build(pathElement:)

public func build(pathElement: AnyScreen) -> Builder.Content? 

IfLetStore(store:then:else:)

Convenience wrapper around PathBuilders.ifLetStore

public func IfLetStore<
    State: Equatable,
    Action,
    If,
    Else,
    IfBuilder: PathBuilder,
    ElseBuilder: PathBuilder
  >(
    store: Store<State?, Action>,
    @NavigationTreeBuilder then: @escaping (Store<State, Action>) -> IfBuilder,
    @NavigationTreeBuilder else: () -> ElseBuilder
  ) -> _PathBuilder<EitherAB<If, Else>> where IfBuilder.Content == If, ElseBuilder.Content == Else 

IfLetStore(store:then:)

Convenience wrapper around PathBuilders.ifLetStore

public func IfLetStore<
    State: Equatable,
    Action,
    If,
    IfBuilder: PathBuilder
  >(
    store: Store<State?, Action>,
    @NavigationTreeBuilder then: @escaping (Store<State, Action>) -> IfBuilder
  ) -> _PathBuilder<EitherAB<If, Never>> where IfBuilder.Content == If 

Requirements

Builder

associatedtype Builder: PathBuilder

builder

@NavigationTreeBuilder var builder: Builder 

Empty()

An empty navigation tree, building no paths.

func Empty() -> PathBuilders.EmptyBuilder

Screen(onAppear:​content:​nesting:​)

PathBuilder responsible of building a single screen. Adds a node to the navigation tree.

func Screen<
    S: Screen,
    Content: View,
    Successor: PathBuilder
  >(
    onAppear: @escaping (Bool) -> Void,
    @ViewBuilder content build: @escaping (S) -> Content,
    @NavigationTreeBuilder nesting: () -> Successor
  ) -> _PathBuilder<NavigationNode<Content, Successor.Content>>

The screen PathBuilder describes how a single screen is built. The content closure is only called if the path element is of type HomeScreen.

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
    Screen(
     onAppear: { initialAppear in ... },
     content: { (screen: HomeScreen) in
       HomeView(...)
     },
     nesting: { ... }
    )
  }
}

The Home screen builder extracts HomeScreen instances from the navigation path and uses it's nesting PathBuilder to build the remaining path.

Parameters

  • onAppear: Called whenever the screen appears. The passed bool is true, if it is the screens initial appear.
  • content: Closure describing how to build a SwiftUI view given the screen data.
  • nesting: Any PathBuilder that can follow after this screen

Screen(onAppear:​content:​)

PathBuilder responsible of building a single screen. Adds a node to the navigation tree.

func Screen<S: Screen, Content: View>(
    onAppear: @escaping (Bool) -> Void,
    @ViewBuilder content build: @escaping (S) -> Content
  ) -> _PathBuilder<NavigationNode<Content, Never>>

The screen PathBuilder describes how a single screen is built. The content closure is only called if the path element is of type HomeScreen.

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
    Screen(
     onAppear: { initialAppear in ... },
     content: { (screen: HomeScreen) in
       HomeView(...)
     }
    )
  }
}

The Home screen builder extracts HomeScreen instances from the navigation path and uses it's nesting PathBuilder to build the remaining path.

Parameters

  • onAppear: Called whenever the screen appears. The passed bool is true, if it is the screens initial appear.
  • content: Closure describing how to build a SwiftUI view given the screen data.

Screen(_:​onAppear:​content:​nesting:​)

PathBuilder responsible of building a single screen. Adds a node to the navigation tree.

func Screen<
    S: Screen,
    Content: View,
    Successor: PathBuilder
  >(
    _ type: S.Type,
    onAppear: @escaping (Bool) -> Void,
    @ViewBuilder content build: @escaping () -> Content,
    @NavigationTreeBuilder nesting: () -> Successor
  ) -> _PathBuilder<NavigationNode<Content, Successor.Content>>

The screen PathBuilder describes how a single screen is built. The content closure is only called if the path element is of type HomeScreen.

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
     Screen(
       HomeScreen.self,
       onAppear: { initialAppear in ... },
       content: { HomeView(...) },
       nesting: { ... }
     )
  }
}

The Home screen builder extracts HomeScreen instances from the navigation path and uses it's nesting PathBuilder to build the remaining path.

Parameters

  • type: Defines which screens are handled by the PathBuilder.
  • onAppear: Called whenever the screen appears. The passed bool is true, if it is the screens initial appear.
  • content: Closure describing how to build a SwiftUI view, if the current path element is of the defined screen type.
  • nesting: Any PathBuilder that can follow after this screen

Screen(_:​onAppear:​content:​)

PathBuilder responsible of building a single screen. Adds a node to the navigation tree.

func Screen<S: Screen, Content: View>(
    _ type: S.Type,
    onAppear: @escaping (Bool) -> Void,
    @ViewBuilder content build: @escaping () -> Content
  ) -> _PathBuilder<NavigationNode<Content, Never>>

The screen PathBuilder describes how a single screen is built. The content closure is only called if the path element is of type HomeScreen.

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
     PathBuilders.screen(
       HomeScreen.self,
       onAppear: { initialAppear in ... },
       content: { HomeView(...) }
     )
  }
}

The Home screen builder extracts HomeScreen instances from the navigation path and uses it's nesting PathBuilder to build the remaining path.

Parameters

  • type: Defines which screens are handled by the PathBuilder.
  • onAppear: Called whenever the screen appears. The passed bool is true, if it is the screens initial appear.
  • content: Closure describing how to build a SwiftUI view, if the current path element is of the defined screen type.

If(screen:​else:​)

The if screen PathBuilder unwraps a screen, if the path element matches the screen type, and provides it to the PathBuilder defining closure.

func If<S: Screen, IfBuilder: PathBuilder, Else: PathBuilder>(
    @NavigationTreeBuilder screen pathBuilder: @escaping (S) -> IfBuilder,
    @NavigationTreeBuilder else: () -> Else
  ) -> _PathBuilder<EitherAB<IfBuilder.Content, Else.Content>>

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
   If { (screen: DetailScreen) in
     DetailScreen.Builder(store.detailStore(for: screen.id))
   }
   else: { ... }
  }
}

Parameters

  • screen: Closure defining the PathBuilder based on the unwrapped screen object.
  • else: Fallback pathbuilder used if the screen cannot be unwrapped.

If(screen:​)

The if screen PathBuilder unwraps a screen, if the path element matches the screen type, and provides it to the PathBuilder defining closure.

func If<S: Screen, IfBuilder: PathBuilder>(
    @NavigationTreeBuilder screen pathBuilder: @escaping (S) -> IfBuilder
  ) -> _PathBuilder<EitherAB<IfBuilder.Content, Never>>

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
   If { (screen: DetailScreen) in
     DetailScreen.Builder(store.detailStore(for: screen.id))
   }
  }
}

Parameters

  • screen: Closure defining the PathBuilder based on the unwrapped screen object.

Wildcard(screen:​pathBuilder:​)

Wildcard PathBuilders replace any screen with a predefined one.

func Wildcard<
    S: Screen,
    ContentBuilder: PathBuilder
  >(
    screen: S,
    pathBuilder: ContentBuilder
  ) -> _PathBuilder<PathBuilders.WildcardView<ContentBuilder.Content, S>>

Based on the example for the conditional PathBuilder, you might run into a situation in which your deeplink parser parses a navigation path that can only be handled by the homeScreenBuilder. This would lead to an empty application, which is unfortunate.

To mitigate this problem, you can combine a conditional PathBuilder with a wildcard PathBuilder:

struct Tree: NavigationTree {
  let user: User

  var builder: some PathBuilder {
     if user.isLoggedIn {
       Wildcard(
         screen: HomeScreen(),
         pathBuilder: HomeScreen.Builder(store: homeStore)
       )
     } else {
       Wildcard(
         screen: LoginScreen(),
         pathBuilder: loginScreen(store: loginStore)
       )
     }
  }
}

This is example basically states: Whatever path I get, the first element should be a defined screen.

⚠️ Warning ⚠️

If you use a Wildcard, make sure it is the last one in the list. If it isn't, it will swallow all screens and the PathBuilders listed after the wildcard will be unreachable.

Parameters

  • screen: The screen that replaces the current path element.
  • pathBuilder: The PathBuilder used to build the altered path.

AnyOf(_:​)

Convenience wrapper around a NavigationTreeBuilder. Similar to SwiftUI's Group:​ View.

func AnyOf<P: PathBuilder>(@NavigationTreeBuilder _ builder: () -> P) -> P

If you ever run into the situation, that your NavigationTree has more than 10 entrypoints, you can use AnyOf to circumvent the fact that NavigationTreeBuilder cannot combine more than 10 PathBuilders.

Example

struct Tree: NavigationTree {
  var builder: some PathBuilder {
     AnyOf {
       Empty()
       ... 9 more path builders
     }

     AnyOf {
       ... the other path builders
     }
  }
}

Parameters

  • builder: NavigationTreeBuilder composing multiple path builders into one.
Clone this wiki locally