2020-08-31 19:28:50 -04:00
|
|
|
//
|
|
|
|
// MainComposeTextView.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 8/29/20.
|
|
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import Pachyderm
|
|
|
|
|
|
|
|
struct MainComposeTextView: View {
|
|
|
|
@ObservedObject var draft: Draft
|
|
|
|
let placeholder: Text
|
|
|
|
|
|
|
|
let minHeight: CGFloat = 150
|
|
|
|
@State private var height: CGFloat?
|
|
|
|
@State private var becomeFirstResponder: Bool = false
|
|
|
|
@State private var hasFirstAppeared = false
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
ZStack(alignment: .topLeading) {
|
|
|
|
MainComposeWrappedTextView(
|
|
|
|
text: $draft.text,
|
|
|
|
visibility: draft.visibility,
|
|
|
|
becomeFirstResponder: $becomeFirstResponder
|
|
|
|
) { (textView) in
|
|
|
|
self.height = max(textView.contentSize.height, minHeight)
|
|
|
|
}
|
|
|
|
.frame(height: height ?? minHeight)
|
|
|
|
|
|
|
|
if draft.text.isEmpty {
|
|
|
|
placeholder
|
|
|
|
.font(.system(size: 20))
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
.offset(x: 4, y: 8)
|
|
|
|
}
|
|
|
|
}.onAppear {
|
|
|
|
if !hasFirstAppeared {
|
|
|
|
hasFirstAppeared = true
|
|
|
|
becomeFirstResponder = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct MainComposeWrappedTextView: UIViewRepresentable {
|
|
|
|
typealias UIViewType = UITextView
|
|
|
|
|
|
|
|
@Binding var text: String
|
|
|
|
let visibility: Status.Visibility
|
|
|
|
@Binding var becomeFirstResponder: Bool
|
|
|
|
var textDidChange: (UITextView) -> Void
|
|
|
|
|
|
|
|
@EnvironmentObject var uiState: ComposeUIState
|
|
|
|
@State var visibilityButton: UIBarButtonItem?
|
|
|
|
|
|
|
|
func makeUIView(context: Context) -> UITextView {
|
|
|
|
let textView = UITextView()
|
|
|
|
textView.delegate = context.coordinator
|
|
|
|
textView.isEditable = true
|
|
|
|
textView.backgroundColor = .secondarySystemBackground
|
|
|
|
textView.font = .systemFont(ofSize: 20)
|
|
|
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
|
|
|
context.coordinator.textView = textView
|
|
|
|
|
2020-08-31 23:07:41 -04:00
|
|
|
let visibilityAction: Selector?
|
|
|
|
if #available(iOS 14.0, *) {
|
|
|
|
visibilityAction = nil
|
|
|
|
} else {
|
|
|
|
visibilityAction = #selector(ComposeHostingController.visibilityButtonPressed(_:))
|
|
|
|
}
|
|
|
|
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: visibilityAction)
|
|
|
|
updateVisibilityMenu(visibilityButton)
|
2020-08-31 19:28:50 -04:00
|
|
|
let toolbar = UIToolbar()
|
|
|
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
toolbar.items = [
|
|
|
|
UIBarButtonItem(title: "CW", style: .plain, target: nil, action: #selector(ComposeHostingController.cwButtonPressed)),
|
|
|
|
visibilityButton,
|
|
|
|
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
|
|
] + createFormattingButtons(coordinator: context.coordinator) + [
|
|
|
|
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
|
|
UIBarButtonItem(title: "Drafts", style: .plain, target: nil, action: #selector(ComposeHostingController.draftsButtonPresed)),
|
|
|
|
]
|
|
|
|
textView.inputAccessoryView = toolbar
|
|
|
|
// can't modify @State during view update
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.visibilityButton = visibilityButton
|
|
|
|
}
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
|
|
|
|
|
|
|
return textView
|
|
|
|
}
|
|
|
|
|
|
|
|
private func createFormattingButtons(coordinator: Coordinator) -> [UIBarButtonItem] {
|
|
|
|
guard Preferences.shared.statusContentType != .plain else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
|
|
|
|
let item: UIBarButtonItem
|
|
|
|
if let image = format.image {
|
|
|
|
item = UIBarButtonItem(image: image, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
|
|
|
} else if let (str, attributes) = format.title {
|
|
|
|
item = UIBarButtonItem(title: str, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
|
|
|
item.setTitleTextAttributes(attributes, for: .normal)
|
|
|
|
item.setTitleTextAttributes(attributes, for: .highlighted)
|
|
|
|
} else {
|
|
|
|
fatalError("StatusFormat must have either an image or a title")
|
|
|
|
}
|
|
|
|
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
|
|
|
item.accessibilityLabel = format.accessibilityLabel
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
|
|
|
|
for i in (1..<StatusFormat.allCases.count).reversed() {
|
|
|
|
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
|
|
|
spacer.width = 8
|
|
|
|
formatButtons.insert(spacer, at: i)
|
|
|
|
}
|
|
|
|
|
|
|
|
return formatButtons
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:07:41 -04:00
|
|
|
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
|
|
|
if #available(iOS 14.0, *) {
|
|
|
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
|
|
|
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
|
|
|
|
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
|
|
|
self.uiState.draft.visibility = visibility
|
|
|
|
}
|
|
|
|
}
|
|
|
|
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-31 19:28:50 -04:00
|
|
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
|
|
|
uiView.text = text
|
2020-09-07 14:46:17 -04:00
|
|
|
if let visibilityButton = visibilityButton {
|
|
|
|
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
|
|
|
updateVisibilityMenu(visibilityButton)
|
|
|
|
}
|
2020-08-31 19:28:50 -04:00
|
|
|
context.coordinator.text = $text
|
|
|
|
context.coordinator.didChange = textDidChange
|
|
|
|
context.coordinator.uiState = uiState
|
|
|
|
|
|
|
|
if becomeFirstResponder {
|
|
|
|
DispatchQueue.main.async {
|
2020-09-06 23:27:43 -04:00
|
|
|
// calling becomeFirstResponder during the SwiftUI update causes a crash on iOS 13
|
|
|
|
uiView.becomeFirstResponder()
|
|
|
|
// can't update @State vars during the SwiftUI update
|
2020-08-31 19:28:50 -04:00
|
|
|
becomeFirstResponder = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// wait until the next runloop iteration so that SwiftUI view updates have finished and
|
|
|
|
// the text view knows its new content size
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.textDidChange(uiView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
|
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
|
|
|
}
|
|
|
|
|
|
|
|
class Coordinator: NSObject, UITextViewDelegate {
|
|
|
|
weak var textView: UITextView?
|
|
|
|
var text: Binding<String>
|
|
|
|
var didChange: (UITextView) -> Void
|
|
|
|
var uiState: ComposeUIState
|
|
|
|
|
|
|
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
|
|
|
self.text = text
|
|
|
|
self.didChange = didChange
|
|
|
|
self.uiState = uiState
|
|
|
|
}
|
|
|
|
|
|
|
|
func textViewDidChange(_ textView: UITextView) {
|
|
|
|
text.wrappedValue = textView.text
|
|
|
|
didChange(textView)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
|
|
|
guard let textView = textView, textView.isFirstResponder else { return }
|
|
|
|
let format = StatusFormat.allCases[sender.tag]
|
|
|
|
guard let insertionResult = format.insertionResult else { return }
|
|
|
|
|
|
|
|
let currentSelectedRange = textView.selectedRange
|
|
|
|
if currentSelectedRange.length == 0 {
|
|
|
|
textView.insertText(insertionResult.prefix + insertionResult.suffix)
|
|
|
|
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
|
|
|
|
} else {
|
|
|
|
let start = textView.text.index(textView.text.startIndex, offsetBy: currentSelectedRange.lowerBound)
|
|
|
|
let end = textView.text.index(textView.text.startIndex, offsetBy: currentSelectedRange.upperBound)
|
|
|
|
let selectedText = textView.text[start..<end]
|
|
|
|
textView.insertText(String(insertionResult.prefix + selectedText + insertionResult.suffix))
|
|
|
|
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.prefix.utf16.count, length: currentSelectedRange.length)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func keyboardWillShow(_ notification: Foundation.Notification) {
|
|
|
|
uiState.delegate?.keyboardWillShow(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func keyboardWillHide(_ notification: Foundation.Notification) {
|
|
|
|
uiState.delegate?.keyboardWillHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func keyboardDidHide(_ notification: Foundation.Notification) {
|
|
|
|
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|