Blog

SwiftUI: Showing an Alert with a Text Field

In Swift Talk Episode 198, we made a wrapper around UIKit alerts.

SwiftUI provides a built-in API just for this: the alert method on View allows us to present an alert to the user, and under the hood it almost certainly uses a UIAlertController.

For our purposes, we wanted an alert controller with a text field. UIAlertController supports this, but so far, SwiftUI’s built-in Alert struct does not. To route around this limitation, we’ve found a hack that seems to work — for us at least! (we haven’t tested this in a project other than our own).

We’ll start by defining an API similar to SwiftUI’s builtin alert API. It will have the usual properties, and an action parameter that gets called whenever the user presses either the OK or Cancel button.

public struct TextAlert {
    public var title: String
    public var placeholder: String = ""
    public var accept: String = "OK"
    public var cancel: String = "Cancel"
    public var action: (String?) -> ()
}

extension View {
    public func alert(isPresented: Binding<Bool>, _ alert: TextAlert) -> some View {
        // ...
    }
}

Now for the hacky part. When we call alert on a view, we wrap that view inside a custom UIViewControllerRepresentable. Inside, we create a UIHostingController for the view, so that we can call .present on it.

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

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

    // ...
}

To store the current alert controller we need to create a coordinator:

struct AlertWrapper<Content: View>: UIViewControllerRepresentable {
    // ...

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

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

Finally, we need to show and hide the alert whenever our binding changes. SwiftUI will automatically observe the binding and call updateUIViewController(_:context:) each time that happens. Inside, there are two code paths: when the binding’s value is true but we’re not yet presenting the alert controller, we need to present it; when the binding’s value is false but we are presenting the alert controller, we need to dismiss it.

func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: UIViewControllerRepresentableContext<AlertWrapper>) {
    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)
    }
}

Note that we’re setting the hosting controller’s root view in the first line of the updateViewController method. This method will be called if our isPresented binding for the alert changes, but also when something changes that affects the content of the alert wrapper. If we omit this first line, SwiftUI will no longer update the view inside the hosting controller.

Now we can use our text alert:

struct ContentView: View {
    @State var showsAlert = false
    var body: some View {
        VStack {
            Text("Hello, World!")
            Button("alert") {
                self.showsAlert = true               
            }
        }
        .alert(isPresented: $showsAlert, TextAlert(title: "Title", action: {
            print("Callback \($0 ?? "<cancel>")")
        }))
    }
}

We hope that SwiftUI will catch up with UIAlertController, making these kinds of hacks unnecessary. In the meantime, if anyone knows of a simpler way to do this, do let us know and we’ll update this post!

Here’s the full code.

Our SwiftUI Collection has 33 episodes and growing, with 8 public episodes. In our latest series, we port App Architecture‘s sample app from MVC to SwiftUI — the first episode is free to watch.

Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts