Home > OS >  I have an UIImageView in an UIScrollView. How do I center at x,y of the image programmatically?
I have an UIImageView in an UIScrollView. How do I center at x,y of the image programmatically?

Time:01-26

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 enter image description here

If all we did was set the .contentOffset to the "target point" for #3, we'd get this:

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

and:

enter image description here

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
    }
    
}

CodePudding user response:

// Assuming your image is the same width and height of 
// the scrollview, the CGPoint for the center of the image will be

let scrollViewCenterX: CGFloat = scrollView.bounds.width / 2
let scrollViewCenterY: CGFloat = scrollView.bounds.height / 2
let imageCenterPoint = CGPoint(x: scrollViewCenterX, y: scrollViewCenterY)
scrollView.setContentOffset(imageCenterPoint)


// if we want to center the scrollview on `point`
let point = CGPoint(x: 600, y: 800)
let newPoint = CGPoint(x: point.x / 2, y: point.y / 2)
scrollView.setContentOffset(newPoint)
  •  Tags:  
  • Related