An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Overview

Composable Navigator

An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

test status



Vanilla SwiftUI navigation

A typical, vanilla SwiftUI application manages its navigation state (i.e. is a sheet or a push active) either directly in its Views or in ObservableObjects.

Let's take a look at a simplified example in which we keep all navigation state locally in the view:

struct HomeView: View {
  @State var isSheetActive: Bool = false
  @State var isDetailShown: Bool = false

  var body: some View {
    VStack {
      NavigationLink(
        destination: DetailView(),
        isActive: $isDetailShown,
        label: {
          Text("Go to detail view")
        }
      )

      Button("Go to settings") {
        isSheetActive = true
      }
    }
    .sheet(
      isPresented: $isSheetActive,
      content: {
        SettingsView()
      }
    )
  }
}

Challenges

How do we test that when the user taps the navigation link, we move to the DetailView and not the SettingsView?

As isSheetActive and isDetailShown are kept locally in the View and their values are directly mutated by a binding, we cannot test any navigation logic unless we write UI tests or implement custom bindings that call functions in an ObservableObject mutating the navigation state.

What if I want to show a second sheet with different content?

We can either introduce an additional isOtherSheetActive variable or a hashable enum HomeSheet: Hashable and keep track of the active sheet in a activeSheet: HomeSheet? variable.

What happens if both isSheetActive and isDetailShown are true?

The sheet is shown on top of the current content, meaning that we can end up in a situation in which the settings sheet is presented on top of a detail view.

How do we programmatically navigate after a network request has finished?

To programmatically navigate, we need to keep our navigation state in an ObservableObject that performs asynchronous actions such as network requests. When the request succeeds, we set isDetailShown or isSheetActive to true. We also need to make sure that all other navigation related variables are set to false/nil or else we might end up with an unexpected navigation tree.

What happens if the NavigationLink is contained in a lazily loaded List view and the view we want to navigate to has not yet been initialized?

The answer to this one is simple: SwiftUI will not navigate. Imagine, we have a list of hundreds of entries that the user can scroll through. If we want to programmatically navigate to an entry detail view, the 'cell' containing the NavigationLink needs to be in memory or else the navigation will not be performed.

NavigationLinks do not navigate when I click them

In order to make NavigationLinks work in our view, we need to wrap our view in a NavigationView.

So, at which point in the view hierarchy do we wrap our content in a NavigationView? As wrapping content in a NavigationView twice will lead to two navigation bars, we probably want to avoid having to multiple nested NavigationViews.

Shallow Deeplinking

Vanilla SwiftUI only supports shallow deeplinking, meaning that we can navigate from the ExampleView to the DetailView by setting the initial value of isDetailShown to true. However, we cannot navigate further down into our application as SwiftUI seems to ignore initial values in pushed/presented views.

Why should I use ComposableNavigator?

ComposableNavigator lifts the burden of manually managing navigation state off your shoulders and allows to navigate through applications along navigation paths. ComposableNavigator takes care of embedding your views in NavigationViews, where needed, and always builds a valid view hierarchy. On top of that, ComposableNavigator unlocks advanced navigation patterns like wildcards and conditional navigation paths.

Core components

ComposableNavigator is built on three core components: the navigation tree, the current navigation path, and the navigator.

Navigation Path

The navigation path describes the order of visible screens in the application. It is a first-class representation of the <url-path> defined in RFC1738. A navigation path consists of identified screens.

Screen

A Screen is a first-class representation of the information needed to build a particular view. Screen objects identify the navigation path element and can contain arguments like IDs, initial values, and flags. detail?id=0 directly translates to DetailScreen(id: 0).

Screens define how they are presented. This decouples presentation logic from business logic, as showing a sheet and pushing a view are performed by invoking the same go(to:, on:) function. Changing a screen's (default) presentation style is a single line change. Currently, sheet and push presentation styles are supported.

Navigator

The navigator manages the application's current navigation path and allows mutations on it. The navigator acts as an interface to the underlying data source. The navigator object is accessible via the view environment.

Navigators allow programmatic navigation and can be injected where needed, even into ViewModels.

