Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI NavigationLink loads destination view immediately, without clicking

Tags:

ios

swift

swiftui

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?

like image 834
Nat Avatar asked Aug 21 '19 14:08

Nat


2 Answers

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") } 
like image 193
MwcsMac Avatar answered Oct 04 '22 00:10

MwcsMac


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)")         }     } } 
like image 45
Fabian Avatar answered Oct 04 '22 00:10

Fabian