如何在 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
可能表现为具有高定制级别的警报。
为弹出窗口创建 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
}
我们还需要配置按钮,为此我们再添加一种类型:
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
}
}
添加可以成为我们可定制弹出窗口的视图:
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()
}
}
}
为了简化使用,让我们为 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 ?? ""
}
)
}
}
有人知道如何在 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
可能表现为具有高定制级别的警报。
为弹出窗口创建
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 }
我们还需要配置按钮,为此我们再添加一种类型:
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 } }
添加可以成为我们可定制弹出窗口的视图:
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() } } }
为了简化使用,让我们为
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 ?? ""
}
)
}
}