NavigationTree

The ComposableNavigator is based on the concept of PathBuilder composition in the form of a NavigationTree. A NavigationTree composes PathBuilders to describe all valid navigation paths in an application. That also means that all screens in our application are accessible via a pre-defined navigation path.

Let's look at an example NavigationTree:

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)
      }
    )
  }
}

Example Tree

Based on AppNavigationTree, the following navigation paths are valid:

/home
/home/detail?id=0
/home/settings

More information on the NavigationTree and how to compose PathBuilders can be found here.

Vanilla SwiftUI + ComposableNavigator

Let's go back to our vanilla SwiftUI home view and enhance it using the ComposableNavigator.

import ComposableNavigator

struct HomeView: View {
  @Environment(\.navigator) var navigator
  @Environment(\.currentScreenID) var currentScreenID

  var body: some View {
    VStack {
      Button(
        action: goToDetail,
        label: { Text("Show detail screen for 0") }
      )

      Button(
        action: goToSettings,
        label: { Text("Go to settings screen") }
      )
    }
  }

  func goToDetail() {
    navigator.go(
      to: DetailScreen(detailID: "0"),
      on: currentScreenID
    )
  }

  func goToSettings() {
    navigator.go(
      to: SettingsScreen(),
      on: HomeScreen()
    )
  }
}

We can now inject the Navigator and currentScreenID in our tests and cover calls to goToDetail / goToSettings on an ExampleView instance in unit tests.

Integrating ComposableNavigator

import ComposableNavigator
import SwiftUI

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)
      }
    )
  }
}

@main
struct ExampleApp: App {
  let dataSource = Navigator.Datasource(root: HomeScreen())

  var body: some Scene {
    WindowGroup {
      Root(
        dataSource: dataSource,
        pathBuilder: AppNavigationTree(...)
      )
    }
  }
}

Deeplinking

As ComposableNavigator builds the view hierarchy based on navigation paths, it is the ideal companion to implement deeplinking. Deeplinks come in different forms and shapes, however ComposableNavigator abstracts it into a first-class representation in the form of the Deeplink type. The ComposableDeeplinking library that is part of the ComposableNavigator contains a couple of helper types that allow easily replace the current navigation path with a new navigation path based on a Deeplink by defining a DeeplinkHandler and a composable DeeplinkParser.

More information on deeplinking and how to implement it in your own application can be found here.

Dependency injection

ComposableNavigator was inspired by The Composable Architecture (TCA) and its approach to Reducer composition, dependency injection and state management. As all view building closures flow together in one central place, the app navigation tree, ComposableNavigator gives you full control over dependency injection. Currently, the helper package ComposableNavigatorTCA is part of this repository and the main package therefore has a dependency on TCA. This will change in the future when ComposableNavigatorTCA gets extracted into its own repository.

Installation

ComposableNavigator supports Swift Package Manager and contains two products, ComposableNavigator and ComposableDeeplinking.

Swift Package

If you want to add ComposableNavigator to your Swift packages, add it as a dependency to your Package.swift.

dependencies: [
    .package(
      name: "ComposableNavigator",
      url: "https://github.com/Bahn-X/swift-composable-navigator.git",
      from: "0.1.0"
    )
],
targets: [
    .target(
        name: "MyAwesomePackage",
        dependencies: [
            .product(name: "ComposableNavigator", package: "ComposableNavigator"),
            .product(name: "ComposableDeeplinking", package: "ComposableNavigator")
        ]
    ),
]

Xcode

