UITextViewにはAttributedTextが使えるため、画像やEmojiを含んだウェブページ的な表現が出来ます。
「これで画像タップ時にプレビューとかもできたらな…」と思ったことはないですか?
僕はありました。
というわけで、以下のようにタップされた画像を開く方法を紹介します。
デフォルトではそのまんまの機能はなかったので、それを実現するコードと使い方の説明になります。
※ タップ判定が概ね正しいことを示すため、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
}
}
基本の使い方は以下の通りです。
- タッププレビューを付けたいUITextViewに対して
UITapGestureRecognizer
を追加し、textAreaTapped
を呼ぶようにする - UITextViewにextension等で
getPartsOfRange
を追加し、Rangeを指定してパーツを吐けるようにする - imageがとれたら、ViewControllerに渡してプレビュー。
※ このコードには画像閲覧用ViewControllerは付属しません
この例ではプレビューする画像はタップした1枚だけにしましたが、少し書き直して全ての画像を取得すればめくれるプレビューの実装も簡単と思います。応用してみてください。