如何在 SwiftUI 中添加一个 TextField 以提醒?

How to add a TextField to Alert in SwiftUI?

有人知道如何在 SwiftUI 中创建包含 TextField 的警报吗?

我发现 SwiftUI 中的模态和警报缺少一些功能。例如,似乎没有办法以 FormSheet 样式呈现模态。

当我需要呈现一个复杂的警报(例如带有文本字段的警报)时,我会创建一个包含警报所有内容的纯 SwiftUI 视图,然后使用 UIHostController 将其呈现为 FormSheet .

如果你周围没有 UIViewController 来调用 present(),你总是可以使用根视图控制器。

通过这种方法,您可以获得一些不错的功能,例如进出的标准警报动画。您也可以向下拖动警报以将其关闭。

当键盘出现时,警报视图也会向上移动。

这在 iPad 上效果很好。在 iPhone 上,FormSheet 是全屏的,因此您可能需要调整代码才能找到解决方案。我想这会给你一个很好的起点。

是这样的:

struct ContentView : View {
    @State private var showAlert = false

    var body: some View {
        VStack {
            Button(action: {
                let alertHC = UIHostingController(rootView: MyAlert())

                alertHC.preferredContentSize = CGSize(width: 300, height: 200)
                alertHC.modalPresentationStyle = UIModalPresentationStyle.formSheet

                UIApplication.shared.windows[0].rootViewController?.present(alertHC, animated: true)

            }) {
                Text("Show Alert")
            }
        }
    }
}

struct MyAlert: View {
    @State private var text: String = ""

    var body: some View {

        VStack {
            Text("Enter Input").font(.headline).padding()

            TextField($text, placeholder: Text("Type text here")).textFieldStyle(.roundedBorder).padding()
            Divider()
            HStack {
                Spacer()
                Button(action: {
                    UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
                }) {

                    Text("Done")
                }
                Spacer()

                Divider()

                Spacer()
                Button(action: {
                    UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
                }) {
                    Text("Cancel")
                }
                Spacer()
            }.padding(0)


            }.background(Color(white: 0.9))
    }
}

如果您发现自己经常使用它,可以将按钮行封装在单独的视图中以便于重复使用。

Alert 目前非常有限,但您可以在纯 SwiftUI 中推出自己的解决方案。

下面是带有文本字段的自定义提醒的简单实现。

struct TextFieldAlert<Presenting>: View where Presenting: View {

    @Binding var isShowing: Bool
    @Binding var text: String
    let presenting: Presenting
    let title: String

    var body: some View {
        GeometryReader { (deviceSize: GeometryProxy) in
            ZStack {
                self.presenting
                    .disabled(isShowing)
                VStack {
                    Text(self.title)
                    TextField(self.$text)
                    Divider()
                    HStack {
                        Button(action: {
                            withAnimation {
                                self.isShowing.toggle()
                            }
                        }) {
                            Text("Dismiss")
                        }
                    }
                }
                .padding()
                .background(Color.white)
                .frame(
                    width: deviceSize.size.width*0.7,
                    height: deviceSize.size.height*0.7
                )
                .shadow(radius: 1)
                .opacity(self.isShowing ? 1 : 0)
            }
        }
    }

}

还有一个 View 扩展来使用它:

extension View {

    func textFieldAlert(isShowing: Binding<Bool>,
                        text: Binding<String>,
                        title: String) -> some View {
        TextFieldAlert(isShowing: isShowing,
                       text: text,
                       presenting: self,
                       title: title)
    }

}

演示:

struct ContentView : View {

    @State private var isShowingAlert = false
    @State private var alertInput = ""

    var body: some View {
        NavigationView {
            VStack {
                Button(action: {
                    withAnimation {
                        self.isShowingAlert.toggle()
                    }
                }) {
                    Text("Show alert")
                }
            }
            .navigationBarTitle(Text("A List"), displayMode: .large)
        }
        .textFieldAlert(isShowing: $isShowingAlert, text: $alertInput, title: "Alert!")
    }
}

您可以直接使用UIAlertController。无需滚动您自己的警报对话框 UI:

private func alert() {
    let alert = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
    alert.addTextField() { textField in
        textField.placeholder = "Enter some text"
    }
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in })
    showAlert(alert: alert)
}

func showAlert(alert: UIAlertController) {
    if let controller = topMostViewController() {
        controller.present(alert, animated: true)
    }
}

