Moving function out of view: Modifying state during view update, this will cause undefined behavior

Viewed 192

I have a picker that let's the user choose an image from their photo library. This is presented in a sheet, that's within another sheet that captures user data and saves it with core data.

When the user picks the image I am thrown the warning:

Modifying state during view update, this will cause undefined behavior

I believe it stems from the function below in the view that modifies the state.

How can I move this function out of the view?

if (showCaptureImageView) {
    CaptureImageView(isShown: $showCaptureImageView, image: $image)
}

Full code

import SwiftUI
import UIKit

struct CaptureImageView {
    @Binding var isShown: Bool
    @Binding var image: Image?
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(isShown: $isShown, image: $image)
    }
}

extension CaptureImageView: UIViewControllerRepresentable {
  func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
    let picker = UIImagePickerController()
    picker.delegate = context.coordinator
    return picker
  }
  
  func updateUIViewController(_ uiViewController: UIImagePickerController,
                              context: UIViewControllerRepresentableContext<CaptureImageView>) {
    
  }
}

struct addBookView: View {
    @Environment(\.managedObjectContext) var moc
    @Environment(\.presentationMode) var presentationMode
    
    @State private var title = ""
    @State private var author = ""
    @State private var currentPage = ""
    @State private var image: Image? = nil
    @State var showCaptureImageView: Bool = false
    
    var body: some View {
        NavigationView {
                Form {
                    Section {
                        TextField("What's the book title", text: $title)
                        TextField("Who's the author", text: $author)
                        TextField("What page are you on", text: $currentPage)
                    }
                    Section {
                        Button (action: {
                            self.showCaptureImageView.toggle()
                        }) {
                            Text("Take a picture")
                        }
                        .sheet(isPresented: $showCaptureImageView) {
                            CaptureImageView(isShown: self.$showCaptureImageView, image: self.$image)
                        }
                        image?.resizable()
                            .frame(width: 250, height: 250)
                            .clipShape(Circle())
                            .overlay(Circle().stroke(Color.white, lineWidth: 4))
                            .shadow(radius: 10)
                        if (showCaptureImageView) {
                          CaptureImageView(isShown: $showCaptureImageView, image: $image)
                        }
                    }
                    Section {
                        Button ("Save") {
                             let newBook = Book(context: self.moc)
                             newBook.title = self.title
                             newBook.author = self.author
                             newBook.currentPage = self.currentPage
                             
                             try? self.moc.save()
                             self.presentationMode.wrappedValue.dismiss()
                         }
                    }
                }
        }
        .navigationBarTitle("Add a book")
    }
}

struct addBookView_Previews: PreviewProvider {
    static var previews: some View {
        addBookView()
    }
}

Coordinator

import SwiftUI

class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    @Binding var isCoordinatorShown: Bool
    @Binding var imageInCoordinator: Image?
    
    init(isShown: Binding<Bool>, image: Binding<Image?>) {
        _isCoordinatorShown = isShown
        _imageInCoordinator = image
    }
    
    func imagePickerController(_ picker: UIImagePickerController,
                               didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        guard let unwrapImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
        imageInCoordinator = Image(uiImage: unwrapImage)
        isCoordinatorShown = false
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        isCoordinatorShown = false
    }
}
1 Answers

Try to defer image update in next event loop (this is usual approach to avoid that run-time error), like

guard let unwrapImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }

DispatchQueue.main.async {
   imageInCoordinator = Image(uiImage: unwrapImage)
}
isCoordinatorShown = false
Related