Swift で 画像ビューワを実装する その2 - ダブルタップで拡大縮小

検証環境:
Xcode 11.1
Swift 5.1

前回の続き。
今回は画像をダブルタップしたときに拡大・縮小するように実装を追加する。
ScrollView のタップ箇所をズームさせる方法は Apple のプログラミングガイドを参考に実装する。

developer.apple.com

まず、画像が乗っている ScrollView に対してダブルタップのジェスチャを追加する。

// ScrollView にダブルタップのジェスチャを追加
let gesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
gesture.numberOfTapsRequired = 2
scrollView.addGestureRecognizer(gesture)

次にタブルタップ時に呼ばれるアクション部分を実装する。
ScrollView の zoomScale が最小値(1倍)だった場合は最大値(4倍)に拡大させ、最小値(1倍)より大きい場合には最小値(1倍)に戻るように縮小する。

private let minZoomScale: CGFloat = 1.0
private let maxZoomScale: CGFloat = 4.0

// ScrollView がダブルタップされた時
@objc private func scrollViewDoubleTapped(_ gesture: UITapGestureRecognizer) {
    guard let scrollView = gesture.view as? UIScrollView else { return }
    if scrollView.zoomScale == minZoomScale {
        // タップされた場所を中心に最大に拡大する
        let location = gesture.location(in: scrollView)
        let rect = zoomRect(for: scrollView, scale: maxZoomScale, center: location)
        scrollView.zoom(to: rect, animated: true)
    } else {
        // 最小に戻す
        scrollView.setZoomScale(minZoomScale, animated: true)
    }
}

private func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
    let size = CGSize(
        width: scrollView.frame.width / scale,
        height: scrollView.frame.height / scale
    )
    let rect = CGRect(
        origin: CGPoint(
            x: center.x - size.width / 2.0,
            y: center.y - size.height / 2.0
        ),
        size: size
    )
    return rect
}

拡大時には UIScrollView の zoom(to:animated:) を使い、縮小時には setZoomScale(_:animated:) を使う。

全コード

class ImageViewerViewController: UIViewController, UIScrollViewDelegate {

    @IBOutlet private weak var scrollView: UIScrollView!

    private let minZoomScale: CGFloat = 1.0
    private let maxZoomScale: CGFloat = 4.0

    private var imageView: UIImageView = {
        let image = UIImage(named: "dog")!
        let imageView = UIImageView(image: image)
        // アスペクト比固定でフィットさせる
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupScrollView()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        updateImageView()
        updateContentInset()
    }

    private func setupScrollView() {
        scrollView.delegate = self
        scrollView.backgroundColor = .black
        scrollView.minimumZoomScale = minZoomScale
        scrollView.maximumZoomScale = maxZoomScale
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.addSubview(imageView)
        
        // ダブルタップのジェスチャを追加
        let gesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
        gesture.numberOfTapsRequired = 2
        scrollView.addGestureRecognizer(gesture)
    }
    
    // 画像のアスペクト比を維持したまま ScrollView にぴったり収まるように ImageView のサイズを調整する
    private func updateImageView() {
        guard let size = imageView.image?.size else { return }
        let wRate = scrollView.bounds.width / size.width
        let hRate = scrollView.bounds.height / size.height
        let rate = min(wRate, hRate, 1)
        imageView.frame.size = CGSize(width: size.width * rate, height: size.height * rate)
        // contentSize を画像サイズと同じにする
        scrollView.contentSize = imageView.frame.size
    }

    // ImageView が常に画面中央に配置されるように contentInset を設定する
    private func updateContentInset() {
        scrollView.contentInset = UIEdgeInsets(
            top: max((scrollView.frame.height - imageView.frame.height) / 2, 0),
            left: max((scrollView.frame.width - imageView.frame.width) / 2, 0),
            bottom: 0,
            right: 0
        )
    }

    // ズーム対象のビューを返す
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }

    // ズームのタイミングで contentInset を更新する
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateContentInset()
    }
    
    // ScrollView がダブルタップされた時
    @objc private func scrollViewDoubleTapped(_ gesture: UITapGestureRecognizer) {
        guard let scrollView = gesture.view as? UIScrollView else { return }
        if scrollView.zoomScale == minZoomScale {
            // タップされた場所を中心に最大に拡大する
            let location = gesture.location(in: scrollView)
            let rect = zoomRect(for: scrollView, scale: maxZoomScale, center: location)
            scrollView.zoom(to: rect, animated: true)
        } else {
            // 最小に戻す
            scrollView.setZoomScale(minZoomScale, animated: true)
        }
    }
    
    private func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
        let size = CGSize(
            width: scrollView.frame.width / scale,
            height: scrollView.frame.height / scale
        )
        let rect = CGRect(
            origin: CGPoint(
                x: center.x - size.width / 2.0,
                y: center.y - size.height / 2.0
            ),
            size: size
        )
        return rect
    }
}

参考:

Cocoaの日々: 簡易スライドビューア [3] ダブルタップで拡大