SwiftUI TextEditor 初始内容大小错误

SwiftUI TextEditor Initial Content Size Is Wrong

iOS 14.4 + Xcode 12.4

我想在 iOS 上的 SwiftUI 中制作一个简单的清单,其中每个项目的文本是 TextEditor

首先,我创建了基本的应用程序结构并在其中填充了一些演示内容:

import SwiftUI

@main
struct TestApp: App {
  @State var alpha = "Alpha"
  @State var bravo = "Bravo is a really long one that should wrap to multiple lines."
  @State var charlie = "Charlie"
  
  init(){
    //Remove the default background of the TextEditor/UITextView
    UITextView.appearance().backgroundColor = .clear
  }
  
  var body: some Scene {
    WindowGroup {
      ScrollView{
        VStack(spacing: 7){
          TaskView(text: $alpha)
          TaskView(text: $bravo)
          TaskView(text: $charlie)
        }
        .padding(20)
      }
      .background(Color.gray)
    }
  }
}

然后每个TaskView代表列表中的一个任务(白框):

struct TaskView:View{
  @Binding var text:String
  
  var body: some View{
    HStack(alignment:.top, spacing:8){
      Button(action: {
        print("Test")
      }){
        Circle()
          .strokeBorder(Color.gray,lineWidth: 1)
          .background(Circle().foregroundColor(Color.white))
          .frame(width:22, height: 22)
      }
      .buttonStyle(PlainButtonStyle())
      
      FieldView(name: $text)
       
    }
    .frame(maxWidth: .infinity, alignment: .leading)
    .padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
    .background(Color.white)
    .cornerRadius(5)
  }
}

最后,每个 TextEditor 都在 FieldView 中,如下所示:

struct FieldView: View{
  @Binding var name: String
  var body: some View{
    ZStack{
      Text(name)
        .padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
        .opacity(0)
      TextEditor(text: $name)
        .fixedSize(horizontal: false, vertical: true)
        .padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
    }
  }
}

如您在上面的屏幕截图中所见,TextEditor 的初始高度不会自动调整大小以适合文本。但是只要我输入它,它就会适当地调整大小。这是一个视频,显示:

如何让视图具有正确的初始高度?在我输入之前,TextEditor 垂直滚动,所以它似乎有错误的固有内容大小。

注意:视图保持半透明并带有边框,因此您可以see/debug了解发生了什么。

struct FieldView: View{
    @Binding var name: String
    @State private var textEditorHeight : CGFloat = 100
    var body: some View{
        ZStack(alignment: .topLeading) {
            Text(name)
                .background(GeometryReader {
                    Color.clear
                        .preference(key: ViewHeightKey.self,
                                           value: [=10=].frame(in: .local).size.height)

                })
                //.opacity(0)
                .border(Color.pink)
                .foregroundColor(Color.red)
                
            TextEditor(text: $name)
                .padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -7))
                .frame(height: textEditorHeight + 12)
                .border(Color.green)
                .opacity(0.4)
        }
        .onPreferenceChange(ViewHeightKey.self) { textEditorHeight = [=10=] }
    }
}

struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value + nextValue()
        print("Reporting height: \(value)")
    }
}

首先,我使用 PreferenceKey 将“不可见”文本视图的高度传递到视图层次结构中。然后,我用该值设置 TextEditor 框架的高度。

请注意,视图现在对齐 topLeading -- 在您的初始示例中,不可见文本居中对齐。

我不喜欢的一件事是边缘插图的使用——这些感觉就像神奇的数字(好吧,它们是……)我宁愿有一个没有它们的解决方案仍然保持TextTextEditor 完全对齐。但是,这暂时有效。

更新,将 UIViewRepresentable 与 UITextView 一起使用

这似乎有效并避免了滚动问题:


struct TaskView:View{
    @Binding var text:String
    @State private var textHeight : CGFloat = 40
    
    var body: some View{
        HStack(alignment:.top, spacing:8){
            Button(action: {
                print("Test")
            }){
                Circle()
                    .strokeBorder(Color.gray,lineWidth: 1)
                    .background(Circle().foregroundColor(Color.white))
                    .frame(width:22, height: 22)
            }
            .buttonStyle(PlainButtonStyle())
            
            FieldView(text: $text, heightToTransmit: $textHeight)
                .frame(height: textHeight)
                .border(Color.red)
            
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
        .background(Color.white)
        .cornerRadius(5)
    }
}

struct FieldView : UIViewRepresentable {
    @Binding var text : String
    @Binding var heightToTransmit: CGFloat
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        let textView = UITextView(frame: .zero, textContainer: nil)
        textView.delegate = context.coordinator
        textView.backgroundColor = .yellow // visual debugging
        textView.isScrollEnabled = false   // causes expanding height
        context.coordinator.textView = textView
        textView.text = text
        view.addSubview(textView)

        // Auto Layout
        textView.translatesAutoresizingMaskIntoConstraints = false
        let safeArea = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
            textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
        ])
        
        return view
    }
    
    func updateUIView(_ view: UIView, context: Context) {
        context.coordinator.heightBinding = $heightToTransmit
        context.coordinator.textBinding = $text
        DispatchQueue.main.async {
            context.coordinator.runSizing()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator : NSObject, UITextViewDelegate {
        var textBinding : Binding<String>?
        var heightBinding : Binding<CGFloat>?
        var textView : UITextView?
        
        func runSizing() {
            guard let textView = textView else { return }
            textView.sizeToFit()
            self.textBinding?.wrappedValue = textView.text
            self.heightBinding?.wrappedValue = textView.frame.size.height
        }
        
        func textViewDidChange(_ textView: UITextView) {
            runSizing()
        }
    }
}