Reputation: 2923
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
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
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