Как добавить текстовое поле в оповещение в SwiftUI?

Кто-нибудь знает, как создать оповещение в SwiftUI, содержащее TextField?

Как добавить текстовое поле в оповещение в SwiftUI?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
57
0
30 178
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

Я обнаружил, что модальные окна и оповещения в SwiftUI лишены нескольких функций. Например, похоже, что нет способа представить модальное окно в стиле FormSheet.

Когда мне нужно представить сложное оповещение (например, с текстовыми полями), я создаю чистое представление SwiftUI со всем содержимым оповещения, а затем представляю его в виде FormSheet, используя UIHostController.

Если у вас нет 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))
    }
}

Если вы обнаружите, что часто используете это, ряд кнопок может быть инкапсулирован в отдельном представлении для удобства повторного использования.

Есть какие-нибудь идеи по обходным путям для iPhone? Кстати, iPad также будет отображать его в полноэкранном режиме при совместном использовании экрана с другим приложением.

Rivera 23.10.2019 15:39
Обновление Swift5 Обновите строку TextField до этой ->. TextField("Введите текст здесь", text: $text).textFieldStyle(RoundedBorderTextFieldStyle()).padding‌​()
uplearned.com 10.11.2019 02:35

Кстати это не будет работать в превью для айфона. Вам придется построить его в симуляторе, и для iPhone он выглядит совсем иначе. вышеприведенное выглядит так только в симуляторе iPad.

uplearned.com 10.11.2019 02:43

kontiki Кажется, это не работает в Xcode 12.2. Создайте новый проект и вставьте код, и значение предпочитаемогоContentSize будет игнорироваться.

xdeleon 23.12.2020 04:41

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

это дает мне ошибку Abort 6. результат не найден Перекрестная ссылка на модуль «SwiftUI»… Просмотр… в расширении в модуле… непрозрачный возвращаемый тип

MobileMon 26.06.2019 21:40

@MobileMon, пожалуйста, дважды проверьте свой код - я только что проверил это на macoS 10.15 b2 / Xcode 11 b2, и он отлично компилируется без проблем. Проверено как на симке, так и на устройстве.

Matteo Pacini 27.06.2019 05:46

Это компилируется для меня, но TextField не позволяет редактировать. Как будто он отключен или что-то в этом роде.

drewster 26.12.2019 21:32

Мне пришлось изменить TextField(self.$text) на TextField(self.title, text: self.$text), чтобы он скомпилировался, но я также не могу редактировать текст.

simibac 13.02.2020 15:16

@simibac Вы можете найти исправление здесь: stackoverflow.com/questions/59493767/…

Alexey 15.03.2020 15:13

чтобы исправить редактирование, я изменил «разрешить представление: () -> Представление» « self.presenting() .blur(radius: self.isShowing ? 2 : 0) .disabled(self.isShowing)» и TextFieldAlert(isShowing: isShowing, текст: текст, представление: {я},

Simone Pistecchia 01.04.2020 16:00

Вы можете просто использовать UIAlertController напрямую. Нет необходимости создавать собственный интерфейс диалогового окна предупреждений:

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
}

Большая часть этого кода — просто шаблон для поиска ViewController, который должен отображать предупреждение. Звоните alert() например. от action кнопки:

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

Пожалуйста, имейте в виду, что в бета-версии 5 и более поздних версиях есть ошибка, которая иногда может привести к зависанию эмулятора после отображения текстового поля: Xcode 11 beta 5: пользовательский интерфейс зависает при добавлении текстовых полей в UIAlertController

Это отлично работает как в iOS, так и в MacOs. Так же в MacOS это стандартное оповещение работает очень нестабильно, но надежно в iOS: .alert(isPresented: $showingAlert) { Alert(title: Text("title"), message: Text("message"), primaryButton: .default(Text("Ok")), secondaryButton: .cancel (Text("Cancel")))} почему вопрос?

Jan Bergström 14.03.2020 04:25

Как вы получаете доступ к значениям в textField?

Genki 28.04.2020 02:27

Это достойный ответ, однако это нарушает мой макет после отображения диалогового окна до тех пор, пока состояние представления не изменится снова. Это приводит к тому, что расстояние слева и справа от вида становится больше...

frankenapps 01.05.2020 06:41

