One approach...
- Use "standard" scroll view zoom/pan functionality
- Use image view as
viewForZooming
in the scroll view
- add "marker views" to the scroll view as siblings of the image view (not subviews of the image view)
For the marker positions, calculate the percent location. So, for example, if your image is 1000x1000, and you want a marker at 100,200
, that marker would have a "point percentage" of 0.1,0.2
. When the image view is zoomed, change the frame origin of the marker by its location percentages.
Here is a complete example (done very quickly, so just to get you going)...
I used this 1600 x 1600
image, with marker locations:

A simple "marker view" class:
class MarkerView: UILabel {
var yPCT: CGFloat = 0
var xPCT: CGFloat = 0
}
And the controller class:
class ZoomWithMarkersViewController: UIViewController, UIScrollViewDelegate {
let imgView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var points: [CGPoint] = [
CGPoint(x: 200, y: 200),
CGPoint(x: 800, y: 300),
CGPoint(x: 500, y: 700),
CGPoint(x: 1100, y: 900),
CGPoint(x: 300, y: 1200),
CGPoint(x: 1300, y: 1400),
]
var markers: [MarkerView] = []
override func viewDidLoad() {
super.viewDidLoad()
// make sure we have an image
guard let img = UIImage(named: "points1600x1600") else {
fatalError("Could not load image!!!!")
}
// set the image
imgView.image = img
// add the image view to the scroll view
scrollView.addSubview(imgView)
// add scroll view to view
view.addSubview(scrollView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// to save on typing
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// scroll view inset 20-pts on each side
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
// square (1:1 ratio)
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor),
// center vertically
scrollView.centerYAnchor.constraint(equalTo: safeG.centerYAnchor),
// constrain all 4 sides of image view to scroll view's Content Layout Guide
imgView.topAnchor.constraint(equalTo: contentG.topAnchor),
imgView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// we will want zoom scale of 1 to show the "native size" of the image
imgView.widthAnchor.constraint(equalToConstant: img.size.width),
imgView.heightAnchor.constraint(equalToConstant: img.size.height),
])
// create marker views and
// add them as subviews of the scroll view
// add them to our array of marker views
var i: Int = 0
points.forEach { pt in
i += 1
let v = MarkerView()
v.textAlignment = .center
v.font = .systemFont(ofSize: 12.0)
v.text = "\(i)"
v.backgroundColor = UIColor.green.withAlphaComponent(0.5)
scrollView.addSubview(v)
markers.append(v)
v.yPCT = pt.y / img.size.height
v.xPCT = pt.x / img.size.width
v.frame = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
}
// assign scroll view's delegate
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(#function)
guard let img = imgView.image else { return }
// max scale is 1.0 (original image size)
scrollView.maximumZoomScale = 1.0
// min scale fits the image in the scroll view frame
scrollView.minimumZoomScale = scrollView.frame.width / img.size.width
// start at min scale (so full image is visible)
scrollView.zoomScale = scrollView.minimumZoomScale
// just to make the markers "appear" nicely
markers.forEach { v in
v.center = CGPoint(x: scrollView.bounds.midX, y: scrollView.bounds.midY)
v.alpha = 0.0
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// animate the markers into position
UIView.animate(withDuration: 1.0, animations: {
self.markers.forEach { v in
v.alpha = 1.0
}
self.updateMarkers()
})
}
func updateMarkers() -> Void {
markers.forEach { v in
let x = imgView.frame.origin.x + v.xPCT * imgView.frame.width
let y = imgView.frame.origin.y + v.yPCT * imgView.frame.height
// for example:
// put bottom-left corner of marker at coordinates
v.frame.origin = CGPoint(x: x, y: y - v.frame.height)
// or
// put center of marker at coordinates
//v.center = CGPoint(x: x, y: y)
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateMarkers()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imgView
}
}
I'm placing the markers so their bottom-left corner is at the marker-point.
It starts like this:

and looks like this after zooming-in on marker #3:
