.onAppear is calling when I navigated back to a view by clicking back button
Asked Answered
N

4

7

I have two views written in swiftUI , say for example ViewA and ViewB. onAppear() of ViewA has an apiCall which calls when initially the view is loaded. I navigate to ViewB from ViewA using navigation link and on clicking back button in ViewB the onAppear() of ViewA is called.

• Is there any way to stop calling onAppear() while navigated back from a view

• I am looking swiftUI for something like 'ViewDidLoad' in UIKit given a sample of my code

struct ContentView: View {
    var body: some View {
        NavigationView{
            List(viewModel.list){ item in
               NavigationLink(
                destination: Text("Destination"),
                label: {
                    Text(item.name)
                })
            }
            .onAppear{
                viewModel.getListApiCall()
            }
     
        }
    }
}
Neukam answered 20/12, 2020 at 17:52 Comment(8)
In genera, we cannot control how/when SwiftUI callbacks are sent, so instead of fighting with SwiftUI avoid redundant call in your API manager, which is much simpler and more reliable, because it is yours and, so under control. Say, using cache, throttle, debounce, etc. approach.Gomes
Could you give an example of what you would like to do the first time ViewA appears? That would help in suggesting an appropriate approachGnathonic
@Gnathonic I am calling a web Api to load a List inViewA (Api is call is given in onappear()). On clicking the list row app will navigate to detail page , and on clicking back button in detail page it navigates back to list page and list api is called again. But I don't want to call that api when navigating backNeukam
@Gomes Actually I'm new with swiftUI. Could you please elaborate how to do this or provide some useful linksNeukam
Would you provide your code?Gomes
Perhaps only get the list if viewModel.list is empty?Manton
A possible solution may be to call viewModel.getListApiCall() in the parent view, once (actual code depends on what exactly the viewModel is, i.e. where is it declared).Stoat
@RickardElimää checking viewModel.list is empty resolves my problem for now, thank uNeukam
G
3

Overview

  • SwiftUI is quite different from the way UIKit works. It would be best to watch the tutorials (links below) to understand how SwiftUI and Combine works.
  • SwiftUI is a declarative framework so the way we approach is quite different. It would be best not to look for a direct comparison to UIKit for equivalent functions.

Model:

  • Let the model do all the work of fetching and maintaining the data
  • Ensure that your model conforms to ObservableObject
  • When ever any @Published property changes, it would imply that the model has changed

View:

  • Just display the contents of the model
  • By using @ObservedObject / @EnvironmentObject SwiftUI would observe the model and ensure that the view states in sync with any changes made to the model
  • Notice that though the model fetches the data after 2 seconds, the view reacts to it and displays the updated data.

Model Code:

class Model: ObservableObject {
    
    @Published var list = [Item]()
    
    init() {
        fetchItems()
    }
    
    private func fetchItems() {
        
        //To simulate some Async API call
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.list = (1...10).map { Item(name: "name \($0)") }
        }
    }
}

struct Item: Identifiable {
    
    var name: String
    
    var id : String {
        name
    }
}

View Code:

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var model: Model
    
    var body: some View {
        NavigationView{
            List(model.list){ item in
                NavigationLink(destination: Text("Destination")) {
                    Text(item.name)
                }
            }     
        }
    }
}

Reference:

SwiftUI

Combine

Gnathonic answered 21/12, 2020 at 6:45 Comment(0)
S
6

You could add a variable to check if the getListApiCall() has been invoked.

struct ContentView: View {
    @State var initHasRun = false
    var body: some View {
        NavigationView{
            List(viewModel.list){ item in
               NavigationLink(
                destination: Text("Destination"),
                label: {
                    Text(item.name)
                })
            }
            .onAppear{
                if !initHasRun {
                    viewModel.getListApiCall()
                    initHasRun=true
                }
            }
 
        }
    }
}
Segovia answered 21/12, 2020 at 1:53 Comment(1)
No change , I think the whole ContentView in refreshing again when I am navigating back, so the initHasRun is always falseNeukam
G
3

Overview

  • SwiftUI is quite different from the way UIKit works. It would be best to watch the tutorials (links below) to understand how SwiftUI and Combine works.
  • SwiftUI is a declarative framework so the way we approach is quite different. It would be best not to look for a direct comparison to UIKit for equivalent functions.

Model:

  • Let the model do all the work of fetching and maintaining the data
  • Ensure that your model conforms to ObservableObject
  • When ever any @Published property changes, it would imply that the model has changed

View:

  • Just display the contents of the model
  • By using @ObservedObject / @EnvironmentObject SwiftUI would observe the model and ensure that the view states in sync with any changes made to the model
  • Notice that though the model fetches the data after 2 seconds, the view reacts to it and displays the updated data.

Model Code:

class Model: ObservableObject {
    
    @Published var list = [Item]()
    
    init() {
        fetchItems()
    }
    
    private func fetchItems() {
        
        //To simulate some Async API call
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.list = (1...10).map { Item(name: "name \($0)") }
        }
    }
}

struct Item: Identifiable {
    
    var name: String
    
    var id : String {
        name
    }
}

View Code:

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var model: Model
    
    var body: some View {
        NavigationView{
            List(model.list){ item in
                NavigationLink(destination: Text("Destination")) {
                    Text(item.name)
                }
            }     
        }
    }
}

Reference:

SwiftUI

Combine

Gnathonic answered 21/12, 2020 at 6:45 Comment(0)
N
0

I faced the same problem, where the viewModel got initialized after i popedBack using:

@Environment(\.presentationMode) var presentation
Button { 
self.presentation.wrappedValue.dismiss()
}    

To initialize the viewModel not again, i declared it as a @StateObject instead as a @ObservedObject :

struct MyScreen: View {

@StateObject var viewModel: MyViewModel

init() {
     self._viewModel = StateObject(wrappedValue: MyViewModel())
}

var body: some View {

  }
}
Nordstrom answered 16/4, 2023 at 9:16 Comment(0)
A
-1

I didn't find a neat solution to this, so I added a static variable that is set when the NavigationLink appears. Note that this will only work when MyView can only appear once in the navigation stack. It's ugly, but it works.

Note that I am not using a @State variable, because it creates an endless loop of View refreshes.

struct MyView: View { 
  static var navigatedBack = false

  ...

  var body: some View {
    Text("Root").onAppear {
            if MyView.navigatedBack == false {
                callOnlyOnFirstAppearance()
            }
            MyView.navigatedBack = false
        }.navigationDestination(isPresented: $subViewShown) {
            Text("Subview").onAppear() {                   
                MyView.navigatedBack = true 
            }
        }
    }
}
Application answered 27/4, 2023 at 9:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.