如何在SwiftUI中将TextField添加到Alert?

Cap*_*ril 9 alert textfield swiftui

有人知道如何在SwiftUI中创建一个包含TextField的Alert吗?

sample_image

Art*_*uro 27

iOS 16+

在此输入图像描述

struct ContentView: View {
    @State private var presentAlert = false
    @State private var username: String = ""
    @State private var password: String = ""
    
    var body: some View {
        Button("Show Alert") {
            presentAlert = true            
        }
        .alert("Login", isPresented: $presentAlert, actions: {
            TextField("Username", text: $username)

            SecureField("Password", text: $password)

            
            Button("Login", action: {})
            Button("Cancel", role: .cancel, action: {})
        }, message: {
            Text("Please enter your username and password.")
        })
    }
}
Run Code Online (Sandbox Code Playgroud)


Mat*_*ini 22

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

}
Run Code Online (Sandbox Code Playgroud)

以及View使用它的扩展:

extension View {

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

}
Run Code Online (Sandbox Code Playgroud)

演示

在此处输入图片说明

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!")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我必须将 `TextField(self.$text)` 更改为 `TextField(self.title, text: self.$text)` 才能编译,但我也无法编辑文本。 (7认同)
  • 这可以为我编译,但 TextField 不允许编辑。就好像它被禁用了或者什么的。 (4认同)
  • 为了修复编辑,我更改了“letpresenting: () -&gt; Presenting " " self.presenting() .blur(radius: self.isShowing ? 2 : 0) .disabled(self.isShowing)" 和 TextFieldAlert(isShowing: isShowing,文本:文本,呈现:{ self }, (2认同)

tan*_*one 17

由于Alert提供的视图SwiftUI不能完成这项工作,您确实需要使用UIAlertControllerfrom UIKit。理想情况下,我们希望有一个TextFieldAlert观点,即我们能够在我们目前的以同样的方式呈现Alert提供SwiftUI

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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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 { ($0.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)
  }
}
Run Code Online (Sandbox Code Playgroud)

2)使用协议TextFieldAlert包装TextFieldAlertViewControllerUIViewControllerRepresentable以便它可以在 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
  }
}
Run Code Online (Sandbox Code Playgroud)

3)TextFieldWrapper是一个简单ZStackTextFieldAlert,背面有 a (仅当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
    }
  }  
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 不要忘记“导入组合”以使“AnyCancellable”可见 (5认同)

kon*_*iki 11

我发现 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))
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • **Swift5 更新** 将 TextField 行更新为此 -&gt;。TextField("在此输入文本", text: $text).textFieldStyle(RoundedBorderTextFieldStyle()).padding() (4认同)

Fab*_*tel 11

您可以直接使用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 {$0.activationState == .foregroundActive}
    .compactMap {$0 as? UIWindowScene}
    .first?.windows.filter {$0.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
}
Run Code Online (Sandbox Code Playgroud)

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

struct TestView: View {
    var body: some View {
        Button(action: { alert() }) { Text("click me") }
     }
}
Run Code Online (Sandbox Code Playgroud)

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


nig*_*ill 10

适用于 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))
    }
    
}
Run Code Online (Sandbox Code Playgroud)
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 {
            $0.placeholder = self.placeholder
            $0.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
    }

}

Run Code Online (Sandbox Code Playgroud)

用法:

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 ?? ""
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的建议。我更新了示例并删除了已弃用的 API。 (3认同)

Sen*_*ful 5

尽管不完全相同,但如果您要寻找的只是带有编辑框的原生、类似模态的视图,则可以使用popover。它开箱即用(减去大小错误)而无需遍历视图层次结构。