DismissibleKeyboardTextField.swift 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. //
  2. // DismissibleKeyboardTextField.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 7/22/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. public struct DismissibleKeyboardTextField: UIViewRepresentable {
  10. @Binding var text: String
  11. var placeholder: String
  12. var font: UIFont
  13. var textColor: UIColor
  14. var textAlignment: NSTextAlignment
  15. var keyboardType: UIKeyboardType
  16. var autocapitalizationType: UITextAutocapitalizationType
  17. var autocorrectionType: UITextAutocorrectionType
  18. var shouldBecomeFirstResponder: Bool
  19. var maxLength: Int?
  20. public init(
  21. text: Binding<String>,
  22. placeholder: String,
  23. font: UIFont = .preferredFont(forTextStyle: .body),
  24. textColor: UIColor = .label,
  25. textAlignment: NSTextAlignment = .natural,
  26. keyboardType: UIKeyboardType = .default,
  27. autocapitalizationType: UITextAutocapitalizationType = .sentences,
  28. autocorrectionType: UITextAutocorrectionType = .default,
  29. shouldBecomeFirstResponder: Bool = false,
  30. maxLength: Int? = nil
  31. ) {
  32. self._text = text
  33. self.placeholder = placeholder
  34. self.font = font
  35. self.textColor = textColor
  36. self.textAlignment = textAlignment
  37. self.keyboardType = keyboardType
  38. self.autocapitalizationType = autocapitalizationType
  39. self.autocorrectionType = autocorrectionType
  40. self.shouldBecomeFirstResponder = shouldBecomeFirstResponder
  41. self.maxLength = maxLength
  42. }
  43. public func makeUIView(context: Context) -> UITextField {
  44. let textField = UITextField()
  45. textField.inputAccessoryView = makeDoneToolbar(for: textField)
  46. textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
  47. textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
  48. textField.delegate = context.coordinator
  49. return textField
  50. }
  51. private func makeDoneToolbar(for textField: UITextField) -> UIToolbar {
  52. let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
  53. let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
  54. let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: textField, action: #selector(UITextField.resignFirstResponder))
  55. doneButton.tintColor = textColor
  56. toolbar.items = [flexibleSpace, doneButton]
  57. toolbar.sizeToFit()
  58. return toolbar
  59. }
  60. public func updateUIView(_ textField: UITextField, context: Context) {
  61. textField.text = text
  62. textField.placeholder = placeholder
  63. textField.font = font
  64. textField.textColor = textColor
  65. textField.textAlignment = textAlignment
  66. textField.keyboardType = keyboardType
  67. textField.autocapitalizationType = autocapitalizationType
  68. textField.autocorrectionType = autocorrectionType
  69. if shouldBecomeFirstResponder && !context.coordinator.didBecomeFirstResponder {
  70. // See https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder for why
  71. // we check the window property here (otherwise it might crash)
  72. if textField.window != nil && textField.becomeFirstResponder() {
  73. context.coordinator.didBecomeFirstResponder = true
  74. }
  75. } else if !shouldBecomeFirstResponder && context.coordinator.didBecomeFirstResponder {
  76. context.coordinator.didBecomeFirstResponder = false
  77. }
  78. }
  79. public func makeCoordinator() -> Coordinator {
  80. Coordinator(self, maxLength: maxLength)
  81. }
  82. public final class Coordinator: NSObject {
  83. var parent: DismissibleKeyboardTextField
  84. let maxLength: Int?
  85. // Track in the coordinator to ensure the text field only becomes first responder once,
  86. // rather than on every state change.
  87. var didBecomeFirstResponder = false
  88. init(_ parent: DismissibleKeyboardTextField, maxLength: Int?) {
  89. self.parent = parent
  90. self.maxLength = maxLength
  91. }
  92. @objc fileprivate func textChanged(_ textField: UITextField) {
  93. parent.text = textField.text ?? ""
  94. }
  95. @objc fileprivate func editingDidBegin(_ textField: UITextField) {
  96. // Even though we are likely already on .main, we still need to queue this cursor (selection) change in
  97. // order for it to work
  98. DispatchQueue.main.async {
  99. textField.moveCursorToEnd()
  100. }
  101. }
  102. }
  103. }
  104. extension DismissibleKeyboardTextField.Coordinator: UITextFieldDelegate {
  105. public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  106. guard let maxLength = maxLength else {
  107. return true
  108. }
  109. let currentString: NSString = (textField.text ?? "") as NSString
  110. let newString: NSString =
  111. currentString.replacingCharacters(in: range, with: string) as NSString
  112. return newString.length <= maxLength
  113. }
  114. }