I would like to programmatically move all subviews of a UIView to another UIView while keeping all constraints between the subviews to each other and to the (new) parent intact.
What is the "correct" way to do this?
My current solution simply loops through subviews and all constraints of the current parent view and checks if it either between the parent and the subview or between two subviews. Matching constraints are store and applied to the new parent:
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
}
While this seems to work fine according to my tests, I would like to make sure that this really correct. Handling constraints is always a difficult issue which can easily lead to errors at some time in the future.
So: Is the copy process enough to transfer the subviews and their constraints correctly? Or is there anything else so consider? Or is there a better way?
EDIT: Context
One use case would be a UICollectionViewCell with a card like layout. I followed a hint in a different answer to achieve this by adding two subviews to the contentView which act as wrapper and apply the shadow and round corners through their layer properties. The goal was to create a UICollectionViewCell subclass which automatically adds these wrapper subviews in awakeFromNib() and moves all subviews of the contentView to this wrapper view.
Of course one could easily add these wrappers in InterfaceBuilder but when handling a lot of different cells in different projects a general approach which handles this automatically would be a good solution.