How to add a TextField to Alert in SwiftUI?

前端 未结 9 1947
温柔的废话
温柔的废话 2021-02-03 21:33

Anyone an idea how to create an Alert in SwiftUI that contains a TextField?

\"sample_image\"

相关标签:
9条回答
  • 2021-02-03 21:47

    You can simply use UIAlertController directly. No need to roll your own alert dialog 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
    }
    

    Most of this code is just boilerplate to find the ViewController that should present the alert. Call alert() e.g. from the action of a button:

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

    Please beware though that there seems to be a bug in beta 5 and onward that can sometimes cause the emulator to freeze once a text field is shown: Xcode 11 beta 5: UI freezes when adding textFields into UIAlertController

    0 讨论(0)
  • 2021-02-03 21:48
    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)
    
    
        }
    

    Usage

    Button(action: { self.dialog()})
     {
    Text("Button")
    .foregroundColor(.white).fontWeight(.bold)
     }
    
    0 讨论(0)
  • 2021-02-03 21:56

    As the Alert view provided by SwiftUI doesn't do the job you will need indeed to use UIAlertController from UIKit. Ideally we want a TextFieldAlert view that we can presented in the same way we would present the Alert provided by 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)
        }
      }
    }
    

    We can achieve this writing a couple of classes and adding a modifier in a View extension.

    1) TextFieldAlertViewController creates a UIAlertController (with a text field of course) and presents it when it appears on screen. User changes to the text field are reflected into a Binding<String> that is passed during initializazion.

    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 wraps TextFieldAlertViewController using the UIViewControllerRepresentable protocol so that it can be used within 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 is a simple ZStack with a TextFieldAlert on the back (only if isPresented is true) and a presenting view on the front. The presenting view is the only one visibile.

    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) The textFieldAlert modifier allows us to smoothly wrap any SwiftUI view in a TextFieldWrapper and obtain the desired behaviour.

    extension View {
      func textFieldAlert(isPresented: Binding<Bool>,
                          content: @escaping () -> TextFieldAlert) -> some View {
        TextFieldWrapper(isPresented: isPresented,
                         presentingView: self,
                         content: content)
      }
    }
    
    0 讨论(0)
  • 2021-02-03 21:58

    Step1: Make root view as ZStack

    Step2: Add variable for show/hide

     @State var showAlert = false
    

    Step 3: Add this custom layout inside root view (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)
            }
    
    0 讨论(0)
  • 2021-02-03 22:02

    As already was mentioned Alert is provide not many functionality and so almost useless in any non-standard cases when using in SwiftUI.

    I ended up with a bit extensive solution - View that may behave as alert with high customisation level.

    1. Create ViewModel for popUp:

      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. we also need to configure buttons, for this purpose let's add one more type:

      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. add View that can become our customizable popUp:

      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. to simplify usage let's add extension to 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)
       }
      }
      

    And usage:

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

    Demo:

    0 讨论(0)
  • 2021-02-03 22:07

    Although not exactly the same, if all you're looking for is a native, modal-like view with an edit box, you could use a popover. It works out of the box (minus a sizing bug) without needing to traverse the view hierarchy.

    0 讨论(0)
提交回复
热议问题