Written by Alex Gibson, on July 2, 2018


Ever wonder how to know the exact frame of an image in a UIImageView that is using aspect fit content mode?  Ever wonder how to know the position of objects inside of a photos position across ALL device sizes. Behold the power of  AVMakeRect.  I have used this function for so many things so I decided a short tutorial and example was in order.

First create a new Xcode Project with a single view. We are going to lay the view out in only code since this is just a simple example.  At the top of the ViewController file declare these 4 variables.

 

Just below import UIKit let’s add an import for AVFoundation

 import AVFoundation

 


 lazy var imageView : UIImageView = {
        let img = UIImageView(frame: CGRect(x: 0, y: 40, width: self.view.frame.width, height: self.view.frame.height - 60))
        img.contentMode = .scaleAspectFit
        img.backgroundColor = .blue
        img.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        return img
    }()
    
    lazy var button : UIButton = {
        let button = UIButton(frame: CGRect(x: 20, y: self.view.frame.height - 50, width: self.view.frame.width - 40, height: 40))
        button.setTitle("Clear", for: .normal)
        button.addTarget(self, action: #selector(clearViews), for: .touchUpInside)
        button.setTitleColor(.black, for: .normal)
        return button
    }()
    
    var viewDots : [UIView] = []
    var imageRectView : UIView?

In this code we will be setting up an image view and a button that will clear the main view. A view array that will hold dots that we will add to the view and a UIView that we will be placing around our image inside the UIImageView. So now to the simple code. First let’s load up an image. I am going to download one so I don’t have to add anything to the project but you could use any image including loaded ones, Photo Library, and downloaded from the web. So I am going to create a function called load image.


    func loadImage(){
        URLSession.shared.dataTask(with: URL(string: "https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg?w=940&h=650&dpr=2&auto=compress&cs=tinysrgb")!) { (data, response, error) in
            guard let dt = data else{print("error");return}
            DispatchQueue.main.async {
                [weak self] in
                guard let image = UIImage(data: dt) else{print("error");return}
                self?.imageView.image = image
                self?.imageView.layoutIfNeeded()
                let realImageRect = AVMakeRect(aspectRatio: image.size, insideRect: (self?.imageView.frame)!)
                
                
                //proof
                self?.imageRectView = UIView(frame: realImageRect)
                self?.imageRectView?.layer.borderColor = UIColor.green.cgColor
                self?.imageRectView?.layer.borderWidth = 3
                self?.view.addSubview(self!.imageRectView!)
                
                let points = [CGPoint(x:0.663446030179084,y:0.469806763285024)]
                self?.loadWithPoints(points: points)
                
            }
            }.resume()
    }
    

This will do a couple of things but the most important is AVMakeRect which gives us the real frame of the image inside of the imageView.  You could do so many things with this. We also set our imageview image and also create the view rect variable using the result of AVMakeRect that you added from above. You will also notice there is a point created and a function called called loadWithPoints. Let’s create that function along with some helpers to make it all more clear.  You will be able to use AVMakeRect and the percentage of the point to translate the position across all devices.

Create these three functions that will deal with new points we want to mark on the image.


    func loadWithPoints(points:[CGPoint]){
        for point in points{
            let point = pointConverter(percentagePoint: point, frame: self.imageRectView!.frame)
            addDot(location: point)
        }
    }
    func pointConverter(percentagePoint : CGPoint,frame:CGRect)->CGPoint{
        return CGPoint(x: frame.width * percentagePoint.x + frame.minX, y: frame.height * percentagePoint.y + frame.minY)
    }
    
    func addDot(location:CGPoint){
        let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
        dot.backgroundColor = .white
        dot.layer.cornerRadius = 5
        dot.center = location
        viewDots.append(dot)
        self.view.addSubview(dot)
        
        if let ir = imageRectView{
            print("copy and past these points")
            for dot in viewDots{
                let center = self.view.convert(dot.center, to: ir)
                let percentageX = center.x/ir.frame.width
                let percentageY = center.y/ir.frame.height
                print("CGPoint(x:\(percentageX),y:\(percentageY))")
            }
        }
        
    }

In these functions we have a function called addDot. It creates a new small view and adds it to the main view at a location inside the image. The loadWithPoints gets the point and calls the addDot to add the new view. Almost finished.

In view did load let’s set it up.


   override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(imageView)
        self.view.addSubview(button)
        loadImage()
    }

