😸

NSAttributedStringを利用してHTMLをSwiftUIで表示する

に公開

<a>タグが含まれる文字列を表示する要件はよくあることだと思いますが、今回SwiftUI上で実装しましたので、その際に工夫した点などを紹介します。
NSAttributedStringを利用してHTMLタグをUIKitで表示はよくある方法だと思いますので、そちらは他の方の記事を参照いただければと思います。

以下が実装です。

extension String {
    func convertToAttributedString(with fontSize: CGFloat, color: Color) -> AttributedString {
        // htmlをNSAttributedStringに変換する
        guard let data = data(using: .utf8, allowLossyConversion: true),
              let attributedString = try? NSAttributedString(
                  data: data,
                  options: [
                      .documentType: NSAttributedString.DocumentType.html,
                      .characterEncoding: String.Encoding.utf8.rawValue
                  ],
                  documentAttributes: nil
              ) else { return AttributedString(NSAttributedString(string: self)) }

        let fullRange = NSRange(location: 0, length: attributedString.length)
        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
        let systemFont = UIFont.systemFont(ofSize: fontSize)

        // 指定のテキストカラーに変換する
        mutableAttributedString.addAttributes([
            .foregroundColor: UIColor(color)
        ], range: fullRange)

        mutableAttributedString.enumerateAttributes(in: fullRange) { objects, range, _ in
            if let attributedFont = objects[NSAttributedString.Key.font] as? UIFont {
                // 標準フォント準拠でfontDescriptorを作成(iOSのバージョンによってfontが変わるためsystemFontを利用)
                let fontDescriptor = attributedFont.fontDescriptor.withFamily(systemFont.familyName)
                // 指定のフォントサイズに変換する
                let scaledFont = UIFont(descriptor: fontDescriptor, size: fontSize)
                mutableAttributedString.addAttribute(.font, value: scaledFont, range: range)
            }
            if objects[NSAttributedString.Key.link] is URL {
                // リンクは標準のカラーを利用
                mutableAttributedString.addAttributes([
                    .foregroundColor: UIColor.link
                ], range: range)
            }
        }
        // AttributedStringに変換して返す
        return AttributedString(mutableAttributedString)
    }
}

CSSを利用する方法もありますが、systemFontを利用したいためにenumerateAttribute(_:in:options:using:)を使いました。

工夫した点としましてはリンク部分のみ色を変えた部分です!
こちらを指定しないと文字がすべて同じ色になってしまいます!

if objects[NSAttributedString.Key.link] is URL {
    // リンクは標準のカラーを利用
    mutableAttributedString.addAttributes([
        .foregroundColor: UIColor.link
    ], range: range)
}

あとはこのextensionをSwiftUIで表示するだけ、簡単ですね!

/// HTMLタグを含むテキストを表示するためのコンポーネント
public struct TextWithHTML: View {
    @State private var attributedString: AttributedString = ""
    var text: String
    var fontSize: CGFloat
    var textColor: Color

    public init(
        text: String,
        fontSize: CGFloat = 14,
        textColor: Color = Color.white,
    ) {
        self.text = text
        self.fontSize = fontSize
        self.textColor = textColor
    }

    public var body: some View {
        Text(attributedString)
            .task {
                attributedString = text.convertToAttributedString(with: fontSize, color: textColor)
            }
    }
}

苦労したところ

以下の様に同期的に実装するとAttributeGraph: cycle detected through attributeとコンソールに表示されてしまいます。こちらを回避するためには非同期で実装すると良いかと思います!

失敗例

public var body: some View {
    Text(text.convertToAttributedString(with: fontSize, color: textColor))
}

うまく行った実装

public var body: some View {
    Text(attributedString)
        .task {
            attributedString = text.convertToAttributedString(with: fontSize, color: textColor)
        }
}

さいごに

今回の記事を書いた経緯としましては最後に書きました失敗例があったからになります。
循環参照を回避するために結構苦労し、サクッと終わる実装だと思っていた仕様変更に時間を取られてしまいました!
本記事がみなさまの業務に役立てられればと思います!

参照

https://qiita.com/saku/items/031eb456345630cf8bd4

株式会社バニッシュ・スタンダード

Discussion