private func keyWindow() -> UIWindow? {
    return UIApplication.shared.connectedScenes
    .filter {[=10=].activationState == .foregroundActive}
    .compactMap {[=10=] as? UIWindowScene}
    .first?.windows.filter {[=10=].isKeyWindow}.first
}

private func topMostViewController() -> UIViewController? {
    guard let rootController = keyWindow()?.rootViewController else {
        return nil
    }
    return topMostViewController(for: rootController)
}

private func topMostViewController(for controller: UIViewController) -> UIViewController {
    if let presentedController = controller.presentedViewController {
        return topMostViewController(for: presentedController)
    } else if let navigationController = controller as? UINavigationController {
        guard let topController = navigationController.topViewController else {
            return navigationController
        }
        return topMostViewController(for: topController)
    } else if let tabController = controller as? UITabBarController {
        guard let topController = tabController.selectedViewController else {
            return tabController
        }
        return topMostViewController(for: topController)
    }
    return controller
}

此代码的大部分只是样板文件,用于查找应显示警报的 ViewController。 调用 alert() 例如来自按钮的 action

struct TestView: View {
    var body: some View {
        Button(action: { alert() }) { Text("click me") }
     }
}

请注意,在 beta 5 及更高版本中似乎存在一个错误,有时会导致模拟器在显示文本字段后冻结:Xcode 11 beta 5: UI freezes when adding textFields into UIAlertController

虽然不完全相同,但如果您要查找的只是带有编辑框的原生、类似模态的视图,则可以使用 popover. It works out of the box (minus a ) 而无需遍历视图层次结构。

func dialog(){

       let alertController = UIAlertController(title: "Contry", message: "Write contrt code here", preferredStyle: .alert)

        alertController.addTextField { (textField : UITextField!) -> Void in
            textField.placeholder = "Country code"
        }

        let saveAction = UIAlertAction(title: "Save", style: .default, handler: { alert -> Void in

            let secondTextField = alertController.textFields![0] as UITextField
            print("county code : ",secondTextField)

        })

        let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil )


        alertController.addAction(saveAction)
        alertController.addAction(cancelAction)

        UIApplication.shared.windows.first?.rootViewController?.present(alertController, animated: true, completion: nil)


    }

用法

Button(action: { self.dialog()})
 {
Text("Button")
.foregroundColor(.white).fontWeight(.bold)
 }

由于 SwiftUI 提供的 Alert 视图无法完成您确实需要使用 UIKit 中的 UIAlertController 的工作。理想情况下,我们想要一个 TextFieldAlert 视图,我们可以用与 SwiftUI:

提供的 Alert 相同的方式呈现
struct MyView: View {

  @Binding var alertIsPresented: Bool
  @Binding var text: String? // this is updated as the user types in the text field

  var body: some View {
    Text("My Demo View")
      .textFieldAlert(isPresented: $alertIsPresented) { () -> TextFieldAlert in
        TextFieldAlert(title: "Alert Title", message: "Alert Message", text: self.$text)
    }
  }
}

我们可以通过编写几个 类 并在 View 扩展名中添加一个修饰符来实现。

1) TextFieldAlertViewController 创建一个 UIAlertController (当然有一个文本字段)并在它出现在屏幕上时显示它。用户对文本字段的更改会反映到初始化期间传递的 Binding<String> 中。

class TextFieldAlertViewController: UIViewController {

  /// Presents a UIAlertController (alert style) with a UITextField and a `Done` button
  /// - Parameters:
  ///   - title: to be used as title of the UIAlertController
  ///   - message: to be used as optional message of the UIAlertController
  ///   - text: binding for the text typed into the UITextField
  ///   - isPresented: binding to be set to false when the alert is dismissed (`Done` button tapped)
  init(title: String, message: String?, text: Binding<String?>, isPresented: Binding<Bool>?) {
    self.alertTitle = title
    self.message = message
    self._text = text
    self.isPresented = isPresented
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: - Dependencies
  private let alertTitle: String
  private let message: String?
  @Binding private var text: String?
  private var isPresented: Binding<Bool>?

  // MARK: - Private Properties
  private var subscription: AnyCancellable?

  // MARK: - Lifecycle
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    presentAlertController()
  }

  private func presentAlertController() {
    guard subscription == nil else { return } // present only once

    let vc = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)

