使用自动布局检索子视图的正确位置

Retrieve the right position of a subView with auto-layout

我想以编程方式在故事板中创建的所有子视图的中心放置一个视图。

在情节提要中,我有一个视图,在一个 Vertical StackView 内部,它具有填充全屏的约束,分布 "Equal spacing"。 在垂直堆栈视图内部,我有 3 个水平堆栈视图,约束高度 = 100,尾部和前导 space : 0 来自超级视图。分布也是 "equal spacing"。 在每个水平堆栈视图中,我有两个视图,约束宽度和高度 = 100,视图是红色的。

到目前为止,很好,我有我想要的界面,

现在我想检索每个红色视图的中心,以在该位置放置另一个视图(实际上,它将成为棋盘上的棋子...)

所以我写道:

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)
        }
    }
}

}

有了这个,我可以看到在 ViewDidLoad 和 ViewWillAppear 中,指标是故事板的指标。然后在 viewWillLayoutSubviews、viewDidLayoutSubviews 和 viewDidAppear 中位置发生了变化。

在 viewDidAppear 之后(所以在所有视图就位之后),我必须将 x 坐标除以 2 并添加类似 35 的内容(参见代码)以使新的黑色视图正确居中于红色视图中。我不明白为什么我不能简单地使用红色视图的中心...为什么它适用于 y 位置?

你在画框上浪费了时间。因为有 AutoLayout,所以框架会移动,您可以使用框架添加视图,但在 ViewController 生命周期中为时已晚。使用起来更容易和有效 constraints/anchors,只需注意在同一层次结构中拥有视图:

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

但要回答最初的问题,可能是因为之间存在堆栈视图,所以坐标可能是相对于堆栈视图而不是容器或类似的东西。

您不需要划分坐标的高度和宽度来获得视图的中心。您可以通过选择多个视图并添加水平居中和垂直居中约束来使用对齐选项。

我发现了你的问题,更换

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

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

转换的时候要从视图原坐标转换过来,这里是horizo​​ntalSv

所以你想要这样的东西:

您应该按照 Beninho85 和 phamot 的建议进行操作,并使用约束使 pawn 在其起始方块上居中。请注意,pawn 而不是 必须是正方形的子视图才能将它们约束在一起,您可以在代码中而不是故事板中添加 pawn 视图及其约束:

@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)
}

以下是设置 x 和 y 约束的辅助函数:

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!])
}

当平移手势开始时,停用现有的 x 和 y 中心约束并创建新的 x 和 y 中心约束以跟踪拖动:

@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))
}

请注意,我设置了拖动器的平移,使其正好等于 pawn 视图的所需中心。

如果取消拖动,将约束恢复到起始方块的中心:

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

如果拖动正常结束,将约束设置到最近的正方形中心:

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: [=14=]) < distance(of: )})!
}

当拖动发生变化(意味着触摸移动)时,更新现有约束的常量以匹配手势的平移:

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