DismissibleKeyboardTextField.swift 5.7 KB

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