【问题标题】:How to move all subviews to a new parentView while keeping all constraints intact?如何在保持所有约束不变的同时将所有子视图移动到新的父视图?
【发布时间】:2021-08-19 15:49:05
【问题描述】:

我想以编程方式将UIView 的所有子视图移动到另一个UIView,同时保持子视图之间的所有约束彼此以及(新)父视图之间的约束不变。

正确”的方法是什么?

我当前的解决方案只是遍历子视图和当前父视图的所有约束,并检查它是在父视图和子视图之间还是在两个子视图之间。匹配约束被存储并应用到新的父级:

var constraints = [NSLayoutConstraint]()
for subview in oldParent.subviews {
    for constraint in oldParent.constraints {
        if let newConstraint = moveConstraint(constraint, ofView: subview, fromView: oldParent, toView: newParent) {
            let active = oldConstraint.isActive
            oldParent.removeConstraint(constraint)

            newConstraint.isActive = active
            constraints.append(newConstraint)
        }
    }

    newParent.addSubview(subview)
}

newParent.addConstraints(constraints)


func moveConstraint(_ constraint: NSLayoutConstraint, ofView view: UIView, fromView prevSuperview: UIView, toView newSuperview: UIView) -> NSLayoutConstraint? {
    // Check prevSuperview + Layout Guides
    var prevSuperviewIsFirstItem: Bool = constraint.firstItem === prevSuperview
    if !prevSuperviewIsFirstItem {
        for layoutGuid in prevSuperview.layoutGuides {
            if constraint.firstItem === layoutGuid {
                prevSuperviewIsFirstItem = true
                break
            }
        }
    }
    
    var prevSuperviewIsSecondItem: Bool = constraint.secondItem === prevSuperview
    if !prevSuperviewIsSecondItem {
        for layoutGuid in prevSuperview.layoutGuides {
            if constraint.secondItem === layoutGuid {
                prevSuperviewIsSecondItem = true
                break
            }
        }
    }
    
    // Move constraints between prevSuperview + View
    var newConstraint: NSLayoutConstraint? = nil 
    if prevSuperviewIsFirstItem && constraint.secondItem === view {
        newConstraint = NSLayoutConstraint(item: newSuperview, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: view, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant)
    } else if prevSuperviewIsSecondItem && constraint.firstItem === view {
        newConstraint = NSLayoutConstraint(item: view, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newSuperview, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant)
    }

    if let newConstraint = newConstraint {
        // handle newConstraint.isActive only after oldConstraint has
        // been removed to avoid conflicts

        newConstraint.identifier = constraint.identifier
        newConstraint.priority = constraint.priority
        newConstraint.shouldBeArchived = constraint.shouldBeArchived
        return newConstraint
    }
    

    // Move constraints between view and other subview
    if constraint.firstItem === view || constraint.secondItem === view {
        return constraint
    }
    
    return nil
}

虽然根据我的测试,这似乎工作正常,但我想确保这真的正确。处理约束始终是一个棘手的问题,很容易在将来的某个时间导致错误。

那么:复制过程是否足以正确传输子视图及其约束?或者还有什么可以考虑的吗?还是有更好的办法?

编辑:上下文

一个用例是UICollectionViewCell,其布局类似于卡片。我遵循了另一个答案中的提示,通过向contentView 添加两个子视图来实现这一点,它们充当包装器并通过它们的图层属性应用阴影和圆角。目标是创建一个UICollectionViewCell 子类,它会自动将这些包装子视图添加到awakeFromNib() 中,并将contentView 的所有子视图移动到此包装器视图中。

当然,可以在 InterfaceBuilder 中轻松添加这些包装器,但在不同项目中处理大量不同单元格时,自动处理此问题的通用方法将是一个很好的解决方案。

【问题讨论】:

  • 我不是专家,但我知道约束与视图层次结构的紧密联系。 (仔细想想就明白了。)这里有一个想法——为什么不直接创建一个带有子视图和子类的单亲呢?约束将继续存在,维护代码更容易,您甚至可以根据需要在每个子类父视图中命名视图和约束以进行编辑。

标签: ios swift uiview nslayoutconstraint


【解决方案1】:

@dfd 是正确的,当涉及到约束时,视图之间是紧密相连的。如果您从其父视图中删除视图,那么这些约束将被破坏,但如果您在视图中有一个视图并且只更改父视图,那么子视图将保留其约束。哇,这听起来比实际上更令人困惑。但是,是的,总的来说,你这样做的方式很好,我想不出更好的方法,也许做一个你可以在 UIView 上调用的扩展?类似于 view.moveTo(newView)?

