macOS 上的 SwiftUI 文本字段孤立

SwiftUI text field orphan on macOS

我有一个这样的文本字段

Text("Hello, one two three four five six seven eight!")
                .frame(width:270)
                .border(.blue)

当它呈现时,它决定将 7 和 8 放在第二行,即使第一行有 space 表示 7。更糟糕的是,它决定缩进截断的顶行,使其在框架内居中。

我该如何解决这个问题,以便在不考虑孤儿的情况下正确包装文本?

编辑:忘了说我想在 macOS 上使用它。我试图将它移植到 Mac。它确实正确地左对齐文本,但它不会换行到第二行。不过,盒子的高度确实得到了相应的计算。

这是我更新后的代码:

struct NonOrphanedText: View
{
    var text: String
    
    @State private var height: CGFloat = .zero
    
    var body: some View
    {
        InternalLabelView(text: text, dynamicHeight: $height)
            .frame(maxHeight: height)
    }
    
    struct InternalLabelView: NSViewRepresentable
    {
        var text: String
        @Binding var dynamicHeight: CGFloat
        
        func makeNSView(context: Context) -> NSTextField
        {
            let label = NSTextField()
            label.isEditable = false
            label.isBezeled = false
            label.drawsBackground = false
            label.isSelectable = false
            label.maximumNumberOfLines = 5
            label.usesSingleLineMode = false
            label.lineBreakStrategy = .init()
            label.lineBreakMode = .byWordWrapping
            label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
            return label
        }
        
        func updateNSView(_ nsView: NSTextField, context: Context)
        {
            nsView.stringValue = text
            
            DispatchQueue.main.async
            {
                dynamicHeight = nsView.sizeThatFits(CGSize(width: nsView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
            }
        }
    }
}

我们需要 lineBreakStrategy,但目前在 SwiftUI 中不可用,因此可能的解决方案是使用 UILabel.

这是一个可能的解决方案。使用 Xcode 13.2 / iOS 15.2

测试

struct LabelView: View {
    var text: String

    @State private var height: CGFloat = .zero

    var body: some View {
        InternalLabelView(text: text, dynamicHeight: $height)
            .frame(maxHeight: height)
    }

    struct InternalLabelView: UIViewRepresentable {
        var text: String
        @Binding var dynamicHeight: CGFloat

        func makeUIView(context: Context) -> UILabel {
            let label = UILabel()
            label.numberOfLines = 0
            label.lineBreakStrategy = .init()          // << here !!
            label.lineBreakMode = .byWordWrapping
            label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

            return label
        }

        func updateUIView(_ uiView: UILabel, context: Context) {
            uiView.text = text

            DispatchQueue.main.async {
                dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
            }
        }
    }
}