SwiftUI - how to avoid navigation hardcoded into the view? SwiftUI - how to avoid navigation hardcoded into the view? swift swift

SwiftUI - how to avoid navigation hardcoded into the view?


The closure is all you need!

struct ItemsView<Destination: View>: View {    let items: [Item]    let buildDestination: (Item) -> Destination    var body: some View {        NavigationView {            List(items) { item in                NavigationLink(destination: self.buildDestination(item)) {                    Text(item.id.uuidString)                }            }        }    }}

I wrote a post about replacing the delegate pattern in SwiftUI with closures.https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/


My idea would pretty much be a combination of Coordinator and Delegate pattern. First, create a Coordinator class:

struct Coordinator {    let window: UIWindow      func start() {        var view = ContentView()        window.rootViewController = UIHostingController(rootView: view)        window.makeKeyAndVisible()    }}

Adapt the SceneDelegate to use the Coordinator :

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {        if let windowScene = scene as? UIWindowScene {            let window = UIWindow(windowScene: windowScene)            let coordinator = Coordinator(window: window)            coordinator.start()        }    }

Inside of ContentView, we have this:

struct ContentView: View {    var delegate: ContentViewDelegate?    var body: some View {        NavigationView {            List {                NavigationLink(destination: delegate!.didSelect(Item())) {                    Text("Destination1")                }            }        }    }}

We can define the ContenViewDelegate protocol like this:

protocol ContentViewDelegate {    func didSelect(_ item: Item) -> AnyView}

Where Item is just a struct which is identifiable, could be anything else (e.g id of some element like in a TableView in UIKit)

Next step is to adopt this protocol in Coordinator and simply pass the view you want to present:

extension Coordinator: ContentViewDelegate {    func didSelect(_ item: Item) -> AnyView {        AnyView(Text("Returned Destination1"))    }}

This has so far worked nicely in my apps. I hope it helps.


I will try to answer your points one by one. I will follow a little example where our View that should be reusable is a simple View that shows a Text and a NavigationLink that will go to some Destination.I created a Gist: SwiftUI - Flexible Navigation with Coordinators if you want to have a look at my full example.

The design problem: NavigationLinks are hardcoded into the View.

In your example it is bound to the View but as other answers have already shown, you can inject the destination to your View type struct MyView<Destination: View>: View. You can use any Type conforming to View as your destination now.

But if the view containing this NavigationLink should be reusable I can not hardcode the destination. There has to be a mechanism which provides the destination.

With the change above, there are mechanisms to provide the type. One example is:

struct BoldTextView: View {    var text: String    var body: some View {        Text(text)            .bold()    }}
struct NotReusableTextView: View {    var text: String    var body: some View {        VStack {            Text(text)            NavigationLink("Link", destination: BoldTextView(text: text))        }    }}

will change to

struct ReusableNavigationLinkTextView<Destination: View>: View {    var text: String    var destination: () -> Destination    var body: some View {        VStack {            Text(text)            NavigationLink("Link", destination: self.destination())        }    }}

and you can pass in your destination like this:

struct BoldNavigationLink: View {    let text = "Text"    var body: some View {        ReusableNavigationLinkTextView(            text: self.text,            destination: { BoldTextView(text: self.text) }        )    }}

As soon as I have multiple reusable screens I run into the logical problem that one reusable view (ViewA) needs a preconfigured view-destination (ViewB). But what if ViewB also needs a preconfigured view-destination ViewC? I would need to create ViewB already in such a way that ViewC is injected already in ViewB before I inject ViewB into ViewA. And so on....

Well, obviously you need some kind of logic that will determine your Destination. At some point you need to tell the view what view comes next. I guess what you're trying to avoid is this:

struct NestedMainView: View {    @State var text: String    var body: some View {        ReusableNavigationLinkTextView(            text: self.text,            destination: {                ReusableNavigationLinkTextView(                    text: self.text,                    destination: {                        BoldTextView(text: self.text)                    }                )            }        )    }}