And let’s add our function to clear the dots from the view that are created in addDot function.


 @objc func clearViews(){
        for view in viewDots{
            view.removeFromSuperview()
        }
        viewDots.removeAll()
    }

Finally, the last thing is to create a way to add more dots on touch and so you can see what we have created. We will use touchesBegan to trigger more dots.


override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if let touch = touches.first{
            let location = touch.location(in: self.view)
            addDot(location: location)
        }
    }

Now the real power is to launch on many different simulators and notice the correct points will be marked. You can add more dots using the console log of the new dot and adding the log of the CGPoint to the loadImage so it will load up more. But for this purpose you will notice a dot at the end of the dock in the ocean on every device.

How useful is this? Well recently I was given the task of animating a view along a game board.  The game board was a PNG.  Rather than code an entire view I used the sample project that is above and made dots around the board until it was completely mapped.  Then it became easy to animate along the array of spaces.  I have also used this function to share a marked location across devices on images and snap handles tightly around a draggable/resizable view .  You could create your own annotation app right now.  Happy coding.

 

The complete file ViewController file:


import UIKit
import AVFoundation

class ViewController: UIViewController {
    
    lazy var imageView : UIImageView = {
        let img = UIImageView(frame: CGRect(x: 0, y: 40, width: self.view.frame.width, height: self.view.frame.height - 60))
        img.contentMode = .scaleAspectFit
        img.backgroundColor = .blue
        img.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        return img
    }()
    
    lazy var button : UIButton = {
        let button = UIButton(frame: CGRect(x: 20, y: self.view.frame.height - 50, width: self.view.frame.width - 40, height: 40))
        button.setTitle("Clear", for: .normal)
        button.addTarget(self, action: #selector(clearViews), for: .touchUpInside)
        button.setTitleColor(.black, for: .normal)
        return button
    }()
    
    var viewDots : [UIView] = []
    var imageRectView : UIView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(imageView)
        self.view.addSubview(button)
        loadImage()
    }
    
    
    func loadImage(){
        URLSession.shared.dataTask(with: URL(string: "https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg?w=940&h=650&dpr=2&auto=compress&cs=tinysrgb")!) { (data, response, error) in
            guard let dt = data else{print("error");return}
            DispatchQueue.main.async {
                [weak self] in
                guard let image = UIImage(data: dt) else{print("error");return}
                self?.imageView.image = image
                self?.imageView.layoutIfNeeded()
                let realImageRect = AVMakeRect(aspectRatio: image.size, insideRect: (self?.imageView.frame)!)
                
                
                //proof
                self?.imageRectView = UIView(frame: realImageRect)
                self?.imageRectView?.layer.borderColor = UIColor.green.cgColor
                self?.imageRectView?.layer.borderWidth = 3
                self?.view.addSubview(self!.imageRectView!)
                
                let points = [CGPoint(x:0.663446030179084,y:0.469806763285024)]
                self?.loadWithPoints(points: points)
                
            }
            }.resume()
    }
    

    
    func loadWithPoints(points:[CGPoint]){
        for point in points{
            let point = pointConverter(percentagePoint: point, frame: self.imageRectView!.frame)
            addDot(location: point)
        }
    }
    func pointConverter(percentagePoint : CGPoint,frame:CGRect)->CGPoint{
        return CGPoint(x: frame.width * percentagePoint.x + frame.minX, y: frame.height * percentagePoint.y + frame.minY)
    }
    
    func addDot(location:CGPoint){
        let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
        dot.backgroundColor = .white
        dot.layer.cornerRadius = 5
        dot.center = location
        viewDots.append(dot)
        self.view.addSubview(dot)
        
        if let ir = imageRectView{
            print("copy and past these points")
            for dot in viewDots{
                let center = self.view.convert(dot.center, to: ir)
                let percentageX = center.x/ir.frame.width
                let percentageY = center.y/ir.frame.height
                print("CGPoint(x:\(percentageX),y:\(percentageY))")
            }
        }
        
    }
    
    
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if let touch = touches.first{
            let location = touch.location(in: self.view)
            addDot(location: location)
        }
    }
    
    @objc func clearViews(){
        for view in viewDots{
            view.removeFromSuperview()
        }
        viewDots.removeAll()
    }
}