Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Created July 5, 2020 16:10
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save JadenGeller/4da35f2a4b651a9b302208b62e775711 to your computer and use it in GitHub Desktop.
SelectableTextField
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