    // add a textField and create a subscription to update the `text` binding
    vc.addTextField { [weak self] textField in
      guard let self = self else { return }
      self.subscription = NotificationCenter.default
        .publisher(for: UITextField.textDidChangeNotification, object: textField)
        .map { ([=11=].object as? UITextField)?.text }
        .assign(to: \.text, on: self)
    }

    // create a `Done` action that updates the `isPresented` binding when tapped
    // this is just for Demo only but we should really inject
    // an array of buttons (with their title, style and tap handler)
    let action = UIAlertAction(title: "Done", style: .default) { [weak self] _ in
      self?.isPresented?.wrappedValue = false
    }
    vc.addAction(action)
    present(vc, animated: true, completion: nil)
  }
}

2) TextFieldAlert 使用 UIViewControllerRepresentable 协议包装 TextFieldAlertViewController 以便它可以在 SwiftUI 中使用。

struct TextFieldAlert {

  // MARK: Properties
  let title: String
  let message: String?
  @Binding var text: String?
  var isPresented: Binding<Bool>? = nil

  // MARK: Modifiers
  func dismissable(_ isPresented: Binding<Bool>) -> TextFieldAlert {
    TextFieldAlert(title: title, message: message, text: $text, isPresented: isPresented)
  }
}

extension TextFieldAlert: UIViewControllerRepresentable {

  typealias UIViewControllerType = TextFieldAlertViewController

  func makeUIViewController(context: UIViewControllerRepresentableContext<TextFieldAlert>) -> UIViewControllerType {
    TextFieldAlertViewController(title: title, message: message, text: $text, isPresented: isPresented)
  }

  func updateUIViewController(_ uiViewController: UIViewControllerType,
                              context: UIViewControllerRepresentableContext<TextFieldAlert>) {
    // no update needed
  }
}

3) TextFieldWrapper 是一个简单的 ZStack,背面有一个 TextFieldAlert(仅当 isPresented 为真时),正面有一个呈现视图。当前视图是唯一可见的视图。

struct TextFieldWrapper<PresentingView: View>: View {

  @Binding var isPresented: Bool
  let presentingView: PresentingView
  let content: () -> TextFieldAlert

  var body: some View {
    ZStack {
      if (isPresented) { content().dismissable($isPresented) }
      presentingView
    }
  }  
}

4) textFieldAlert 修饰符允许我们在 TextFieldWrapper 中平滑地包装任何 SwiftUI 视图并获得所需的行为。

extension View {
  func textFieldAlert(isPresented: Binding<Bool>,
                      content: @escaping () -> TextFieldAlert) -> some View {
    TextFieldWrapper(isPresented: isPresented,
                     presentingView: self,
                     content: content)
  }
}

如前所述,Alert 提供的功能不多,因此在任何 non-standard 情况下在 SwiftUI 中使用时几乎没有用。

