Created
July 5, 2020 16:10
Star
You must be signed in to star a gist
SelectableTextField
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct SelectableTextField: UIViewRepresentable { | |
let titleKey: String | |
@RangeReplaceableBinding var text: String | |
@Binding var selectedRange: Range<String.Index>? | |
let onCommit: () -> Void | |
init(_ titleKey: String, text: Binding<String>, selectedRange: Binding<Range<String.Index>?>, onCommit: @escaping () -> Void = {}) { | |
self.init(titleKey, text: RangeReplaceableBinding(text), selectedRange: selectedRange, onCommit: onCommit) | |
} | |
init(_ titleKey: String, text: RangeReplaceableBinding<String>, selectedRange: Binding<Range<String.Index>?>, onCommit: @escaping () -> Void = {}) { | |
self.titleKey = titleKey | |
self._text = text.didReplaceSubrange(perform: { range, newText in | |
let newInsertionIndex = text.wrappedValue.index(range.lowerBound, offsetBy: newText.count) | |
selectedRange.wrappedValue = newInsertionIndex..<newInsertionIndex | |
}) | |
self._selectedRange = selectedRange | |
self.onCommit = onCommit | |
} | |
var keyboardType: UIKeyboardType = .default | |
func makeUIView(context: Context) -> UITextField { | |
let textField = UITextField() | |
textField.delegate = context.coordinator | |
textField.setContentHuggingPriority(.required, for: .vertical) | |
return textField | |
} | |
func updateUIView(_ textField: UITextField, context: Context) { | |
textField.placeholder = NSLocalizedString(titleKey, comment: "Text field placeholder") | |
textField.keyboardType = keyboardType | |
context.coordinator.updateTextField(textField, from: self) | |
} | |
class Coordinator: UIResponder, UITextFieldDelegate { | |
var parent: SelectableTextField | |
init(_ parent: SelectableTextField) { | |
self.parent = parent | |
} | |
private var isUpdatingViewFromState = false | |
private var pendingSelection: UITextRange? | |
func updateTextField(_ textField: UITextField, from parent: SelectableTextField) { | |
self.parent = parent // https://twitter.com/JadenGeller/status/1279171264779743232 | |
ignore = false | |
isUpdatingViewFromState = true | |
defer { isUpdatingViewFromState = false } | |
textField.text = parent.text | |
if let selectedRange = parent.selectedRange { | |
let selectedTextRange = textField.textRange(from: selectedRange) | |
if textField.isFirstResponder { | |
textField.selectedTextRange = selectedTextRange | |
} else { | |
pendingSelection = selectedTextRange | |
textField.becomeFirstResponder() | |
} | |
} else { | |
if textField.isFirstResponder { | |
textField.resignFirstResponder() | |
} | |
} | |
} | |
func textFieldDidChangeSelection(_ textField: UITextField) { | |
guard !isUpdatingViewFromState else { return } | |
parent.selectedRange = textField.indexRange(from: textField.selectedTextRange!) | |
} | |
func textFieldDidBeginEditing(_ textField: UITextField) { | |
if let pendingSelection = pendingSelection { | |
textField.selectedTextRange = pendingSelection | |
} | |
guard !isUpdatingViewFromState else { return } | |
parent.selectedRange = textField.indexRange(from: textField.selectedTextRange!) | |
} | |
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { | |
guard !isUpdatingViewFromState else { return } | |
parent.selectedRange = nil | |
} | |
func textFieldShouldReturn(_ textField: UITextField) -> Bool { | |
parent.onCommit() | |
textField.resignFirstResponder() | |
return true | |
} | |
var ignore = false | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
guard !isUpdatingViewFromState else { return false } | |
guard !ignore else { return false } | |
parent.$text.replaceSubrange(textField.indexRange(from: range), with: string) | |
ignore = true | |
return false | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(self) | |
} | |
} | |
extension SelectableTextField { | |
func keyboardType(_ type: UIKeyboardType) -> SelectableTextField { | |
var copy = self | |
copy.keyboardType = type | |
return copy | |
} | |
} | |
extension Range { | |
static func empty(at bound: Bound) -> Self { | |
bound..<bound | |
} | |
} | |
extension UITextField { | |
fileprivate func position(from index: String.Index) -> UITextPosition { | |
position(from: beginningOfDocument, offset: text!.distance(from: text!.startIndex, to: index))! // FIXME | |
} | |
fileprivate func textRange(from range: Range<String.Index>) -> UITextRange { | |
textRange(from: position(from: range.lowerBound), to: position(from: range.upperBound))! // FIXME | |
} | |
fileprivate func index(from position: UITextPosition) -> String.Index { | |
index(from: offset(from: beginningOfDocument, to: position)) | |
} | |
fileprivate func indexRange(from range: UITextRange) -> Range<String.Index> { | |
index(from: range.start)..<index(from: range.end) | |
} | |
fileprivate func index(from offset: Int) -> String.Index { | |
text!.index(text!.startIndex, offsetBy: offset) | |
} | |
fileprivate func indexRange(from range: NSRange) -> Range<String.Index> { | |
index(from: range.location)..<index(from: range.location + range.length) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment