SwiftUI - how to avoid navigation hardcoded into the view?

10,813

Solution 1

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/

Solution 2

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.

Solution 3

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.

Solution 4

Here is a fun example of drilling down infinitely and changing your data for the next detail view programmatically

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var navigationManager: NavigationManager

    var body: some View {
        NavigationView {
            DynamicView(viewModel: ViewModel(message: "Get Information", type: .information))
        }
    }
}

struct DynamicView: View {
    @EnvironmentObject var navigationManager: NavigationManager

    let viewModel: ViewModel

    var body: some View {
        VStack {
            if viewModel.type == .information {
                InformationView(viewModel: viewModel)
            }
            if viewModel.type == .person {
                PersonView(viewModel: viewModel)
            }
            if viewModel.type == .productDisplay {
                ProductView(viewModel: viewModel)
            }
            if viewModel.type == .chart {
                ChartView(viewModel: viewModel)
            }
            // If you want the DynamicView to be able to be other views, add to the type enum and then add a new if statement!
            // Your Dynamic view can become "any view" based on the viewModel
            // If you want to be able to navigate to a new chart UI component, make the chart view
        }
    }
}

struct InformationView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.blue)


            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct PersonView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.red)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ProductView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                    .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.green)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ChartView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                    .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.green)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ViewModel {
    let message: String
    let type: DetailScreenType
}

enum DetailScreenType: String {
    case information
    case productDisplay
    case person
    case chart
}

class NavigationManager: ObservableObject {
    func destination(forModel viewModel: ViewModel) -> DynamicView {
        DynamicView(viewModel: generateViewModel(context: viewModel))
    }

    // This is where you generate your next viewModel dynamically.
    // replace the switch statement logic inside with whatever logic you need.
    // DYNAMICALLY MAKE THE VIEWMODEL AND YOU DYNAMICALLY MAKE THE VIEW
    // You could even lead to a view with no navigation link in it, so that would be a dead end, if you wanted it.
    // In my case my "context" is the previous viewMode, by you could make it something else.
    func generateViewModel(context: ViewModel) -> ViewModel {
        switch context.type {
        case .information:
            return ViewModel(message: "Serial Number 123", type: .productDisplay)
        case .productDisplay:
            return ViewModel(message: "Susan", type: .person)
        case .person:
            return ViewModel(message: "Get Information", type: .chart)
        case .chart:
            return ViewModel(message: "Chart goes here. If you don't want the navigation link on this page, you can remove it! Or do whatever you want! It's all dynamic. The point is, the DynamicView can be as dynamic as your model makes it.", type: .information)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
        .environmentObject(NavigationManager())
    }
}

Solution 5

Something that occurs to me is that when you say:

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.... but as the data which at that time has to be passed is not available the whole construct fails.

it's not quite true. Rather than supplying views, you can design your re-usable components so that you supply closures which supply views on demand.

That way the closure which produces ViewB on demand can supply it with a closure which produces ViewC on demand, but the actual construction of the views can happen at a time when the contextual information that you need is available.

Share:
10,813
Darko
Author by

Darko

Just loving Apples "no-compromise-on-quality" philosophy and their great products.

Updated on June 28, 2022

Comments

  • Darko
    Darko almost 2 years

    I try to do the architecture for a bigger, production ready SwiftUI App. I am running all the time into the same problem which points to a major design flaw in SwiftUI.

    Still nobody could give me a full working, production ready answer.

    How to do reusable Views in SwiftUI which contain navigation?

    As the SwiftUI NavigationLink is strongly bound to the view this is simply not possible in such a way that it scales also in bigger Apps. NavigationLink in those small sample Apps works, yes - but not as soon as you want to reuse many Views in one App. And maybe also reuse over module boundaries. (like: reusing View in iOS, WatchOS, etc...)

    The design problem: NavigationLinks are hardcoded into the View.

    NavigationLink(destination: MyCustomView(item: item))
    

    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. I asked this here and got quite a good answer, but still not the full answer:

    SwiftUI MVVM Coordinator/Router/NavigationLink

    The idea was to inject the Destination Links into the reusable view. Generally the idea works but unfortunately this does not scale to real Production Apps. 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.... but as the data which at that time has to be passed is not available the whole construct fails.