Хотя это и не совсем то же самое, но если все, что вам нужно, это нативное модальное представление с полем редактирования, вы можете использовать трещать. Он работает из коробки (минус ошибка размера) без необходимости прохождения иерархии представлений.

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)
 }
Ответ принят как подходящий

Поскольку представление Alert, предоставляемое SwiftUI, не выполняет свою работу, вам действительно нужно будет использовать UIAlertController из 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)
    }
  }
}

Мы можем добиться этого, написав пару классов и добавив модификатор в расширение 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 { ($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)
  }
}

2) TextFieldAlert оборачивает TextFieldAlertViewController протокол UIViewControllerRepresentable, чтобы его можно было использовать в 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 позволяет плавно обернуть любое представление SwiftUI в TextFieldWrapper и получить желаемое поведение.

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

Не забудьте import Combine сделать AnyCancellable видимым

Leo 18.01.2021 06:11

Отобразится текущая страница

Purkylin 24.05.2021 03:57

Кто-нибудь использовал это с API .alert(item: Item, alert: (Item) -> Alert)?

Will Alexander 16.07.2021 12:00

Я обрабатываю это предупреждение под кнопкой. Каким-то образом я могу зафиксировать значение, введенное в модальном окне. Как я могу получить от него значение после нажатия кнопки в модальном окне?

user4150758 18.10.2021 18:43

Как уже упоминалось, Alert предоставляет не так много функций и поэтому почти бесполезен в любых нестандартных случаях при использовании в 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[$0]
    
                 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 $0 != 0 {
                         Divider().frame(height: 44)
                     }
                     let current = buttons[$0]
    
                     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. У меня есть несколько незначительных изменений: (i) проверка !requireHorizontalPositioning в horizontalButtonsPadFor(..) не требуется, (ii) я хотел, чтобы горизонтальный разделитель между двумя кнопками не имел верхнего и нижнего пробелов, как стандартные предупреждения... поэтому VStack(spacing : 0) в buttonPad(..) И когда !requireHorizontalPositioning в основном теле UniAlert не имеет «нижнего» viewModel.contentPadding для VStack с представлениями содержимого и панели кнопок И удаляется последний разделитель под ZStack.

Kpalser 26.11.2020 09:43

спасибо за комментарий, рад, что этот ответ помогает вам. Я обязательно проверю и обновлю код чуть позже. Между тем, вы также можете столкнуться со случаем, когда вы хотите представить это предупреждение в полном контексте вашего представления. как это сделать - небольшую статью можно найти здесь - khorbushko.github.io/article/2020/11/24/…

hbk 26.11.2020 11:18

Это пример, основанный на классе SwiftUI Sheet, который отображает диалоговое окно с приглашением, текстовым полем и классической кнопкой «ОК» и «Отклонить».

Сначала давайте создадим наш класс Dialog, который появится, когда пользователь захочет отредактировать значение:

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 = $0 ?? ""}))
    }
}
#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

Шаг 1: Сделайте корневой вид как ZStack

Шаг 2: Добавьте переменную для отображения/скрытия

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

Очень четкий и точный ответ. Спасибо.

Tchelyzt 19.02.2021 17:17

На основе идеи танзолона

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: 需要补充这些参数
            // $0.placeholder = alert.placeholder
            // $0.keyboardType = alert.keyboardType
            // $0.text = alert.defaultValue ?? ""
            $0.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 {
            $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
    }

}

Применение:

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

Я получил это работает, и он делает то, что мне нужно. Однако использование примера было довольно расплывчатым. Я потратил некоторое время, пытаясь устранить неполадки, почему появляются ошибки. Вы можете показать пример с блоком кода вместо rename. Кроме того, в строке UIApplication.shared.windows.first?.rootViewController?.pres‌​ent(alertController, animated: true) используется устаревший подход. Его следует заменить на (UIApplication.shared.connectedScenes.first as! UIWindowScene).windows.first?.rootViewController?.present(al‌​ertController, animated: true). Я думал, что произошла еще одна ошибка, которую легко исправить.

Jason Cramer 07.02.2022 17:10

Спасибо за предложения. Я обновил пример и удалил устаревший API.

nightwill 10.02.2022 10:54

Как я могу установить ввод TextField как Autocapitalization? Спасибо

Cyrille 05.03.2022 14:43

Это очень просто) Просто добавьте код $0.autocapitalizationType = .words после $0.text = self.text

nightwill 07.03.2022 07:29

Другие вопросы по теме