Are StackView Constraints behaviour different in a NIB/XIB? - xib

I'm trying to create reusable views in a xib/nib file in swift iOS 10.
When I create a stack view and have 2 objects in it and make sure my stackview is constrained to the container view of the nib/xib (top to top, bottom to bottom, leading to leading and finally trailing to trailing), I get an error saying I'm missing my Y position for the first and the second object. Creating just one of them usually fixes it. Although this is where things to go sideways. In all my previous research, I shouldn't need to do this if I have my distribution of my stackview to Fill. Although this seems to quiet Xcode, it creates a over constraint issue when I try to run my program.
Here is what I used to load my nib in a view from my main storyboard :
public protocol NibOwnerLoadable: class {
static var nib: UINib { get }
}
//MARK:- Generic Implementation
public extension NibOwnerLoadable {
// Use the xib file with the same name as your UIView subclass located in the bundle of that class
static var nib: UINib {
return UINib(nibName: String(describing: self), bundle: Bundle(for: self))
}
}
//MARK:- Support for instation from the XIB file
public extension NibOwnerLoadable where Self: UIView {
// Function to load content and constraints automatically
func loadNibContent() {
let layoutAttributes: [NSLayoutAttribute] = [.top, .leading, .bottom, .trailing]
for view in Self.nib.instantiate(withOwner: self, options: nil) {
if let view = view as? UIView {
view.translatesAutoresizingMaskIntoConstraints = false
view.frame = bounds
self.addSubview(view)
layoutAttributes.forEach{ attribute in self.addConstraint(NSLayoutConstraint(item: view, attribute: attribute, relatedBy: .equal, toItem: self, attribute: attribute, multiplier: 1, constant: 0.0))
}
}
}
}
}
I know my issue is an extra constraint being added by Auto Layout but why and where. This other question is very close to what I'm trying to do but for some reason my AutoLayout knowledge is a jumbled in my head. Adding a subclassed UIView to a Nib with its auto layout constraints
A little help would be appreciate. Please explain the why, not just how to do it. I'm a guy who likes to understand the back story. It makes it logical and easier to retain.
Here are pictures of my filterView as a nib/xib that is loaded inside searchAndFilterView as FilterView (UIView) that is loaded inside one my view controller inside my storyboard. Think reusable Tool Views.

Woohou!
Here is how I figured this out.
I realized that my height constraints on my views using XIB/NIB are necessary to setup the view when its not hidden. But when I set the .isHidden to true value this would conflict with my height constraint.
I needed to let Autolayout do its auto layout when the view changed from hidden to not hidden and vice-versa. By setting a constraint with a Priority default value of 1000, I was telling to Autolayout to absolutely make sure this constraint is always active.
I realized that by setting the priority to 999, I told Autolayout it could override my constraint when its really needed to do it. Why 999, I assume that when a view isHidden, it really has no size and that is very high priority for Autolayout.
I wish I knew this or knew how to find out default autolayout priorities before.
If anyone knows more about this I would appreciate more info or a link!!!

Related

Programmed Slider Constraint is Not Updating

