Reputation: 18765
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.
Upvotes: 3
Views: 470
Reputation: 560
In order to avoid a crash in case of a conflict of constraints, they must be activated at the end
var constraints = [NSLayoutConstraint]()
var deferred:[NSLayoutConstraint] = []
for subview in oldParent.subviews {
for constraint in oldParent.constraints {
if let newConstraint = moveConstraint(constraint, ofView: subview, fromView: oldParent, toView: newParent) {
let active = constraint.isActive
oldParent.removeConstraint(constraint)
if active {
deferred.append(newConstraint)
}
//newConstraint.isActive = active
constraints.append(newConstraint)
}
}
newParent.addSubview(subview)
}
newParent.addConstraints(constraints)
deferred.forEach{$0.isActive = true}
Upvotes: 0
Reputation: 10112
Disclaimer : This is in NO WAY a complete answer.
Here are a few things that I noticed -
#1 Access to these properties is not recommended. Use the firstAnchor
and secondAnchor
properties instead.
/* 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 }
Here's the supporting debug output for why firstItem
& secondItem
values are NOT always UILayoutGuide
instances.
▿ 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 The current code does NOT account for priority
value
open var priority: UILayoutPriority
#3 The current code does NOT account for shouldBeArchived
value
open var shouldBeArchived: Bool
Also I didn't know about it until today - and I'm not sure what's the correct way to handle this.
#4 The current code does NOT account for firstAnchor
& secondAnchor
values
/* 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 The current code does NOT account for isActive
value
open var isActive: Bool
#6 The current code does NOT account for identifier
value
open var identifier: String?
If you are working this out for your own app and can make sure that you are not using some of these values on the originally created constraints, that should be fine.
In case you are planning to put this into a library that will be used in other apps, that opens it up for all of the current issues + future changes / issues.
I would HIGHLY RECOMMEND that you should try to avoid this if possible and rethink what should be done instead to achieve the same goal. Maybe you want to have a wrapper UIView
instance that has all of these subviews
and this wrapper UIView
instance is transferred to another view. This should minimize the number of constraints this code has to touch/recreate.
In any case - you MUST do following (if you are planning to go ahead with this).
firstAnchor
& secondAnchor
values from old to new constraint. - NOT POSSIBLE, these are GET only, see in the comments.priority
value from old to new constraint.identifier
value from old to new constraint.shouldBeArchived
value from old to new constraint.isActive = false
originally.Upvotes: 1
Reputation: 534
@dfd is correct, views are heavily linked to each other when it comes to constraints. If you remove a view from its superview then those constraints will be broken, but if you had a view within a view and only change the parent view then the subview would keep its constraints. wow, that sounds a lot more confusing than it actually is. But yea, in general, the way you're doing it is fine, I can't think of a better way off the top of my head, maybe make an extension that you could call on a UIView? something like view.moveTo(newView)?
Upvotes: 1