Kim Thompson

Web (+ more) developer.


I wrote an extension for displaying a UIAlertController in a SwiftUI view because alerts with text fields aren't yet supported for iOS 14.

I built this for work and figured others might find it useful. I may ultimately try to publish this via Swift Package Manager, but for now I simply wanted to make sure I didn't lose the code or my idle thoughts around it. You can find a demo here.

This wrapper is based on the example in this blog post by Chris Eidhof. It's about the fourth approach I found and tried, and I thought it could use a little more promotion. I also modified it to work for my current project and I believe made it more flexible overall, so I thought it deserved its own post.

There are two parts to this. The first part is comprised of a wrapper that essentially takes any UIAlertController and packages it for use on SwiftUI views and an .alert extension on the View itself that allows it to be displayed.

import SwiftUI
import UIKit

struct AlertWrapper<Content: View>: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    let alert: UIAlertController
    let content: Content

    func makeUIViewController(context: UIViewControllerRepresentableContext<AlertWrapper>) -> UIHostingController<Content> {
        UIHostingController(rootView: content)
    }

    final class Coordinator {
        var alertController: UIAlertController?
        init(_ controller: UIAlertController? = nil) {
            self.alertController = controller
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }

    func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: UIViewControllerRepresentableContext<AlertWrapper>) {
        uiViewController.rootView = content

        if isPresented && uiViewController.presentedViewController == nil {
            context.coordinator.alertController = 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: UIAlertController) -> some View {
        AlertWrapper(isPresented: isPresented, alert: alert, content: self)
    }
}

From there, you can create an UIAlertController object and simply attach it to your SwiftUI view using the above .alert extension. The page I needed to construct needed to be able to show three different alerts, so I stored the alert object in local state and swapped it out as needed.

import SwiftUI

struct DemoView: View {
    @State var isShowingAlert = false
    @State var alert = UIAlertController(title: "Default", message: "Placeholder", preferredStyle: .alert)

    var body: some View {
        VStack(spacing: 0) {
            ScrollView {
                VStack(spacing: 16) {
                    SwiftUI.Button {
                        DispatchQueue.main.async {
                            alert = buildPasswordAlert()
                            isShowingAlert = true
                        }
                    } label: {
                        Text("Password Alert")
                    }

                    SwiftUI.Button {
                        DispatchQueue.main.async {
                            alert = buildOtherAlert()
                            isShowingAlert = true
                        }
                    } label: {
                        Text("Other Alert")
                    }
                }
                .padding()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .alert(isPresented: $isShowingAlert, alert: alert)
    }

    private func buildPasswordAlert() -> UIAlertController {
        let alert = UIAlertController(title: "Password", message: "Please enter your password.", preferredStyle: .alert)

        alert.addTextField { field in
            field.placeholder = "Password"
            field.returnKeyType = .next
            field.isSecureTextEntry = true
        }

        let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in
            print("Cancelled")
            isShowingAlert = false
        })
        let enterAction: UIAlertAction = UIAlertAction(title: "Enter", style: .default, handler: { _ in
            guard let fields = alert.textFields, fields.count == 1 else { return }
            let passwordField = fields[0]
            if let passwordFieldText = passwordField.text {
                // In reality you would check this password against a backend or something and either act or refuse to act, but I'm just going to print out the password entered
                print("Entered \(passwordFieldText)")
            }
            isShowingAlert = false
        })

        alert.addAction(cancelAction)
        alert.addAction(enterAction)

        return alert
    }

    private func buildOtherAlert() -> UIAlertController {
        let alert = UIAlertController(title: "Are You Sure?", message: nil, preferredStyle: .alert)

        let yesAction: UIAlertAction = UIAlertAction(title: "Yes", style: .default, handler: { _ in
            print("Yes")
            isShowingAlert = false
        })
        let noAction: UIAlertAction = UIAlertAction(title: "No", style: .default, handler: { _ in
            print("No")
            isShowingAlert = false
        })

        alert.addAction(yesAction)
        alert.addAction(noAction)

        return alert
    }
}

Note that because they are building UI these "buildAlert" functions should be run on the main thread by wrapping them in DispatchQueue.main.async.