I have am attempting to learn how to populate a view in my storyboard with sliders and buttons, programmatically. I am trying, currently, to get one programmed slider to adhere to a programmed NSLayoutConstraint
Here is my code:
let centerXConstraint = NSLayoutConstraint(item: self.volumeSliderP, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 10.0)
self.view.addConstraint(centerXConstraint)
I should mention, that when I substitute the first item for a slider which already exists on the view (which was placed via Storyboard, with it's own constraints also placed with IB/Storyboard), it does updated correctly with the above NSLayoutConstraint code. Also, I have been able to update my programmed volumeSliderP with custom code to change it's handle and rotate it to vertical successfully.
What step am I missing to allow this NSLayoutConstraint code to work upon my programmed slider?
Thank you for any help!
When working with constraints in code, you need to do two (maybe three) things, regardless of control type:
Set the translatesAutoresizingMaskIntoConstraints to false.
Failure to do so will set off constraint conflicts, which will appear in the console log. I usually create an extension to UIView for this:
public func turnOffAutoResizing() {
self.translatesAutoresizingMaskIntoConstraints = false
for view in self.subviews as [UIView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
Then in viewDidLoad (after adding my subviews) I simply add a line:
view.turnOffAutoResizing()
Consider if any subviews have intrinsic content size.
As explained in the linked Apple doc, if you have label and a text field, the text field will expand to fit the label without the need for setting widths. A UISlider does not have an intrinsic width but it does have an intrinsic height.
So in your case you need to not only set position, it needs to define the width.
A combination of top and leading will yield enough for the layout engine to know "where" and "height", but not "width". Same would go if you defined "centerX" and something - you didn't list any code - for the Y factor (top, bottom, centerY).
If I'm stating this clearly, you should be able to see that the engine will know enough to say (in frame coordinates) "start the slider at X/Y, height is XX points (it has intrinsic height), but how long should it be?"
I typically set either top, leading, and trailing... or top, centerX, and width. But it varies with the need.

Keyboard extension loses height in iOS 10 when trying to size automatically in some cases

You can download a sample project demonstrating the issue below here:
https://github.com/DimaVartanian/keyboard-extension-height-bug
When creating a keyboard extension and not specifying a concrete height for its components but instead anchoring them to the view/inputView so that in theory the system will determine their height based on environment and orientation, in some situations that height instead turns into 0 and the keyboard is crushed (with the exception of anything that has a concrete height such as a self sized label or button).
This only seems to occur on iOS 10. On iOS 9, the child views resized correctly to fit the default automatic keyboard height.
There are several scenarios this can manifest and this project demonstrates a basic one. It starts with the basic keyboard extension template with the default "next keyboard" button and the 2 size constraints it comes with:
self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.nextKeyboardButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
Next, we create a single other view that we want to fill the space of the superview without defining a concrete size for itself:
let anotherView = UIView()
anotherView.backgroundColor = UIColor.red
anotherView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(anotherView)
anotherView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
anotherView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
anotherView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
Now, let's say we just want to anchor this new view to the bottom of our keyboard superview. We would just do something like:
anotherView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
The result looks like this:
iOS 9
iOS 10
This layout is exactly what we expect. Now instead, let's anchor the new view to the top of our next keyboard button. We get rid of the constraint we just added and replace it with
anotherView.bottomAnchor.constraint(equalTo: self.nextKeyboardButton.topAnchor).isActive = true
Logically, the resulting height should be the same (determined by the system)
The result is now this:
iOS 9
iOS 10
On iOS 9 it behaves as expected but on iOS 10, the flexible height view is resized down to 0 and all that is left is the fixed height button.
There are no messages about conflicting constraints. I'm trying to figure out what could be causing this and why it would only be happening on iOS 10.
Apple has responded to my DTS ticket and told me to file a bug report, so this is actually an iOS 10 bug. I have filed a radar (#28532959) and will update this answer if I ever get a response. If someone else comes up with a concrete solution that allows me to still use autolayout to achieve an automatic height, answers are still accepted.
I got it solved by setting a new constrain for the height.
Here's my workaround. It is a little laggy when the device rotates, but it will do the job until Apple fixes this bug. I first thought it had something to do with inputView.allowSelfSizing, but that variable didn't seem to change anything.
First, declare heightConstraint:
var heightConstraint: NSLayoutConstraint!
In viewDidLoad, Add your custom view:
let nibName: String! = UIDevice.isPhone ? "KeyboardViewiPhone" : "KeyboardViewiPad"
customView = Bundle.main.loadNibNamed(nibName, owner: self, options: nil)?.first as! UIView
customView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(customView)
Add a constraint for the width as you would do normally:
let widthConstraint = NSLayoutConstraint(item: view, attribute: .width, relatedBy: .equal, toItem: customView, attribute: .width, multiplier: 1.0, constant: 0.0)
Add a constant constraint for the height:
heightConstraint = NSLayoutConstraint(item: customView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: view.frame.height)
view.addConstraints([widthConstraint, heightConstraint])
Now comes the fix:
override func viewDidLayoutSubviews() {
heightConstraint.constant = view.bounds.height
}
As viewDidLayoutSubviews is called every time view.bounds changes, it will handle orientation changes correctly.
I also faced the same issue. this is because of the Autolayout Constraints. Just remove all constraints. and set auto resizing.
I have also faced the same problem for the custom keyboard extension in Xcode 8.2. This is caused by the auto resizing. In my case, I solved this in the below manner.
Initially, my custom keyboard have 3 views.
In this, I was fixed the trailing, leading, top and height for the first and last view. And place the middle view like in the image.
after that select the middle view and open the show the size inspector in the storyboard. In the size inspector, you will find an option auto resizing. In that select the constraint indicators for that view.
After selecting that you run your project in a device and it will work correctly without missing any view.
Note: - It will work for both portrait and landscape modes. And mainly you don't have to give constraints for the middle view.
IMHO, best working solution is using "Proportional Height". For example, in my case, I finally ended with 2 views. Top one got 0.8 of height of superview, bottom - 0.2. It's not perfect solution, but you can still benefits from autolayout.

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

Sizing a Container View with a controller of dynamic size inside a scrollview

I'm trying to create a container view, with a controller that has a dynamic height, inside a UIScrollView and have it sized automatically using auto layout.
View Controller A is the scrollview, which has the container view included, along with more content below.
View Controller B is the view controller that I want to have a dynamic size and for all the content to be displayed in full height in View Controller A's Scroll View.
I'm having some problems getting the dynamic size of B to automatically set the size of the Container View in A. However if I set a height constraint on the Container View in A ,
It would be the expected output if View Controller B would also have 250 height. It also works fine for height 1000, so as far as I know, all the auto layout constraints are properly setup. Unfortunately, since the height should actually be dynamic, I would like to avoid setting a height constraint at all.
I'm not sure if there are any settings for view controller B I can set for it to automatically update its size depending on its contents, or if there are any other tricks I've missed. Any help would be much appreciated!
Is there any way to size the Container View in A according to how big the size of View Controller B is without setting a height constraint?
Yup, there is. I managed to achieve that kind of behavior in one of my own projects.
All you gotta do is to tell the system that it should not add constraints that mimic the fixed frame set for your root view in Interface Builder. The best place to do this is in your container view controller when your embed segue is triggered:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// You might want to check if this is your embed segue here
// in case there are other segues triggered from this view controller.
segue.destinationViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
}
Important:
You gotta make sure that the view that you load into the container is constrained from top to bottom and you need to set the priority of one of the vertical constraints to a value lower than 1000. (It's a good practice to always use the bottom constraint for this.) This is necessary because otherwise Interface Builder will complain — with a good reason:
At design time your root view has a fixed size (height). Now if all your subviews have a fixed height and are connected with fixed constraints that all have the same priority it's impossible to fulfil all these requirements unless the height of your fixed root view coincidentally matches exactly the total height of your subviews and the vertical constraints. If you lower the priority of one of the constraints to 999 Interface Builder knows which constraint to break. At runtime however — when the translatesAutoresizingMaskIntoConstraints property is set as stated above — there is no fixed frame for your root view anymore and the system will use your 999 priority constraint instead.
From the answer of #Mischa I was able to make the height of a containerView dynamic depending on its content doing this:
In the viewController of the containerView write:
override func loadView() {
super.loadView()
view.translatesAutoresizingMaskIntoConstraints = false
}
And taking care that the vertical constraints in IB are all set. Doing this you do not need to set view.translatesAutoresizingMaskIntoConstraints = false from outside the view controller.
In my case I was trying to resize the container to a tableView inside the viewController. Because the tableView has a flexible height depending on its superview (so all OK for IB), I completed the vertical constraints in code by doing this:
#IBOutlet private var tableView: UITableView! {
didSet {
tableView.addConstraint(tableViewHeight)
}
}
private lazy var tableViewHeight: NSLayoutConstraint = NSLayoutConstraint(item: self.tableView, attribute: NSLayoutAttribute.Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 0)
And then observe the contentSize height of the tableview and adjust the constant of the tableViewHeight constraint programmatically when needed.
Swift 4, Xcode 9
The accepted answer alone didn't solve the problem for me.
My hierarchy: ScrollView --> Content View (UIView) --> Views | Container View | Other Views.
I had to add the following constraints to make both the ScrollView and Container dynamically adjust:
ScrollView: Top, Bottom, Leading, Trailing to Superview (Safe Areas)
Content View: Top, Bottom, Leading, Trailing, Equal Width to ScrollView, but also with Equal Heights (constraint with a lower priority: 250).
Views: normal auto-layout constraints.
Container View: Top, Bottom to neighbor views, Leading and Trailing to Safe Area.
Container View's embedded VC: Everything constraint connected vertically with the bottom constraint set to a lower priority, but bigger than the Content View's Equal Height one! In this case, a priority of 500 did the trick.
Set view.translatesAutoresizingMaskIntoConstraints = false in either prepareForSegue() or in loadView() as other answers stated.
Now I have a dynamically adjustable Container View inside an auto-resizing Scroll View.
Building on #Mischa's great solution to this problem, if the container embeds a dynamically sized UITableView, then you need to look at overriding it's contentSize and intrinsicContentSize, as well as setting translatesAutoresizingMaskIntoConstraints = false as per the accepted answer.
When the parent view loads and sets up the container view, the intrinsic height of each of the UITableViewCells is inferred to be 0, therefore the UITableViewDelegate method of:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
will not be called. This makes the UITableView appear as though it has no content.
Overriding the intrinsic content size to return the actual content size means that the tableview is displayed with the size it's contents require.
A great article by Emilio Peláez goes into more depth on this topic.
I tried this solution and worked for me.
In destination(child) view controller try to access to parent view controller like this:
if let parentVC = self.parent as? EmbededContinerViewController {
if let myParent = parentVC.parent as? ParentViewController {
myParent.subViewHeight.constant += 2000
myParent.subView.layoutIfNeeded()
}
}
Maybe this not normal solution but worked for me and solved my problem.
I tried this code in Swift 5.1 .

Loading view on UITableViewController

I am attempting to show a loading view on top of a UITableViewController when a user taps on a cell or a button in the cell. For some reason, the view does not show up, nor does it show any constraint failures. Can someone spot error in my code. I need this view to show up covering the tableview on both orientations, when shown. I thought this to be a view render issue, tried the same code in viewWillAppear. Still does not work. Hence I eliminated layout rendering issues. Same code works perfectly fine on UIViewController derived classes. Seems to have issues on UITableViewController classes alone!!!
private func initActivityView() {
let overlayView = UIView(frame: CGRectZero)
overlayView.backgroundColor = UIColor.lightGrayColor()
overlayView.translatesAutoresizingMaskIntoConstraints = false
overlayView.alpha = 0.5
self.view.addSubview(overlayView)
// add constraints
let viewDictionary = ["overlayView":overlayView]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
}
You should let the constraints engine know that it needs update. See the triggering Auto layout section of the UIView Class Reference. Also I would consider making your overlay view an instance variable or find some other way to keep a reference of it so that when the time comes you can remove it.
Do not call updateConstraints() but rather setNeedsUpdateContraints()
From the UIView Class Reference.
Discussion
When a property of your custom view changes in a way that
would impact constraints, you can call this method to indicate that
the constraints need to be updated at some point in the future. The
system will then call updateConstraints as part of its normal layout
pass. Updating constraints all at once just before they are needed
ensures that you don’t needlessly recalculate constraints when
multiple changes are made to your view in between layout passes.
If your having trouble yo can always set a breakpoint on the UIViewController's
viewWillLayoutSubViews() and viewDidLayoutSubViews() and inspect your subview frames.
It is also pretty helpful to keep update with Apple's documentation for Auto Layout as Auto Layout is constantly changing and improving.
Apparently doing it the autolayout way will not work, due to the inherent nature of UITableViewController. However a better solution for this problem will be something like this
let overlayView1 = UIView(frame: self.tableView.frame)
overlayView1.autoresizingMask = self.tableView.autoresizingMask
overlayView1.backgroundColor = UIColor.lightGrayColor()
overlayView1.alpha = 0.5
self.tableView.addSubview(overlayView1)

Resources