2

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.

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 ?

FredericP
  • 1,119
  • 1
  • 14
  • 27

4 Answers4

4

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

Beninho85
  • 3,273
  • 27
  • 22
  • It's exactly what I wanted! The original view coordinates are always the superview's coordinate, I assume? So I changed to a more generic ’ let redCenter = redView.superview!.convert(redView.center, to:self.view) ’ – FredericP Jun 02 '17 at 17:33
  • @FredericP `center` and `frame` is in the coordinate system of the superview, while `bounds` is in the coordinate system of its own. So another way is `redView.convert(CGPoint(x: redView.bounds.midX, y: redView.bounds.midY), to:self.view)` – Cosyn Jun 05 '17 at 06:01
3

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
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
1

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.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Beninho85
  • 3,273
  • 27
  • 22
  • I can't add the black view as subView of the red ones, because after creation, they will be dragged from square to square. My understanding was that ‘redView.convert(redView.center, to:self.view)‘ give the redView center in the self.view coordinate system? – FredericP Jun 01 '17 at 19:44
  • What is the value of redCenter in your case, is it the right one? And yes it should normally but I never know beetween from/to what is the right one – Beninho85 Jun 01 '17 at 20:13
  • I don't know, in fact! Actually, on the iPhone 7 emulator, it gives me viewDidAppear - (66.0, 78.0) - (552.0, 78.0) - (66.0, 337.5) - (552.0, 337.5) - (66.0, 597.0) - (552.0, 597.0) . What i know, it's that If I place a view at that position , it is about 2 time too far in the x direction (it's the same result with iPhone SE, iPhone 7 and iPhone 7 +) – FredericP Jun 01 '17 at 20:27
  • Have you tried my solution? If you want to solve it with frames, try to print everything to see if all the frames have good values or not. If you're still stuck, just give me your view and I will try to solve it – Beninho85 Jun 01 '17 at 20:49
  • No, I can't restrain my view in the red view, because I have to move it later... The center's value aren't correct, IMHO: the 552-597 position seems to exceed the 375x667 size of iPhone 7's screen. To reproduce my test, I think it's easy to create a project from my question. I described the steps I took to do the storyboard, and I copied the full ViewController code. – FredericP Jun 01 '17 at 21:18
  • it's not an issue, you can activate an deactivate constraints based on where you want to move your view! I will send you an example with your code – Beninho85 Jun 01 '17 at 21:45
0

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.

phamot
  • 384
  • 2
  • 12
  • Thanks, but I don't want to place the new black View in IB, I have to do it by program ( in fact, after creation, the black views will move from square to square) – FredericP Jun 01 '17 at 19:41
  • I think instead of moving views. you can hide and show the views at different places with cool animation. Which improves performance drastically. – phamot Jun 01 '17 at 19:59
  • Sorry, it will not work for me. In the end the black views are moved when dragged by the user, so they have to be a subview of a "full screen" view, and I have to know where they will be moved after that. – FredericP Jun 01 '17 at 20:32