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)
}
Вы видите, в чем проблема? Или у вас другой подход?