I have two viewControllers:
ViewController1
A complex stack of sub viewcontrollers with somewhere in the middle an imageView
ViewController2
A scrollView with an imageView embedded in it
What I'm trying to achieve is a transition between the two viewControllers which gets triggered by pinching the imageView from viewController 1 causing it to zoom in and switch over to viewController 2. When the transition has ended, the imageView should be zoomed in as far as it's been zoomed during the pinch gesture triggered transition.
At the same time I want to support panning the image while performing the zoom transition so that just like with the zoom, the image in the end state will be transformed to the place it's been panned to.
So far I've tried the Hero transitions pod and a custom viewController transitions I wrote myself. The problem with the hero transitions is that the image doesn't properly get snapped to the end state in the second viewController. The problem I had with the custom viewController transition is that I couldn't get both zooming and panning to work at the same time.
Does anyone have an idea of how to implement this in Swift? Help is much appreciated.
The question can be divided in to two:
imageView
imageView
in vc2) positioned same as a subview (imageView
in vc1) in the presenting view controllerPinch gesture zoom: Pinch zooming is easier to implement using UIScrollView
as it supports it out of the box with out a need to add the gesture recogniser. Create a scrollView
and add the view you'd like to zoom with pinch as its subview (scrollView.addSubview(imageView)
). Don't forget to add the scrollView
itself as well (view.addSubview(scrollView)
).
Configure the scrollView's
min and max zoom scales: scrollView.minimumZoomScale, scrollView.maximumZoomScale
. Set a delegate for scrollView.delegate
and implement UIScrollViewDelegate
:
func viewForZooming(in scrollView: UIScrollView) -> UIView?
Which should return your imageView
in this case and,
Also conform to UIGestureRecognizerDelegate
and implement:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
Which should return true. This is the key that allows us have pan gesture recogniser work with the internal pinch gesture recogniser.
Pan gesture dragging: Simply create a pan gesture recogniser with a target and add it to your scroll view scrollView.addGestureRecognizer(pan)
.
Handling gestures: Pinch zoom is working nicely by this stage except you'd like to present the second view controller when pinching ends. Implement one more UIScrollViewDelegate
method to be notified when zooming ends:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
And call your method that presents the detail view controller presentDetail()
, we'll implement it in a bit.
Next step is to handle the pan gesture, I'll let the code explain itself:
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
The implementation moves imageView
around following the pan location and calls presentDetail()
when gesture ends.
Before we implement presentDetail()
, head to the detail view controller and add properties to hold imageViewFrame
and the image
itself. Now in vc1, we implement presentDetail()
as such:
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
// Note that we do not need the animation.
present(detail, animated: false, completion: nil)
}
In your DetailViewController
, make sure to set the imageViewFrame
and the image in e.g. viewDidLoad
and you'll be set.
Complete working example:
class ViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let imageView: UIImageView = UIImageView()
let scrollView: UIScrollView = UIScrollView()
lazy var pan: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = // set your image
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.frame = view.frame
let w = view.bounds.width - 30 // padding of 15 on each side
imageView.frame = CGRect(x: 0, y: 0, width: w, height: w)
imageView.center = scrollView.center
scrollView.addGestureRecognizer(pan)
}
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
presentDetail()
}
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: Private
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
present(detail, animated: false, completion: nil)
}
}
class DetailViewController: UIViewController {
let imageView: UIImageView = UIImageView()
var imageViewFrame: CGRect!
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = imageViewFrame
imageView.image = image
view.addSubview(imageView)
view.addSubview(backButton)
}
lazy var backButton: UIButton = {
let button: UIButton = UIButton(frame: CGRect(x: 10, y: 30, width: 60, height: 30))
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
button.setTitle("back", for: .normal)
return button
}()
@objc func back(_ sender: UIButton) {
dismiss(animated: false, completion: nil)
}
}