Retrieve the right position of a subView with auto-layout

I want to place programmatically a view at the center of all the the subviews created in a storyboard.

In the storyboard, I have a view, and inside a Vertical StackView, which has constraint to fill the full screen, distribution “Equal spacing”.
Inside of the Vertical Stack View, I have 3 horizontal stack views, with constraint height = 100, and trailing and leading space : 0 from superview. The distribution is “equal spacing” too.
In each horizontal stack view, I have two views, with constraint width and height = 100, that views are red.

  • On iOS, what are the differences between margins, edge insets, content insets, alignment rects, layout margins, anchors…?
  • UIScrollView with iOS Auto Layout Constraints: Wrong size for subviews
  • iOS: Multi-line UILabel in Auto Layout
  • How to turn on/off auto layout for a single view controller in storyboard
  • Adding constraints programmatically in UIScrollView with dynamic buttons - Swift
  • How can I change constraints priority in run time
  • So far, so good, I have the interface I wanted,
    enter image description here

    Now I want to retrieve the center of each red view, to place another view at that position (in fact, it’ll be a pawn over a checkboard…)

    So I wrote that:

    import UIKit
    
    class ViewController: UIViewController {
    
    @IBOutlet weak var verticalStackView:UIStackView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print ("viewDidLoad")
        printPos()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print ("viewWillAppear")
        printPos()
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print ("viewWillLayoutSubviews")
        printPos()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print ("viewDidLayoutSubviews")
        printPos()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print ("viewDidAppear")
        printPos()
        addViews()
    }
    
    
    func printPos() {
        guard let verticalSV = verticalStackView else { return }
        for horizontalSV in verticalSV.subviews {
            for view in horizontalSV.subviews {
                let center = view.convert(view.center, to:nil)
                print(" - \(center)")
            }
        }
    }
    
    func addViews() {
        guard let verticalSV = verticalStackView else { return }
        for horizontalSV in verticalSV.subviews {
            for redView in horizontalSV.subviews {
                let redCenter = redView.convert(redView.center, to:self.view)
                let newView = UIView(frame: CGRect(x:0, y:0, width:50, height:50))
                //newView.center = redCenter
                newView.center.x = 35 + redCenter.x / 2
                newView.center.y = redCenter.y
                newView.backgroundColor = .black
                self.view.addSubview(newView)
            }
        }
    }
    
    }
    

    With that, I can see that in ViewDidLoad and ViewWillAppear, the metrics are those of the storyboard. The positions changed then in viewWillLayoutSubviews, in viewDidLayoutSubviews and again in viewDidAppear.

    After viewDidAppear (so after all the views are in place), I have to divide x coordinate by 2 and adding something like 35 (see code) to have the new black view correctly centered in the red view. I don’t understand why I can’t simply use the center of the red view… And why does it works for y position ?

    4 Solutions Collect From Internet About “Retrieve the right position of a subView with auto-layout”

    I found your issue, replace

    let redCenter = redView.convert(redView.center, to:self.view)
    

    with

    let redCenter = horizontalSV.convert(redView.center, to: self.view)
    

    When you convert, you have to convert from the view original coordinates, here it was the horizontalSv

    You lose your time with frames. Because there is AutoLayout, frames are moved and with frames you can add your views but it’s too late in the ViewController lifecycle. Much easier and effective to use constraints/anchors, just take care to have views in the same hierarchy:

    let newView = UIView()
    self.view.addSubview(newView)
    newView.translatesAutoresizingMaskIntoConstraints = false
    newView.centerXAnchor.constraint(equalTo: self.redView.centerXAnchor).isActive = true
    newView.centerYAnchor.constraint(equalTo: self.redView.centerYAnchor).isActive = true
    newView.widthAnchor.constraint(equalToConstant: 50.0).isActive = true
    newView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
    

    But to answer the initial problem maybe it was due to the fact that there is the stackview between so maybe coordinates are relative to the stackview instead of the container or something like that.

    So you want something like this:

    demo

    You should do what Beninho85 and phamot suggest and use constraints to center the pawn over its starting square. Note that the pawn does not have to be a subview of the square to constrain them together, and you can add the pawn view and its constraints in code instead of in the storyboard:

    @IBOutlet private var squareViews: [UIView]!
    
    private let pawnView = UIView()
    private var pawnSquareView: UIView?
    private var pawnXConstraint: NSLayoutConstraint?
    private var pawnYConstraint: NSLayoutConstraint?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        let imageView = UIImageView(image: #imageLiteral(resourceName: "Pin"))
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        pawnView.addSubview(imageView)
        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: pawnView.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: pawnView.centerYAnchor),
            imageView.widthAnchor.constraint(equalTo: pawnView.widthAnchor, multiplier: 0.8),
            imageView.heightAnchor.constraint(equalTo: pawnView.heightAnchor, multiplier: 0.8)])
    
        pawnView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pawnView)
        let squareView = squareViews[0]
        NSLayoutConstraint.activate([
            pawnView.widthAnchor.constraint(equalTo: squareView.widthAnchor),
            pawnView.heightAnchor.constraint(equalTo: squareView.heightAnchor)])
        constraintPawn(toCenterOf: squareView)
    
        let dragger = UIPanGestureRecognizer(target: self, action: #selector(draggerDidFire(_:)))
        dragger.maximumNumberOfTouches = 1
        pawnView.addGestureRecognizer(dragger)
    }
    

    Here are the helper functions for setting up the x and y constraints:

    private func constraintPawn(toCenterOf squareView: UIView) {
        pawnSquareView = squareView
        self.replaceConstraintsWith(
            xConstraint: pawnView.centerXAnchor.constraint(equalTo: squareView.centerXAnchor),
            yConstraint: pawnView.centerYAnchor.constraint(equalTo: squareView.centerYAnchor))
    }
    
    private func replaceConstraintsWith(xConstraint: NSLayoutConstraint, yConstraint: NSLayoutConstraint) {
        pawnXConstraint?.isActive = false
        pawnYConstraint?.isActive = false
        pawnXConstraint = xConstraint
        pawnYConstraint = yConstraint
        NSLayoutConstraint.activate([pawnXConstraint!, pawnYConstraint!])
    }
    

    When the pan gesture starts, deactivate the existing x and y center constraints and create new x and y center constraints to track the drag:

    @objc private func draggerDidFire(_ dragger: UIPanGestureRecognizer) {
        switch dragger.state {
        case .began: draggerDidBegin(dragger)
        case .cancelled: draggerDidCancel(dragger)
        case .ended: draggerDidEnd(dragger)
        case .changed: draggerDidChange(dragger)
        default: break
        }
    }
    
    private func draggerDidBegin(_ dragger: UIPanGestureRecognizer) {
        let point = pawnView.center
        dragger.setTranslation(point, in: pawnView.superview)
        replaceConstraintsWith(
            xConstraint: pawnView.centerXAnchor.constraint(equalTo: pawnView.superview!.leftAnchor, constant: point.x),
            yConstraint: pawnView.centerYAnchor.constraint(equalTo: pawnView.superview!.topAnchor, constant: point.y))
    }
    

    Note that I set the translation of the dragger so that it is exactly equal to the desired center of the pawn view.

    If the drag is cancelled, restore the constraints to the starting square’s center:

    private func draggerDidCancel(_ dragger: UIPanGestureRecognizer) {
        UIView.animate(withDuration: 0.2) {
            self.constraintPawn(toCenterOf: self.pawnSquareView!)
            self.view.layoutIfNeeded()
        }
    }
    

    If the drag ends normally, set constraints to the nearest square’s center:

    private func draggerDidEnd(_ dragger: UIPanGestureRecognizer) {
        let squareView = nearestSquareView(toRootViewPoint: dragger.translation(in: view))
        UIView.animate(withDuration: 0.2) {
            self.constraintPawn(toCenterOf: squareView)
            self.view.layoutIfNeeded()
        }
    }
    
    private func nearestSquareView(toRootViewPoint point: CGPoint) -> UIView {
        func distance(of candidate: UIView) -> CGFloat {
            let center = candidate.superview!.convert(candidate.center, to: self.view)
            return hypot(point.x - center.x, point.y - center.y)
        }
        return squareViews.min(by: { distance(of: $0) < distance(of: $1)})!
    }
    

    When the drag changes (meaning the touch moved), update the constants of the existing constraint to match the translation of the gesture:

    private func draggerDidChange(_ dragger: UIPanGestureRecognizer) {
        let point = dragger.translation(in: pawnView.superview!)
        pawnXConstraint?.constant = point.x
        pawnYConstraint?.constant = point.y
    }
    

    enter image description here

    You need not divide the height and width of the coordinates to get the center of the view. You can use align option by selecting multiple views and add horizontal centers and vertical centers constraints.