I have an UIImageView in an UIScrollView, and the image has size say 2000x3000. I can pinch to zoom and drag to pan on the screen. (I followed this video https://www.youtube.com/watch?v=0Tz0vI721c8).
My question is if I want to center the display at a point on the image programmatically (i.e. 600, 800), I think I need to use scrollView.setContentOffset(CGPoint), how do I figure out the CGPoint?
You have to take a couple things into account, beyond just setting the .contentOffset
to match the "target center point" ...
First, the goal...
Using this 2000 x 3000
image:
With this set of "center points":
let points: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 800, y: 600),
CGPoint(x: 1600, y: 1200),
CGPoint(x: 600, y: 2000),
CGPoint(x: 1900, y: 2900),
]
We want to programmatically set the centers to the center of the scroll view:
If all we did was set the .contentOffset
to the "target point" for #3, we'd get this:
So, we need to subtract one-half of the width and height of the scroll view frame:
scrollView.contentOffset.x = points[2].x - (scrollView.frame.width * 0.5)
scrollView.contentOffset.y = points[2].y - (scrollView.frame.height * 0.5)
That puts our "target point" in the center of the scroll view, but...
If we've zoomed out to, say, 90% (scrollView.zoomScale = 0.9
), we get this:
So, we need to translate the "target point" to the zoom scale:
// translate target point to zoomScale
var x: CGFloat = points[2].x * scrollView.zoomScale
var y: CGFloat = points[2].y * scrollView.zoomScale
x = x - (scrollView.frame.width * 0.5)
y = y - (scrollView.frame.height * 0.5)
scrollView.contentOffset.x = x
scrollView.contentOffset.y = y
and, woo hoo, we have this:
The next problem, though, is that we don't want to center the "target point" if that would exceed the scroll view limits.
For example, if we try to center on "1" or "5" we get:
and:
the Points are centered in the scroll view, but as soon as we touch it to scroll or zoom, it will snap to the corner.
We need to limit the .contentOffset
to avoid that.
So, for point "1" (points[0]
):
// translate target point to zoomScale
var x: CGFloat = points[0].x * scrollView.zoomScale
var y: CGFloat = points[0].y * scrollView.zoomScale
// don't want to set offset below Zero
let minOffset: CGPoint = .zero
// don't want to set offset greater than what will fit in the scroll view
let maxOffset: CGPoint = CGPoint(x: imgView.frame.width - scrollView.frame.width, y: imgView.frame.height - scrollView.frame.height)
// this prevents x and from being negative
x = max(minOffset.x, x - (scrollView.frame.width * 0.5))
y = max(minOffset.y, y - (scrollView.frame.height * 0.5))
// this prevents x and y from exceeding the frame of the scroll view
x = min(maxOffset.x, x)
y = min(maxOffset.y, y)
scrollView.contentOffset.x = x
scrollView.contentOffset.y = y
Here's a complete example... as in the above images, tapping a "Number Button" will scroll that center point to the center of the scroll view (or as close as possible, if it would exceed the limits). All you need is to add that 2000x3000 image (named "img2000x3000") ... no @IBOutlet
or @IBAction
connections needed:
class ScrollPosViewController: UIViewController, UIScrollViewDelegate {
let points: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 800, y: 600),
CGPoint(x: 1600, y: 1200),
CGPoint(x: 600, y: 2000),
CGPoint(x: 1900, y: 2900),
]
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .yellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let imgView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
// make sure we can load the image
guard let img = UIImage(named: "img2000x3000") else {
print("Could not load image!!!")
return
}
// assing image to image view
imgView.image = img
// create a buttons stack view
let stack: UIStackView = {
let v = UIStackView()
v.distribution = .fillEqually
v.spacing = 20
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
for i in 0..<points.count {
let b = UIButton()
b.setTitle("\(i + 1)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .systemBlue
b.layer.cornerRadius = 8
b.addTarget(self, action: #selector(centerOn(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
// add image view to scroll view
scrollView.addSubview(imgView)
// add scroll view to view
view.addSubview(scrollView)
// add buttons stack view to view
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
let c = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain background image view
// Leading / Trailing at 20-pts
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// height proportional to image size
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: img.size.height / img.size.width),
// centered vertically
scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view to all 4 sides of scroll view's Content Layout Guide
imgView.topAnchor.constraint(equalTo: c.topAnchor),
imgView.leadingAnchor.constraint(equalTo: c.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: c.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: c.bottomAnchor),
// constrain image view's width/height to image width/height
imgView.widthAnchor.constraint(equalToConstant: img.size.width),
imgView.heightAnchor.constraint(equalToConstant: img.size.height),
// constrain buttons stack view at bottom
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
let xScale = scrollView.frame.width / imgView.frame.width
let yScale = scrollView.frame.height / imgView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
@objc func centerOn(_ sender: Any?) -> Void {
guard let btn = sender as? UIButton,
let t = btn.currentTitle,
let n = Int(t),
n > 0,
n <= points.count
else {
return
}
// translate target point to zoomScale
var x: CGFloat = points[n - 1].x * scrollView.zoomScale
var y: CGFloat = points[n - 1].y * scrollView.zoomScale
// don't want to set offset below Zero
let minOffset: CGPoint = .zero
// don't want to set offset greater than what will fit in the scroll view
let maxOffset: CGPoint = CGPoint(x: imgView.frame.width - scrollView.frame.width, y: imgView.frame.height - scrollView.frame.height)
// this prevents x and from being negative
x = max(minOffset.x, x - (scrollView.frame.width * 0.5))
y = max(minOffset.y, y - (scrollView.frame.height * 0.5))
// this prevents x and y from exceeding the frame of the scroll view
x = min(maxOffset.x, x)
y = min(maxOffset.y, y)
// if we want to animate the point to the new offset
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: {
// set the new content offset
self.scrollView.contentOffset = CGPoint(x: x, y: y)
}, completion: nil)
// or, without animation
// set the new content offset
//scrollView.contentOffset = CGPoint(x: x, y: y)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imgView
}
}