Textview

自适应 TextView 的实现。

import SwiftUI

fileprivate struct CustomUITextView: UIViewRepresentable {
    typealias UIViewType = UITextView
    @Binding var text: String
    @Binding var height: CGFloat?

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
            
        textView.delegate = context.coordinator
        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = .zero
        textView.backgroundColor = .clear
        textView.font = .preferredFont(forTextStyle: .body)
        textView.showsVerticalScrollIndicator = false
        
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
        let width = uiView.frame.size.width
        let newSize = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
        if newSize.height != height {
            DispatchQueue.main.async {
                self.height = newSize.height
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text, height: $height)
    }
}

extension CustomUITextView {
    final class Coordinator: NSObject, UITextViewDelegate {
        @Binding var text: String
        @Binding var height: CGFloat?
        
        init(text: Binding<String>, height: Binding<CGFloat?>) {
            self._text = text
            self._height = height
        }
        
        func textViewDidChange(_ textView: UITextView) {
            // 中文输入未完成时,直接返回
            if textView.markedTextRange?.isEmpty == false { return }
            
            self.text = textView.text
            let width = textView.frame.size.width
            let newSize = textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
            if newSize.height != height {
                DispatchQueue.main.async {
                    self.height = newSize.height
                }
            }
        }
    }
}

struct TextView: View {
    private let placeholder: LocalizedStringKey
    private let maxHeight: CGFloat
    @Binding private var text: String
    @State private var height: CGFloat?
    
    init(_ placeholder: LocalizedStringKey, text: Binding<String>, maxHeight: CGFloat) {
        self.placeholder = placeholder
        self.maxHeight = maxHeight
        self._text = text
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            if text.isEmpty  {
                Text(placeholder)
                    .foregroundColor(Color.primary.opacity(0.25))
            }
            CustomUITextView(text: $text, height: $height)
                .frame(height: (height ?? 0) < maxHeight ? height : maxHeight)
        }
    }
}