You can add ComposableNavigator to your project via Xcode. Open your project, click on File → Swift Packages → Add Package Dependency…, enter the repository url (https://github.com/Bahn-X/swift-composable-navigator.git) and add the package products to your app target.

Example application

The ComposableNavigator repository contains an example application showcasing a wide range of library features and path builder patterns that are also applicable in your application. The example app is based on ComposableNavigator + TCA but also shows how to navigate via the navigator contained in a view's environment as you could do it in a Vanilla SwiftUI application.

The Example application contains a UI test suite that is run on every pull request. In that way, we can make sure that, even if SwiftUI changes under the hood, ComposableNavigator behaves as expected.

Documentation

The latest ComposableNavigator documentation is available in the wiki.

Contribution

The contribution process for this repository is described in CONTRIBUTING. We welcome contribution and look forward to your ideas.

License

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

Comments
  • Navigation not causing view to appear

    Navigation not causing view to appear

    Question

    What would cause a view not to appear after pushing a screen onto the nav stack?

    Problem description

    I added debug() to my navigator. On the left is the initial navigation event that happens when the app loads. On the right is after sending a navigation event like this:

    environment.navigator.go(to: TabNavigationScreen(), on: screenID)
    

    where screenID is the ID of the screen that has the button. It also happens to be the root screen, i.e. 00000000-0000-0000-0000-000000000000.

    image

    I don't have a concise code sample to post, and I'll be making a sample project as my next debugging step, but I wanted to post this in case there was something obvious I was missing. How can I get hasAppeared: false to turn into hasAppeared: true?

    opened by ZevEisenberg 7
  • Can a screen have itself nested inside?

    Can a screen have itself nested inside?

    Question

    [//]: # Is it possible to have a Screen that contains itself in its nesting?

    (I'm not sure if this is more of a question or a bug)

    Problem description

    [//]: # I first observed this behavior when I had a screen (A) that could nest a screen (B) that could then nest the original screen (A) again. The compiler would crash with the error Illegal instruction: 4. I was able to reproduce the issue in the example and it happens if you simply try to nest screen A inside of screen A.

    Is there a recommended approach to solve this issue?

    struct DetailScreen: Screen {
      let train: Train
      let presentationStyle: ScreenPresentationStyle = .push
      
      struct Builder: NavigationTree {
        var builder: some PathBuilder {
          Screen(
            content: { (screen: DetailScreen) in
              DetailView(train: screen.train)
            },
            nesting: {
              CapacityScreen.Builder()
                DetailScreen.Builder()
            }
          )
        }
      }
    }
    

    I forked the example to modify it and show the issue here: https://github.com/andrewjmeier/swift-composable-navigator/blob/main/Example/Vanilla%20SwiftUI%20Example/Vanilla%20SwiftUI%20Example/DetailView.swift#L16

    v0.2.0 
    opened by andrewjmeier 6
  • Move Project to unmaintained status

    Move Project to unmaintained status

    Dear users of The Composable Navigator, Unfortunately, I must inform you that I will no longer continue to maintain this project. This has several reasons, first of all taking care of my mental health.

    Over the last year, like many of you, I have been working from home and the boundaries of work and life have blurred over time. This led to my mind not being able to shut down, even after I closed the lid of my laptop in the evening. While changing jobs in May last year has definitely improved the situation, I noticed that being the single maintainer of an open source project created a lot of extra pressure to deliver and perform with an invisible set of eyes watching. This project is one of the few reasons I developed a minor case of burnout and fell into a depression over the course of the year.

    To take back my life and enjoy every second of it, I have taken some steps to reduce the amount of stress I am exposed to and part of these steps is stepping back as a maintainer of this project. This allows me to let go of the negative feelings attached to it.

    I am grateful that so many of you have used or at least tried this framework. There are not many alternatives out there, but I would recommend checking out Stinsen (rundfunk47/stinsen: Coordinators in SwiftUI. Simple, powerful and elegant.) and the PointFree episodes on navigation in SwiftUI.

    Have a great 2022 and stay safe. If you ever feel like you’re running out of energy and that nothing in life has a purpose, get help and talk to people. Life is way too precious to be miserable.

    _ Daniel

    opened by ohitsdaniel 3
  • Provider

    Provider

    Resolves #58.

    Problem

    Currently, ComposableNavigator is very TCA focused and assumes that people "hand-down" their view models into the NavigationTree. Flutter solves this by wrapping Widgets in ProviderWidgets that take care of initialising the Widget's dependencies and handing it down the WidgetTree through the context.

    As NavigationTrees and View Hierarchies are regenerated whenever the path changes, NavigationTree/Views cannot retain ViewModels / ObservableObjects themselves as they would be reinitialised on every redraw.

    Solution

    Provider takes care of initialising and retaining screen dependencies. Dependencies are stored in a screen scope in a global DependencyStore. When a path builder builds its content, the Provider view is built and initialises the dependency once. Whenever a screen is removed from the navigation path, the dependency is removed from the store.

    Draft

    This PR is currently marked as draft as I want to add an example of provider usage to the Readme and Example app.

    opened by ohitsdaniel 3
  • Add NavigationTree

    Add NavigationTree

    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.

    opened by ohitsdaniel 3
  • Initializer called multiple times

    Initializer called multiple times

    Question

    How to ensure that ViewModel would be initialized once?

    Problem description

    I am wondering what should I do to be sure that ViewModel object would be initialized once, because in my current implementation it doesn't work as I expect.

    Here is my Screen:

    struct TestScreen: Screen {
        var presentationStyle: ScreenPresentationStyle = .push
        
        struct Builder: NavigationTree {
            var builder: some PathBuilder {
                Screen(
                    TestScreen.self,
                    content: {
                        TestView(viewModel: .init()) // <- called multiple times.
                    },
                    nesting: {
                        TestSecondScreen.Builder()
                    }
                )
            }
        }
    }
    

    So I changed implementation of Screen to:

    struct TestScreen: Screen {
        var presentationStyle: ScreenPresentationStyle = .push
        
        struct Builder: NavigationTree {
            let viewModel: TestViewModel = .init()
    
            var builder: some PathBuilder {
                Screen(
                    TestScreen.self,
                    content: {
                        TestView(viewModel: viewModel) // <- called multiple times, but ViewModel is the same.
                    },
                    nesting: {
                        TestSecondScreen.Builder()
                    }
                )
            }
        }
    }
    

    But when it comes to initialize ViewModel that expects data previous implementation won't work. Here is the example of what I have in that case:

    struct TestSecondScreen: Screen {
        let title: String
        let id: String
        
        var presentationStyle: ScreenPresentationStyle = .push
        
        struct Builder: NavigationTree {
            var builder: some PathBuilder {
                Screen(
                    content: { (screen: TestSecondScreen) in
                        TestSecondView(viewModel: .init(id: screen.id), title: screen.title)
                    }
                )
            }
        }
    }
    
    opened by onl1ner 2
  • Changes fetch-depth in test workflow to 0

    Changes fetch-depth in test workflow to 0

    Problem

    Danger fails on PRs from forks.

    Solution

    According to https://github.com/danger/danger/issues/1103, setting the fetch-depth to 0, i.e. fetching all branches and tags, should fix this.

    opened by ohitsdaniel 2
  • Open source documentation

    Open source documentation

    Problem

    We are currently lacking a CODEOWNERS, MAINTAINERS and CONTRIBUTING files, which are common practice in open source projects. Also, our contribution process is not defined in the README.

    Solution

    Add the mentioned files. Outline the contribution process.

    opened by ohitsdaniel 2
  • README

    README

    • Fix typo in main README
    • Add Example README with installation steps

    Problem

    The Example app was missing installation steps

    Solution

    Add a minimal Example/README.md

    opened by oliverlist 2
  • Xcode project file is not committed

    Xcode project file is not committed

    Bug description

    The main Xcode project file is not committed into the repo. When cloning and opening the Example workspace there is a missing Xcode project file reference.

    Steps to reproduce

    Clone open the workspace in Example folder. Hit run.

    Expected behavior

    Example should build and run.

    Environment

    • Xcode 12.4 / 12.5 beta 3
    • Swift Xcode version.
    opened by AlexisQapa 2
  • Allow erasing circular paths

    Allow erasing circular paths

    Resolves #62.

    Problem

    As described in #62, circular navigation paths currently lead to failing builds as the compiler is not able to resolve recursive content types.

    Solution

    Allow erasing circular navigation paths to AnyView. This has a performance impact as SwiftUI will no longer be able to use the View type to perform its diffing for all erased successors. As circular paths are the exception and not the rule, I think this is fine for now.

    opened by ohitsdaniel 1
  • Navigator fails to update view when a variable is passed via the screen

    Navigator fails to update view when a variable is passed via the screen

    Bug description

    Basically on startup the app will show AScreen with a variable value of nil to display a loading screen. The deeplinker will parse the link into the following path [AScreen(value: 20), BScreen()]. Since the deeplinker uses replace(path:) it will successfully replace AScreen but fails at presenting BScreen. Parsing the link so that the value of AScreen is nil will present BScreen fine. So it seems the issue is with the value changing.

    Steps to reproduce

    Example App

    Using the code below seems to solve the issue

    extension Screen {
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.presentationStyle == rhs.presentationStyle && type(of: lhs) == type(of: rhs)
        }
    }
    
    public extension AnyScreen {
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.screen == rhs.screen
        }
    }
    

    Expected behavior

    App should navigate to BScreen even when the value in AScreen changes

    Environment

    • Xcode 13
    • Swift 5.3
    • OS: iOS 15
    • Package v0.2.0

    Additional context

    Related: https://github.com/Bahn-X/swift-composable-navigator/issues/74#issuecomment-979748812

    opened by KenLPham 1
  • Get notified when a ScreenID is no longer valid?

    Get notified when a ScreenID is no longer valid?

    Question

    Is it possible to find out when the screen for a particular ScreenID is permanently dismissed? By "permanently" I mean that if the same screen is presented again, it would have a different ScreenID?

    Problem description

    I'm wrapping an analytics library that has strong ties to UIKit. In particular, it has the concept of a "page" object that is tied to the lifecycle of a UIViewController in memory. I'm trying to replicate this behavior with SwiftUI Views. I can't use onDisappear because that is triggered if a view is covered by a nav push, and in UIKit, views don't get deallocated when they're covered by navigation.

    I tried attaching a @StateObject to my views, and having it notify on deinit, but @StateObject seems not to make guarantees about when or if it will deallocate things.

    My current thinking is to keep a dictionary that maps ScreenIDs to my analytics page objects, and remove them from the array when a ScreenID becomes invalid. But I would need the composable navigator to tell me when that happens, and I'm not sure if that's possible. I'm thinking something like:

    // naming subject to discussion
    Navigator.Datasource(root: someRoot, screenIDDidBecomeInvalid: { screenID in
    })
    

    I'm not super familiar with the composable navigator's API surface, so maybe there's a better place to put it, but that's the gist of what I'm looking for.

    opened by ZevEisenberg 3
  • goBack fails to dismiss multiple views correctly.

    goBack fails to dismiss multiple views correctly.

    Bug description

    [//]: # Using goBack to navigate back to a previous screen doesn't dismiss all of the views when each view is presented modally.

    Steps to reproduce

    [//]: # Open a series of at least 3 modal views and then try to navigate back to the original view. One modal view will remain.

    
    struct RootView: View {
        
        var body: some View {
            let dataSource = Navigator.Datasource(root: MainScreen())
            let navigator = Navigator(dataSource: dataSource)
            
            return Root(dataSource: dataSource, navigator: navigator, pathBuilder: MainScreen.Builder())
        }
        
    }
    
    struct MainScreen: Screen {
        
        var presentationStyle: ScreenPresentationStyle = .push
        
        struct Builder: NavigationTree {
            var builder: some PathBuilder {
                Screen(
                    content: { (_: MainScreen) in MainView() },
                    nesting: { ModalScreen.Builder().eraseCircularNavigationPath() }
                )
            }
        }
    }
    
    struct MainView: View {
        @Environment(\.navigator) private var navigator
        @Environment(\.currentScreenID) private var currentScreenID
        
        var body: some View {
            VStack {
                Button {
                    navigator.go(to: ModalScreen(viewCount: 1, onDismiss: {
                        print(currentScreenID)
                        navigator.goBack(to: currentScreenID)
                    }), on: currentScreenID)
                } label: {
                    Text("Show new view")
                }
            }
        }
    }
    
    struct ModalScreen: Screen {
        
        var presentationStyle: ScreenPresentationStyle = .sheet(allowsPush: true)
        var viewCount: Int
        var onDismiss: () -> Void
        
        struct Builder: NavigationTree {
            var builder: some PathBuilder {
                Screen(
                    content: { (screen: ModalScreen) in ModalView(viewCount: screen.viewCount, onDismiss: screen.onDismiss) },
                    nesting: { ModalScreen.Builder().eraseCircularNavigationPath() }
                )
            }
        }
    }
    
    extension ModalScreen: Equatable {
        
        static func == (lhs: ModalScreen, rhs: ModalScreen) -> Bool {
            return lhs.viewCount == rhs.viewCount
        }
        
    }
    
    extension ModalScreen: Hashable {
        func hash(into hasher: inout Hasher) {
            hasher.combine(viewCount)
        }
    }
    
    struct ModalView: View {
        @Environment(\.navigator) private var navigator
        @Environment(\.currentScreenID) private var currentScreenID
        
        var viewCount: Int
        var onDismiss: () -> Void
        
        var body: some View {
            VStack {
                Text("View \(viewCount)")
                Button {
                    navigator.go(to: ModalScreen(viewCount: viewCount + 1, onDismiss: onDismiss), on: currentScreenID)
                } label: {
                    Text("Show new view")
                }
                Button {
                    onDismiss()
                } label: {
                    Text("dismiss all views")
                }
            }
        }
        
    }
    
    

    Expected behavior

    [//]: I would expect the original screen to be fully visible and all of the modal views to be dismissed.

    Screenshots

    [//]: Simulator Screen Recording - iPod touch (7th generation) - 2021-05-14 at 10 13 21

    Environment

    • Xcode 12.5
    • Swift 5.4
    • iOS 14
    opened by andrewjmeier 1
  • Tabbed screen

    Tabbed screen

    Helps with #19.

    Problem

    To enable Tab bar supports, we need to refactor all Navigation path mutations.

    • [x] path

    • [x] lastOccurenceOf(screen) -> ScreenID

    • [x] goTo(id:)

    • [x] goTo(screen:)

    • [x] go(to newPath: [AnyScreen], on: id)

    • [x] go(to newPath: [AnyScreen], on: id)

    • [x] goBack(to: id)

    • [x] goBack(to: screen)

    • [x] replacePath

    • [x] dismiss(id)

    • [x] dismiss(screen)

    • [x] dismissSuccessorOf(id)

    • [x] dismissSuccessorOf(screen)

    • [x] replaceContent(of: id)

    • [x] replaceContent(of: screen)

    • [x] setActive(id)

    • [x] setActive(screen)

    • [x] didAppear(id)

    • [x] View layer for native tab bar

    • [x] initialiseDefaultContents(for screenID:, contents)

    • [ ] check onDismiss and see if adjustments are needed

    • [ ] add tab bar to example app, write UI tests

    • [ ] pushing content onto the tab view

    Solution

    Do it. And write tests for it, to make sure nothing breaks. 🚀

    opened by ohitsdaniel 10
  • Danger fails on PRs from forks

    Danger fails on PRs from forks

    Bug description

    GITHUB_TOKEN does not have write permission, i.e. cannot comment on issues, in PRs triggered from forks.

    Steps to reproduce

    Open a pull request from a fork. Danger fails.

    Expected behavior

    Danger succeeds and posts a comment.

    Solution

    Add a bot user with write permission on issues / PRs and expose a personal access token of said user to the PR workflows. (See Danger docs)

    opened by ohitsdaniel 0
Owner
Bahn-X
Bahn-X
Navigation helpers for SwiftUI applications build with ComposableArchitecture

Swift Composable Presentation ?? Description Navigation helpers for SwiftUI applications build with ComposableArchitecture. More info about the concep

Dariusz Rybicki 52 Dec 14, 2022
SwiftUINavigator: a lightweight, flexible, and super easy library which makes SwiftUI navigation a trivial task

The logo is contributed with ❤️ by Mahmoud Hussein SwiftUINavigator is a lightwe

OpenBytes 22 Dec 21, 2022
Router is a library that assists with SwiftUI view transitions.

Router Router is a library that assists with SwiftUI view transitions. Installation .package(name: "Router", url: "[email protected]:1amageek/Router.git"

1amageek 69 Dec 23, 2022
NavigationViewKit is a NavigationView extension library for SwiftUI.

NavigationViewKit 中文版说明 NavigationViewKit is a NavigationView extension library for SwiftUI. For more detailed documentation and demo, please visit 用N

东坡肘子 74 Dec 28, 2022
A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles

A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles while pushing or popping a view controller for all orientations. And you don't need to write any line of code for it, it all happens automatically.

Zhouqi Mo 3.3k Dec 21, 2022
iOS UI library to show and hide an extension to your UINavigationBar

ADNavigationBarExtension is a UI library written in Swift. It allows you to show and hide an extension to your UINavigationBar Features Use Extensible

FABERNOVEL 58 Aug 29, 2022
FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator

FlowStacks allow you to manage complex SwiftUI navigation and presentation flows with a single piece of state. This makes it easy to hoist that state into a high-level coordinator view. Using this pattern, you can write isolated views that have zero knowledge of their context within the navigation flow of an app.

John Patrick Morgan 471 Jan 3, 2023
Coordinators in SwiftUI. Simple, powerful and elegant.

Simple, powerful and elegant implementation of the Coordinator pattern in SwiftUI. Stinsen is written using 100% SwiftUI which makes it work seamlessl

Narek Mailian 618 Jan 7, 2023
An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation.

swiftui-navigation-stack An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations

Matteo 753 Jan 2, 2023
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

SwiftUI Navigation Tools for making SwiftUI navigation simpler, more ergonomic and more precise. Motivation Tools Navigation overloads Navigation view

Point-Free 1.1k Jan 1, 2023
Customizable multi platform menu bar component with the dark and light scheme support - SwiftUI

Menu bar component (SwiftUI) Features Observing menu selection changes via generic PreferenceKey The color intensity automatically adjusts depending o

Igor 3 Aug 13, 2022
custom navigationBar in swiftui

SwiftUI-WRNavigationBar custom navigationBar in swiftui Requirements iOS 14.0 Installation pod 'SwiftUI-WRNavigationBar', '~>1.1.1' Overview debug cus

wangrui460 29 Nov 7, 2022
sRouting - The lightweight navigation framework for SwiftUI.

sRouting The lightweight navigation framework for SwiftUI. Overview sRouting using the native navigation mechanism in SwiftUI. It's easy to handle nav

Shiro 8 Aug 15, 2022
Change SwiftUI Navigation Bar Color for different View

SwiftUINavigationBarColor Change SwiftUI NavigationBar background color per screen. Usage For NavigationBarColor to work, you have to set the Navigati

Hai Feng Kao 18 Jul 15, 2022
Path based routing in SwiftUI

Easy and maintainable app navigation with path based routing for SwiftUI. With SwiftUI Router you can power your SwiftUI app with path based routing.

Freek 652 Dec 28, 2022
🧭 SwiftUI navigation done right

?? NavigationKit NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. ?? Installation ?? Swift Package Manager Usi

Alex Nagy 152 Dec 27, 2022
Make SwiftUI Navigation be easy

VNavigator VNavigator is a clean and easy-to-use navigation in SwiftUI base on UINavigationController in UIKit Installation From CocoaPods CocoaPods i

Vu Vuong 10 Dec 6, 2022
SwiftUINavigation provides UIKit-like navigation in SwiftUI

SwiftUINavigation About SwiftUINavigation provides UIKit-like navigation in Swif

Bhimsen Padalkar 1 Mar 28, 2022
A lightweight iOS mini framework that enables programmatic navigation with SwiftUI, by using UIKit under the hood.

RouteLinkKit A lightweight iOS mini framework that enables programmatic navigation with SwiftUI. RouteLinkKit is fully compatible with native Navigati

Αθανάσιος Κεφαλάς 4 Feb 8, 2022