3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【iOS】UITextViewに貼り付けた画像をタップでプレビューする

Last updated at Posted at 2020-02-25

UITextViewにはAttributedTextが使えるため、画像やEmojiを含んだウェブページ的な表現が出来ます。
「これで画像タップ時にプレビューとかもできたらな…」と思ったことはないですか?
僕はありました。

というわけで、以下のようにタップされた画像を開く方法を紹介します。
デフォルトではそのまんまの機能はなかったので、それを実現するコードと使い方の説明になります。
imageview.gif
※ タップ判定が概ね正しいことを示すため、TextViewをSelectableにしています

コードと使い方

このコードは、大半を以下の質問を参考にさせていただきました。
https://stackoverflow.com/questions/48498366/detect-tap-on-images-attached-in-nsattributedstring-while-uitextview-editing-is

import UIKit

class MemoViewController: UIViewController {
    @IBOutlet weak var textArea: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // [1] UITextViewにタップ判定を追加
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textAreaTapped(sender:)))
        textArea.addGestureRecognizer(tapGesture)
    }

    @objc func textAreaTapped(sender: UITapGestureRecognizer) {
        guard case let senderView = sender.view, (senderView is UITextView) else { return }

        // [2] UITextView、LayoutManager、Locationを取得
        let textView = senderView as! UITextView
        let layoutManager = textView.layoutManager
        var location = sender.location(in: textView)
        // [3] LocationがInset分ずれるようなので訂正
        location.x -= textView.textContainerInset.left
        location.y -= textView.textContainerInset.top

        // [4] LayoutManagerを使ってタップした場所にあるAttributedTextのIndexを取得
        let textContainer = textView.textContainer
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let textStorage = textView.textStorage
        guard characterIndex < textStorage.length else { return }

        // [5] タップ位置が対象AttributedText表示領域範囲内か検証。端にある場合など、判定領域が表示領域より大きいことがあるため
        let range = NSMakeRange(characterIndex, 1)
        let attributeBounds = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)
        if attributeBounds.minX < location.x,
           attributeBounds.maxX > location.x,
           attributeBounds.minY < location.y,
           attributeBounds.maxY > location.y,
        // [6] 対象のAttributedTextのパーツを取り出し、画像ならプレビュー用ViewControllerに渡す
           let image = textView.getPartsOfRange(range).first as? UIImage {
            let viewController = ImagePreviewController(image: image) // ※プレビュー用VCは別途用意して下さい
            present(viewController, animated: true, completion: nil)
        }
    }
}

extension UITextView {
    func getPartsOfRange(_ range: NSRange) -> [AnyObject] {
        guard self.attributedText != nil else { return [] }
        var parts = [AnyObject]()

        let attributedString = self.attributedText
        attributedString?.enumerateAttributes(in: range, options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (object, range, stop) in
            if object.keys.contains(NSAttributedString.Key.attachment) {
                if let attachment = object[NSAttributedString.Key.attachment] as? NSTextAttachment {
                    if let image = attachment.image {
                        parts.append(image)
                    } else if let image = attachment.image(forBounds: attachment.bounds, textContainer: nil, characterIndex: range.location) {
                        parts.append(image)
                    }
                }
            } else {
                let stringValue : String = attributedString!.attributedSubstring(from: range).string
                if (!stringValue.trimmingCharacters(in: .whitespaces).isEmpty) {
                    parts.append(stringValue as AnyObject)
                }
            }
        }
        return parts
    }
}

基本の使い方は以下の通りです。

  1. タッププレビューを付けたいUITextViewに対してUITapGestureRecognizerを追加し、textAreaTappedを呼ぶようにする
  2. UITextViewにextension等でgetPartsOfRangeを追加し、Rangeを指定してパーツを吐けるようにする
  3. imageがとれたら、ViewControllerに渡してプレビュー。

※ このコードには画像閲覧用ViewControllerは付属しません

この例ではプレビューする画像はタップした1枚だけにしましたが、少し書き直して全ての画像を取得すればめくれるプレビューの実装も簡単と思います。応用してみてください。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?