The ghost of NSLayoutConstraint haunts my view hierarchy? - ios

I'm trying to programmatically modify autolayout constraints to move a table view up, but only for an iPhone 6 Plus in landscape mode, because I couldn't achieve the precise visual effect I wanted on all devices using Xcode 6.2 beta 3 Interface Builder's autolayout (got it right from IB for the other supported devices/orientations. Just that iPhone 6 Plus is a bit of an outlier between an iPhone and an iPad, thus a little trickier)
One constraint I remove seems to be deleted after removal (e.g. disappears from the containing view's constraints, as expected), however, the layout manager still seems to finds it and warns that it is a conflict with other constraints and breaks a constraint at runtime with the fortunate result that the app produces the intended visual result, but the unfortunate side-effect of an ugly console warning message, that I want to fix, because it's ugly and Apple documentation blames such warnings user code bug(s).
My code intercepts orientation change (only on iPhone 6 Plus),
and then:
=============
• Iterates over constraints in tableview's owner view
• Prints properties of any constraint with an attribute of .Top
• Removes Center Y constraint referenced via IBOutlet, for the tableview
• Removes constraint with .Top attribute
• Adds new .Top attribute with a different multiplier
============
Here is the swift code in my View Controller:
override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
switch(toInterfaceOrientation) {
case .LandscapeLeft:
fallthrough
case .LandscapeRight:
var deviceType = UIDevice().deviceType
if (deviceType == .iPhone6plus || deviceType == .simulator) {
if centerYconstraint != nil {
self.view.removeConstraint(centerYconstraint)
centerYconstraint = nil
for constraint in self.view.constraints() {
if (constraint.firstItem as NSObject == self.tableView) {
if (constraint.firstAttribute == NSLayoutAttribute.Top) {
println("found item \(constraint)")
let view1 = constraint.firstItem as UIView
let attr1 = constraint.firstAttribute
let view2 = constraint.secondItem as UIView
let attr2 = constraint.secondAttribute
let relation = constraint.relation
let constant = constraint.constant
let newConstraint = NSLayoutConstraint(
item: view1,
attribute: attr1,
relatedBy: relation,
toItem: view2,
attribute: attr2,
multiplier: 0.02,
constant: constant)
self.view.removeConstraint(constraint as NSLayoutConstraint)
self.view.addConstraint(newConstraint)
self.view.layoutIfNeeded()
}
}
}
}
}
default:
break
}
}
Here is Xcode simulator's output. Notice the first line "found item" where I print the constraint I delete.
But you can see the same view1 and view2, multiplier and attribute in the list of potential contflicts layout manager complains about afterward. That's what I'm confused about.
found item <NSLayoutConstraint:0x7f8a05101bb0 UITableView:0x7f8a05853000.top == 0.03*_UILayoutGuide:0x7f8a035517e0.top>
2015-01-03 14:36:35.290 Interphase[46388:74323123] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7f8a0354da40 V:[UITableView:0x7f8a05853000(336)]>",
"<_UILayoutSupportConstraint:0x7f8a0514df70 V:[_UILayoutGuide:0x7f8a035517e0(49)]>",
"<_UILayoutSupportConstraint:0x7f8a051908e0 _UILayoutGuide:0x7f8a035517e0.bottom == UIView:0x7f8a03551480.bottom>",
"<NSLayoutConstraint:0x7f8a051b53d0 'UIView-Encapsulated-Layout-Height' V:[UIView:0x7f8a03551480(414)]>",
"<NSLayoutConstraint:0x7f8a050d9080 UITableView:0x7f8a05853000.top == 0.02*_UILayoutGuide:0x7f8a035517e0.top>",
"<NSLayoutConstraint:0x7f8a0354ef80 UITableView:0x7f8a05853000.centerY == UIView:0x7f8a03551480.centerY>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7f8a0354da40 V:[UITableView:0x7f8a05853000(336)]>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
2015-01-03 14:36:56.720 Interphase[46388:74323123] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<_UILayoutSupportConstraint:0x7f8a0514df70 V:[_UILayoutGuide:0x7f8a035517e0(49)]>",
"<_UILayoutSupportConstraint:0x7f8a051908e0 _UILayoutGuide:0x7f8a035517e0.bottom == UIView:0x7f8a03551480.bottom>",
"<NSLayoutConstraint:0x7f8a05101bb0 UITableView:0x7f8a05853000.top == 0.03*_UILayoutGuide:0x7f8a035517e0.top>",
"<NSLayoutConstraint:0x7f8a051b53d0 'UIView-Encapsulated-Layout-Height' V:[UIView:0x7f8a03551480(736)]>",
"<NSLayoutConstraint:0x7f8a050d9080 UITableView:0x7f8a05853000.top == 0.02*_UILayoutGuide:0x7f8a035517e0.top>"
)

