// // DismissibleKeyboardTextField.swift // LoopKitUI // // Created by Michael Pangburn on 7/22/20. // Copyright © 2020 LoopKit Authors. All rights reserved. // import SwiftUI public struct DismissibleKeyboardTextField: UIViewRepresentable { @Binding var text: String var placeholder: String var font: UIFont var textColor: UIColor var textAlignment: NSTextAlignment var keyboardType: UIKeyboardType var autocapitalizationType: UITextAutocapitalizationType var autocorrectionType: UITextAutocorrectionType var shouldBecomeFirstResponder: Bool var maxLength: Int? var doneButtonColor: UIColor var isDismissible: Bool var textFieldDidBeginEditing: (() -> Void)? public init( text: Binding, placeholder: String, font: UIFont = .preferredFont(forTextStyle: .body), textColor: UIColor = .label, textAlignment: NSTextAlignment = .natural, keyboardType: UIKeyboardType = .default, autocapitalizationType: UITextAutocapitalizationType = .sentences, autocorrectionType: UITextAutocorrectionType = .default, shouldBecomeFirstResponder: Bool = false, maxLength: Int? = nil, doneButtonColor: UIColor = .blue, isDismissible: Bool = true, textFieldDidBeginEditing: (() -> Void)? = nil ) { self._text = text self.placeholder = placeholder self.font = font self.textColor = textColor self.textAlignment = textAlignment self.keyboardType = keyboardType self.autocapitalizationType = autocapitalizationType self.autocorrectionType = autocorrectionType self.shouldBecomeFirstResponder = shouldBecomeFirstResponder self.maxLength = maxLength self.doneButtonColor = doneButtonColor self.isDismissible = isDismissible self.textFieldDidBeginEditing = textFieldDidBeginEditing } public func makeUIView(context: Context) -> UITextField { let textField = UITextField() textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField) : nil textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged) textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin) textField.delegate = context.coordinator return textField } private func makeDoneToolbar(for textField: UITextField) -> UIToolbar { let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: textField, action: #selector(UITextField.resignFirstResponder)) doneButton.tintColor = doneButtonColor toolbar.items = [flexibleSpace, doneButton] toolbar.sizeToFit() return toolbar } public func updateUIView(_ textField: UITextField, context: Context) { textField.text = text textField.placeholder = placeholder textField.font = font textField.textColor = textColor textField.textAlignment = textAlignment textField.keyboardType = keyboardType textField.autocapitalizationType = autocapitalizationType textField.autocorrectionType = autocorrectionType if shouldBecomeFirstResponder && !context.coordinator.didBecomeFirstResponder { // See https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder for why // we check the window property here (otherwise it might crash) if textField.window != nil && textField.becomeFirstResponder() { context.coordinator.didBecomeFirstResponder = true } } else if !shouldBecomeFirstResponder && context.coordinator.didBecomeFirstResponder { context.coordinator.didBecomeFirstResponder = false } } public func makeCoordinator() -> Coordinator { Coordinator(self, maxLength: maxLength) } public final class Coordinator: NSObject { var parent: DismissibleKeyboardTextField let maxLength: Int? // Track in the coordinator to ensure the text field only becomes first responder once, // rather than on every state change. var didBecomeFirstResponder = false init(_ parent: DismissibleKeyboardTextField, maxLength: Int?) { self.parent = parent self.maxLength = maxLength } @objc fileprivate func textChanged(_ textField: UITextField) { parent.text = textField.text ?? "" } @objc fileprivate func editingDidBegin(_ textField: UITextField) { // Even though we are likely already on .main, we still need to queue this cursor (selection) change in // order for it to work DispatchQueue.main.async { textField.moveCursorToEnd() } } } } extension DismissibleKeyboardTextField.Coordinator: UITextFieldDelegate { public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let maxLength = maxLength else { return true } let currentString: NSString = (textField.text ?? "") as NSString let newString: NSString = currentString.replacingCharacters(in: range, with: string) as NSString return newString.length <= maxLength } public func textFieldDidBeginEditing(_ textField: UITextField) { parent.textFieldDidBeginEditing?() } }