SwiftUI Alert with Picker

422 Views Asked by At

I'm trying to create a SwiftUI alert dialog that contains a picker view inside of it, similar to the example here https://github.com/zhiyao92/PickerView-on-AlertController.

This is the code I've built so far in PickerAlert.swift

import SwiftUI

public struct PickerAlert {
    public var title: String = "" // Title of the dialog
    public var message: String = "" // Dialog message
    public var pickerData: [String] = ["red", "green"] // Data for the picker
    public var action: (String?) -> Void // Triggers when either of the two buttons closes the dialog
}

extension UIAlertController {
    convenience init(alert: PickerAlert) {
        self.init(title: alert.title, message: alert.message, preferredStyle: .alert)
        
        let pickerFrame = UIPickerView(frame: CGRect(x: 5, y: 20, width: 250, height: 140))
        pickerFrame.reloadAllComponents()
        
        // Set the picker view as the accessory view of the alert controller
        self.view.addSubview(pickerFrame)
        self.view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            pickerFrame.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 60),
            pickerFrame.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
            pickerFrame.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
            pickerFrame.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50)
        ])
        
        addAction(UIAlertAction(title: "Cancel", style: .destructive) { _ in
            alert.action(nil)
        })
                
        addAction(UIAlertAction(title: "OK", style: .default) { _ in
            alert.action("dummy data")
        })
    }
}

struct PickerAlertWrapper<Content: View>: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    let alert: PickerAlert
    let content: Content
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<PickerAlertWrapper>) -> UIHostingController<Content> {
        UIHostingController(rootView: content)
    }
    
    final class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var alertController: UIAlertController?
        let alert: PickerAlert
        
        init(alert: PickerAlert, controller: UIAlertController? = nil) {
            self.alert = alert
            self.alertController = controller
            
            super.init()
            
            // Create the picker view instance and add it as a subview to the alert controller's view
            if let pickerFrame = alertController?.view.subviews.first as? UIPickerView {
                pickerFrame.dataSource = self
                pickerFrame.delegate = self
                pickerFrame.reloadAllComponents()
            }
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return alert.pickerData.count
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return alert.pickerData[row]
        }
    }
    
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator(alert: alert)
        return coordinator
    }
    
    func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: UIViewControllerRepresentableContext<PickerAlertWrapper>) {
        uiViewController.rootView = content
        if isPresented && uiViewController.presentedViewController == nil {
            var alert = self.alert
            alert.action = {
                self.isPresented = false
                self.alert.action($0)
            }
            context.coordinator.alertController = UIAlertController(alert: alert)
            uiViewController.present(context.coordinator.alertController!, animated: true)
        }
        if !isPresented && uiViewController.presentedViewController == context.coordinator.alertController {
            uiViewController.dismiss(animated: true)
        }
    }
}

extension View {
    public func alert(isPresented: Binding<Bool>, _ alert: PickerAlert) -> some View {
        PickerAlertWrapper(isPresented: isPresented, alert: alert, content: self)
    }
}

This is the code of a SwiftUI view that shows the alert with the picker.

struct MyView: View {
    @StateObject var viewModel = MyViewModel()
    
    var body: some View {
        ZStack {
            Text("")
                .hidden()
                .alert(isPresented: $viewModel.showSelectHomeAlert,
                       PickerAlert(
                        title: "asdf"
                       ) { selection in
                           if let selection = selection {
                               print("selection: \(selection)")
                           }
                       }
                )
            ...
        }
    }
}

This is a screenshot of the output I'm seeing. It doesn't look like the UIPickerView is getting populated with the default values of "red" and "green" that I put. How can I get this to work?

enter image description here

1

There are 1 best solutions below

0
hockeyfan530 On

I did something similar. I have the picker in two different alerts. I did not add the alert. as you did in your code below. I'm new to this stuff so take my answer with a grain of salt

 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return **alert**.pickerData.count    *(mine would be return pickerData.count)*
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return **alert**.pickerData[row] *(mine would be return pickerData[row])*
        }