【讨论】:

    【解决方案2】:

    免责声明:这绝不是一个完整的答案。

    以下是我注意到的一些事情 -


    #1 不建议访问这些属性。请改用firstAnchorsecondAnchor 属性。

         /* accessors
         firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant
         Access to these properties is not recommended. Use the `firstAnchor` and `secondAnchor` properties instead.
         */
        unowned(unsafe) open var firstItem: AnyObject? { get }
    
        unowned(unsafe) open var secondItem: AnyObject? { get }
    

    以下是有关为什么 firstItemsecondItem 值并不总是 UILayoutGuide 实例的支持调试输出。

      ▿ Optional<AnyObject>
        - some : <_UILayoutGuide: 0x10b89b810; frame = (0 0; 0 0); hidden = YES; layer = <CALayer: 0x2827d2dc0>>
    
      ▿ Optional<AnyObject>
        - some : <UILayoutGuide: 0x281f9b2c0 - "UIViewSafeAreaLayoutGuide", layoutFrame = {{0, 0}, {375, 603}}, owningView = <UIView: 0x10b89b6a0; frame = (0 0; 375 603); autoresize = W+H; layer = <CALayer: 0x2827d2da0>>>
    
      ▿ Optional<AnyObject>
        - some : <UIScrollView: 0x10b19b800; frame = (0 0; 375 603); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x2829f8000>; layer = <CALayer: 0x2827ba1c0>; contentOffset: {0, 145}; contentSize: {375, 748}; adjustedContentInset: {0, 0, 0, 0}>
    
      ▿ Optional<AnyObject>
        - some : <UIView: 0x10b89b6a0; frame = (0 0; 375 603); autoresize = W+H; layer = <CALayer: 0x2827d2da0>>
    

    #2 当前代码不考虑priority

    open var priority: UILayoutPriority
    

    #3 当前代码不考虑shouldBeArchived

    open var shouldBeArchived: Bool
    

    而且我直到今天才知道这件事 - 我不确定处理这个问题的正确方法是什么。


    #4 当前代码不考虑firstAnchor & secondAnchor

         /* accessors
         firstAnchor{==,<=,>=} secondAnchor * multiplier + constant
         */
        @available(iOS 10.0, *)
        @NSCopying open var firstAnchor: NSLayoutAnchor<AnyObject> { get }
    
        @available(iOS 10.0, *)
        @NSCopying open var secondAnchor: NSLayoutAnchor<AnyObject>? { get }
    

    #5 当前代码不考虑isActive

    open var isActive: Bool
    

    #6 当前代码不考虑identifier

    open var identifier: String?
    

    如果您正在为自己的应用解决此问题,并且可以确保您没有在最初创建的约束上使用其中一些值,那应该没问题。

    如果您打算将其放入将在其他应用程序中使用的库中,这将为所有当前问题 + 未来更改/问题打开它。


    我会强烈建议您应该尽可能避免这种情况,并重新考虑应该做些什么来实现相同的目标。也许您希望拥有一个包含所有这些 subviews 的包装器 UIView 实例,并且此包装器 UIView 实例被转移到另一个视图。这应该最大限度地减少此代码必须接触/重新创建的约束数量。


    无论如何 - 您必须执行以下操作(如果您打算继续此操作)。

    1. firstAnchorsecondAnchor 值从旧约束复制到新约束。 - 不可能,这些只是 GET,请参阅 cmets。
    2. priority 值从旧约束复制到新约束。
    3. identifier 值从旧约束复制到新约束。
    4. shouldBeArchived 值从旧约束复制到新约束。
    5. 最初不要激活带有isActive = false 的任何约束。

    【讨论】:

    • 非常感谢您的详细解答!我已更新代码以复制您提到的属性:priorityidentifiershouldBeArchivedisActive。但是我不确定锚点?我虽然NSLayoutAnchor 只是一个更容易使用NSLayoutConstraints 的API,不是吗?锚属性对应于NSLayoutConstraint.Attributes,并且是只读属性。因此,当使用与oldConstraint 相同的itemsattributes 创建newConstraint 时,锚点应该相同,不是吗?
    • 是的,他们应该是。我粘贴的文档通过 { get } 部分清楚地说明了这一点。我现在会更新答案。
    猜你喜欢
    • 2014-04-15
    • 1970-01-01
    • 1970-01-01
    • 2015-02-09
    • 1970-01-01
    • 1970-01-01
    • 2018-01-10
    • 1970-01-01
    • 2018-09-16
    相关资源
    最近更新 更多