How to track the scaling of a UIScrollView?
I have a scaling UIScrollView and a non-scalable overlay view on which I animate the markers. These markers should track the location of some UIScrollView content (much like a dropped pin should track a spot on a map when panning and zooming).
I do this by calling the update of the overlay view in response to the UIScrollView layoutSubviews
. This works and the overlays are great when zooming and panning.
But when the pinch gesture ends, the UIScrollView automatically performs the final animation, and the overlay view is out of sync throughout this animation.
I made a simplified project to isolate this issue. The UIScrollView contains an orange square, and the overlay shows a 2-pixel red outline around the border of that orange square. As you can see below, the red outline always moves to where it should be, except for a short period of time after being touched, when it clearly jumps to the final position of the orange square.
The complete Xcode project for this test is available here: https://github.com/Clafou/ScrollviewZoomTrackTest , but all the code is in the two files shown below:
TrackedScrollView.swift
class TrackedScrollView: UIScrollView {
@IBOutlet var overlaysView: UIView?
let square: UIView
required init(coder aDecoder: NSCoder) {
square = UIView(frame: CGRect(x: 50, y: 300, width: 300, height: 300))
square.backgroundColor = UIColor.orangeColor()
super.init(coder: aDecoder)
self.addSubview(square)
self.maximumZoomScale = 1
self.minimumZoomScale = 0.5
self.contentSize = CGSize(width: 500, height: 900)
self.delegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
overlaysView?.setNeedsLayout()
}
}
extension TrackedScrollView: UIScrollViewDelegate {
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return square
}
}
OverlaysView.swift
class OverlaysView: UIView {
@IBOutlet var trackedScrollView: TrackedScrollView?
let outline: CALayer
required init(coder aDecoder: NSCoder) {
outline = CALayer()
outline.borderColor = UIColor.redColor().CGColor
outline.borderWidth = 2
super.init(coder: aDecoder)
self.layer.addSublayer(outline)
}
override func layoutSubviews() {
super.layoutSubviews()
if let trackedScrollView = self.trackedScrollView {
CATransaction.begin()
CATransaction.setDisableActions(true)
let frame = trackedScrollView.convertRect(trackedScrollView.square.frame, toView: self)
outline.frame = CGRectIntegral(CGRectInset(frame, -3, -3))
CATransaction.commit()
}
}
}
Among the things I tried was using CADisplayLink
and presentationLayer
, and it allowed me to animate the overlay, but the coordinates I got from presentationLayer
were slightly behind the actual UIScrollView, so it still doesn't look straight. I think the correct approach would be to tie my overlay update to the system generated UIScrollView animation, but I haven't had time to hack that yet.
How can I update this code to always track the scaling of the UIScrollView content?
source to share
UIScrollView
sends scrollViewDidZoom:
to its delegate in its animation block if it "bounces" to the minimum or maximum magnification when the pinch ends. Update the frames of your overlays to scrollViewDidZoom:
if zoomBouncing
true. If you are using automatic call layout layoutIfNeeded
.
scrollViewDidZoom:
is called only once during a zoom animation, but tweaking your frames or calling layoutIfNeeded
will ensure that those changes animate along with the zoom bounce, thus keeping them perfect.
Demo:
Fixed example project: https://github.com/mayoff/ScrollviewZoomTrackTest
source to share