Skip to content

Instantly share code, notes, and snippets.

@ObuchiYuki
Created February 25, 2025 14:06
Show Gist options
  • Save ObuchiYuki/d0feeab2aa268824cef921f0f4af2dcf to your computer and use it in GitHub Desktop.
Save ObuchiYuki/d0feeab2aa268824cef921f0f4af2dcf to your computer and use it in GitHub Desktop.
SwiftUI Alert with non declarative style.
//
// NonDeclarativeAlert.swift
// MFileViewer
//
// Created by yuki on 2025/02/25.
//
import SwiftUI
public struct AlertAction<Value> {
let content: AnyView
let role: ButtonRole?
var value: Value
public init<Content: View>(
value: Value = (),
role: ButtonRole? = nil,
@ViewBuilder content: () -> Content
) {
self.value = value
self.role = role
self.content = AnyView(content())
}
public init(
_ titleKey: LocalizedStringKey,
value: Value = (),
role: ButtonRole? = nil
) {
self.value = value
self.role = role
self.content = AnyView(Text(titleKey))
}
}
// MARK: - Alert -
extension View {
public func showAlert<ActionValue>(
_ titleKey: LocalizedStringKey,
message: LocalizedStringKey? = nil,
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> ActionValue {
await self.showAlert(
titleKey,
message: {
if let message = message {
Text(message)
} else {
EmptyView()
}
},
actions: actions
)
}
public func showAlert<Message: View, ActionValue>(
_ titleKey: LocalizedStringKey,
@ViewBuilder message: @escaping () -> Message,
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> ActionValue {
guard let topMostViewController = UIApplication.shared.topMostViewController() else {
fatalError("Failed to get top most view controller")
}
return await withCheckedContinuation { continuation in
var hostingVC: UIHostingController<ShowAlertView<Message, ActionValue>>!
hostingVC = UIHostingController(
rootView: ShowAlertView(
titleKey: titleKey,
message: message,
actions: actions(),
onDismiss: { value in
continuation.resume(returning: value)
hostingVC.removeFromParent()
hostingVC.view.removeFromSuperview()
}
)
)
topMostViewController.addChild(hostingVC)
topMostViewController.view.addSubview(hostingVC.view)
NSLayoutConstraint.activate([
hostingVC.view.topAnchor.constraint(equalTo: topMostViewController.view.topAnchor),
hostingVC.view.leadingAnchor.constraint(equalTo: topMostViewController.view.leadingAnchor),
hostingVC.view.heightAnchor.constraint(equalToConstant: 0),
hostingVC.view.widthAnchor.constraint(equalToConstant: 0)
])
}
}
}
private struct ShowAlertView<Message: View, ActionValue>: View {
let titleKey: LocalizedStringKey
let message: () -> Message
let actions: [AlertAction<ActionValue>]
let onDismiss: (ActionValue) -> Void
@State private var isPresented = true
var body: some View {
EmptyView()
.alert(
self.titleKey,
isPresented: self.$isPresented,
actions: {
ForEach(self.actions.indexed(), id: \.index) { (_, action) in
Button(role: action.role, action: {
self.onDismiss(action.value)
}) {
action.content
}
}
},
message: self.message
)
}
}
// MARK: - Alert with TextField -
extension View {
public func showAlertWithTextField<ActionValue>(
_ titleKey: LocalizedStringKey,
message: LocalizedStringKey? = nil,
prompt: LocalizedStringKey? = nil,
initialValue: String = "",
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> String? {
await self.showAlertWithTextField(
titleKey,
message: {
if let message = message {
Text(message)
} else {
EmptyView()
}
},
prompt: prompt,
initialValue: initialValue,
actions: actions
)
.text
}
public func showAlertWithTextField<Message: View, ActionValue>(
_ titleKey: LocalizedStringKey,
@ViewBuilder message: @escaping () -> Message,
prompt: LocalizedStringKey? = nil,
initialValue: String = "",
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> String? {
await self.showAlertWithTextField(
titleKey,
message: message,
prompt: prompt,
initialValue: initialValue,
actions: actions
)
.text
}
public func showAlertWithTextField<ActionValue>(
_ titleKey: LocalizedStringKey,
message: LocalizedStringKey? = nil,
prompt: LocalizedStringKey? = nil,
initialValue: String = "",
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> (value: ActionValue, text: String?) {
await self.showAlertWithTextField(
titleKey,
message: {
if let message = message {
Text(message)
} else {
EmptyView()
}
},
prompt: prompt,
initialValue: initialValue,
actions: actions
)
}
public func showAlertWithTextField<Message: View, ActionValue>(
_ titleKey: LocalizedStringKey,
@ViewBuilder message: @escaping () -> Message,
prompt: LocalizedStringKey? = nil,
initialValue: String = "",
@ArrayBuilder<AlertAction<ActionValue>> actions: () -> [AlertAction<ActionValue>]
) async -> (value: ActionValue, text: String?) {
guard let topMostViewController = UIApplication.shared.topMostViewController() else {
fatalError("Failed to get top most view controller")
}
return await withCheckedContinuation { continuation in
var hostingVC: UIHostingController<ShowAlertWithTextFieldView<Message, ActionValue>>!
hostingVC = UIHostingController(
rootView: ShowAlertWithTextFieldView(
titleKey: titleKey,
message: message,
prompt: prompt,
initialValue: initialValue,
actions: actions(),
onDismiss: { actionValue, text in
continuation.resume(returning: (actionValue, text))
hostingVC.removeFromParent()
hostingVC.view.removeFromSuperview()
}
)
)
topMostViewController.addChild(hostingVC)
topMostViewController.view.addSubview(hostingVC.view)
NSLayoutConstraint.activate([
hostingVC.view.topAnchor.constraint(equalTo: topMostViewController.view.topAnchor),
hostingVC.view.leadingAnchor.constraint(equalTo: topMostViewController.view.leadingAnchor),
hostingVC.view.heightAnchor.constraint(equalToConstant: 0),
hostingVC.view.widthAnchor.constraint(equalToConstant: 0)
])
}
}
}
private struct ShowAlertWithTextFieldView<Message: View, ActionValue>: View {
let titleKey: LocalizedStringKey
let message: () -> Message
let prompt: LocalizedStringKey?
let actions: [AlertAction<ActionValue>]
let onDismiss: (ActionValue, String?) -> Void
@State private var isPresented = true
@State private var text: String
init(
titleKey: LocalizedStringKey,
message: @escaping () -> Message,
prompt: LocalizedStringKey?,
initialValue: String,
actions: [AlertAction<ActionValue>],
onDismiss: @escaping (ActionValue, String?) -> Void
) {
self.titleKey = titleKey
self.message = message
self.prompt = prompt
self.actions = actions
self.onDismiss = onDismiss
self._text = State(initialValue: initialValue)
}
var body: some View {
EmptyView()
.alert(
self.titleKey,
isPresented: self.$isPresented,
actions: {
TextField("", text: self.$text, prompt: self.prompt.map { Text($0) })
ForEach(self.actions.indexed(), id: \.index) { (_, action) in
Button(role: action.role, action: {
if action.role == .cancel {
self.onDismiss(action.value, nil)
} else {
self.onDismiss(action.value, self.text)
}
}) {
action.content
}
}
},
message: self.message
)
}
}
extension UIApplication {
fileprivate func topMostViewController(
base: UIViewController? = nil
) -> UIViewController? {
let base = base ?? UIApplication.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }?
.rootViewController
if let nav = base as? UINavigationController {
return topMostViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController,
let selected = tab.selectedViewController {
return topMostViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topMostViewController(base: presented)
}
return base
}
}
@ObuchiYuki
Copy link
Author

Usage

SwiftUI alert with non declarative style. (Swift Concurrency style)

// reguler
let value: Int = await self.showAlert("Hello World") {
    AlertAction(value: 0) { Text("OK") }
    AlertAction(value: 1, role: .cancel) { Text("Cancel") }

    // or

    AlertAction("OK", value: 0)
    AlertAction("Cancel", value: 1, role: .cancel)
}

// with TextField
let value: String? = await self.showAlertWithTextField(
    "Account Name",
    message: "Enter your account name",
    prompt: "Account Name"
) {
    AlertAction("OK")
    AlertAction("Cancel", role: .cancel)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment