如何在 SwiftUI 列表中以正确的高度呈现多行文本?

How to render multiline text in SwiftUI List with correct height?

我想要一个显示多行文本的 SwiftUI 视图,并满足以下要求:

感觉最合适的解决方案是拥有一个列表视图,包装原生 UITextView/NSTextView。

这是我目前所拥有的。它实现了大部分要求,除了具有正确的行高。

//
//  ListWithNativeTexts.swift
//  SUIToy
//
//  Created by Jaanus Kase on 03.05.2020.
//  Copyright © 2020 Jaanus Kase. All rights reserved.
//

import SwiftUI

let number = 20

struct ListWithNativeTexts: View {
    var body: some View {
        List(texts(count: number), id: \.self) { text in
            NativeTextView(string: text)
        }
    }
}

struct ListWithNativeTexts_Previews: PreviewProvider {
    static var previews: some View {
        ListWithNativeTexts()
    }
}

func texts(count: Int) -> [String] {
    return (1...count).map {
        (1...[=10=]).reduce("Hello https://example.com:", { [=10=] + " " + String() })
    }
}

#if os(iOS)
typealias NativeFont = UIFont
typealias NativeColor = UIColor

struct NativeTextView: UIViewRepresentable {

    var string: String

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()

        textView.isEditable = false
        textView.isScrollEnabled = false
        textView.dataDetectorTypes = .link
        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textView.textContainer.lineFragmentPadding = 0

        let attributed = attributedString(for: string)
        textView.attributedText = attributed

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
    }

}
#else
typealias NativeFont = NSFont
typealias NativeColor = NSColor

struct NativeTextView: NSViewRepresentable {

    var string: String

    func makeNSView(context: Context) -> NSTextView {
        let textView = NSTextView()
        textView.isEditable = false
        textView.isAutomaticLinkDetectionEnabled = true
        textView.isAutomaticDataDetectionEnabled = true
        textView.textContainer?.lineFragmentPadding = 0
        textView.backgroundColor = NSColor.clear

        textView.textStorage?.append(attributedString(for: string))
        textView.isEditable = true
        textView.checkTextInDocument(nil) // make links clickable
        textView.isEditable = false

        return textView
    }

    func updateNSView(_ textView: NSTextView, context: Context) {

    }

}
#endif

func attributedString(for string: String) -> NSAttributedString {
    let attributedString = NSMutableAttributedString(string: string)
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.lineSpacing = 4
    let range = NSMakeRange(0, (string as NSString).length)

    attributedString.addAttribute(.font, value: NativeFont.systemFont(ofSize: 24, weight: .regular), range: range)
    attributedString.addAttribute(.foregroundColor, value: NativeColor.red, range: range)
    attributedString.addAttribute(.backgroundColor, value: NativeColor.yellow, range: range)
    attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
    return attributedString
}

这是它在 iOS 上的输出。 macOS 输出类似。

如何获得此解决方案以调整文本视图的高度?

我尝试过但未在此处显示的一种方法是“从外向内”给出高度 - 以指定列表行本身的框架高度。当我知道宽度时,我可以计算 NSAttributedString 的高度,我可以使用 geoReader 获得它。这几乎可以工作,但是有错误,而且感觉不对,所以我不在这里展示。

耶阿努斯

我不确定我是否完全理解您的问题,但是您可以添加一些环境变量和 Insets 以更改 SwiftUI 列表视图间距...这是我正在谈论的示例。

请注意,将它们添加到右视图很重要,listRowInsets 在 ForEach 上,环境在列表视图上。

    List {
      ForEach((0 ..< self.selections.count), id: \.self) { column in
        HStack(spacing:0) {
          Spacer()
            Text(self.selections[column].name)
            .font(Fonts.avenirNextCondensedBold(size: 22))    
          Spacer()
        }
      }.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
    }.environment(\.defaultMinListRowHeight, 20)
      .environment(\.defaultMinListHeaderHeight, 0)
      .frame(width: UIScreen.main.bounds.size.width, height: 180.5, alignment: .center)
      .offset(x: 0, y: -64)

马克

调整列表行的大小不适用于 SwiftUI。

但是,我已经弄清楚如何在堆栈中显示本机 UITextView 的滚动,其中每个项目都根据其 attributedText 的高度动态调整大小。

I have put 2 point spacing between each item and tested with 80 items using your text generator.

Here are the first three screenshots of scroll, and another screenshot showing the very end of the scroll.

这是完整的 class,其中还包含对 attributedText 高度和常规字符串大小的扩展。

import SwiftUI

let number = 80

struct ListWithNativeTexts: View {
    let rows = texts(count:number)
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack(spacing: 2) {
                    ForEach(0..<self.rows.count, id: \.self) { i in
                        self.makeView(geometry, text: self.rows[i])
                    }
                }
            }
        }
    }
    func makeView(_ geometry: GeometryProxy, text: String) -> some View {
        print(geometry.size.width, geometry.size.height)

        // for a regular string size (not attributed text)
//        let textSize = text.size(width: geometry.size.width, font: UIFont.systemFont(ofSize: 17.0, weight: .regular), padding: UIEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0))
//        print("textSize: \(textSize)")
//        return NativeTextView(string: text).frame(width: geometry.size.width, height: textSize.height)
        let attributed = attributedString(for: text)
        let height = attributed.height(containerWidth: geometry.size.width)
        print("height: \(height)")
        return NativeTextView(string: text).frame(width: geometry.size.width, height: height)
    }
}

struct ListWithNativeTexts_Previews: PreviewProvider {
    static var previews: some View {
        ListWithNativeTexts()
    }
}

func texts(count: Int) -> [String] {
    return (1...count).map {
        (1...[=10=]).reduce("Hello https://example.com:", { [=10=] + " " + String() })
    }
}

#if os(iOS)
typealias NativeFont = UIFont
typealias NativeColor = UIColor

struct NativeTextView: UIViewRepresentable {

    var string: String

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()

        textView.isEditable = false
        textView.isScrollEnabled = false
        textView.dataDetectorTypes = .link
        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
         textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textView.textContainer.lineFragmentPadding = 0

        let attributed = attributedString(for: string)
        textView.attributedText = attributed

        // for a regular string size (not attributed text)
//        textView.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
//        textView.text = string

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
    }

}
#else
typealias NativeFont = NSFont
typealias NativeColor = NSColor

struct NativeTextView: NSViewRepresentable {

    var string: String

    func makeNSView(context: Context) -> NSTextView {
        let textView = NSTextView()
        textView.isEditable = false
        textView.isAutomaticLinkDetectionEnabled = true
        textView.isAutomaticDataDetectionEnabled = true
        textView.textContainer?.lineFragmentPadding = 0
        textView.backgroundColor = NSColor.clear

        textView.textStorage?.append(attributedString(for: string))
        textView.isEditable = true
        textView.checkTextInDocument(nil) // make links clickable
        textView.isEditable = false

        return textView
    }

    func updateNSView(_ textView: NSTextView, context: Context) {

    }

}
#endif

func attributedString(for string: String) -> NSAttributedString {
    let attributedString = NSMutableAttributedString(string: string)
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.lineSpacing = 4
    let range = NSMakeRange(0, (string as NSString).length)

    attributedString.addAttribute(.font, value: NativeFont.systemFont(ofSize: 24, weight: .regular), range: range)
    attributedString.addAttribute(.foregroundColor, value: NativeColor.red, range: range)
    attributedString.addAttribute(.backgroundColor, value: NativeColor.yellow, range: range)
    attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
    return attributedString
}

extension String {
    func size(width:CGFloat = 220.0, font: UIFont = UIFont.systemFont(ofSize: 17.0, weight: .regular), padding: UIEdgeInsets? = nil) -> CGSize {
        let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
        label.numberOfLines = 0
        label.lineBreakMode = NSLineBreakMode.byWordWrapping
        label.font = font
        label.text = self

        label.sizeToFit()

        if let pad = padding{
         // add padding
            return CGSize(width: label.frame.width + pad.left + pad.right, height: label.frame.height + pad.top + pad.bottom)
        } else {
        return CGSize(width: label.frame.width, height: label.frame.height)
        }
    }
}

extension NSAttributedString {

    func height(containerWidth: CGFloat) -> CGFloat {

        let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
                                     options: [.usesLineFragmentOrigin, .usesFontLeading],
                                     context: nil)
        return ceil(rect.size.height)
    }

    func width(containerHeight: CGFloat) -> CGFloat {

        let rect = self.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: containerHeight),
                                     options: [.usesLineFragmentOrigin, .usesFontLeading],
                                     context: nil)
        return ceil(rect.size.width)
    }
}