Created
February 25, 2025 14:06
-
-
Save ObuchiYuki/d0feeab2aa268824cef921f0f4af2dcf to your computer and use it in GitHub Desktop.
SwiftUI Alert with non declarative style.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
SwiftUI alert with non declarative style. (Swift Concurrency style)