LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-15

概要

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

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

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

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



1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1