I put together a simple example that uses Coordinators to pass around dependencies and to create the views. There is a protocol for the Coordinator and you can implement specific use cases based on that.

protocol ReusableNavigationLinkTextViewCoordinator {    associatedtype Destination: View    var destination: () -> Destination { get }    func createView() -> ReusableNavigationLinkTextView<Destination>}

Now we can create a specific Coordinator that will show the BoldTextView when clicking on the NavigationLink.

struct ReusableNavigationLinkShowBoldViewCoordinator: ReusableNavigationLinkTextViewCoordinator {    @Binding var text: String    var destination: () -> BoldTextView {        { return BoldTextView(text: self.text) }    }    func createView() -> ReusableNavigationLinkTextView<Destination> {        return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)    }}

If you want, you can also use the Coordinator to implement custom logic that determines the destination of your view. The following Coordinator shows the ItalicTextView after four clicks on the link.

struct ItalicTextView: View {    var text: String    var body: some View {        Text(text)            .italic()    }}
struct ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator: ReusableNavigationLinkTextViewCoordinator {    @Binding var text: String    let number: Int    private var isNumberGreaterThan4: Bool {        return number > 4    }    var destination: () -> AnyView {        {            if self.isNumberGreaterThan4 {                let coordinator = ItalicTextViewCoordinator(text: self.text)                return AnyView(                    coordinator.createView()                )            } else {                let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(                    text: self.$text,                    number: self.number + 1                )                return AnyView(coordinator.createView())            }        }    }    func createView() -> ReusableNavigationLinkTextView<AnyView> {        return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)    }}

If you have data that needs to be passed around, create another Coordinator around the other coordinator to hold the value. In this example I have a TextField -> EmptyView -> Text where the value from the TextField should be passed to the Text. The EmptyView must not have this information.

struct TextFieldView<Destination: View>: View {    @Binding var text: String    var destination: () -> Destination    var body: some View {        VStack {            TextField("Text", text: self.$text)            NavigationLink("Next", destination: self.destination())        }    }}struct EmptyNavigationLinkView<Destination: View>: View {    var destination: () -> Destination    var body: some View {        NavigationLink("Next", destination: self.destination())    }}

This is the coordinator that creates views by calling other coordinators (or creates the views itself). It passes the value from TextField to Text and the EmptyView doesn't know about this.

struct TextFieldEmptyReusableViewCoordinator {    @Binding var text: String    func createView() -> some View {        let reusableViewBoldCoordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)        let reusableView = reusableViewBoldCoordinator.createView()        let emptyView = EmptyNavigationLinkView(destination: { reusableView })        let textField = TextFieldView(text: self.$text, destination: { emptyView })        return textField    }}

To wrap it all up, you can also create a MainView that has some logic that decides what View / Coordinator should be used.

struct MainView: View {    @State var text = "Main"    var body: some View {        NavigationView {            VStack(spacing: 32) {                NavigationLink("Bold", destination: self.reuseThenBoldChild())                NavigationLink("Reuse then Italic", destination: self.reuseThenItalicChild())                NavigationLink("Greater Four", destination: self.numberGreaterFourChild())                NavigationLink("Text Field", destination: self.textField())            }        }    }    func reuseThenBoldChild() -> some View {        let coordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)        return coordinator.createView()    }    func reuseThenItalicChild() -> some View {        let coordinator = ReusableNavigationLinkShowItalicViewCoordinator(text: self.$text)        return coordinator.createView()    }    func numberGreaterFourChild() -> some View {        let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(text: self.$text, number: 1)        return coordinator.createView()    }    func textField() -> some View {        let coordinator = TextFieldEmptyReusableViewCoordinator(text: self.$text)        return coordinator.createView()    }}

I know that I could also create a Coordinator protocol and some base methods, but I wanted to show a simple example on how to work with them.

By the way, this is very similar to the way that I used Coordinator in Swift UIKit apps.

If you have any questions, feedback or things to improve it, let me know.