// // ComposeContentWarningTextField.swift // Tusker // // Created by Shadowfacts on 10/12/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import SwiftUI struct ComposeEmojiTextField: UIViewRepresentable { typealias UIViewType = UITextField @EnvironmentObject private var uiState: ComposeUIState @Binding var text: String let placeholder: String let maxLength: Int? let becomeFirstResponder: Binding? let focusNextView: Binding? private var didChange: ((String) -> Void)? = nil private var didEndEditing: (() -> Void)? = nil private var backgroundColor: UIColor? = nil init(text: Binding, placeholder: String, maxLength: Int? = nil, becomeFirstResponder: Binding? = nil, focusNextView: Binding? = nil) { self._text = text self.placeholder = placeholder self.maxLength = maxLength self.becomeFirstResponder = becomeFirstResponder self.focusNextView = focusNextView self.didChange = nil self.didEndEditing = nil } mutating func didChange(_ didChange: @escaping (String) -> Void) -> Self { self.didChange = didChange return self } mutating func didEndEditing(_ didEndEditing: @escaping () -> Void) -> Self { self.didEndEditing = didEndEditing return self } mutating func backgroundColor(_ color: UIColor) -> Self { self.backgroundColor = color return self } func makeUIView(context: Context) -> UITextField { let view = UITextField() view.placeholder = placeholder view.borderStyle = .roundedRect view.font = .preferredFont(forTextStyle: .body) view.adjustsFontForContentSizeCategory = true view.backgroundColor = backgroundColor view.delegate = context.coordinator view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged) view.addTarget(context.coordinator, action: #selector(Coordinator.returnKeyPressed), for: .primaryActionTriggered) // otherwise when the text gets too wide it starts expanding the ComposeView view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) context.coordinator.textField = view context.coordinator.uiState = uiState context.coordinator.text = $text return view } func updateUIView(_ uiView: UITextField, context: Context) { if context.coordinator.skipSettingTextOnNextUpdate { context.coordinator.skipSettingTextOnNextUpdate = false } else { uiView.text = text } context.coordinator.maxLength = maxLength context.coordinator.didChange = didChange context.coordinator.didEndEditing = didEndEditing context.coordinator.focusNextView = focusNextView if becomeFirstResponder?.wrappedValue == true { DispatchQueue.main.async { uiView.becomeFirstResponder() becomeFirstResponder?.wrappedValue = false } } } func makeCoordinator() -> Coordinator { return Coordinator() } class Coordinator: NSObject, UITextFieldDelegate, ComposeInput { weak var textField: UITextField? var text: Binding! // break retained cycle through ComposeUIState.currentInput unowned var uiState: ComposeUIState! var maxLength: Int? var didChange: ((String) -> Void)? var didEndEditing: (() -> Void)? var focusNextView: Binding? var skipSettingTextOnNextUpdate = false var toolbarElements: [ComposeUIState.ToolbarElement] { [.emojiPicker] } @objc func didChange(_ textField: UITextField) { text.wrappedValue = textField.text ?? "" didChange?(text.wrappedValue) } @objc func returnKeyPressed() { focusNextView?.wrappedValue = true } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let maxLength { return ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string).count <= maxLength } else { return true } } func textFieldDidBeginEditing(_ textField: UITextField) { uiState.currentInput = self updateAutocompleteState(textField: textField) } func textFieldDidEndEditing(_ textField: UITextField) { uiState.currentInput = nil updateAutocompleteState(textField: textField) didEndEditing?() } func textFieldDidChangeSelection(_ textField: UITextField) { // see MainComposeTextView.Coordinator.textViewDidChangeSelection(_:) skipSettingTextOnNextUpdate = true self.updateAutocompleteState(textField: textField) } func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { var actions = suggestedActions if range.length == 0 { actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in self?.uiState.shouldEmojiAutocompletionBeginExpanded = true self?.beginAutocompletingEmoji() })) } return UIMenu(children: actions) } func beginAutocompletingEmoji() { textField?.insertText(":") } func applyFormat(_ format: StatusFormat) { } 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.. 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.. 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) guard cursorIndex != text.startIndex else { return nil } 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 } } }