【iOS】UITextViewでタップ遷移可能なバナーを表示する方法

はじめに

UITextViewにHtmlの記述を反映させたいことがあります。その場合は下記のようなコードを書くことで、<a>タグでのリンクの表示などが反映可能です。 Stringのextensionメソッドとして作成しておくと便利です。

extension String {
    func convertToHtml() -> NSAttributedString {
        // Stringをdata化
        guard let data = self.data(using: .utf8) else { return NSAttributedString(string: self) }
        
        // dataからNSAttributedStringを生成
        // この際にdocument typeを.htmlとすることでhtmlのタグを機能させることができます
        guard let attributedString = try? NSAttributedString(
            data: data,
            options: [.documentType: NSAttributedString.DocumentType.html,
                      .characterEncoding: String.Encoding.utf8.rawValue],
            documentAttributes: nil) else { return NSAttributedString(string: self) }
        return attributedString
    }
}

しかし、バナーのような画像を含めた場合、そのままでは画面サイズにうまく合わせることができません。例えば<img src="${banner image url}" alt="サンプル" width=100%> とした場合に表示するUITextViewの横幅いっぱいにバナー画像が広がって欲しいですが、そうはいきません。

これは、画像部分がUIImageやUIImageViewではなく、NSTextAttachmentによって作成されているためです。 本稿ではそのようなHtmlに含んだバナーを適正なサイズにして表示する方法を記載します。

環境設定

以下の環境を使用しています。

内容

いかに作成したextensionメソッドを記載します。

extension NSAttributedString {

    /// htmlに含まれる画像部分をwidthの幅まで縦横比を保ちながら拡大・圧縮します
    func resizeHtmlImages(to width: CGFloat) -> NSAttributedString {
        
        let mutableString = NSMutableAttributedString(attributedString: self)
        
        // NSMutableAttributedStringのattributeからNSTextAttachmentを探します
        // Html内に含まれる<img>タグはNSTextAttachmentとして扱われいます
        // NSRange(location: 0, length: mutableString.length)で文字列全体を指定しています
        mutableString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: mutableString.length), options: []) { (value, range, _) in
            
            if let attachment = value as? NSTextAttachment {
                
                // 見つかったNSTextAttachmentから画像を取得します
                guard let image = attachment.image ?? attachment.image(
                    forBounds: attachment.bounds, textContainer: nil, characterIndex: range.location) else { return }
                
                // 画像を比率を保ったまま拡大・縮小します
                let ratio = width / image.size.width
                guard let resizedImage = image.resize(ratio: ratio) else { return }
                let resizedAttachment = NSTextAttachment()
                resizedAttachment.image = resizedImage

                // ここが本稿の肝です。
                // 画像タップ時のイベントハンドリングなどを除去しないため、画像のattributeのみ除去し、リサイズ後のattributeに入れ替えます。
                mutableString.removeAttribute(.attachment, range: range)
                mutableString.addAttribute(.attachment, value: resizedAttachment, range: range)
            }
        }
        
        return mutableString
    }

}

ここでの肝はMutableNSAttributedStringに含まれるNSTextAttachmentの画像サイズを変換し、Attributeを入れ替える際に下記のようにしていないことです。

let attributedString = NSAttributedString(attachment: resizedAttachment)
mutableString.replaceCharacters(in: range, with: attributedString)

検索すると上記のようにAttributeを入れ替える記載が出てきますが、このようにすると画像はリサイズされて表示されますが、 バナータップ時の画面遷移等を実現することができません。これは実際に含まれるAttributeを見ることで理解できます。

例えば次のようなhtmlタグを表示したいとします。 <a href="https://google.com"><img src="${banner image url}" alt="サンプル"></a>

NSAttriburtedStringのattributeをみるとわかりますが、下記のようにattachmentだけでなく NSLink等も含まれています。

{
    NSAttachment = "<NSTextAttachment: 0x600002bb3330> \"${image}"";
    NSColor = "kCGColorSpaceModelRGB 0 0 0.933333 1 ";
    NSFont = "<UICTFont: 0x7fa05cfeb2b0> font-family: \".SFUIText\"; font-weight: normal; font-style: normal; font-size: 18.00pt";
    NSKern = 0;
    NSLink = "https://google.com/";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 15/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n), DefaultTabInterval 36, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0";
    NSStrokeColor = "kCGColorSpaceModelRGB 0 0 0.933333 1 ";
    NSStrokeWidth = 0;
}

replaceCharactersを使用すると、該当の箇所のattributeがNSAttachmentのみになってしまい、NSLinkがなくなってしまうことで押下時の遷移等が再現できなくなります。

{
    NSAttachment = "<NSTextAttachment: 0x600000ec00e0>";
}

該当の箇所のattributeの入れ替えではなく、リサイズ前のattachmentを削除し、新しいattachmentを該当箇所のattributeとして追加することで上記の状況を回避しています。

mutableString.removeAttribute(.attachment, range: range)
mutableString.addAttribute(.attachment, value: resizedAttachment, range: range)

まとめ

NSAttributedStringを用いてHtmlを表示するとサーバーからの返却値を用いて幅広い表現が可能です。ぜひ試してみてください。