UIImageView with Aspect Fill inside custom UITableViewCell using AutoLayout - ios

I've been struggling with fitting an UIImageView which shows images of variable widths and heights with Aspect Fill. The cell height is not adapting to the new height of the UIImageView and persist it's height.
The hierarchy of the views is this
UITableViewCell
UITableViewCell.ContentView
UIImageView
I tried these scenarios in XCode Auto Layout :
set the UIImageView => Height to remove at build time
set the Intrinsic Value of the UIImageView to placeholder
set the Intrinsic Value for each of the UIImageView, ContentView and UITableViewCell to placeholder
With any of these combinations I get this view:
http://i.stack.imgur.com/Cw7hS.png
The blue lines represent the cell borders (boundaries) and the green ones represent the UIImageView border (boundaries). There are four cells in this example, the 1st and the 3rd ones have no images and the 2nd and the 4th ones have the same image (overflowing over the ones which have none).

I cobbled together a solution based on two previous answers:
https://stackoverflow.com/a/26056737/3163338 (See point 1)
https://stackoverflow.com/a/25795758/3163338 (See point 2)
I wanted to keep the AspectRatio of the image regardless of its Height while fixing up the Width according to that of the UIImageView, the container of the image.
The solution comprises of :
Adding a new AspectRatio constraint
let image = UIImage(ContentFromFile: "path/to/image")
let aspect = image.size.width / image.size.height
aspectConstraint = NSLayoutConstraint(item: cardMedia, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: cardMedia, attribute: NSLayoutAttribute.Height, multiplier: aspect, constant: 0.0)
when adding this constraint, xCode will complain about the new "redundant" constraint and attempt to break it, rendering it useless, yet displaying the image exactly like I want. This leads me to the second solution
Lowering the priority of the new constrain to '999' seems to stop xcode from breaking it, and it stopped showing warning message about the new constraint
aspectConstraint?.priority = 999
Not sure why xCode automatically adds UIView-Encapsulated-Layout-Height and UIView-Encapsulated-Layout-Height at build/run time; however, I learned how to respect that and live with it :)
Just leaving the solution here for anyone to check. This is working on iOS 8. I tried with iOS7 but it doesn't work the same as you need to implement tableView:heightForRowAtIndexPath calculating the height of the cell based on all the items contained within it and disable setting up:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0

Suppose you have an UIImage with dimensions 768 x 592 of width and height respectively, the height always remains equals, where the device it's rotated for example in the dimensions above (iPad), the width of the image change to 1024 and the height remains equal.
What you can do to maintain the aspect of the image is scale it in the dimensions you want, for example if you know that the images coming always have the same dimensions, we say for example 1280x740 , you can set the UIImage to .ScaleToFill and calculate in the following way:
(widthOfTheImage / heightOfTheImage) * heightWhereYouWanToSet = widthYouHaveToSet
For example :
(1280 / 740) * 592 = 1024
And it's the width I have to set in my UIImage to maintain the proportions of the image when it's change it's width.
I hope you understand where I try to say to you.

Related

UIStackView - Positioning Image up against edge of sibling

I am trying to create a custom UITableViewCell in iOS that contains a Label and a related image. The image needs to be as close the the trailing edge of the label as possible.
Below is an image of the progress so far.The Red area is the Horizontal UIStackView in which I have placed the UILabel (Green) and the UIImageView (Cyan).
The UILabel as been set to Lines = 0.
I've played around with a number of the UIStackView Distribution and Alignment properties and have in the past made good use of the approach outlined in this article A UIStackView Hack for Stacking Child Views Compactly. In line with the technique in that article I have a third transparent View that has Lower ContentHugggingPriority so takes up most of the room. This almost works but something is causing the label to wrap at that fix point, it looks like it's a 1/3 of the overall width.
Not all rows will show the image.
Does anyone have any other suggestions for how to achieve this layout? I have tried plain old Autolayout (no UIStackView) but that had other issues
This may be what you're after...
The stack view is constrained to Top and Bottom margins, Leading Margin + 12, and Trailing Margin >= 12, with these settings:
Axis: Horizontal
Alignment: Top
Distribution: Fill
Spacing: 0
The image view has Width and Height constraints of 24
The label has no constraints, but has:
Content Compression Resistance Priority
Horizontal: Required (1000)
The result (top set with image view, bottom set without):
Whilst I have marked DonMag's answer as THE answer for this question I am including my own answer as I am creating the Views programatically as that was my final solution.
First up create the UISTackView container for the label and image
this.drugNameStackView = new UIStackView()
{
TranslatesAutoresizingMaskIntoConstraints = false,
Axis = UILayoutConstraintAxis.Horizontal,
Alignment = UIStackViewAlignment.Top,
Distribution = UIStackViewDistribution.Fill,
Spacing = 0
};
Then having already created the UILabel add it to the StackView and set Compression Resistance
this.drugNameStackView.AddArrangedSubview(this.drugNameLabel);
this.drugNameLabel.SetContentCompressionResistancePriority(1000.0f, UILayoutConstraintAxis.Horizontal);
The key part of the solution and the main thing I learned from DonMag's answer where these two constraints that I added to the ContentView
this.ContentView.AddConstraints(
new[]
{
NSLayoutConstraint.Create(this.drugNameStackView, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.ContentView, NSLayoutAttribute.LeadingMargin, 1.0f, 0),
NSLayoutConstraint.Create(this.ContentView, NSLayoutAttribute.TrailingMargin, NSLayoutRelation.GreaterThanOrEqual, this.drugNameStackView, NSLayoutAttribute.Trailing, 1.0f, 0),
});
Note that it is the ContentView that is the the first item in the constraint and the UISTackView the second for the NSLayoutRelation.GreaterThanOrEqual constraint