    Another idea I had was to use the Environment as dependency injection mechanism to inject destinations for NavigationLink. But I think this should be considered more or less as a hack and not a scalable solution for large Apps. We would end up using the Environment basically for everything. But because Environment also can be used only inside View's (not in separate Coordinators or ViewModels) this would again create strange constructs in my opinion.

    Like business logic (e.g. view model code) and view have to be separated also navigation and view have to be separated (e.g. the Coordinator pattern) In UIKit it's possible because we access to UIViewController and UINavigationController behind the view. UIKit's MVC already had the problem that it mashed up so many concepts that it become the fun-name "Massive-View-Controller" instead of "Model-View-Controller". Now a similar problem continues in SwiftUI but even worse in my opinion. Navigation and Views are strongly coupled and can not be decoupled. Therefore it's not possible to do reusable views if they contain navigation. It was possible to solve this in UIKit but now I can't see a sane solution in SwiftUI. Unfortunately Apple did not provide us an explanation how to solve architectural issues like that. We got just some small sample Apps.

    I would love to be proven wrong. Please show me a clean App design pattern which solves this for big production ready Apps.

    Thanks in advance.


    Update: this bounty will end in a few minutes and unfortunately still nobody was able to provide a working example. But I will start a new bounty to solve this problem if I can't find any other solution and link it here. Thanks to all for their great Contribution!


    Update 18th June 2020: I got an answer from Apple regarding this issue, proposing something like this to decouple views and models:

    enum Destination {
      case viewA
      case viewB 
      case viewC
    }
    
    struct Thing: Identifiable {
      var title: String
      var destination: Destination
      // … other stuff omitted …
    }
    
    struct ContentView {
      var things: [Thing]
    
      var body: some View {
        List(things) {
          NavigationLink($0.title, destination: destination(for: $0))
        }
      }
    
      @ViewBuilder
      func destination(for thing: Thing) -> some View {
        switch thing.destination {
          case .viewA:
            return ViewA(thing)
          case .viewB:
            return ViewB(thing)
          case .viewC:
            return ViewC(thing)
        }
      }
    }
    

    My response was:

    Thanks for the feedback. But as you see you still have the strong coupling in the View. Now "ContentView" needs to know all the views (ViewA, ViewB, ViewC) it can navigate too. As I said, this works in small sample Apps, but it does not scale to big production ready Apps.

    Imagine that I create a custom View in a Project in GitHub. And then import this view in my App. This custom View does not know anything about the other views it can navigate too, because they are specific to my App.

    I hope I explained the problem better.

    The only clean solution I see to this problem is to separate Navigation and Views like in UIKit. (e.g. UINavigationController)

    Thanks, Darko

    So still no clean & working solution for this problem. Looking forward to WWDC 2020.


    Update September 2021: Using AnyView is not a good general solution for this problem. In big Apps basically all views have to be designed in a reusable way. This would mean that AnyView get's used everywhere. I had a session with two Apple developers and they clearly explained to me the AnyView creates a way worse performance then View and it should be only used in exceptional cases. The underlying reason for this is that the type of AnyView can't be resolved during compile time so it has to be allocated on the heap.

