SwiftUI: Атрибутированная строка из HTML, которая обертывает

У меня есть две строки, которые могут содержать строки HTML.

  • Строка HTML может содержать только простое форматирование текста, такое как жирный шрифт, курсив и т. д.
  • Мне нужно объединить их в строку с атрибутами и показать в списке.
  • Каждый элемент списка может иметь начальный или конечный элемент или оба элемента одновременно.

я используюUILabelобертка, чтобы иметь вид переноса текста. Я использую эту оболочку, чтобы показать эту атрибутированную строку. Эта оболочка работает правильно, только если я установилpreferredMaxLayoutWidthценить. Поэтому мне нужно указать ширину обертке. когда текстовое представление является единственным элементом на горизонтальной оси, оно работает нормально, потому что я задаю ему постоянную ширину:

В случае списка с атрибутированными строками текстовый вид случайным образом имеет дополнительные отступы вверху и внизу:

Вот как я генерирую строку с атрибутами из строки HTML:

      func htmlToAttributedString(
    fontName: String = AppFonts.hebooRegular.rawValue,
    fontSize: CGFloat = 12.0,
    color: UIColor? = nil,
    lineHeightMultiple: CGFloat = 1
) -> NSAttributedString? {

    guard let data = data(using: .unicode, allowLossyConversion: true) else { return nil }

    do {
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineHeightMultiple = lineHeightMultiple
        paragraphStyle.alignment = .left
        paragraphStyle.lineBreakMode = .byWordWrapping
        paragraphStyle.lineSpacing = 0

        let attributedString = try NSMutableAttributedString(
            data: data,
            options: [
                .documentType: NSAttributedString.DocumentType.html,
                .characterEncoding: String.Encoding.utf8.rawValue
            ],
            documentAttributes: nil
        )
        attributedString.addAttribute(
            NSAttributedString.Key.paragraphStyle,
            value: paragraphStyle,
            range: NSMakeRange(0, attributedString.length)
        )
        
        if let font = UIFont(name: fontName, size: fontSize) {
            attributedString.addAttribute(
                NSAttributedString.Key.font,
                value: font,
                range: NSMakeRange(0, attributedString.length)
            )
        }
        
        if let color {
            attributedString.addAttribute(
                NSAttributedString.Key.foregroundColor,
                value: color,
                range: NSMakeRange(0, attributedString.length)
            )
        }
        
        return attributedString

    } catch {
        return nil
    }
}

А это моя оболочка UILabel:

      public struct AttributedLabel: UIViewRepresentable {
    
    public func makeUIView(context: Context) -> UIViewType {
        let label = UIViewType()
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        
        let result = items.map { item in
            let result = item.text.htmlToAttributedString(
                fontName: item.fontName,
                fontSize: item.fontSize,
                color: item.color,
                lineHeightMultiple: lineHeightMultiple
            )
            guard let result else {
                return NSAttributedString(string: "")
            }
            return result
        }.reduce(NSMutableAttributedString(string: "")) { result, text in
            if result.length > 0 {
                result.append(NSAttributedString(string: " "))
            }
            result.append(text)
            return result
        }
        
        let height = result.boundingRect(
            with: .init(width: width, height: .infinity),
            options: [
                .usesFontLeading
            ],
            context: nil
        )
        debugPrint("Calculated height: \(height)")
        onHeightCalculated?(height.height)
        
        label.attributedText = result
        label.preferredMaxLayoutWidth = width
        label.textAlignment = .left
        return label
    }
    
    public func updateUIView(_ uiView: UIViewType, context: Context) {
        uiView.sizeToFit()
    }
    
    public typealias UIViewType = UILabel
    public let items: [AttributedItem]
    public let width: CGFloat
    public let lineHeightMultiple: CGFloat
    public let onHeightCalculated: ((CGFloat) -> Void)?
    
    public struct AttributedItem {
        public let color: UIColor?
        public var fontName: String
        public var fontSize: CGFloat
        public var text: String
        
        public init(
            fontName: String,
            fontSize: CGFloat,
            text: String,
            color: UIColor? = nil
        ) {
            self.fontName = fontName
            self.fontSize = fontSize
            self.text = text
            self.color = color
        }
    }
    
    public init(
        items: [AttributedItem],
        width: CGFloat,
        lineHeightMultiple: CGFloat = 1,
        onHeightCalculated: ((CGFloat) -> Void)? = nil
    ) {
        self.items = items
        self.width = width
        self.lineHeightMultiple = lineHeightMultiple
        self.onHeightCalculated = onHeightCalculated
    }
}

Я используюboundingRectфункция для вычисления высоты и передачи ее в родительский вид, чтобы правильно установить высоту текстового представления. Другое текстовое представление тисков, получающее случайные значения высоты от 300 до 1000.

Этот подход точно вычисляет только высоту одной строки. Для многострочных текстов текст выходит за расчетную границу:

И это мой компонент элемента:

      private func ItemView(_ item: AppNotification) -> some View {
    HStack(alignment: .top, spacing: 10) {
        Left(item)
        SingleAxisGeometryReader(
            axis: .horizontal,
            alignment: .topLeading
        ) { width in
            AttributedLabel(
                items: [
                    .init(
                        fontName: AppFonts.hebooRegular.rawValue,
                        fontSize: 16,
                        text: item.data?.message ?? "",
                        color: UIColor(hexString: "#222222")
                    ),
                    .init(
                        fontName: AppFonts.hebooRegular.rawValue,
                        fontSize: 16,
                        text: item.timeAgo ?? "",
                        color: UIColor(hexString: "#727783")
                    )
                ],
                width: width,
                lineHeightMultiple: 0.8,
                onHeightCalculated: { textHeight = $0 }
            )
            .frame(alignment: .topLeading)
            .onTapGesture {
                if let deepLink = item.data?.deepLink {
                    viewStore.send(.openDeepLink(deepLink))
                }
            }
        }
        .frame(maxHeight: textHeight)
        .background(Color.yellow)
        Right(item)
    }
    .padding(.bottom, 16)
}

Вы видите, в чем проблема? Или у вас другой подход?

0 ответов

Другие вопросы по тегам