Set UIImageView AspectRatio constraint programmatically in swift 3

I have an UIImageView in storyboard which AspectRatio is 1:1, that I want to change to 2:1 programmatically in ViewController in some cases. I create reference of that constraint in ViewController but unable to set the constraint.
You can change constraint programmatically in swift 3
let aspectRatioConstraint = NSLayoutConstraint(item: self.YourImageObj,attribute: .height,relatedBy: .equal,toItem: self.YourImageObj,attribute: .width,multiplier: (2.0 / 1.0),constant: 0)
self.YourImageObj.addConstraint(aspectRatioConstraint)
As it's stated in Apple's guide, there're three ways to set constraints programmatically:
You can use layout anchors
You can use the NSLayoutConstraint class
You can use the Visual Format Language
The most convenient and fluent way to set constraints is using Layout Anchors.
It's just one line of code to change aspect ratio for your ImageView
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0/2.0).isActive = true
To avoid "[LayoutConstraints] Unable to simultaneously satisfy constraints." you should add reference to your ImageView's height constraint then deactivate it:
heightConstraint.isActive = false
Set the multiplier of the constraint to 0.5 or 2 depending on your constraint condition, It'll become 2:1

How do i make the constraints resize the buttons correctly?

I've added a stoplight image and red, yellow, and green buttons. I want to have the buttons resize to iPhone 4S and iPhone 6S screens, but the buttons either disappear off the page or are the wrong size for the iPhone 4S. I thought the number of point would resize proportionately, but it appears it does not. Any help would be appreciated, I really want to understand constraints but I am just not getting it! Normally I would just do a x-position/screensize, y-position/screensize to relocated it, but this could be noticeably too long.
Here is the constraints of the latest incorrect location. When I try to select the stoplight image, it won't provide a constraint for the leading and trailing edge to the stoplight image.
The yellow button is placed against the stoplight image, but it won't resize.
The easiest solution would be to give all images fixed values for their width and height constraints. Then you can align the spotlightImage in the superview as you wish and define the alignment of the circle images relative to the stoplight image.
However, if you would like to stretch the width of the stoplight image depending on the width of the screen, this is a complex problem. I played around a bit trying to define all constraints in storyboard, but could not come up with a proper solution. What one ideally would like to do, for example, is define the centreX of the circles proportionally to the spotlight image's width. Similarly for the y position. Unfortunately this is not possible.
In code one have a little bit more control. Here is a solution that will work. It is not pretty, because you are actually recalculating the width of the spotlightImage, but it works :-)
class ViewController: UIViewController {
lazy var stopLightImageView: UIImageView = {
return UIImageView(image: UIImage(named:"stopLight"))
}()
lazy var circleImageView: UIImageView = {
return UIImageView(image: UIImage(named:"circle"))
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
//Values at start. This is used to calculate the proportional values, since you know they produce the correct results.
let stoplightStartWidth: CGFloat = 540
let stoplightStartHeight: CGFloat = 542
let circleStartWidth: CGFloat = 151
let circleStartLeading: CGFloat = 231
let circleStartTop: CGFloat = 52
let screenWidth = UIScreen.mainScreen().bounds.size.width
let stoplightMargin: CGFloat = 20
self.view.addSubview(stopLightImageView)
stopLightImageView.translatesAutoresizingMaskIntoConstraints = false
//stoplightImage constraints
stopLightImageView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: stoplightMargin).active = true
stopLightImageView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor, constant: -stoplightMargin).active = true
stopLightImageView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: 0).active = true
stopLightImageView.heightAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: stoplightStartWidth/stoplightStartHeight).active = true
self.view.addSubview(circleImageView)
circleImageView.translatesAutoresizingMaskIntoConstraints = false
//circle constraints
circleImageView.widthAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: circleStartWidth/stoplightStartWidth).active = true
circleImageView.heightAnchor.constraintEqualToAnchor(circleImageView.widthAnchor, multiplier: 1).active = true
let stoplightWidth = screenWidth - 2*stoplightMargin
let stoplightHeight = stoplightWidth * stoplightStartHeight/stoplightStartWidth
circleImageView.leadingAnchor.constraintEqualToAnchor(stopLightImageView.leadingAnchor, constant: stoplightWidth*circleStartLeading/stoplightStartWidth).active = true
circleImageView.topAnchor.constraintEqualToAnchor(stopLightImageView.topAnchor, constant: stoplightHeight*circleStartTop/stoplightStartHeight).active = true
}
}
Constraints are tricky, and it looks like you have a lot going on there. It's hard to tell you exactly what to do for this so, here's what I would try to do if I was having this issue(hopefully one works for you):
Set the images in the Attributes Inspector to either Aspect Fit or Redraw... That should fix your issue with them being different shapes.
Also look through the list of constraints to see if one relies on another, (for example the red and yellow seem to have similar constraints). If they rely on each other, ensure to satisfy any constraints that aren't yet - based off of the "parent" image.
Select everything and set to "Reset to Suggested Constraints". Build and run. If that doesn't fix it then there's only a few things left you can do.
Remove all the constraints on every object. Start with the black image and add missing constraints... or set it to "Center Horizontally in Container". Right click and drag the image or asset to your "view" or to the yellow "First" circle located above.
Hopefully this helps.

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

Adding vertical space constraint equal to superview height multiple

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)

Resources