UIScrollViewでコンテンツの拡大縮小を行う

はじめに

実装を進めていく中で当初CGAffineTransformで拡大縮小できないか検証をしていたんですが、絶対にScrollViewの方が簡単なのでScrollViewで作りましょう!

CGAffineTransformでやる場合Gestureを拡大縮小したいViewに対してかけるので、Viewが範囲外の領域に出てしまうと操作ができなくなってしまったり使いにくい形になってしまいました。

実装

public class MediaGestureView: UIView {
    private let scrollView: UIScrollView = UIScrollView()
    private let zoomingView: UIView
    private let contentSize: CGSize
    private let doubleTapZoomScale: CGFloat

    /// Initializer
    /// - Parameters:
    ///   - zoomingView: 拡大する対象のView
    ///   - contentSize: 拡大するViewのサイズ(ImageViewはImageのサイズ)
    ///   - maximumZoomScale: 最大拡大倍率
    ///   - minimumZoomScale: 最小拡大倍率
    ///   - doubleTapZoomScale: ダブルタップで拡大する倍率
    init(zoomingView: UIView, contentSize: CGSize, delegate: MediaGestureViewDelegate, maximumZoomScale: CGFloat = 10, minimumZoomScale: CGFloat = 1, doubleTapZoomScale: CGFloat = 5) {
        self.zoomingView = zoomingView
        self.contentSize = CGSize(width: abs(contentSize.width), height: abs(contentSize.height))
        self.doubleTapZoomScale = doubleTapZoomScale
        super.init(frame: .zero)

        scrollView.delegate = self
        scrollView.maximumZoomScale = maximumZoomScale
        scrollView.minimumZoomScale = minimumZoomScale
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.contentInsetAdjustmentBehavior = .never
        zoomingView.isUserInteractionEnabled = true

        scrollView.addSubview(zoomingView)
        addSubview(scrollView)

        let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTap(_:)))
        doubleTapGesture.numberOfTapsRequired = 2
        zoomingView.isUserInteractionEnabled = true
        zoomingView.addGestureRecognizer(doubleTapGesture)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func layoutSubviews() {
        super.layoutSubviews()
        scrollView.frame = bounds

        adjustZoomingViewSize()
        updateContentSize()
        updateContentInset()
    }

    private func adjustZoomingViewSize() {
        let rate = min(scrollView.bounds.width / contentSize.width, scrollView.bounds.height / contentSize.height)
        zoomingView.frame.size = CGSize(width: contentSize.width * rate, height: contentSize.height * rate)
    }

    private func updateContentSize() {
        scrollView.contentSize = zoomingView.frame.size
    }

    private func updateContentInset() {
        let edgeInsets = UIEdgeInsets(
            top: max((self.frame.height - zoomingView.frame.height) / 2, 0),
            left: max((self.frame.width - zoomingView.frame.width) / 2, 0),
            bottom: 0,
            right: 0)
        scrollView.contentInset = edgeInsets
    }
}

// MARK: Gesture
extension MediaGestureView {
    @objc private func doubleTap(_ sender: UITapGestureRecognizer) {
        if scrollView.zoomScale > scrollView.minimumZoomScale {
            scrollView.contentInset = .init(top: .zero, left: .zero, bottom: .zero, right: .zero)
            scrollView.zoom(to: scrollView.frame, animated: true)
        } else {
            let tapPoint = sender.location(in: zoomingView)
            let size = CGSize(
                width: scrollView.frame.size.width / doubleTapZoomScale,
                height: scrollView.frame.size.height / doubleTapZoomScale)
            let origin = CGPoint(
                x: tapPoint.x - size.width / 2,
                y: tapPoint.y - size.height / 2)
            scrollView.zoom(to: CGRect(origin: origin, size: size), animated: true)
        }
    }
}

// MARK: - UIScrollViewDelegate
extension MediaGestureView: UIScrollViewDelegate {
    public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return zoomingView
    }

    public func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateContentInset()
    }
}

解説

ScrollViewで作っているのでピンチイン、ピンチアウトはviewForZoomingで返却したUIViewを自動でいい感じに処理してくれます。

あとはよくあるダブルタップで特定の倍率まで拡大するような処理を入れています。

基本的にはMediaGestureViewにViewを渡すことでどんなViewでも拡大縮小できるはずです🙋

scrollViewDidZoomについて

表示するコンテンツの幅が端末の幅と一致していればこの処理は必要ないのですが、端末より横幅や縦幅が小さい場合、拡大後も原点が変わらないので表示位置がずれてしまいます。

そのため拡大してずれた分だけcontentInsetに入れて位置調整をすることで綺麗に表示することが可能になります。

参考文献

SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる

【Swift】ScrollViewとImageViewを使ってスクロール・ズーム・クロップ機能を実装してみた(LINEのアイコン登録機能風)

UIGestureRecognizerに入門した

ScrollView でズームした部分画像