我最终得到了一个有点广泛的解决方案 - View 可能表现为具有高定制级别的警报。

  1. 为弹出窗口创建 ViewModel

    struct UniAlertViewModel {
    
     let backgroundColor: Color = Color.gray.opacity(0.4)
     let contentBackgroundColor: Color = Color.white.opacity(0.8)
     let contentPadding: CGFloat = 16
     let contentCornerRadius: CGFloat = 12
    }
    
  2. 我们还需要配置按钮,为此我们再添加一种类型:

    struct UniAlertButton {
    
     enum Variant {
    
         case destructive
         case regular
     }
    
     let content: AnyView
     let action: () -> Void
     let type: Variant
    
     var isDestructive: Bool {
         type == .destructive
     }
    
     static func destructive<Content: View>(
         @ViewBuilder content: @escaping () -> Content
     ) -> UniAlertButton {
         UniAlertButton(
             content: content,
             action: { /* close */ },
             type: .destructive)
     }
    
     static func regular<Content: View>(
         @ViewBuilder content: @escaping () -> Content,
         action: @escaping () -> Void
     ) -> UniAlertButton {
         UniAlertButton(
             content: content,
             action: action,
             type: .regular)
     }
    
     private init<Content: View>(
         @ViewBuilder content: @escaping () -> Content,
         action: @escaping () -> Void,
         type: Variant
     ) {
         self.content = AnyView(content())
         self.type = type
         self.action = action
     }
    }
    
  3. 添加可以成为我们可定制弹出窗口的视图:

    struct UniAlert<Presenter, Content>: View where Presenter: View, Content: View {
    
     @Binding private (set) var isShowing: Bool
    
     let displayContent: Content
     let buttons: [UniAlertButton]
     let presentationView: Presenter
     let viewModel: UniAlertViewModel
    
     private var requireHorizontalPositioning: Bool {
         let maxButtonPositionedHorizontally = 2
         return buttons.count > maxButtonPositionedHorizontally
     }
    
     var body: some View {
         GeometryReader { geometry in
             ZStack {
                 backgroundColor()
    
                 VStack {
                     Spacer()
    
                     ZStack {
                         presentationView.disabled(isShowing)
                         let expectedWidth = geometry.size.width * 0.7
    
                         VStack {
                             displayContent
                             buttonsPad(expectedWidth)
                         }
                         .padding(viewModel.contentPadding)
                         .background(viewModel.contentBackgroundColor)
                         .cornerRadius(viewModel.contentCornerRadius)
                         .shadow(radius: 1)
                         .opacity(self.isShowing ? 1 : 0)
                         .frame(
                             minWidth: expectedWidth,
                             maxWidth: expectedWidth
                         )
                     }
    
                     Spacer()
                 }
             }
         }
     }
    
     private func backgroundColor() -> some View {
         viewModel.backgroundColor
             .edgesIgnoringSafeArea(.all)
             .opacity(self.isShowing ? 1 : 0)
     }
    
     private func buttonsPad(_ expectedWidth: CGFloat) -> some View {
         VStack {
             if requireHorizontalPositioning {
                 verticalButtonPad()
             } else {
                 Divider().padding([.leading, .trailing], -viewModel.contentPadding)
                 horizontalButtonsPadFor(expectedWidth)
             }
         }
     }
    
     private func verticalButtonPad() -> some View {
         VStack {
             ForEach(0..<buttons.count) {
                 Divider().padding([.leading, .trailing], -viewModel.contentPadding)
                 let current = buttons[[=12=]]
    
                 Button(action: {
                     if !current.isDestructive {
                         current.action()
                     }
    
                     withAnimation {
                         self.isShowing.toggle()
                     }
                 }, label: {
                     current.content.frame(height: 35)
                 })
             }
         }
     }
    
     private func horizontalButtonsPadFor(_ expectedWidth: CGFloat) -> some View {
         HStack {
             let sidesOffset = viewModel.contentPadding * 2
             let maxHorizontalWidth = requireHorizontalPositioning ?
                 expectedWidth - sidesOffset :
                 expectedWidth / 2 - sidesOffset
    
             Spacer()
    
             if !requireHorizontalPositioning {
                 ForEach(0..<buttons.count) {
                     if [=12=] != 0 {
                         Divider().frame(height: 44)
                     }
                     let current = buttons[[=12=]]
    
                     Button(action: {
                         if !current.isDestructive {
                             current.action()
                         }
    
                         withAnimation {
                             self.isShowing.toggle()
                         }
                     }, label: {
                         current.content
                     })
                     .frame(maxWidth: maxHorizontalWidth, minHeight: 44)
                 }
             }
             Spacer()
         }
     }
    }
    
  4. 为了简化使用,让我们为 View 添加扩展名:

    extension View {
    
     func assemblyAlert<Content>(
         isShowing: Binding<Bool>,
         viewModel: UniAlertViewModel,
         @ViewBuilder content: @escaping () -> Content,
         actions: [UniAlertButton]
     ) -> some View where Content: View {
         UniAlert(
             isShowing: isShowing,
             displayContent: content(),
             buttons: actions,
             presentationView: self,
             viewModel: viewModel)
     }
    }
    

和用法:

struct ContentView: View {
    
    @State private var isShowingAlert: Bool = false
    @State private var text: String = ""
    
    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    isShowingAlert.toggle()
                }
            }, label: {
                Text("Show alert")
            })
        }
        .assemblyAlert(isShowing: $isShowingAlert,
                       viewModel: UniAlertViewModel(),
                       content: {
                        Text("title")
                        Image(systemName: "phone")
                            .scaleEffect(3)
                            .frame(width: 100, height: 100)
                        TextField("enter text here", text: $text)
                        Text("description")
                       }, actions: buttons)
        }
   }
 }

演示:

