Search code examples
swiftuimvvminfinite-loop

Infinite loop when setting a @Published property


I have a simple view that shows some photos, through a list. Clicking on any row should display a detailed view of that photo. I'm using the MVVM pattern. However, an infinite loop occurs when I try to set the “selectedPhoto” property of the view model. Is there any way to avoid this loop without having to create a property in the detailed view itself?

Here is the Photo struct:

struct Photo: Identifiable {
        var id = UUID()
        var name: String
}

Here is the ContentView with an extension (the “updatePhoto” method is causing the infinite loop):

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.photos) { selectedPhoto in
                    showDetailView(with: selectedPhoto)
                }
            }
            .navigationTitle("Favorite Photo")
        }
    }
}

extension ContentView {
    func showDetailView(with selectedPhoto: Photo?) -> some View {
        if let selectedPhoto = selectedPhoto {
            viewModel.updatePhoto(selectedPhoto)
        }
        
        return DetailView(viewModel: viewModel)
    }
}

Here is the view model:

class ViewModel: ObservableObject {
    @Published var photos = [
        Photo(name: "Photo 1"),
        Photo(name: "Photo 2"),
        Photo(name: "Photo 3")
    ]
    
    @Published var selectedPhoto: Photo?
        
    func updatePhoto(_ selectedPhoto: Photo?) {
        self.selectedPhoto = selectedPhoto 
    }
}

And here is the DetailView:

struct DetailView: View {
    @ObservedObject private var viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        Text(viewModel.selectedPhoto?.name ?? "Unknown photo name")
    }
}

Solution

  • Try this approach, using a NavigationLink to present the DetailView, and passing the selectedPhoto to it using @State var selectedPhoto: Photo.

    struct Photo: Identifiable {
        let id = UUID()
        var name: String
    }
    
    class ViewModel: ObservableObject {
        @Published var photos = [Photo(name: "Photo 1"),Photo(name: "Photo 2"),Photo(name: "Photo 3")]
    }
    
    struct DetailView: View {
        @State var selectedPhoto: Photo
        
        var body: some View {
            Text(selectedPhoto.name)
        }
    }
    
    struct ContentView: View {
        @StateObject var viewModel = ViewModel()
        
        var body: some View {
            NavigationView {
                List {
                    ForEach(viewModel.photos) { selectedPhoto in
                        NavigationLink(selectedPhoto.name, destination: DetailView(selectedPhoto: selectedPhoto))
                    }
                }
                .navigationTitle("Favorite Photo")
            }
        }
    }
    

    Note that NavigationView is being deprecated and you will have to use NavigationStack instead.