Adding and removing constraints to a view is a bit flaky. It's never entirely clear which view they should be added to and then it makes it hard to find later when you want to remove it.
A better solution is to keep a reference to the constraint(s) you care about (either as outlets if you're doing it from interface builder, or just store them in properties) and then activate or deactivate them as required.
Activating constraints instead of adding them also prevents you having to decide which is the appropriate view to add them to - the system does this automatically.

Related

iOS - Broken layout when removing and activating new constraints programatically in Swift 3

I've had a very frustrating time working with constraints programatically in Swift 3. At a very basic level, my application displays a number of views with initial constraints, and then applies new constraints upon rotation, to allow the views to resize and reposition as needed. Unfortunately this has been far from easy as I am still new to iOS development and Swift. I've spent a lot of time trying many different solutions offered on StackOverflow and elsewhere, but I keep reaching the same outcome (detailed at the end).
I have a view controller (let's call it "Main View Controller") whose root view contains two subviews, View A and View B Container. The root view has a pink background color.
View A contains a single label inside, centered vertically and horizontally, as well as an orange background color. View A has 4 constraints - [Leading Space to Superview], [Top Space to Top Layout Guide], [Trailing Space to Superview] and [Bottom Space to Bottom Layout Guide].
View B Container initially has no content. It has 4 constraints - [Width Equals 240], [Height Equals 128], [Leading Space to Superview] and [Leading Space to Superview].
I also have another view controller (let's call it "View B View Controller") that drives the content for the View B Container. For the sake of simplicity, this is just a default view controller with no custom logic. The root view of View B View Controller contains a single subview, View B.
View B is almost identical to View A above - single label centered vertically and horizontally and a blue background color. View B has 4 constraints - [Leading Space to Superview], [Top Space to Superview], [Trailing Space to Superview] and [Bottom Space to Superview].
In the Main View Controller class, I've maintained IBOutlet references to View A and View B Container, as well as their respective constraints mentioned above. In the below code, the Main View Controller instantiates the View B View Controller and adds the subsequent view to the View B Container, applying a flexible width/height auto-resizing mask to ensure it fills the available space. Then it fires a call to the internal _layoutContainers() function which performs a number of constraint-modifying operations depending on the device's orientation. The current implementation does the following:
removes the known constraints from View A
removes the known constraints from View B Container
depending on device orientation, activate new constraints for both View A and View B Container according to a specific design (detailed in code comments below)
fire off updateConstraintsIfNeeded() and layoutIfNeeded() against all views
When a resize event occurs, the code allows the viewWillTransition() to fire and then calls the _layoutContainers() function in the completion callback, so that the device is in a new state and can follow the necessary logic path.
The entire Main View Controller unit is below:
import UIKit
class ViewController: UIViewController {
// MARK: Variables
#IBOutlet weak var _viewAView: UIView!
#IBOutlet weak var _viewALeadingConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewATopConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewATrailingConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewABottomConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewBContainerView: UIView!
#IBOutlet weak var _viewBContainerWidthConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewBContainerHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewBContainerTopConstraint: NSLayoutConstraint!
#IBOutlet weak var _viewBContainerLeadingConstraint: NSLayoutConstraint!
// MARK: UIViewController Overrides
override func viewDidLoad() {
super.viewDidLoad()
// Instantiate View B's controller
let viewBViewController = self.storyboard!.instantiateViewController(withIdentifier: "ViewBViewController")
self.addChildViewController(viewBViewController)
// Instantiate and add View B's new subview
let view = viewBViewController.view
self._viewBContainerView.addSubview(view!)
view!.frame = self._viewBContainerView.bounds
view!.autoresizingMask = [.flexibleWidth, .flexibleHeight]
viewBViewController.didMove(toParentViewController: self)
self._layoutContainers()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: { _ in
self._layoutContainers()
})
}
// MARK: Internal
private func _layoutContainers() {
// Remove View A constraints
self._viewAView.removeConstraints([
self._viewALeadingConstraint,
self._viewATopConstraint,
self._viewATrailingConstraint,
self._viewABottomConstraint,
])
// Remove View B Container constraints
var viewBContainerConstraints: [NSLayoutConstraint] = [
self._viewBContainerTopConstraint,
self._viewBContainerLeadingConstraint,
]
if(self._viewBContainerWidthConstraint != nil) {
viewBContainerConstraints.append(self._viewBContainerWidthConstraint)
}
if(self._viewBContainerHeightConstraint != nil) {
viewBContainerConstraints.append(self._viewBContainerHeightConstraint)
}
self._viewBContainerView.removeConstraints(viewBContainerConstraints)
// Portrait:
// View B - 16/9 and to bottom of screen
// View A - anchored to top and filling the remainder of the vertical space
if(UIDevice.current.orientation != .landscapeLeft && UIDevice.current.orientation != .landscapeRight) {
let viewBWidth = self.view.frame.width
let viewBHeight = viewBWidth / (16/9)
let viewAHeight = self.view.frame.height - viewBHeight
// View A - anchored to top and filling the remainder of the vertical space
NSLayoutConstraint.activate([
self._viewAView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self._viewAView.bottomAnchor.constraint(equalTo: self._viewBContainerView.topAnchor),
])
// View B - 16/9 and to bottom of screen
NSLayoutConstraint.activate([
self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
self._viewBContainerView.heightAnchor.constraint(equalToConstant: viewBHeight),
self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: viewAHeight),
self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
])
}
// Landscape:
// View B - 2/3 of screen on left
// View A - 1/3 of screen on right
else {
let viewBWidth = self.view.frame.width * (2/3)
// View B - 2/3 of screen on left
NSLayoutConstraint.activate([
self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
self._viewBContainerView.heightAnchor.constraint(equalToConstant: self.view.frame.height),
self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor),
self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
])
// View A - 1/3 of screen on right
NSLayoutConstraint.activate([
self._viewAView.leadingAnchor.constraint(equalTo: self._viewBContainerView.trailingAnchor),
self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self._viewAView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
// Fire off constraints and layout update functions
self.view.updateConstraintsIfNeeded()
self._viewAView.updateConstraintsIfNeeded()
self._viewBContainerView.updateConstraintsIfNeeded()
self.view.layoutIfNeeded()
self._viewAView.layoutIfNeeded()
self._viewBContainerView.layoutIfNeeded()
}
}
My problem is that, although the initial load of the application displays the expected result (View B maintaining a 16/9 ratio and sitting at the bottom of the screen, View A taking up the remaining space):
Any subsequent rotation breaks the views completely and doesn't recover:
Additionally, the following constraints warnings are thrown once the application loads:
TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 0 (active)>",
"<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110] (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
"<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60] (active)>",
"<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60] (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin (active)>",
"<NSLayoutConstraint:0x608000094e60 H:|-(0)-[UIView:0x7f8d4f413e60] (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<_UILayoutSupportConstraint:0x600000096d50 _UILayoutGuide:0x7f8d4f40f4b0.height == 0 (active)>",
"<_UILayoutSupportConstraint:0x600000096d00 _UILayoutGuide:0x7f8d4f40f4b0.bottom == UIView:0x7f8d4f40f9e0.bottom (active)>",
"<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0] (active)>",
"<NSLayoutConstraint:0x608000092070 UIView:0x7f8d4f40fd90.bottom == UIView:0x7f8d4f413e60.top (active)>",
"<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60] (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
"<NSLayoutConstraint:0x600000096e40 'UIView-Encapsulated-Layout-Height' UIView:0x7f8d4f40f9e0.height == 667 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 20 (active)>",
"<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110] (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
"<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90] (active)>",
"<NSLayoutConstraint:0x608000093b50 V:|-(0)-[UIView:0x7f8d4f40fd90] (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
Thank you for reading if you got this far! Surely someone has encountered (and hopefully solved) this or a similar issue. Any help would be immensely appreciated!
Instead of trying to add and remove constraints consider just adjusting a priority to transform your view instead.
So for you default layout have a constraint with priority 900. Then add a second conflicting constraint with priority 1. Now to toggle the display mode just move that second constraint priority up above 900, and then back below to reverse. Easy to test it all in Interface Builder by just changing the priority too.
Also you can put the change in an animation block to get a nice smooth transition.
-
One other thing to consider using is size classes. Using this you can specify that particular constraints only apply for certain orientations so you could probably get your desired behaviour entirely 'for free', just set it all up in IB.
Part of the issue is that in _layoutContainers you remove the constaints from the storyboard and add now ones, but on subsequent rotations you don't remove the previous ones you added. You should store the new constraints that you create so that the next time the screen rotates, you can get the old constraints and remove them.
Also, calling _layoutContainers from viewDidLoad is too early in the VCs lifecycle since the views frame won't have the correct value yet. You can crate aspect ratio constraints so you don't have to calculate the size manually.
For example, the portrait constraint for
// View B - 16/9 and to bottom of screen
NSLayoutConstraint.activate([
self._viewBContainerView.heightAnchor.constraint(equalToConstant: self._viewBContainerView.widthAnchor, multiplier: 16.0 / 9.0),
self._viewBContainerView.topAnchor.constraint(equalTo: self.view.bottomAnchor),
self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
// should there be a constraint for self._viewBContainerView.trailingAnchor?
])

How to properly resize the most outside view in a custom keyboard extension ViewController using swift 3?

I have a function that sets the height of a custom keyboard extension, depending on the phone. I originally just tried this in viewDidLoad():
self.view.heightAnchor.constraint(equalToConstant: 100)
This didn't seem to work, so I made a function:
func updateHeightOfView() {
var currentKeyboardInView: String!
if currentViewHeightConstraint != nil {
view.removeConstraint(currentViewHeightConstraint!)
}
currentViewHeightConstraint = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1.0, constant: desiredHeight)
view.addConstraint(currentViewHeightConstraint!)
}
that way when the screen changes it's orientation, I resize the view. The second block of code works perfectly, but it throws warnings of layoutConstraints, so I was wondering why the first block of code isn't working, and if there is an easier way to UPDATE constraints of the height property of a view rather than add and remove them. This view is the most outside view in the viewController.
Here's the warning the second block spits out:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x608000298b00 App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 258 (active)>",
"<NSLayoutConstraint:0x600000297e80 'UIView-Encapsulated-Layout-Height' App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 216 (active)>"
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x608000298b00 App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 258 (active)>
As much detail you given here according to this I think where ever you set height of this view it is not static constant value but in view didload you are setting constant value for its height. So that's by it is not working.
In second block you are getting warning because may be your view is getting height from more than one way. To check this in updathHeight method just remove the constraint , don't add and try to run.
Exact solution could be given only looking on your constraints.

Programmatic aspect ratio constraints break when table view cells are dequeued

I am essentially trying to mimic the look and feel of Instagram's timeline view, which allows for photos of various aspect ratios to be displayed in a UITableViewCell, and to sit flush against the left and right margins of the view.
As of now, I have auto-layout constraints set for trailing and leading set to the superview, both set with a constant of 0, and bottom space and top space constraints set for the surrounding elements. As far as the image itself, I have it set to an aspect ratio constraint of 16:9, but ticked to "remove at build time", as images may sometimes have a different aspect ratio (16:12 is one).
Since I'll have access to the image's dimensional information in the downloaded JSON file before downloading the related images asynchronously, I want to set the height / width constraints of the image when the tableView is created with the JSON data. As of now, I'm creating the constraints in the UITableViewCell subclass within a function that is called from the UITableViewController's cellForRowAtIndexPath. Here is the code I'm using to create constraints:
func configurePostTableViewCell(post: Post) {
self.newsfeedPhotoImageView.translatesAutoresizingMaskIntoConstraints = false
let photoHeight: CGFloat = CGFloat(post.photoHeight)
let photoWidth: CGFloat = CGFloat(post.photoWidth)
let aspectRatioConstraint: NSLayoutConstraint = NSLayoutConstraint(item: self.timelinePhotoImageView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.timelinePhotoImageView, attribute: NSLayoutAttribute.Width, multiplier: (photoHeight / photoWidth), constant: 0)
aspectRatioConstraint.identifier = "$programmaticAspectRatio$"
self.timelinePhotoImageView.addConstraint(aspectRatioConstraint)
}
As I mentioned, the function itself is called within cellForRowAtIndexPath, and I've tried using the same code with cell. within the tableViewController, but the end result is the same:
When I build and run, the code at first seems to work perfectly, with both photos of different aspects being displayed correctly. The problem however, is when I scroll down 11 or so rows and (I'm assuming) the first few cells are dequeued for re-use. I set up a property observer in the cell to print to console when the cell is de-initialized, and the following error appeared at the same time the cells were dequeued:
2016-03-08 23:59:34.277 MyProject[12255:8479975] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x7fe8c2c31820 '$timelinePhotoLeading$' H:|-(0)-[MyProject.AsyncImageView:0x7fe8c2d16080] (Names: '|':UITableViewCellContentView:0x7fe8c2d13cb0 )>",
"<NSLayoutConstraint:0x7fe8c2d0b230 '$timelinePhotoTrailing$' H:[MyProject.AsyncImageView:0x7fe8c2d16080]-(0)-| (Names: '|':UITableViewCellContentView:0x7fe8c2d13cb0 )>",
"<NSLayoutConstraint:0x7fe8c2d1b3d0 '$programmaticAspectRatio$' MyProject.AsyncImageView:0x7fe8c2d16080.height == 0.75*MyProject.AsyncImageView:0x7fe8c2d16080.width>",
"<NSLayoutConstraint:0x7fe8c0dae7f0 '$programmaticAspectRatio$' MyProject.AsyncImageView:0x7fe8c2d16080.height == 0.5625*MyProject.AsyncImageView:0x7fe8c2d16080.width>",
"<NSLayoutConstraint:0x7fe8c2d2e220 'UIView-Encapsulated-Layout-Width' H:[UITableViewCellContentView:0x7fe8c2d13cb0(375)]>")
Will attempt to recover by breaking constraint <NSLayoutConstraint:0x7fe8c2d1b3d0 '$programmaticAspectRatio$' MyProject.AsyncImageView:0x7fe8c2d16080.height == 0.75*MyProject.AsyncImageView:0x7fe8c2d16080.width>
When I scroll back up to the top, the 16:12 photo no longer sits flush with the left and right margins, and appears to be taking the constraint of the 16:9 photo (which is consistent from what I'm inferring from the error message). If I'm understanding the error correctly, it seems the cell is trying to apply both constraints (16:9 and 16:12) to the same cell, despite what is specified in the code... which is what's causing the conflict. The tableViewController subclass is holding an array of "post" objects, each of which has a height / width value saved for its associated image, but it doesn't seem that data is being used after the cell is dequeued.
So my question is what am I supposed to do, to prevent these constraints from being messed up after being dequeued? Furthermore, after the error message appears, none of the following 16:12 images are appearing correctly, and I get the same error message a few more times (additional JSON is downloaded every 8 posts or so... similar to Instagram or Facebook, etc).
Here is a visual explanation of what's going wrong...
The first two images in that gallery reflect the intended appearance, but this third image depicts the error.
I've been struggling with this problem for at least a week now, and I'm not sure if there's something wrong in my strategy or if there is an additional function that I need to be calling when new cells are added and old ones are recycled. If anyone has any ideas or would like to see more of my code, I'd be very grateful for the assistance.
EDIT: Thanks to the help of the commenters below, I was able to fix my problem by removing the already existing aspect ratio constraint and then assigning the desired values from the UITableViewController to create a new one:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = timelineComponent.content[indexPath.row]
let photoHeight: CGFloat = CGFloat(post.photoHeight)
let photoWidth: CGFloat = CGFloat(post.photoWidth)
cell.aspectRatio = photoHeight / photoWidth
cell.configurePostTableViewCell(post)
return cell
}
And in the UITableViewCell itself:
var aspectRatioLayoutConstraint: NSLayoutConstraint!
var aspectRatio: CGFloat! {
didSet {
self.postPhotoImageView.translatesAutoresizingMaskIntoConstraints = false
if self.aspectRatioLayoutConstraint != nil {
self.postPhotoImageView.removeConstraint(self.aspectRatioLayoutConstraint)
}
self.aspectRatioLayoutConstraint = NSLayoutConstraint(item: self.postPhotoImageView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.postPhotoImageView, attribute: NSLayoutAttribute.Width, multiplier: aspectRatio, constant: 0)
self.postPhotoImageView.addConstraint(aspectRatioLayoutConstraint)
self.setNeedsLayout()
}

Nested UIStackViews Broken Constraints

I have a complex view hierarchy, built in Interface Builder, with nested UIStackViews. I get "unsatisfiable constraints" notices every time I hide some of my inner stackviews. I've tracked it down to this:
(
"<NSLayoutConstraint:0x1396632d0 'UISV-canvas-connection' UIStackView:0x1392c5020.top == UILabel:0x13960cd30'Also available on iBooks'.top>",
"<NSLayoutConstraint:0x139663470 'UISV-canvas-connection' V:[UIButton:0x139554f80]-(0)-| (Names: '|':UIStackView:0x1392c5020 )>",
"<NSLayoutConstraint:0x139552350 'UISV-hiding' V:[UIStackView:0x1392c5020(0)]>",
"<NSLayoutConstraint:0x139663890 'UISV-spacing' V:[UILabel:0x13960cd30'Also available on iBooks']-(8)-[UIButton:0x139554f80]>"
)
Specifically, the UISV-spacing constraint: when hiding a UIStackView its high constraint gets a 0 constant, but that seems to clash with the inner stackview's spacing constraint: it requires 8 points between my Label and Button, which is irreconcilable with the hiding constraint and so the constraints crash.
Is there a way around this? I've tried recursively hiding all the inner StackViews of the hidden stack view, but that results in strange animations where content floats up out of the screen, and causes severe FPS drops to boot, while still not fixing the problem.
This is a known problem with hiding nested stack views.
There are essentially 3 solutions to this problem:
Change the spacing to 0, but then you'll need to remember the previous spacing value.
Call innerStackView.removeFromSuperview(), but then you'll need to remember where to insert the stack view.
Wrap the stack view in a UIView with at least one 999 constraint. E.g. top#1000, leading#1000, trailing#1000, bottom#999.
The 3rd option is the best in my opinion. For more information about this problem, why it happens, the different solutions, and how to implement solution 3, see my answer to a similar question.
So, you have this:
And the problem is, when you first collapse the inner stack, you get auto layout errors:
2017-07-02 15:40:02.377297-0500 nestedStackViews[17331:1727436] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x62800008ce90 'UISV-canvas-connection' UIStackView:0x7fa57a70fce0.top == UILabel:0x7fa57a70ffb0'Top Label of Inner Stack'.top (active)>",
"<NSLayoutConstraint:0x62800008cf30 'UISV-canvas-connection' V:[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']-(0)-| (active, names: '|':UIStackView:0x7fa57a70fce0 )>",
"<NSLayoutConstraint:0x62000008bc70 'UISV-hiding' UIStackView:0x7fa57a70fce0.height == 0 (active)>",
"<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...'] (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
The problem, as you noted, is that the outer stack view applies a height = 0 constraint to the inner stack view. This conflicts with the 8 point padding constraint applied by the inner stack view between its own subviews. Both constraints cannot be satisfied simultaneously.
The outer stack view uses this height = 0 constraint, I believe, because it looks better when animated than just letting the inner view be hidden without shrinking first.
There's a simple fix for this: wrap the inner stack view in a plain UIView, and hide that wrapper. I'll demonstrate.
Here's the scene outline for the broken version above:
To fix the problem, select the inner stack view. From the menu bar, choose Editor > Embed In > View:
Interface Builder created a width constraint on the wrapper view when I did this, so delete that width constraint:
Next, create constraints between all four edges of the wrapper and the inner stack view:
At this point, the layout is actually correct at runtime, but Interface Builder draws it incorrectly. You can fix it by setting the vertical hugging priorities of the inner stack's children higher. I set them to 800:
We haven't actually fixed the unsatisfiable constrain problem at this point. To do so, find the bottom constraint that you just created and set its priority to less than required. Let's change it to 800:
Finally, you presumably had an outlet in your view controller connected to the inner stack view, because you were changing its hidden property. Change that outlet to connect to the wrapper view instead of the inner stack view. If your outlet's type is UIStackView, you'll need to change it to UIView. Mine was already of type UIView, so I just reconnected it in the storyboard:
Now, when you toggle the wrapper view's hidden property, the stack view will appear to collapse, with no unsatisfiable constraint warnings. It looks virtually identical, so I won't bother posting another GIF of the app running.
You can find my test project in this github repository.
I hit a similar problem with UISV-hiding. For me, the solution was to reduce the priorities of my own constraints from Required (1000) to something less than that. When UISV-hiding constrains are added, they take priority and the constraints no longer clash.
Ideally we could just set the priority of the UISV-spacing constraint to a lower value, but there doesn't appear to be any way to do that. :)
I am having success setting the spacing property of the nested stack views to 0 before hiding, and restoring to the proper value after making it visible again.
I think doing this recursively on nested stack views would work. You could store the original value of the spacing property in a dictionary and restore it later.
My project only has a single level of nesting, so I am unsure if this would result in FPS problems. As long as you don't animate the changes in spacing, I don't think it would create too much of a hit.
Another approach
Try to avoid nested UIStackViews. I love them and build almost everything with them. But as I recognized that they secretly add constraints I try to only use them at the highest level and non-nested where possible. This way I can specify the 2nd highest priority .defaultHighto the spacing constraint which resolves my warnings.
This priority is just enough to prevent most layout issues.
Of course you need to specify some more constraints but this way you have full control of them and make your view layout explicit.
Here's implementation of Senseful's suggestion #3 written as Swift 3 class using SnapKit constraints. I also tried overriding the properties, but never got it working without warnings, so I'll stick with wrapping UIStackView:
class NestableStackView: UIView {
private var actualStackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame);
addSubview(actualStackView);
actualStackView.snp.makeConstraints { (make) in
// Lower edges priority to allow hiding when spacing > 0
make.edges.equalToSuperview().priority(999);
}
}
convenience init() {
self.init(frame: CGRect.zero);
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addArrangedSubview(_ view: UIView) {
actualStackView.addArrangedSubview(view);
}
func removeArrangedSubview(_ view: UIView) {
actualStackView.removeArrangedSubview(view);
}
var axis: UILayoutConstraintAxis {
get {
return actualStackView.axis;
}
set {
actualStackView.axis = newValue;
}
}
open var distribution: UIStackViewDistribution {
get {
return actualStackView.distribution;
}
set {
actualStackView.distribution = newValue;
}
}
var alignment: UIStackViewAlignment {
get {
return actualStackView.alignment;
}
set {
actualStackView.alignment = newValue;
}
}
var spacing: CGFloat {
get {
return actualStackView.spacing;
}
set {
actualStackView.spacing = newValue;
}
}
}
In my case I was adding width and height constraint to a navigation bar button, as per the advice above I only added lower priority to the constraints.
open func customizeNavigationBarBackButton() {
let _selector = #selector(UIViewController._backButtonPressed(_:))
let backButtonView = UIButton(type: .custom)
backButtonView.setImage(UIImage(named: "icon_back"), for: .normal)
backButtonView.imageEdgeInsets = UIEdgeInsets.init(top: 0, left: -30, bottom: 0, right: 0)
backButtonView.snp.makeConstraints { $0.width.height.equalTo(44).priority(900) }
backButtonView.addTarget(self, action: _selector, for: .touchUpInside)
let backButton = UIBarButtonItem(customView: backButtonView)
self.navigationItem.leftBarButtonItem = backButton
}

How to apply auto layout properly

I have set of images(thumbs) in UITableviewcell. When tapping on each image, a popup(custom view) will be displayed which is a UISCrollview. i am adding all images(big) in Scroll View. So user can scroll to see images.
I am adding UISCrollView to RootViewController's view. so that it covers the entire screen. Below is my code
My Code:
self.mainView = self.superview?.window?.rootViewController?.view
imageScrollView = UIScrollView(frame: CGRectMake(0, 0, self.mainView!.frame.size.width, self.mainView!.frame.size.height))
imageScrollView.delegate = self
self.mainView.addsubview(imageScrollView)
Constraints:
self.mainView!.addConstraint(NSLayoutConstraint(item: imageScrollView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.mainView!, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0))
self.mainView!.addConstraint(NSLayoutConstraint(item: imageScrollView, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: self.mainView!, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0))
I got the error in console:
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0xab1090 h=--- v=--- H:[UIWindow:0xa724f0(768)]>",
"<NSLayoutConstraint:0x1133f450 UIScrollView:0x115723a0.centerY == UIView:0xa49fe0.centerY>",
"<NSAutoresizingMaskLayoutConstraint:0x483cc70 h=--& v=--& UIScrollView:0x115723a0.midY == + 512>",
"<NSAutoresizingMaskLayoutConstraint:0x115b7290 h=-&- v=-&- UIView:0xa49fe0.width == UIWindow:0xa724f0.width>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x1133f450 UIScrollView:0x115723a0.centerY == UIView:0xa49fe0.centerY>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
2014-12-08 01:13:27.379 afipad[349:60b] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0xab10c0 h=--- v=--- V:[UIWindow:0xa724f0(1024)]>",
"<NSLayoutConstraint:0x1133f090 UIScrollView:0x115723a0.centerX == UIView:0xa49fe0.centerX>",
"<NSAutoresizingMaskLayoutConstraint:0x115b6dd0 h=--& v=--& UIScrollView:0x115723a0.midX == + 384>",
"<NSAutoresizingMaskLayoutConstraint:0x115b72f0 h=-&- v=-&- UIView:0xa49fe0.height == UIWindow:0xa724f0.height>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x1133f090 UIScrollView:0x115723a0.centerX == UIView:0xa49fe0.centerX>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
When i check with break point, i got UIWindow size as 768 and 1024 in both portrait and Landscape. When i rotate the screen from Portrait to Landscape, the "imageScrollView" size is 768(width) and 1024(height) instead of 1024x768. What is the actual cause? How can i resolve it.
There are a couple different conflicting things happening here.
First, adding a subview to the root ViewController's view probably isn't going to work because it breaks encapsulation on several levels. Instead, present a new view controller containing your scrollView -- either as a modal or by pushing from self.navigationController -- directly from your table cell.
Second, autolayout constraints on UIScrollviews are kinda counterintuitive. See https://developer.apple.com/library/ios/technotes/tn2154/_index.html for an explanation of how to set them up.
Third, you'll probably need to set translatesAutoresizingMaskIntoConstraints to FALSE in this new viewController. (It explains that in the document, too)
Also, see my sample project for some examples of how to put images and other things inside a scrollView using autolayout:
https://github.com/annabd351/AutolayoutTemplate

Resources