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.
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!