(iOS 14 以前向け) SwiftUIで、TextField 付き Alert を作る

# 概要 

(普段 React をメインで仕事している者ですが、只今SwiftUI勉強中です。 
勘違い等あるかもしれませんが、何か誤り等あればコメント欄でご指摘いただければ幸いです。)



個人開発でアプリを作っていて、TextField 付きの Alert を表示したい場面があったのですが、色々と詰まった箇所があったので、備忘録としてやり方を書きます。 

なお、作成にあたって、以下の記事を参考にさせていただきました。 

- [UIKitとSwiftUIでテキストフィールド付きアラートを表示する | DevelopersIO](https://web.archive.org/web/20220815184927/https://dev.classmethod.jp/articles/ios-alert-with-text-field/)
- [ios - SwiftUI: UIAlertController's textField does not responding in UIAlertAction - Stack Overflow](https://stackoverflow.com/questions/61266301/swiftui-uialertcontrollers-textfield-does-not-responding-in-uialertaction)


 
# 1. AlertTextField を作成 

- UIAlertController をラップした AlertTextField を作成します。
- バインドした textFieldText をリアルタイムで書き換える実装になっています。


```swift: 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()` モディファイアを使って実装しています。

```swift: 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 を作成します。

```swift: 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
            )
        )
    }
}

```

# 使用例 

- ファイルの作成は以上です。
- この項では、実際に使用例を書きます。

```swift 
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)
    }
  }
}



```