SnapKit and Dynamic UITableViewCell not being laid out correctly - ios

I'm currently building a reusable view that takes advantage of the dynamic resizing that UITableView has on offer with its cells.
What I'm trying to achieve is to give a view within the cell a width and a height depending on the size it has been given within the size property that is defined within a struct, I pass this through using a configure function, after setting the size of the view.
I then center it on the xAxis and apply any margins around the view using a property within the struct which is just a UIEdgeInset property. Using the SnapKit library this looks like this.
if let formImageItem = formImageItem,
let size = formImageItem.size {
formItemImgVw = UIImageView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
formItemImgVw?.image = formImageItem.image
formItemImgVw?.contentMode = formImageItem.aspectFit
contentView.addSubview(formItemImgVw!)
formItemImgVw?.snp.makeConstraints({ (make) in
make.size.equalTo(CGSize(width: size.width, height: size.height))
make.top.equalTo(formImageItem.margin.top)
make.bottom.equalTo(formImageItem.margin.bottom)
make.centerX.equalToSuperview()
})
}
The issue I seem to be having is i'm getting the following warning.
2018-10-12 14:57:34.101532+0100 xxxx Dev[6104:2160548] [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.
(
"<SnapKit.LayoutConstraint:0x2830ec720#FormImageViewTableViewCell.swift#61 UIImageView:0x10530a940.height == 114.0>",
"<SnapKit.LayoutConstraint:0x2830ec780#FormImageViewTableViewCell.swift#62 UIImageView:0x10530a940.top == UITableViewCellContentView:0x105503500.top>",
"<SnapKit.LayoutConstraint:0x2830ec7e0#FormImageViewTableViewCell.swift#63 UIImageView:0x10530a940.bottom == UITableViewCellContentView:0x105503500.bottom>",
"<NSLayoutConstraint:0x2837e2580 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x105503500.height == 44 (active)>"
)
Will attempt to recover by breaking constraint
<SnapKit.LayoutConstraint:0x2830ec720#FormImageViewTableViewCell.swift#61 UIImageView:0x10530a940.height == 114.0>
I understand what this is telling me basically that it's choosing to use the height 44 that is set by default for a table view cell. But I can't seem to get my head around why it's using this default height rather than the one that I have defined within my constraints and applying the height to the view whilst also adding the spacing to the top and bottom.
Also when using the debugger to inspect the UI it seems like a height isn't being set at all and as you can see the height of 44 is just clipping the entire imageview.
So ultimately what I'm trying to achieve is the ability to give a view a defined height and width, and use values to apply top and bottom spacing(margins) in order to resize the cell that it's currently within.

You need to lower the priority of the bottom constraints so replace this
make.bottom.equalTo(formImageItem.margin.bottom)
with
make.bottom.equalTo(formImageItem.margin.bottom).priority(999)
plus in viewDidLoad
tableView.estimatedRowHeight = 120
tableView.rowHeight = UITableViewAutomaticDimension
abd don't implement heightForRowAt

Related

How to corrently use UIViews systemLayoutSizeFitting to get the height to show all subviews using a given width?

TD;DR
It seems that in some cases systemLayoutSizeFitting does not return the correct height to correctly show / position all subviews of a view. Am I using systemLayoutSizeFitting wrong or is there some other way to avoid this?
Long story:
The XIB file of a UIViewController does not only contain the main view but also a number of other views which are added to the view controllers view at runtime. All these additional views should get the same height when they are added to the view controllers view.
The views might look like this: A simple container view holding some subviews which are stacked on top of each other.
Since the height of the container view should be flexible, the vertical spacing between the bottom button and the lable above it, uses a grater-than constraint.
To give all views the same height, I tried to measure the necessary height of each view using systemLayoutSizeFitting:
#IBOutlet var pageViews: [UIView]!
override func viewDidLoad() {
super.viewDidLoad()
var maxHeight: CGFloat = 0
for pageView in pageViews {
// Add pageView somewhere on view and give it leading, trailing and top
// constraint, but no height constraint yet.
addToView(pageView)
maxHeight = max(maxHeight, pageView.systemLayoutSizeFitting(CGSize(width: view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height)
}
for pageView in pageViews {
// Give all pageViews the same height
pageView.heightAnchor.constraint(equalToConstant: maxHeight).isActive = true
}
}
This does not work, when the label text becomes to long:
In the right example the height is not large enough and thus the button is squeezed. I can counter act this by raising the vertical compression resistance of the button, however in this case the other controls (e.g. the title label) is squeezed...
Why is this? Why does not systemLayoutSizeFitting return a height which is sufficent to show all controls without any squeezing?
Its actually smash button's height when label text is getting bigger . You are setting top and bottom constraints but button height is not declared so when label getting bigger , view basically say "I can reduce button height before updating my height , I have space.Bottom and top constraints are still same , didn't effect."
Giving the constant height constraints of button might be fix your issue.
If you want your view to resist to compression you should use the defaultHigh priority as a verticalFittingPriority instead of fittingSizeLevel.

Increasing height of UIView.frame programmatically works till iPhone 8 plus but doesn't works beyond model iphone x

I am increasing the height for UIView programmatically in runtime on click of button, it works fine on iPhone 6,7,8 and plus models but doesn't works on iPhone X and beyond models. i have initially set constraints programmatically as i views are autoLayout based. I also tried to set constraints at runtime which didn't helped too.
//Tried this and working till iPhone 8+
self.view.frame.size.height += CGFloat(280)
//or
self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width,
height: self.view.frame.height + 20.0)
//Tried setting height constraint
self.view.autoSetDimension(.height, toSize: 270)
Debug view hierarchy and look for conflicting constraints that might interfere with your height constraint. Also make sure you set translatesAutoresizingMaskIntoConstraints to false if you set constraints in code.
Also: if you set your constraint like this in code (apparently you are using PureLayout), that method returns the reference to that height constraint. So if you want to modify it, you need to update the constant value of your constraint, instead of setting a new one.
For any further help you certainly need to share more information.

Modify height of a container view

Here is my app with the profile on the left (ProfileViewController) and a containerView on the right (ContainerViewController). What I am trying to achieve is once the containerViewController has done its job, it will update the height of the UIView.
// ContainerViewController.swift
let heightConstraint = self.view.constraints.filter { $0.identifier == "Height" }.first
heightConstraint?.constant = height
When browsing the list of constraints, they're all emtpy, and I did set up some constraints in the storyboard. So Why ? Is there any way to access the UIView within the ContainerViewController ?
Ps:
DispatchQueue.main.async {
self.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
}
Sounds to work, but I think that modify constraint is proper ? Nope ?
What I would do, is to set IBOutlet for the constraint, and store it inside ContainerView.
This way, you don't depend on order number or anything else, to get this constraint, even if code (or storyboard) will changed in the future.
And Yes, the right way is to set the constraint constant, and not changing frame.
But be aware, that even constraint constant change need to be put on the main (UI) thread - (by the look of your code, I assume, you are dealing with the threads).

iOS Swift Setting View Frame Position Programmatically

I having two sub views inside scrollview. I need to position that both subviews programmatically. I did it correctly by writing code inside DispatchQueue.main.async. Here is the code:
DispatchQueue.main.async {
self.SelectClientDetailView.frame = CGRect(x: 0, y: 637, width: self.SelectClientDetailView.frame.size.width, height: self.SelectClientDetailView.frame.size.height)
self.SelectClientDetailView2.frame = CGRect(x: 0, y: 837, width: self.SelectClientDetailView2.frame.size.width, height: self.SelectClientDetailView2.frame.size.height)
}
Its working good but when I scrolling my scrollview this both views set back to old positions. How to fix it. Its Default y position will be SelectClientDetailView:400 and SelectClientDetailView2: 600
If you are using Auto Layout then setting frame will cause some weird effects. Auto Layout and Frames doesn't go together. You'll need to rearrange the constrains, not the frames. While using Auto Layout changing the frames will cause some weird effects and will eventually revert back to the constraints you've created in the original UIView.
Some solutions:
If you want to use Autolayout approach then, You need to create an outlet to each constrain just like you would to a view and change its constant when needed.
disable Auto Layout in that specific xib and start playing with frames.
If you only want to change the frame Y position, try this instead:
self.SelectClientDetailView.frame.origin.y = 637
self.SelectClientDetailView.frame.origin.y = 837
As already mentioned, you might need to check your view hierarchy to be sure you are actually adding them to the UIScrollView (and not elsewhere).

How to add leading padding to view added inside an UIStackView

This is my setup: I have an UIScrollView with leading,top, trialing edge set to 0. Inside this I add an UIStackView with this constraints:
stackView.centerYAnchor.constraintEqualToAnchor(selectedContactsScrollView.centerYAnchor).active = true
stackView.leadingAnchor.constraintEqualToAnchor(selectedContactsScrollView.leadingAnchor).active = true
Inside the stack view I add some views.
My issue is that because of the constraints the first view added to stack view will also have leading edge = 0.
What are the ways that I could add some padding to the first view ? Without adjusting the scroll view constraints.
When isLayoutMarginsRelativeArrangement property is true, the stack view will layout its arranged views relative to its layout margins.
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
stackView.isLayoutMarginsRelativeArrangement = true
But it affects all arranged views inside to the stack view. If you want this padding for only one arranged view, you need to use nested UIStackView
I have found that constraints don't work inside a Stack View, or they seem somewhat strange.
(Such as if I add leading/trailing constraint to selected on image stackview, that adds leading to collectionview too, but doesn't add trailing; and it be conflict for sure).
To set layout margins for all views inside the stackview, select:
Stack View > Size Inspector > Layout Margins > Fixed
Note: "Fixed" option was formerly called "Explicit", as seen in the screenshots.
Then add your padding:
The solution you have provided doesn't add a padding for your views inside your UIStackView (as you wanted in the question), but it adds a leading for the UIStackView.
A solution could be to add another UIStackView inside your original UIStackView and give the leading to this new UIStackVIew. Then, add your views to this new UIStackView.
Hint, you can do that completely using Interface Builder. In other words, no need to write code for it.
What worked for me is to add to stack view another UIView that's just a spacer (works at least with stackView.distribution = .Fill):
let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
stackView.addArrangedSubview(spacerView)
stackView.addArrangedSubview(viewThatNeedsSpaceBeforeIt)
stackView.addArrangedSubview(NextView)...
If you only need leading padding, then you can set the stack view's Alignment to "Trailing" and then you will be free to specify unique Leading constraints on each of its contained subviews.
As a bonus, you can also set the stack view's alignment to "Center" and then you can use Leading and/or Trailing constraints to give each item its own padding on both sides.
Set your stackview alignment to "center". After that you can give every subview different leading and trailing.
swift 3:
You just need set offset by:
firstView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 200).isActive = true
Be sure this constraint set after you parentView.addArrangdSubView(firstView)
The solution would be to have a regular view in the stack view to hold whatever views you are wanting to add constraints to, and then you can add constraints for your items that are relative to the views in the stack view. That way, your original views can have leading and trailing constraints within the stack view.
This can be done in the interface builder and programatically.
This question already has good answers,
One suggestion though, use spacing property to set the spacing between the views. For first and last views there can be two options, either set insets as #tolpp suggested or add constraint attaching to parent (stackview) with constant to add padding.
What we did was add transparent components (e.g., UIButton/UIView) as the first and last children of the UIStackView. Then set constrain the width of these invisible children to adjust the padding.
It seems that the solution was pretty simple. Instead of:
stackView.leadingAnchor.constraintEqualToAnchor(selectedContactsScrollView.leadingAnchor).active = true
I just wrote:
stackView.leadingAnchor.constraintEqualToAnchor(selectedContactsScrollView.leadingAnchor, constant: 15).active = true
Just add an empty view at the beginning of the stack view (also constraining its width and/or height):
stackView.insertArrangedSubview(UIView().constrain(width: 0, height: 0), at: 0)
and/or at the end:
stackView.addArrangedSubview(UIView().constrain(width: 0, height: 0))
I created this simple UIView extension to add the constraints:
extension UIView {
func constrain(width: CGFloat, height: CGFloat) -> Self {
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: width),
heightAnchor.constraint(equalToConstant: height)
])
return self
}
}

Resources