BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

Refactor confirmationDialog

Forums > SwiftUI

Currently in one of my views I have setup a conformation dialog that triggers appropriate booleans to show corresponding sheets. Each sheet shown is a view, more precisely a UIViewControllerRepresentable. All works perfectly!

               @State private var showingImagePickerView = false
               @State private var showingCameraPickerView = false
               @State private var showingDocumentsPickerView = false
               @State private var showingSafariWebPickerView = false

            (...)

            .confirmationDialog("Select", isPresented: $showingImagePickerOptions) {
                Button("Take Photo") { showingCameraPickerView = true }
                Button("Photo library") { showingImagePickerView = true }
                Button("Documents") { showingDocumentsPickerView = true }
                Button("Web") { showingSafariWebPickerView = true}
                Button("Paste from clipboard") { pasteImageFromClipboard() }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("Select an Image source...")
            }
            .sheet(isPresented: $showingImagePickerView) {
                CameraPicker(sourceType: .photoLibrary, selectedImage: $selectedImage)
            }
            .sheet(isPresented: $showingCameraPickerView) {
                CameraPicker(selectedImage: $selectedImage)
            }
            .sheet(isPresented: $showingDocumentsPickerView) {
                DocumentsPicker(selectedImage: $selectedImage)
            }
            .sheet(isPresented: $showingSafariWebPickerView) {

I find this multiple sheet appraoch very messy and besides I am going to need the same setup at least once elsewhere. So I want to refactor this and pull the dialog out in a way that i have an object that can show the dialog (options) and consequently show the corresponding sheet. I say sheet, because I think I can't just present a view, right? (iOS 16 can?). Tried several approaches, but non succesful so far. The closest to my original setup in the original view is this:

            .confirmationDialog("Select", isPresented: $showingImagePickerOptions) {
                MultipleSourcesImagePicker(selectedImage: $selectedImage)
            }

          (...)

   struct MultipleSourcesImagePicker: View {    
    @Binding var selectedImage: UIImage?

    @Environment(\.dismiss) var dismiss
    @State private var option: Options = .photos
    @State private var showingImagePickerView = false
    @State private var showingCameraPickerView = false
    @State private var showingDocumentsPickerView = false
    @State private var showingSafariWebPickerView = false

    var body: some View {
        VStack {
        Button("Camera") { showingCameraPickerView = true }
            Button("Photo library") {  showingImagePickerView = true }
            Button("Documents") { showingDocumentsPickerView = true }
            Button("Web") { showingSafariWebPickerView = true }
            Button("Paste from clipboard") {  pasteFromClipboard() }
            Button("Cancel", role: .cancel) { cancel() }
        }
        .sheet(isPresented: $showingImagePickerView) {
            CameraPicker(sourceType: .photoLibrary, selectedImage: $selectedImage)
        }
        .sheet(isPresented: $showingCameraPickerView) {
            CameraPicker(selectedImage: $selectedImage)
        }
        .sheet(isPresented: $showingDocumentsPickerView) {
            DocumentsPicker(selectedImage: $selectedImage)
        }
        .sheet(isPresented: $showingSafariWebPickerView) {
            SafariWebPickerWithOverlay(selectedImage: $selectedImage)
        }
    }

    func pasteFromClipboard() {
        let pasteboard = UIPasteboard.general

        //image was returned by Copy
        if pasteboard.hasImages {
            guard let image = pasteboard.image else { return }
            selectedImage = image
            //Image Url was returned by Copy
        } else if pasteboard.hasURLs {
            guard let url = pasteboard.url else { return }
            if let data = try? Data(contentsOf: url) {
                if let image = UIImage(data: data) {
                    selectedImage = image
                }
            }
        }

        pasteboard.items.removeAll()
        //dismiss()
    }

    private func cancel() {
        dismiss()
    }
}

This builds, but the sheets do not get presented... Ant nudges in the right direction would be helpfull. Other approaches too!

   

Rather than a collection of booleans, consider creating an enum for the different types of sheet that may be needed.

enum ImageSource {
  case camera
  case photoLibrary
  case documents
  case safari
}

@State private var imageSource: ImageSource? = nil

Then your buttons can set this state variable, e.g.

Button("Photo Library") { imageSource = .photoLibrary }

This allows you to have a single sheet based on your enum. It will display when imageSource is not nil, and reset it to nil when the sheet is dismissed.

.sheet(object: $imageSource) { source in 
  switch source {
  case .camera:
    CameraPicker(selectedImage: $selectedImage)
  case .documents:
    DocumentsPicker(selectedImage: $selectedImage)
  // etc.
}

   

@https://www.hackingwithswift.com/users/scottmatthewman I tried that approach, but it somehow didn't work for me. Gonna have another go at it and report back here. Thanks!

   

@scottmatthewman shouldn't it be

.sheet(item: $imageSource) { source in 
  switch source {
  case .camera:
    CameraPicker(selectedImage: $selectedImage)
  case .documents:
    DocumentsPicker(selectedImage: $selectedImage)
  // etc.
}

I'm still on 13.4.1, so maybe object is new?

   

@scottmatthewman I see what you mean, those changes in the calling view. I've implemeted that, and its a little bit cleaner with just one sheet, but I'd like to be able to have one class/struct that can be called from anywhere, that presents a confirmation dialog (ie list of buttons) and handles serving the right views (sheet or otherwise) and handles the result (return the selectedImage).

Fort completion sake, my current implementation based upon your tips:

// Must conform to Identifiable!
enum ImagePickerSource: Int, Identifiable {
    var id: Int {
        self.rawValue
    }

    case camera = 0, photos = 1, documents = 2, web = 3
}

    @State private var imagePickerSource: ImagePickerSource? = nil

            .confirmationDialog("Select", isPresented: $showingImagePickerOptions) {
                Button("Camera") {
                    imagePickerSource = .camera
                }
                Button("Photo library") {
                    imagePickerSource = .photos
                }
                Button("Documents") {
                    imagePickerSource = .documents
                }
                Button("Web") {
                    imagePickerSource = .web
                }
                Button("Paste from clipboard") { pasteImageFromClipboard() }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("Select an Image source...")
            }
            .sheet(item: $imagePickerSource) { source in
                switch source {
                case .camera:
                    CameraPicker(selectedImage: $selectedImage)
                case .photos:
                    CameraPicker(sourceType: .photoLibrary, selectedImage: $selectedImage)
                case .documents:
                    DocumentsPicker(selectedImage: $selectedImage)
                case .web:
                    SafariWebPickerWithOverlay(selectedImage: $selectedImage)
                }
            }

   

any pointers anyone?

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.