107 lines
2.8 KiB
Swift
107 lines
2.8 KiB
Swift
//
|
|
// ComposeInput.swift
|
|
// ComposeUI
|
|
//
|
|
// Created by Shadowfacts on 3/5/23.
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
import UIKit
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
protocol ComposeInput: AnyObject, ObservableObject {
|
|
var toolbarElements: [ToolbarElement] { get }
|
|
var textInputMode: UITextInputMode? { get }
|
|
|
|
var autocompleteState: AutocompleteState? { get }
|
|
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { get }
|
|
|
|
func autocomplete(with string: String)
|
|
|
|
func applyFormat(_ format: StatusFormat)
|
|
|
|
func beginAutocompletingEmoji()
|
|
}
|
|
|
|
enum ToolbarElement {
|
|
case emojiPicker
|
|
case formattingButtons
|
|
}
|
|
|
|
private struct FocusedComposeInput: FocusedValueKey {
|
|
typealias Value = (any ComposeInput)?
|
|
}
|
|
|
|
extension FocusedValues {
|
|
// double optional is necessary pre-iOS 16
|
|
var composeInput: (any ComposeInput)?? {
|
|
get { self[FocusedComposeInput.self] }
|
|
set { self[FocusedComposeInput.self] = newValue }
|
|
}
|
|
}
|
|
|
|
@propertyWrapper
|
|
final class MutableObservableBox<Value>: ObservableObject {
|
|
@Published var wrappedValue: Value
|
|
|
|
init(wrappedValue: Value) {
|
|
self.wrappedValue = wrappedValue
|
|
}
|
|
}
|
|
|
|
private struct FocusedComposeInputBox: EnvironmentKey {
|
|
static let defaultValue: MutableObservableBox<(any ComposeInput)?> = .init(wrappedValue: nil)
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var composeInputBox: MutableObservableBox<(any ComposeInput)?> {
|
|
get { self[FocusedComposeInputBox.self] }
|
|
set { self[FocusedComposeInputBox.self] = newValue }
|
|
}
|
|
}
|
|
|
|
struct FocusedInputModifier: ViewModifier {
|
|
@StateObject var box: MutableObservableBox<(any ComposeInput)?> = .init(wrappedValue: nil)
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.environment(\.composeInputBox, box)
|
|
.focusedValue(\.composeInput, box.wrappedValue)
|
|
}
|
|
}
|
|
|
|
@propertyWrapper
|
|
struct FocusedInputAutocompleteState: DynamicProperty {
|
|
@FocusedValue(\.composeInput) private var input
|
|
@StateObject private var updater = Updater()
|
|
|
|
var wrappedValue: AutocompleteState? {
|
|
input??.autocompleteState
|
|
}
|
|
|
|
func update() {
|
|
updater.update(input: input ?? nil)
|
|
}
|
|
|
|
@MainActor
|
|
private class Updater: ObservableObject {
|
|
private var lastInput: (any ComposeInput)?
|
|
private var cancellable: AnyCancellable?
|
|
|
|
func update(input: (any ComposeInput)?) {
|
|
guard lastInput !== input else {
|
|
return
|
|
}
|
|
lastInput = input
|
|
cancellable = input?.autocompleteStatePublisher.sink { [unowned self] _ in
|
|
// the autocomplete state sometimes changes during a view update, so defer this
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.objectWillChange.send()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|