SwiftUI NavigationLink loads destination view immediately, without clicking

IosSwiftSwiftui

Ios Problem Overview


With following code:

struct HomeView: View {
    var body: some View {
        NavigationView {
			List(dataTypes) { dataType in
			    NavigationLink(destination: AnotherView()) {
					HomeViewRow(dataType: dataType)
				}
			}
        }
    }
}

What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear. My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.

Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?

Ios Solutions


Solution 1 - Ios

The best way I have found to combat this issue is by using a Lazy View.

struct NavigationLazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()

NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }

Solution 2 - Ios

EDIT: See @MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.

It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression

NavigationLink(destination: AnotherView()) {
    HomeViewRow(dataType: dataType)
}

for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.

So to avoid this a custom ForEach is necessary.

import SwiftUI

struct LoadLaterView: View {
    var body: some View {
        HomeView()
    }
}

struct DataType: Identifiable {
    let id = UUID()
    var i: Int
}

struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
    var data: Data
    var destination: (Data.Element) -> (Destination)
    var content: (Data.Element) -> (Content)
    
    @State var selected: Data.Element? = nil
    @State var active: Bool = false
    
    var body: some View {
        VStack{
            NavigationLink(destination: {
                VStack{
                    if self.selected != nil {
                        self.destination(self.selected!)
                    } else {
                        EmptyView()
                    }
                }
            }(), isActive: $active){
                Text("Hidden navigation link")
                    .background(Color.orange)
                    .hidden()
            }
            List{
                ForEach(data) { (element: Data.Element) in
                    Button(action: {
                        self.selected = element
                        self.active = true
                    }) { self.content(element) }
                }
            }
        }
    }
}

struct HomeView: View {
    @State var dataTypes: [DataType] = {
        return (0...99).map{
            return DataType(i: $0)
        }
    }()
    
    var body: some View {
        NavigationView{
            ForEachLazyNavigationLink(data: dataTypes, destination: {
                return AnotherView(i: $0.i)
            }, content: {
                return HomeViewRow(dataType: $0)
            })
        }
    }
}

struct HomeViewRow: View {
    var dataType: DataType
    
    var body: some View {
        Text("Home View \(dataType.i)")
    }
}

struct AnotherView: View {
    init(i: Int) {
        print("Init AnotherView \(i.description)")
        self.i = i
    }
    
    var i: Int
    var body: some View {
        print("Loading AnotherView \(i.description)")
        return Text("hello \(i.description)").onAppear {
            print("onAppear AnotherView \(self.i.description)")
        }
    }
}

Solution 3 - Ios

I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).

The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).

struct AnotherView: View {
    var body: some View {
        VStack{
            Text("Hello World!")
        }.onAppear {
            print("I only printed when the view appeared")
            // trigger whatever you need to here instead of on init
        }
    }
}

Solution 4 - Ios

For iOS 14 SwiftUI.

Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.

extension View {
    func navigate<Value, Destination: View>(
        item: Binding<Value?>,
        @ViewBuilder content: @escaping (Value) -> Destination
    ) -> some View {
        return self.modifier(Navigator(item: item, content: content))
    }
}

private struct Navigator<Value, Destination: View>: ViewModifier {
    let item: Binding<Value?>
    let content: (Value) -> Destination
    
    public func body(content: Content) -> some View {
        content
            .background(
                NavigationLink(
                    destination: { () -> AnyView in
                        if let value = self.item.wrappedValue {
                            return AnyView(self.content(value))
                        } else {
                            return AnyView(EmptyView())
                        }
                    }(),
                    isActive: Binding<Bool>(
                        get: { self.item.wrappedValue != nil },
                        set: { newValue in
                            if newValue == false {
                                self.item.wrappedValue = nil
                            }
                        }
                    ),
                    label: EmptyView.init
                )
            )
    }
}

Call it like this:

struct ExampleView: View {
    @State
    private var date: Date? = nil
    
    var body: some View {
        VStack {
            Text("Source view")
            Button("Send", action: {
                self.date = Date()
            })
        }
        .navigate(
            item: self.$date,
            content: {
                VStack {
                    Text("Destination view")
                    Text($0.debugDescription)
                }
            }
        )
    }
}

Solution 5 - Ios

In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:

struct DestinationView: View {
    var body: some View {
        Text("Hello world!")
        .onAppear {
            // Do something important here, like fetching data from REST API
            // This code will only be executed when the view appears
        }
    }
}

Solution 6 - Ios

I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:

@State private var shouldShowDestination = false

NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
    Button("More info") {
        self.shouldShowDestination = true
    }
}

Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.

Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer

    /// Creates an instance that presents `destination` when `selection` is set
    /// to `tag`.
    public init<V>(destination: Destination, tag: V, selection: Binding<V?>, @ViewBuilder label: () -> Label) where V : Hashable

https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init

Along the lines of this example:

struct ContentView: View {
    @State private var selection: String? = nil

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
                    Button("Tap to show second") {
                        self.selection = "Second"
                    }
                }
                NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
                    Button("Tap to show third") {
                        self.selection = "Third"
                    }
                }
            }
            .navigationBarTitle("Navigation")
        }
    }
}

More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").

Alternatively, create a custom view component (with embedded NavigationLink), such as this one

struct FormNavigationRow<Destination: View>: View {
    
    let title: String
    let destination: Destination
    
    var body: some View {
        NavigationLink(destination: destination, isActive: $shouldShowDestination) {
            Button(title) {
                self.shouldShowDestination = true
            }
        }
    }
    
    // MARK: Private
    
    @State private var shouldShowDestination = false
}

and use it repeatedly as part of a Form (or List):

Form {
    FormNavigationRow(title: "One", destination: Text("1"))
    FormNavigationRow(title: "Two", destination: Text("2"))
    FormNavigationRow(title: "Three", destination: Text("3"))
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNatView Question on Stackoverflow
Solution 1 - IosMwcsMacView Answer on Stackoverflow
Solution 2 - IosFabianView Answer on Stackoverflow
Solution 3 - IosErnest CunninghamView Answer on Stackoverflow
Solution 4 - IosbteapotView Answer on Stackoverflow
Solution 5 - IosvegidioView Answer on Stackoverflow
Solution 6 - IosXtian D.View Answer on Stackoverflow