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()
}
Related
Swift 4 + Xcode 9.
I have been working on this problem for weeks, trying to solve it myself. I would appreciate any help that anyone could provide. I'm not posting any code in my initial question because it's proprietary. I will be happy to provide pieces of it if it's required to help solve my problem.
I have a UITableView with a custom cell, which contains a very complex layout of subviews, some of which are hidden or shown (using 1000-priority height=0 constraints which are added and removed during cellForRowAt) depending on the data. There is also an ImageView which should always be full width, and should change height to match the image, which is loaded via Kingfisher.shared.retrieveImage(). Once I have the image, I update the aspectRatio constraint on the image for that cell, and the cells display. This works perfectly for the first 15-20 cells, but as I scroll through more rows, it simply stops functioning. The images are small and centered, certain data fields are not updated, etc. If I keep scrolling, sometimes a cell will behave correctly here and there, but nearly all do not.
Now for the interesting part: If I scroll BACK UP, every single cell reformats itself automatically to look as it should, and after that, every cell is perfect. The code obviously works - and I feel like this may be a bug in the platform, but before I assume that, I wanted to see if anyone else had run into something like this.
Again, thank you very much for any help you can provide - I'm very anxious to solve this.
UPDATE: To answer a couple of the questions, here is a snippet of code that is part of the custom cell class. This is how I set the cell's image (which includes updating the aspect ratio constraint), and how I reset the cell for re-use.
internal var aspectConstraint : NSLayoutConstraint? {
didSet {
if oldValue != nil {
imageView.removeConstraint(oldValue!)
}
if aspectConstraint != nil {
imageView.addConstraint(aspectConstraint!)
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
aspectConstraint = nil
imageView.image = nil
for view in subviews {
for c in view.constraints {
if c.firstAttribute == NSLayoutAttribute.height && c.constant == 0 {
view.removeConstraint(c)
}
}
}
}
func setCustomImage(image : UIImage) {
let aspect = image.size.width / image.size.height
let constraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: imageView, attribute: NSLayoutAttribute.height, multiplier: aspect, constant: 0.0)
constraint.priority = UILayoutPriority(999)
aspectConstraint = constraint
imageView.image = image
}
For anyone who comes to this question, due to complete lack of resolution on this issue, I have concluded that UIKit is unsuitable for my purposes. I went with Texture (formerly AsyncDisplayKit), and my progress has resumed.
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.
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.
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.
I want to achieve a very simple thing. I have a UIView, i want the vertical space between my UIView bottom and bottom layout guide to be 10% of the container height (in this case viewController.view). How can achieve this in storyboards?
So some thing like this
UIView.bottom = Height of superView * 0.1 + 0 from the Bottom layout guide
is there anyway to achieve this in storyboards. Currently i can just some constant magic number which will not work on iPhone 4s all the way till iPhone 6 plus.
Clicking on the constraint shows this properties, so how can i put something like superViewHeight * 0.1 in here. I understand that i can do this if i am setting the height of the view but how to do in this case.
Thanks
You need to invert the first and second item in this case. Simply click on first item dropdown and you will see the option.
Secondly, give a value of 0.9 in multiplier section. That will make the gap 10% of total height.
If I understand correctly, you want to create a constraint in proportion to the superview's height and not the height of the view itself.
You can do this programmatically by creating an NSLayoutConstraint and specify it's constant an run time.
let marginToBottomLayout = customView.superview!.frame.size.height * 0.1
let constraint = NSLayoutConstraint(item: customView,
attribute: .Bottom,
relatedBy: .Equal,
toItem: self.bottomLayoutGuide,
attribute: .Top, multiplier: 1.0,
constant: marginToBottomLayout)