// // AttachmentDescriptionTextView.swift // ComposeUI // // Created by Shadowfacts on 3/12/23. // import SwiftUI private var placeholder: some View { Text("Describe for the visually impaired…") } struct InlineAttachmentDescriptionView: View { @ObservedObject private var attachment: DraftAttachment private let minHeight: CGFloat @State private var height: CGFloat? init(attachment: DraftAttachment, minHeight: CGFloat) { self.attachment = attachment self.minHeight = minHeight } private var placeholderOffset: CGSize { #if os(visionOS) CGSize(width: 8, height: 8) #else CGSize(width: 4, height: 8) #endif } var body: some View { ZStack(alignment: .topLeading) { if attachment.attachmentDescription.isEmpty { placeholder .font(.body) .foregroundColor(.secondary) .offset(placeholderOffset) } WrappedTextView( text: $attachment.attachmentDescription, backgroundColor: .clear, textDidChange: self.textDidChange ) .frame(height: height ?? minHeight) } } private func textDidChange(_ textView: UITextView) { height = max(minHeight, textView.contentSize.height) } } struct FocusedAttachmentDescriptionView: View { @ObservedObject var attachment: DraftAttachment var body: some View { ZStack(alignment: .topLeading) { WrappedTextView( text: $attachment.attachmentDescription, backgroundColor: .secondarySystemBackground, textDidChange: nil ) .edgesIgnoringSafeArea([.bottom, .leading, .trailing]) if attachment.attachmentDescription.isEmpty { placeholder .font(.body) .foregroundColor(.secondary) .offset(x: 4, y: 8) .allowsHitTesting(false) } } } } private struct WrappedTextView: UIViewRepresentable { typealias UIViewType = UITextView @Binding var text: String let backgroundColor: UIColor let textDidChange: (((UITextView) -> Void))? @Environment(\.isEnabled) private var isEnabled func makeUIView(context: Context) -> UITextView { let view = UITextView() view.delegate = context.coordinator view.backgroundColor = backgroundColor view.font = .preferredFont(forTextStyle: .body) view.adjustsFontForContentSizeCategory = true view.textContainer.lineBreakMode = .byWordWrapping #if os(visionOS) view.borderStyle = .roundedRect view.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) #endif return view } func updateUIView(_ uiView: UITextView, context: Context) { uiView.text = text uiView.isEditable = isEnabled context.coordinator.textView = uiView context.coordinator.text = $text context.coordinator.didChange = textDidChange if let textDidChange { // 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 { textDidChange(uiView) } } } func makeCoordinator() -> Coordinator { Coordinator(text: $text, didChange: textDidChange) } class Coordinator: NSObject, UITextViewDelegate, TextViewCaretScrolling { weak var textView: UITextView? var text: Binding var didChange: ((UITextView) -> Void)? var caretScrollPositionAnimator: UIViewPropertyAnimator? init(text: Binding, didChange: ((UITextView) -> Void)?) { self.text = text self.didChange = didChange super.init() NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil) } @objc private func keyboardDidShow() { guard let textView, textView.isFirstResponder else { return } ensureCursorVisible(textView: textView) } func textViewDidChange(_ textView: UITextView) { text.wrappedValue = textView.text didChange?(textView) ensureCursorVisible(textView: textView) } } }