Retrieve the right position of a subView with auto-layout

拟墨画扇 提交于 2019-12-08 02:29:10

问题


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,

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 ?


回答1:


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




回答2:


So you want something like this:

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
}



回答3:


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.




回答4:


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.



来源:https://stackoverflow.com/questions/44314549/retrieve-the-right-position-of-a-subview-with-auto-layout

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!