概要
(普段 React をメインで仕事している者ですが、只今SwiftUI勉強中です。
勘違い等あるかもしれませんが、何か誤り等あればコメント欄でご指摘いただければ幸いです。)
個人開発でアプリを作っていて、TextField 付きの Alert を表示したい場面があったのですが、色々と詰まった箇所があったので、備忘録としてやり方を書きます。
なお、作成にあたって、以下の記事を参考にさせていただきました。
- UIKitとSwiftUIでテキストフィールド付きアラートを表示する | DevelopersIO
- ios - SwiftUI: UIAlertController's textField does not responding in UIAlertAction - Stack Overflow
1. AlertTextField を作成
- UIAlertController をラップした AlertTextField を作成します。
- バインドした textFieldText をリアルタイムで書き換える実装になっています。
AlertTextField.swift
import SwiftUI
import UIKit
struct AlertTextFieldActionButton {
let title: String
let style: UIAlertAction.Style
let aciton: Optional<() -> Void>
init(title: String, style: UIAlertAction.Style, action: Optional<() -> Void> = nil) {
self.title = title
self.style = style
self.aciton = action
}
}
struct AlertTextField: UIViewControllerRepresentable {
@Binding var textFieldText: String
@Binding var isPresented: Bool
let title: String?
let message: String?
let placeholderText: String
let primaryButton: AlertTextFieldActionButton?
let secondaryButton: AlertTextFieldActionButton?
func makeUIViewController(context: UIViewControllerRepresentableContext<AlertTextField>) -> UIViewController {
return UIViewController() // holder controller - required to present alert
}
// SwiftUIから新しい情報を受け、viewControllerが更新されるタイミングで呼ばれる
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<AlertTextField>) {
guard context.coordinator.alert == nil else {
return
}
guard isPresented else {
return
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
context.coordinator.alert = alert
// TextFieldの追加
alert.addTextField { textField in
textField.placeholder = placeholderText
textField.returnKeyType = .done
textField.text = self.textFieldText // << initial value if any
textField.delegate = context.coordinator // << use coordinator as delegate
}
// 左側のボタン (デフォルトのラベル: キャンセル)
let primaryAction = UIAlertAction(title: primaryButton?.title ?? "キャンセル", style: primaryButton?.style ?? .cancel) { _ in
if let primaryActionClosure = primaryButton?.aciton {
primaryActionClosure()
}
}
// 右側のボタン (デフォルトのラベル: 決定)
let secondaryAction = UIAlertAction(title: secondaryButton?.title ?? "決定", style: secondaryButton?.style ?? .default) { _ in
if let secondaryActionClosure = secondaryButton?.aciton {
secondaryActionClosure()
}
}
alert.addAction(primaryAction)
alert.addAction(secondaryAction)
DispatchQueue.main.async {
uiViewController.present(alert, animated: true) {
self.isPresented = false
context.coordinator.alert = nil
}
}
}
func makeCoordinator() -> AlertTextField.Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
var alert: UIAlertController?
var control: AlertTextField
init(_ control: AlertTextField) {
self.control = control
}
// TextField への入力のたびに発火
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as NSString? {
self.control.textFieldText = text.replacingCharacters(in: range, with: string)
} else {
self.control.textFieldText = ""
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.resignFirstResponder()
}
}
}
2. AlertTextField を含んだ ViewModifier を作成
- 1 で作成した AlertTextField をラップした AlertTextFieldModifier を作成します。
- 参考元記事では ZStack が使用されていましたが、実際に AlertTextFieldViewModifier を使用する際、使用する場所によってレイアウト崩壊が発生したりするので、
.background()
モディファイアを使って実装しています。
AlertTextFieldModifier.swift
import SwiftUI
struct AlertTextFieldModifier: ViewModifier {
@Binding var textFieldText: String
@Binding var isPresented: Bool
let title: String?
let message: String?
let placeholderText: String
let primaryButton: AlertTextFieldActionButton?
let secondaryButton: AlertTextFieldActionButton?
func body(content: Content) -> some View {
content
.background(
AlertTextField(
textFieldText: $textFieldText,
isPresented: $isPresented,
title: title,
message: message,
placeholderText: placeholderText,
primaryButton: primaryButton,
secondaryButton: secondaryButton
)
)
}
}
3. View から直接 AlertTextFieldModifier を生やせるよう、extension 作成
- 2 で作成した AlertTextFieldModifier を、各 View で
.alertTextField()
モディファイアとして使用できるよう、extension を作成します。
View+.swift
import Foundation
import SwiftUI
extension View {
func alertTextField(
_ text: Binding<String>,
isPresented: Binding<Bool>,
title: String?,
message: String? = nil,
placeholderText: String,
primaryButton: AlertTextFieldActionButton? = nil,
secondaryButton: AlertTextFieldActionButton? = nil
) -> some View {
self.modifier(
AlertTextFieldModifier(
textFieldText: text,
isPresented: isPresented,
title: title,
message: message,
placeholderText: placeholderText,
primaryButton: primaryButton,
secondaryButton: secondaryButton
)
)
}
}
使用例
- ファイルの作成は以上です。
- この項では、実際に使用例を書きます。
import SwiftUI
struct ExampleView: View {
@State var shouldShowAlertTextField: Bool = false
@State var textFieldValue: String = ""
var body: some View {
VStack {
Button(action: {
shouldShowAlertTextField = true
}) {
Text("Show alert")
}
.alertTextField(
$textFieldValue,
isPresented: $shouldShowAlertTextField,
title: "タイトルを入力",
message: "タイトルを入力してください。",
placeholderText: "タイトル",
)
Spacer()
Text(textFieldValue)
}
}
}