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のアイコン登録機能風)