这是一个基于 SwiftUI Sheet class 的示例,它显示一个带有提示的对话框、一个文本字段和 classic OK 和 Dismiss 按钮

首先让我们制作 Dialog class,当用户想要编辑一个值时它会弹出:

import SwiftUI

struct Dialog: View {
    @Environment(\.presentationMode) var presentationMode

    /// Edited value, passed from outside
    @Binding var value: String?

    /// Prompt message
    var prompt: String = ""
    
    /// The value currently edited
    @State var fieldValue: String
    
    /// Init the Dialog view
    /// Passed @binding value is duplicated to @state value while editing
    init(prompt: String, value: Binding<String?>) {
        _value = value
        self.prompt = prompt
        _fieldValue = State<String>(initialValue: value.wrappedValue ?? "")
    }

    var body: some View {
        VStack {
            Text(prompt).padding()
            TextField("", text: $fieldValue)
            .frame(width: 200, alignment: .center)
            HStack {
            Button("OK") {
                self.value = fieldValue
                self.presentationMode.wrappedValue.dismiss()
            }
            Button("Dismiss") {
                self.presentationMode.wrappedValue.dismiss()
            }
            }.padding()
        }
        .padding()
    }
}

#if DEBUG
struct Dialog_Previews: PreviewProvider {

    static var previews: some View {
        var name = "John Doe"
        Dialog(prompt: "Name", value: Binding<String?>.init(get: { name }, set: {name = [=10=] ?? ""}))
    }
}
#endif

现在我们在调用者视图中这样使用它:

import SwiftUI

struct ContentView: View {
    /// Is the input dialog displayed
    @State var dialogDisplayed = false
    
    /// The name to edit
    @State var name: String? = nil
    
