Home > Net >  Can't show view in Swift UI with layers of views and onAppear
Can't show view in Swift UI with layers of views and onAppear

Time:03-24

There is a strange case where if you show a view through another view the contents (list of 3 items) of the second view won't show when values are set using onAppear. I'm guessing SwiftUI gets confused since the second views onAppear is called prior to the first views onAppear, but I still think this is weird since both of the views data are only used in their own views. Also, there is no problem if I don't use view models and instead have the data being set using state directly in the view, but then there is yet another problem that the view model declaration must be commented out otherwise I get "Thread 1: EXC_BAD_ACCESS (code=1, address=0x400000008)". Furthermore, if I check for nil in the first view on the data that is set there before showing the second one, then the second view will be shown the first time you navigate (to the first containing the second), but no other times. I also tried removing content view and starting directly at FirstView and then the screen is just black. I want to understand why these problems happen, setting data through init works but then the init will be called before it's navigated to since that's how NavigationView works, which in turn I guess I could work around by using a deferred view, but there are cases where I would like to do stuff in the background with .task as well and it has the same problem as .onAppear. In any case I would like to avoid work arounds and understand the problem. See comments for better explanation:

struct ContentView: View {
    var body: some View {
        NavigationView {
            // If I directly go to SecondView instead the list shows
            NavigationLink(destination: FirstView()) {
                Text("Go to first view")
            }
        }
    }
}

class FirstViewViewModel: ObservableObject {
    @Published var listOfItems: [Int]?
    
    func updateList() {
        listOfItems = []
    }
}
struct FirstView: View {
    @ObservedObject var viewModel = FirstViewViewModel()
    
    // If I have the state in the view instead of the view model there is no problem.
    // Also need to comment out the view model when using the state otherwise I get Thread 1: EXC_BAD_ACCESS runtime exception
    //@State private var listOfItems: [Int]?
    
    var body: some View {
        // Showing SecondView without check for nil and it will never show
        SecondView()
        
        // If I check for nil then the second view will show the first time its navigated to, but no other times.
        /*Group {
            if viewModel.listOfItems != nil {
                SecondView()
            } else {
                Text("Loading").hidden() // Needed in order for onAppear to trigger, EmptyView don't work
            }
        }*/
        
        // If I comment out onAppear there is no problem
       .onAppear {
           print("onAppear called for first view after onAppear in second view")
           viewModel.updateList()
           // listOfItems = []
        }
    }
}
   


class SecondViewViewModel: ObservableObject {
    @Published var listOfItems = [String]()
    
    func updateList() {
        listOfItems = ["first", "second", "third"]
    }
}
struct SecondView: View {
    @ObservedObject var viewModel = SecondViewViewModel()
    
    // If I set the items through init instead of onAppear the list shows every time
    init() {
        // viewModel.updateList()
    }
    
    var body: some View {
        Group {
            List {
                ForEach(viewModel.listOfItems, id: \.self) { itemValue in
                    VStack(alignment: .leading, spacing: 8) {
                        Text(itemValue)
                    }
                }
            }
        }
        .navigationTitle("Second View")
        .onAppear {
            viewModel.updateList()
            // The items are printed even though the view don't show
            print("items: \(viewModel.listOfItems)")
        }
    }
}

CodePudding user response:

We don't use view model objects in SwiftUI. For data transient to a View we use @State and @Binding to make the View data struct behave like an object.

And FYI initing an object using @ObservedObject is an error causing a memory leak, it will be discarded every time the View struct is init. When we are creating a Combine loader/fetcher object that we want to have a lifetime tied to the view we init the object using @StateObject.

Also you must not do id: \.self with ForEach for an array of value types cause it'll crash when the data changes. You have to make a struct for your data that conforms to Identifiable to be used with ForEach. Or if you really do want a static ForEach you can do ForEach(0..<5) {

  • Related