Ricardo
Ricardo

Reputation: 2923

Infinite loop when using onAppear in SwiftUI

I have a strange infinite loop when using onAppear and I cannot identify the root of the problem. It only happens when the view is the detail view of a navigation view, but it works fine when it's the root view. Another interesting thing is that if I wrap the detail view in a NavigationView (so, now we have a navigation view inside a navigation view), then the issue does not appear anymore. Is this a bug in SwiftUI? Is conceptually my design OK? I mean, using onAppear like viewDidLoad to trigger the initial sequence. Thanks for suggestions.

Here is the source code. ContentView.swift:

import SwiftUI

struct ContentView: View {

    @StateObject var viewModel = ContentViewModel()

    var body: some View {
        NavigationView {
            VStack {
                Group {
                    switch viewModel.state {
                    case .loading:
                        Text("Loading...")
                    case .loaded:
                        HStack {
                            Text("Loaded")
                            Button("Retry") {
                                viewModel.fetchData()
                            }
                        }
                    }
                }
                .padding(.bottom, 20)
                NavigationLink("Go to detail screen", destination: DetailView())
            }
        }
        .onAppear() {
            viewModel.fetchData()
        }
    }
}

class ContentViewModel: ObservableObject  {

    enum State {
        case loading
        case loaded
    }

    @Published var state: State = .loading

    func fetchData() {
        state = .loading
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.state = .loaded
        }
    }
}

And here the code of the detail view:

import SwiftUI

struct DetailView: View {

    @StateObject var viewModel = DetailViewModel()

    var body: some View {
        Group {
            switch viewModel.state {
            case .loading:
                Text("Loading...")
            case .loaded:
                HStack {
                    Text("Loaded")
                    Button("Retry") {
                        viewModel.fetchData()
                    }
                }
            }
        }
        .onAppear() {
            print("infinite loop here")
            viewModel.fetchData()
        }
    }
}

class DetailViewModel: ObservableObject  {

    enum State {
        case loading
        case loaded
    }

    @Published var state: State = .loading

    func fetchData() {
        state = .loading
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.state = .loaded
        }
    }
}

Here I attach the project: https://www.dropbox.com/s/5alokj3q81jbpj7/TestBug.zip?dl=0

I'm using Xcode Version 12.5.1 (12E507) and iOS 14.5

Thanks a lot.

Upvotes: 1

Views: 2934

Answers (2)

Joseph Levy
Joseph Levy

Reputation: 222

I saw something similar with iOS 15 and moving all the onAppear stuff to an initializer fixed things. No more infinite looping trying to make the dialog appear. Weirdly this only happens with the settings in a certain state.

In onAppear I had code like:

useSecondary = data.settings.showSecondaryAxis
...

But in the init function needs this to do the job:

   init(data: Binding<PlotData> {
      self._data = data
      _useSecondary = State(initialValue: data.wrappedValue.settings.showSecondaryAxis)
... 

}

… Cumbersome but it works.

Upvotes: 1

Charles Maria
Charles Maria

Reputation: 2195

This issue is a bug in iOS 14 with Group, and will still happen even when building with Xcode 13.

What's happening is the SwiftUI runtime is messing up its view diff, so it believes that it "appeared" when really it only reloaded, and thus did not disappear (which is a requirement for onAppear to be called, hence a bug).

To compensate for this issue, you'll need to use initialisers rather than relying on onAppear.

I've amended your code, this amended version does not infinitely loop.

struct DetailView: View {

    @StateObject var viewModel = DetailViewModel()

    var body: some View {
        Group {
            switch viewModel.state {
            case .loading:
                Text("Loading...")
            case .loaded:
                HStack {
                    Text("Loaded")
                    Button("Retry") {
                        viewModel.fetchData()
                    }
                }
            }
        }
    }
}

class DetailViewModel: ObservableObject  {

    enum State {
        case loading
        case loaded
    }

    @Published var state: State = .loading

    init() {
        self.fetchData()
    }

    func fetchData() {
        state = .loading
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.state = .loaded
        }
    }
}

Hope the project goes well!

Upvotes: 3

Related Questions