    var body: some View {
        VStack {
            Text(name ?? "Unnamed").frame(width: 200).padding()
            Button(name == nil ? "Set Name" : "Change Name") {
                dialogDisplayed = true
            }
            .sheet(isPresented: $dialogDisplayed) {
                Dialog(prompt: name == nil ? "Enter a name" : "Enter a new name", value: $name)
            }
            .onChange(of: name, perform: { value in
                print("Name Changed : \(value)")
            }
            .padding()
        }
        .padding()
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

第一步:将根视图设为 ZStack

第 2 步:为 show/hide

添加变量
 @State var showAlert = false

第 3 步: 在根视图 (ZStack) 中添加此自定义布局

  if $showAlert.wrappedValue {
            ZStack() {
                Color.grayBackground
                VStack {
                    //your custom layout text fields buttons 
                   
                }.padding()
            }
            .frame(width: 300, height: 180,alignment: .center)
            .cornerRadius(20).shadow(radius: 20)
        }

基于坦佐龙的想法

import Foundation
import Combine
import SwiftUI

class TextFieldAlertViewController: UIViewController {
    
    /// Presents a UIAlertController (alert style) with a UITextField and a `Done` button
    /// - Parameters:
    ///   - title: to be used as title of the UIAlertController
    ///   - message: to be used as optional message of the UIAlertController
    ///   - text: binding for the text typed into the UITextField
    ///   - isPresented: binding to be set to false when the alert is dismissed (`Done` button tapped)
    init(isPresented: Binding<Bool>, alert: TextFieldAlert) {
        self._isPresented = isPresented
        self.alert = alert
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    @Binding
    private var isPresented: Bool
    private var alert: TextFieldAlert
    
    // MARK: - Private Properties
    private var subscription: AnyCancellable?
    
    // MARK: - Lifecycle
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        presentAlertController()
    }
    
    private func presentAlertController() {
        guard subscription == nil else { return } // present only once
        
        let vc = UIAlertController(title: alert.title, message: alert.message, preferredStyle: .alert)
        // add a textField and create a subscription to update the `text` binding
        vc.addTextField {
            // TODO: 需要补充这些参数
            // [=10=].placeholder = alert.placeholder
            // [=10=].keyboardType = alert.keyboardType
            // [=10=].text = alert.defaultValue ?? ""
            [=10=].text = self.alert.defaultText
        }
        if let cancel = alert.cancel {
            vc.addAction(UIAlertAction(title: cancel, style: .cancel) { _ in
                //                self.action(nil)
                self.isPresented = false
            })
        }
        let textField = vc.textFields?.first
        vc.addAction(UIAlertAction(title: alert.accept, style: .default) { _ in
            self.isPresented = false
            self.alert.action(textField?.text)
        })
        present(vc, animated: true, completion: nil)
    }
}

struct TextFieldAlert {
    
    let title: String
    let message: String?
    var defaultText: String = ""
    public var accept: String = "好".localizedString // The left-most button label
    public var cancel: String? = "取消".localizedString // The optional cancel (right-most) button label
    public var action: (String?) -> Void // Triggers when either of the two buttons closes the dialog
    
}

struct AlertWrapper:  UIViewControllerRepresentable {
    
    @Binding var isPresented: Bool
    let alert: TextFieldAlert
    
    typealias UIViewControllerType = TextFieldAlertViewController
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<AlertWrapper>) -> UIViewControllerType {
        TextFieldAlertViewController(isPresented: $isPresented, alert: alert)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext<AlertWrapper>) {
        // no update needed
    }
}

struct TextFieldWrapper<PresentingView: View>: View {
    
    @Binding var isPresented: Bool
    let presentingView: PresentingView
    let content: TextFieldAlert
    
    
    var body: some View {
        ZStack {
            if (isPresented) {
                AlertWrapper(isPresented: $isPresented, alert: content)
            }
            presentingView
        }
    }
}

extension View {
    
    func alert(isPresented: Binding<Bool>, _ content: TextFieldAlert) -> some View {
        TextFieldWrapper(isPresented: isPresented, presentingView: self, content: content)
    }
    
}

如何使用

        xxxView
        .alert(isPresented: $showForm, TextFieldAlert(title: "添加分组", message: "") { (text) in
            if text != nil {
                self.saveGroup(text: text!)
            }
        })

HostingWindow+现在

extension UIWindow {
    public func showAlert(alertController: UIAlertController, placeholder: String, primaryTitle: String, cancelTitle: String, primaryAction: @escaping (String) -> Void) {

        alertController.addTextField { textField in
            textField.placeholder = placeholder
        }

        let primaryButton = UIAlertAction(title: primaryTitle, style: .default) { _ in
            guard let text = alertController.textFields?[0].text else { return }
            primaryAction(text)
        }

        let cancelButton = UIAlertAction(title: cancelTitle, style: .cancel, handler: nil)

        alertController.addAction(primaryButton)
        alertController.addAction(cancelButton)

        self.rootViewController?.present(alertController, animated: true)
    }
}

iOS

的简单本机解决方案
extension View {

    public func textFieldAlert(
        isPresented: Binding<Bool>,
        title: String,
        text: String = "",
        placeholder: String = "",
        action: @escaping (String?) -> Void
    ) -> some View {
        self.modifier(TextFieldAlertModifier(isPresented: isPresented, title: title, text: text, placeholder: placeholder, action: action))
    }
    
}
public struct TextFieldAlertModifier: ViewModifier {

    @State private var alertController: UIAlertController?

    @Binding var isPresented: Bool

    let title: String
    let text: String
    let placeholder: String
    let action: (String?) -> Void

    public func body(content: Content) -> some View {
        content.onChange(of: isPresented) { isPresented in
            if isPresented, alertController == nil {
                let alertController = makeAlertController()
                self.alertController = alertController
                guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
                    return
                }
                scene.windows.first?.rootViewController?.present(alertController, animated: true)
            } else if !isPresented, let alertController = alertController {
                alertController.dismiss(animated: true)
                self.alertController = nil
            }
        }
    }

    private func makeAlertController() -> UIAlertController {
        let controller = UIAlertController(title: title, message: nil, preferredStyle: .alert)
        controller.addTextField {
            [=11=].placeholder = self.placeholder
            [=11=].text = self.text
        }
        controller.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
            self.action(nil)
            shutdown()
        })
        controller.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            self.action(controller.textFields?.first?.text)
            shutdown()
        })
        return controller
    }

    private func shutdown() {
        isPresented = false
        alertController = nil
    }

}

用法:

struct ContentView: View {

    @State private var isRenameAlertPresented = false
    @State private var title = "Old title"

    var body: some View {
        VStack {
            Button("Rename title") {
                isRenameAlertPresented = true
            }

            Text(title)
        }
        .textFieldAlert(
            isPresented: $isRenameAlertPresented,
            title: "Rename",
            text: "Title",
            placeholder: "",
            action: { newText in
                title = newText ?? ""
            }
        )
    }
}