onDismiss() is called before binding object is updated

Viewed 106

I'm not quite sure I've used the correct phrase in my title but here's what I'm trying to achieve. I have a page with a button that presents a DocumentPicker to allow users to select items from their documents directory. I've wrapped UIDocumentPickerViewController like this:

struct DocumentPicker: UIViewControllerRepresentable {
        
    class Coordinator: NSObject, UIDocumentPickerDelegate {
        var parent: DocumentPicker
        
        init(_ parent: DocumentPicker) {
            self.parent = parent
        }
        
        func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
            parent.urls = urls
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
        
    @Environment(\.presentationMode) var presentationMode
    @Binding var urls: [URL]?
            
    func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
        let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.movie])
        pickerViewController.allowsMultipleSelection = true
        pickerViewController.delegate = context.coordinator
        return pickerViewController
    }
    
    func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
}

And I have a view like this:

struct VideoList: View {

    @ObservedObject var playlistStore = PlaylistStore()
    
    @State private var showingDocumentPicker = false
    @State private var urls: [URL]?

    // MARK: - View

    var body: some View {

        NavigationView {
            List(playlistStore.items) { item in
                Text(item.title)
            }
            .navigationBarItems(trailing: Button("Add Items") { self.showingDocumentPicker = true })
        }
        .sheet(isPresented: $showingDocumentPicker, onDismiss: updatePlaylistStore) {
            DocumentPicker(urls: self.$urls)
        }
    }

    private func updatePlaylistStore() {
        guard let urls = urls else { return }
        playlistStore.addItems(items: urls)
        self.urls = nil
    }
}

The problem occurs when I select items and the sheet dismisses. onDismiss gets called, which then calls updatePlaylistStore. However, at this point the URLs I just picked aren't yet available. If I add more items again, when onDismiss is called the second time, the items I selected the first time around are available. Since I'm using DocumentPicker(urls: self.$urls) I would expect that the URLs update immediately when selected. I've left an object called PlaylistStore() in for context, but I've just been trying to print the values of urls before adding to the store to check my issue isn't related to PlaylistStore(). Can anyone spot what I'm doing wrong please? Any guidance appreciated!

1 Answers

The code in question based on assumption about some order or calls, but there is not promise about that order.

Instead we should just react on changes, whenever they happened.

Here is fixed variant. Tested with Xcode 12.1 / iOS 14.1

struct VideoList: View {
    
    @ObservedObject var playlistStore = PlaylistStore()
    
    @State private var showingDocumentPicker = false
    @State private var urls: [URL]?
    
    // MARK: - View
    
    var body: some View {
        
        NavigationView {
            List(playlistStore.items) { item in
                Text(item.title)
            }
            .navigationBarItems(trailing: Button("Add Items") { self.showingDocumentPicker = true })
        }
        .sheet(isPresented: $showingDocumentPicker) { // no needs in onDismiss
            DocumentPicker(urls: self.$urls)
        }
        .onChange(of: urls) { _ in       // update when urls changed !!
            updatePlaylistStore()
        }
    }
    
    private func updatePlaylistStore() {
        guard let urls = urls else { return }
        playlistStore.addItems(items: urls)
        self.urls = nil
    }
}
Related