Tusker/Tusker/Screens/Compose/ComposeContentWarningTextFi...

170 lines
6.7 KiB
Swift

//
// ComposeContentWarningTextField.swift
// Tusker
//
// Created by Shadowfacts on 10/12/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
struct ComposeContentWarningTextField: UIViewRepresentable {
typealias UIViewType = UITextField
@Binding var text: String
@EnvironmentObject private var uiState: ComposeUIState
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.placeholder = "Write your warning here"
view.borderStyle = .roundedRect
view.delegate = context.coordinator
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
context.coordinator.textField = view
context.coordinator.uiState = uiState
context.coordinator.text = $text
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
weak var textField: UITextField?
var text: Binding<String>!
var uiState: ComposeUIState!
@objc func didChange(_ textField: UITextField) {
text.wrappedValue = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
uiState.autocompleteHandler = self
updateAutocompleteState(textField: textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
updateAutocompleteState(textField: textField)
}
func textFieldDidChangeSelection(_ textField: UITextField) {
// Update text binding before potentially triggering SwiftUI view update.
// See comment in MainComposeTextView.Coordinator.textViewDidChangeSelection
text.wrappedValue = textField.text ?? ""
updateAutocompleteState(textField: textField)
}
func autocomplete(with string: String) {
guard let textField = textField,
let text = textField.text,
let selectedRange = textField.selectedTextRange,
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
return
}
let distanceToEnd = textField.offset(from: selectedRange.start, to: textField.endOfDocument)
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
let insertSpace: Bool
if distanceToEnd > 0 {
let charAfterCursor = text[characterBeforeCursorIndex]
insertSpace = charAfterCursor != " " && charAfterCursor != "\n"
} else {
insertSpace = true
}
let string = insertSpace ? string + " " : string
textField.text!.replaceSubrange(lastWordStartIndex..<characterBeforeCursorIndex, with: string)
self.didChange(textField)
self.updateAutocompleteState(textField: textField)
// keep the cursor at the same position in the text, immediately after what was inserted
// if we inserted a space, move the cursor 1 farther so it's immediately after the pre-existing space
let insertSpaceOffset = insertSpace ? 0 : 1
let newCursorPosition = textField.position(from: textField.endOfDocument, offset: -distanceToEnd + insertSpaceOffset)!
textField.selectedTextRange = textField.textRange(from: newCursorPosition, to: newCursorPosition)
}
private func updateAutocompleteState(textField: UITextField) {
guard let selectedRange = textField.selectedTextRange,
let text = textField.text,
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
uiState.autocompleteState = nil
return
}
if lastWordStartIndex > text.startIndex {
let c = text[text.index(before: lastWordStartIndex)]
if isPermittedForAutocomplete(c) || c == ":" {
uiState.autocompleteState = nil
return
}
}
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
if lastWordStartIndex >= text.startIndex {
let lastWord = text[lastWordStartIndex..<cursorIndex]
let exceptFirst = lastWord[lastWord.index(after: lastWord.startIndex)...]
if lastWord.first == ":" {
uiState.autocompleteState = .emoji(String(exceptFirst))
} else {
uiState.autocompleteState = nil
}
} else {
uiState.autocompleteState = nil
}
}
private func isPermittedForAutocomplete(_ c: Character) -> Bool {
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"
}
private func findAutocompleteLastWord(textField: UITextField) -> String.Index? {
guard textField.isFirstResponder,
let selectedRange = textField.selectedTextRange,
selectedRange.isEmpty,
let text = textField.text,
!text.isEmpty else {
return nil
}
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
var lastWordStartIndex = text.index(before: cursorIndex)
while true {
let c = text[lastWordStartIndex]
if !isPermittedForAutocomplete(c) {
break
}
if lastWordStartIndex > text.startIndex {
lastWordStartIndex = text.index(before: lastWordStartIndex)
} else {
break
}
}
return lastWordStartIndex
}
}
}