  • Darko
    Darko about 4 years
    The closure is a good idea, thanks! But how would that look like in a deep view hierarchy? Imagine I have a NavigationView which goes 10 levels deeper, detail, into detail, into detail, etc...
  • Darko
    Darko about 4 years
    I would like to invite you to show some simple example code of only three levels deep.
  • Darko
    Darko about 4 years
    But how does the creation of such „closure-tree“ differ from actual views? The item providing problem would be solved, but not the needed nesting. I create a closure which creates a view - ok. But in that closure I would already need to provide the creation of the next closure. And in the last one the next. Etc... but maybe I misunderstand you. Some code example would help. Thanks.
  • Darko
    Darko about 4 years
    -> some View forces you to always return just one type of View.
  • Darko
    Darko about 4 years
    The dependency injection with EnvironmentObject solves one part of the problem. But: should something crucial and important in an UI framework should be so complex... ?
  • Darko
    Darko about 4 years
    I mean - if dependency injection is the only solution for this then I would reluctantly accept it. But this would really smell...
  • MScottWaller
    MScottWaller about 4 years
    You could also make this return AnyView and wrap it in that to type erase. Not sure about performance. You could also simply pass in a navigationManager as a parameter, not as EnvironmentObject, but EnvironmentObject is a pretty standard use at this point. Heck, if there is not state associated with navigationManager, you could construct one per view and pass what you need through the destinationForTitle method. Or listen for notifications, etc. A lot of ways to go for this.
  • Darko
    Darko about 4 years
    It may seem easy just thinking about it but please try to actually write the code down and compile it. I did it. It's not that easy. What would be the return type of destinationForTitle? As you see View does not work. And AnyView has serious performance problems. (I mention this also in my linked post) The solution which comes nearest until now is the Generic Solution with Closures. But still not there. Nevertheless - thanks for the suggestion.
  • Darko
    Darko about 4 years
    Thanks for the sample code. I would like to invite you to change Text("Returned Destination1") to something like MyCustomView(item: ItemType, destinationView: View). So that MyCustomView also needs some data and destination injected. How would you solve that?
  • Darko
    Darko about 4 years
    You run into the nesting problem which I describe in my post. Please correct me if I am wrong. Basically this approach works if you have one reusable view and that reusable view does not contain another reusable view with NavigationLink. Which is quite a simple use-case but does not scale to big Apps. (where almost every view is reusable)
  • MScottWaller
    MScottWaller about 4 years
    Hi, I added a more complex detail view. Basically you make it so that the detail view is completely dynamic based on what your needs are, according to the data you feed it. You might wonder about the complexity of having everything inside one view, but of course each SwiftUI app is only one view, the initial ContentView. Of course, the best result would be a performance-keeping type erasure and such, as you've noted. That would be a BIG improvement. But for now, you can keep things pretty dynamic as such.
  • Darko
    Darko about 4 years
    Thank you for your efforts Scott, I appreciate that. But maybe I didn’t explain my intentions good enough. See also my original post: stackoverflow.com/questions/61188131/…
  • Darko
    Darko about 4 years
    There is no dynamism in your example. Pretty everything is hardcoded and not replaceable. Only DetailView is possible as destination. Imagine you are developing a 3rd party Chart UI Component in SwiftUI and want to sell it. (that’s not what I want but it explains it quite good) The NavigationLink has to be completely decoupled.
  • MScottWaller
    MScottWaller about 4 years
    Alright, I refactored to make the dynamism more clear. Basically, don't be thrown off by the "one" return type. The example I have effectively returns 4 different views depending on the view model. Dynamically make the view model, and you dynamically make the view. If you want the possibility of the DynamicView transforming into a different view, add the enum and if statement! The example I have is simple, but you could add more properties to the view model, like a ProductInformation struct to pull data from, and thus customize your ProductView even more
  • Darko
    Darko about 4 years
    Please put the DynamicView into an own module. E.g. a Swift Package or CocoaPod. You will see that it does not compile as it has implicit knowledge and is hardwired with the rest of the system. It's strongly coupled. I am impressed by your motivation but I'm afraid I can't explain you the problem good enough or you don't understand it. Really - the Swift Package would be a good test for you if you are still motivated to solve this. Imagine you are doing a reusable view which can be used on iOS, macOS, watchOS, etc.. then you have to put it into a separate, decoupled module.
  • MScottWaller
    MScottWaller about 4 years
    Hmmm, I didn't see a request to make this a framework in your original question. Yes, unfortunately, you would need to explain your business requirements more clearly. Your initial question was about architectural patterns. Abstracting into a framework is a different game and I would need to understand how your framework is supposed to interact with clients, etc. For now, if someone wanted to make an app that dynamically drills down into 10 or 30 or 10,000 different views their app has defined, this is a way to do that.
  • Darko
    Darko about 4 years
    I don't request putting in into a framework. But putting it into a framework would be a good test if it's decoupled.
  • Nikola Matijevic
    Nikola Matijevic about 4 years
    This highly depends on how you manage your app dependencies and their flow. If you are having dependencies in a single place, as you should IMO (also known as composition root), you shouldn't run into this problem.
  • Nikola Matijevic
    Nikola Matijevic about 4 years
    What works for me is defining all your dependencies for a view as a protocol. Add conformance to the protocol in the composition root. Pass dependencies to the coordinator. Inject them from the coordinator. In theory, you should end up with more than three parameters, if done properly never more than dependencies and destination.
  • MScottWaller
    MScottWaller about 4 years
    Yeah, it's a tightly coupled pattern, but effective for the app you were wanting to build in your original post. Meanwhile, on a different note, if you want SwiftUI charts :) github.com/AppPear/ChartView
  • Darko
    Darko about 4 years
    That's great, thanks for the link! And as you see - no Navigation on that chart. :)
  • MScottWaller
    MScottWaller about 4 years
    Sure thing! And use the pattern, and you get infinite dynamic chart navigation 😜
  • Nikola Matijevic
    Nikola Matijevic about 4 years
    And after trying it, I see that with a properly implemented delegation, it doesn't matter how deep you go. It always gets instantiated from Coordinator, passed into a view with a simple two-parameter initialization. @Darko
  • Darko
    Darko about 4 years
    I would love to see a concrete example. As I already mentioned, let's start at Text("Returned Destination1"). What if this needs to be a MyCustomView(item: ItemType, destinationView: View). What are you going to inject there? I understand dependency injection, loose coupling thru protocols, and shared dependencies with coordinators. All of that is not the problem - it's the needed nesting. Thanks.
  • jasongregori
    jasongregori about 4 years
    I don’t see why you couldn’t use this with your framework example. If you’re talking about a framework that vends an unknown view I would imagine it could just return some View. I also wouldn’t be surprised if an AnyView inside of a NavigationLink isn’t actually that big of a pref hit since the parent view is completely separated from the actual layout of the child. I’m no expert though, it would have to be tested. Instead of asking everyone for sample code where they can’t fully understand your requirements why don’t you write a UIKit sample and ask for translations?
  • jasongregori
    jasongregori about 4 years
    This design is basically how the (UIKit) app I work on works. Models are generated which link to other models. A central system determines what vc should be loaded for that model and then the parent vc pushes it onto the stack.
  • Nikola Matijevic
    Nikola Matijevic about 4 years
    Hmm, cool. I will make a sample project on GitHub and link it here so everyone can see. @Darko
  • Darko
    Darko about 4 years
    Would be great if you put the reusable view in another module/framework. It’s the best test to really see that it’s decoupled from the main target. Thanks!
  • Darko
    Darko about 4 years
    @jasongregori There are many different architecture which works without any issues with UIKit. MVC, MVP, MVVM, Viper, Redux, etc.. but there is this one difference between SwiftUI and UIKit which creates lot of headache. Navigation and View are strongly coupled together. You can see here a MVP example I created which shows how to decouple routing, building, presenting and the models. The result is: 100% of business logic is testable and views are reusable: github.com/DarkoDamjanovic/SolidRock.AppTemplate.iOS But this is just one example. But I do not ask for this in particular.
  • Darko
    Darko about 4 years
    My current impression is that a working & clean solution needs a dependency injection container (e.g. Environment, Swinject, etc...). Additionally @Mecids idea with the generic closure return will be most likely part of it. If you start a GitHub project I will participate with PR's if you accept or need my help. Thanks.
  • Nikola Matijevic
    Nikola Matijevic about 4 years
    Sure, would you like to talk more in detail about the creation of possible SwiftUI navigation ? @Darko
  • MScottWaller
    MScottWaller about 4 years
    " I also wouldn’t be surprised if an AnyView inside of a NavigationLink isn’t actually that big of a pref hit since the parent view is completely separated from the actual layout of the child. " This comment seems important. @Darko what measurements are you seeing if you use this method vs. loosely coupling with AnyView? Have you done any testing to see whether the performance hit would be significant, or whether it would be negligible, and thus scalable?
  • MScottWaller
    MScottWaller about 4 years
    Sometimes the AnyView performance difference doesn't have an impact. This could be one of those times. See here medium.com/swlh/…
  • Darko
    Darko about 4 years
    Also using AnyView would solve just one part of the problem. However - I would ask you to step back and see the whole picture: should something that foundational in an UI framework be so cumbersome? I talked already to dozen of experts with no clear answer. But still hope I am wrong.
  • MScottWaller
    MScottWaller about 4 years
    Hmmm, it seems like you would have a framework with 1 class and two structs in it, maybe a protocol, and you get infinite navigation. It's just MVVM with a central component. Certainly less than VIPER. What part do you find cumbersome?
  • MScottWaller
    MScottWaller about 4 years
    Also, if you can use AnyView, what is the other part of the problem you are trying to solve? It looks like with that, you have a SwiftUI navigation architecture that you can extract into framework in a performative, scalable way that is ready for production enterprise apps. Is there another thing you're trying to solve?
  • Darko
    Darko about 4 years
    @MScottWaller The other problem is one reusable view which links to another reusable view which links to another reusable view (etc...). Please try it out in code. For now it seems that only a Dependency Injection Container (e.g. SwiftUI Environment) would be the solution for that. If you look at Macids solution - this closure pattern solves how to provide needed items on demand. Using AnyView would replace the need for the Generics in Macid Solution. But Generic or AnyView is not that important for me. Now I would like a code example which shows all of this to hand over the bounty. Thanks!
  • MScottWaller
    MScottWaller about 4 years
    MVC, MVVM and most patterns use "dependency injection." In MVVM the viewModel is a dependency injection that is not a UIView. A UINavigationController is a dependency injection that is not a UIView. You don't only use UIView in UIKit (you also use UIViewController), and you don't only use View in SwiftUI. It is an expected design pattern in SwiftUI to use EnvironmentObject. And this solution is MORE dynamic because you don't have to make all your closures ahead of time. You wanted a "clean App design pattern which solves this for big production ready Apps." I've given you two.
  • Darko
    Darko about 4 years
    @MScottWaller I am sorry If I made you upset, this was not my intention. But I still can't see the solution you are proposing. The code you posted above clearly does not work. I know by myself all the concepts by as of today I can't write down a working solution which compiles and works. The edge points - multiple nested reusable Views with Navigation. Those Views in separated modules which are decoupled from the main App target and therefore can be reused among multiple targets. I hope you understand what I mean. Thanks for your contribution Scott.
  • Darko
    Darko about 4 years
    It would be great if you can just start a GitHub project. I will start than a new bounty as the current one is running out. Thanks in advance.
  • Jim lai
    Jim lai about 4 years
    struct ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordi‌​nator: ReusableNavigationLinkTextViewCoordinator. You probably should use comments instead of super long name.
  • Fattie
    Fattie over 3 years
    au contraire, code should be self documenting. Long names are the way to go (and indeed what Apple do)
  • nikmin
    nikmin over 3 years
    @NikolaMatijevic has there been an update to this? Have you been able to create a sample project? I'm currently going into this and would really appreciate if you have a solution that I could have a look at. Thank you!
  • Admin
    Admin about 3 years
    Maybe this article will help, Coordinator Pattern in SwiftUI: quickbirdstudios.com/blog/coordinator-pattern-in-swiftui
  • Nikola Matijevic
    Nikola Matijevic about 3 years
    I have built a full app using this approach, you just need to break it down into multiple coordinators based on the complexity of navigation. The composition worked really well for me. I am aware that this might not be the ideal solution but I am working full-time in SwiftUI. Another interesting approach to the whole SwiftUI paradigm is The composable architecture by point free. It's similar to Redux :)
  • Darko
    Darko over 2 years
    Using 'AnyView' is not a good general solution for this problem. In big Apps basically all views have to be designed in a reusable way. This would mean that 'AnyView' get's used everywhere. I had a session with two Apple developers and they clearly explained to me the AnyView creates a way worse performance then View and it should be only used in exceptional cases. The underlying reason for this is that the type of AnyView can't be resolved during compile time so it has to be allocated on the heap.
  • CouchDeveloper
    CouchDeveloper over 2 years
    @Darko Thanks for the comment. You are right that AnyView should not be used generally - and it doesn't need to be used to build a view hierarchy within one "scene" (page, screen). In this use case, an AnyView is returned where it starts a complete new flow by pushing the returned view on the navigation stack. There is no other way to use AnyView if you want to to completely decouple your destination view from the parent view. There is also no performance issue.
  • CouchDeveloper
    CouchDeveloper over 2 years
  • Kyle Browning
    Kyle Browning about 2 years
    You shouldn't use AnyView as it hides away details that allow SwiftUI to optimize for transitions, comparisons, and animations.
  • nikolsky
    nikolsky about 2 years
    Hi @KyleBrowning do you mind sharing a bit more details how exactly using AnyView is degrading performance please? What would be your solution to this?
  • Kyle Browning
    Kyle Browning about 2 years
    I use the .background modifier to solve this problem.
  • Kyle Browning
    Kyle Browning about 2 years