【SwiftUI✖️アラート】アラートをUIKitのように使う実装
このコードの懸念点
iPadでMultipleWindow有効の場合に、正しくWindowSceneがとれず、アラート表示できない可能性がある。
詳しくは、下記のWindowSceneの取得の話の実装部分
MultipleWindowへの対処
追記 ...対応後の実装コード
- SwiftUIから、UIKitのようにアラート表示が可能。
- 【AlertWidnow】が、自作アラート。
Button("Show Alert(Cancel)") {
// アラートを表示
// 「キャンセルボタン」のみアラート
AlertWindow.show(
title: "タイトル",
message: "アラートのメッセージ",
cancelButtonTitle: "cancel",
onTap: {
print("!!!")
}
)
}
Button("Show Alert(Cancel-OK)") {
// 「キャンセル、OKボタン」のアラート
AlertWindow.showOKAndCancel(
title: "タイトル",
message: "アラートのメッセージ",
onTapOk: {
print("OK tapped!")
},
onTapCancel: {
print("Cancel Tapped tapped!")
}
)
}
背景
SwiftUIで、「異なるパターンのアラート」を実装しようとすると、現状、工夫が必要。
(ViewModifier作ったり、Alert用のクラス作ったり、モデル側で対応したり...)
SwiftUIだと、アラートが状態(bool) だが、
UIKitだと、「状態ではなく処理( present() )」 なので、View以外でも利用可能で、柔軟性がある。(※...2)
SwiftUIで、実装しつつ、UIKitのように、アラート表示がしたくて、実装。
(SwiftUI標準のアラートは、より進化してから移行したい)
対応した実装のポイント
- 「UIKitのUIAlertController」を「UIWindow」に乗せて表示(※...1)
実装
import Foundation
import UIKit
final class AlertWindow: UIAlertController {
// MARK: - static
// アラート表示(キャンセルのみ)
static func show(title: String,
message: String,
cancelButtonTitle: String = "閉じる",
onTap: (() -> Void)? = nil
) {
DispatchQueue.main.async {
let alertController = AlertWindow(title: title,
message: message,
preferredStyle: .alert)
let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel, handler: { _ in
onTap?()
})
alertController.addAction(cancelAction)
alertController.presentAsWindow(animated: true, completion: nil)
}
}
// アラート表示(キャンセル・OKボタン)
static func showOKAndCancel(title: String,
message: String,
okButtonTitle: String = "OK",
cancelButtonTitle: String = "Cancel",
onTapOk: (() -> Void)? = nil,
onTapCancel: (() -> Void)? = nil
) {
DispatchQueue.main.async {
let alertController = AlertWindow(title: title,
message: message,
preferredStyle: .alert)
let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel, handler: { _ in
onTapCancel?()
})
alertController.addAction(cancelAction)
let okAction = UIAlertAction(title: okButtonTitle, style: .default, handler: { _ in
onTapOk?()
})
alertController.addAction(okAction)
alertController.presentAsWindow(animated: true, completion: nil)
}
}
// MARK: - fileprivate
// 表示もとのwindow
fileprivate var baseWindow: UIWindow?
fileprivate func presentAsWindow(animated: Bool, completion: (() -> Void)?) {
baseWindow = UIWindow(frame: UIScreen.main.bounds)
if let currentWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
baseWindow?.windowScene = currentWindowScene
}
baseWindow?.rootViewController = UIViewController()
baseWindow?.backgroundColor = .clear
DispatchQueue.main.async {
self.baseWindow?.makeKeyAndVisible()
self.baseWindow?.rootViewController?.present(self, animated: animated, completion: completion)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Window自体をnil
baseWindow?.isHidden = true
baseWindow = nil
}
}
使い所
View以外でも使えるので、「ViewModel」でのアラート処理
表示するだけの「エラーアラート」など。
※...1 SwiftUIから「UIWindow」を使ってごにょごにょ、は、SwiftUIで不足する実装において、役立つパターンの1つと思います。(最前面系のUIの利用。ローディング、トーストUI、エラーアラート...etc)
※...2 余談ですが、同じ宣言的UIの「Flutter」は、アラート表示は「処理」です(UIKitと同じような仕組み)
MultipleWindowへの対処
Windowの撮り方が、iPadのMultipleWindowが無効状態の場合の実装です。
そのため、MutlpleWindow対応時には、アラート表示可能性があります。
MultipleWindow有効時でも、アラートを表示させたい場合は、
- 起動時にWindowを保持(AppDelegate,SceneDelegate等で)して、そのWindowを使うような改良
- アラート閉じる際の処理で、「window = nil」はせず、「window.hidden = true」
をすることで対応できるかと。
(SwiftUIで、最前面にLoadingUIを表示するような実装では、そのように対応)
-
WindowSceneの取得の参考
https://zenn.dev/matsuji/articles/0ee306ddfd10dc
Discussion