UIScrollView dynamic height content (autolayout) not filling whole space - ios

I am creating a small chat app in which I use a custom subclass of UIScrollView to present the messages. This app is just for practicing so I don't want to use a third party library. I am implementing this UIScrollView via autolayout, after reading the technical note 2154 by Apple and several tutorials explaining this, and my implementation is almost working but the content view of my UIScrollView doesn't seem to fill all the space available.
The code which presents the ScrollView is:
public class ChatView: UIScrollView {
private var contentView: UIView
...
// This get called by all the init methods. contentView is already created ( contentView = UIView() )
private func setupViews() {
self.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
let constraint1 = NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1.0, constant: 0.0)
let constraint2 = NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
let constraint3 = NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, toItem: contentView, attribute: .Width, multiplier: 1.0, constant: 0.0)
let constraint4 = NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal, toItem: contentView, attribute: .CenterX, multiplier: 1.0, constant: 0.0)
self.addConstraints([constraint1, constraint2, constraint3, constraint4])
self.layoutIfNeeded()
// Later, the messages are added to the contentView. I don't think is relevant to see the exact code (but I can post it if needed)
// Each message is added using autolayout and the constraints only reference the messages themselves and contentView
}
}
When I add a ChatView to my view controller (using storyboards), with its four sides pinned to views which are not in his hierarchy, the following problem happens:
In the image, the scrollView cannot be scrolled upwards any more. There seem to be a space which should be filled and isn't. If I scroll down, I have the exact same problem but the empty space is below the content. In the following images you can see that the contentView is smallest than the ChatView itself:
And the same view hierarchy but with the constraints shown:
In both images the view in the background is the ChatView and the selected one is the contentView. I haven't been able to figure why the content view doesn't cover the full ChatView space.
Thanks in advance!

I finally stumbled upon the answer while searching a different problem, in another stackoverflow question. The key is to open the storyboard and set in the container view controller "AdjustsScrollViewInsets" to "NO".
In code it's simply (inside the view controller):
self.automaticallyAdjustsScrollViewInsets = NO;

Related

Center the subview in UIView using constraints in IOS

i have three subview like subview1,subview2,subview3 in a UIViewController inside a UIView.
I'm using constraints on them, the constraints which i'm using to show then in a center when the button is clicked are these,
Horizantally in Container,
Vertically in Container,
width and
height.
Issue :
When I run the app and click the button once it comes in the right position. But when i dismiss it and try again to open it, the subview change its position itself and comes on the top left of the UIViewcontroller ,
As you can see in a screen shot, but i want to show the view every time when i click the button in the center of the UIViewcontroller.
I'm confused that constraints are right but why it always changes its position when we open it again.
My code is ,
- (IBAction)Precord:(id)sender {
_DateView.hidden=NO;
[self.DateView bounceIntoView:self.view direction:DCAnimationDirectionLeft];
}
The contents in story board are like this ,
enter image description here
It depends on the way you are dismissing the View. I just tried below code and it worked for me
- (IBAction)btn_Tapped:(id)sender {
_smallView.hidden = false;
[_smallView bounceIntoView:self.view direction:DCAnimationDirectionLeft];
}
- (IBAction)hide_Tapped:(id)sender {
_smallView.hidden = true;
}
Basically i am just hiding the view. And when btn_Tapped, everytime the view is displaying with bouncing animation in the center. Let me know how you go!
try this
yourSubView.center = CGPointMake(yourMainView.frame.size.width / 2,
yourMainView.frame.size.height / 2);
Simple way to programmatically add constrains. Modify it with your desire size.
Notice 1: toItem: self, where self is just a view where u added the subView.
Notice 2: SubView MUST be added to self or a subView of self.
subView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: subView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: subView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: subView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 0.5, constant: 0).isActive = true
NSLayoutConstraint(item: subView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.5, constant: 0).isActive = true
I would recommend adding constrains in Storyboard. Easier to see/read, clear code and error validator.
On your problem i would say it a animation problem. Just before adding your view run again the method that set the view frames/constrains (to reset the animation position modification).
Or just when animation is finish, instead of hiding the view, remove it from super view. And when u need it again, just add it again.

Collapsable Tableview with Dynamic sizing cells

I've been trying to follow this github tutorial, however instead of having static cells, they need to be dynamic since I don't know the height at compile time.
In the tableViewController I have set the heightForRowAt to be 0 or UITableViewAutomaticDimension depending if the section is collapsed or not. Similar with the estimatedHeightForRowAtthe value is 0 or a higher value if it's expanded.
Getting to the problem now, I have subclassed the UITableViewCell and when the cellForRowAt is called it will add a subview like the following:
override func addSubview(_ view: UIView) {
guard view != nil else {return}
for view in self.subviews {
view.removeFromSuperview()
}
super.addSubview(view)
//view.applyLeadingAndTrailingPinConstraint(toSuperview: 0)
//view.applyTopAndBottomPinConstraint(toSuperview: 0)
let addedView = self.subviews[0]
let bottom = NSLayoutConstraint(item: addedView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
bottom.priority = 750
bottom.isActive = true
NSLayoutConstraint(item: addedView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
//NSLayoutConstraint.reportAmbiguity(v: self)
if addedView.hasAmbiguousLayout {
print("Horizontal: \(addedView.constraintsAffectingLayout(for: .horizontal))")
print("Vertical: \(addedView.constraintsAffectingLayout(for: .vertical))")
print("Trace: \(addedView.value(forKey: "_autolayoutTrace"))")
}
//addedView.exerciseAmbiguityInLayout()
//print("self: \(self.frame), subview: \(self.subviews[0].frame)")
}
As you may see, the problem is that there's an ambiguity with the constraints, hence the hasAmbigousLayout. When the view is collapsed there is no error. However when I expand the view, which has also a height constraint, the ambiguity happens. The following is the output of the 3 print statements inside hasAmbigousLayout:
Horizontal: []
Vertical: [<NSLayoutConstraint:0x60800028ea10 UIView:0x7fcfe856a790.height == 100 (active)>]
Trace: Optional(
•ViraVira_Info.WellnessCell:0x7fcfe8859a00'cell', MISSING HOST CONSTRAINTS
| *UIView:0x7fcfe856a790- AMBIGUOUS LAYOUT for UIView:0x7fcfe856a790.minX{id: 776}, UIView:0x7fcfe856a790.minY{id: 771}, UIView:0x7fcfe856a790.Width{id: 778}
Legend:
* - is laid out with auto layout
+ - is laid out manually, but is represented in the layout engine because translatesAutoresizingMaskIntoConstraints = YES
• - layout engine host)
I have tried to lower the bottom constraint priority as sugested in this question. Also I have tried many other sites, which I sadly lost track of.
The result in the app is that the view does not even show up, since it should have a red background, also when i try to print the frame of the subview, the width is 0 which seems odd.
I probably made a silly mistake with the constraint but after 2 days trying I still can't figure it out.
Thanks for your time
-Jorge
There are several issues to note (and may be part of the problem):
override func addSubview(_ view: UIView) -> I don't think thats the right place for your code. I develop several years now iOS and there was never a reason to override that method. View layout should be done in Interface Builder or in the parent view.
guard view != nil else {return} -> should give you a warning, because view is not an optional (UIView?), so it can't be nil. This check does simply nothing useful.
for view in self.subviews { view.removeFromSuperview() } super.addSubview(view) -> This will remove every previously added view, the last added view will win the race, thats completely unexpected by the caller of addSubview - If a cell adds more than one subview this will cause trouble and headache.
let addedView = self.subviews[0] -> must be equal view, so why not using view in the later code?
6 * NSLayoutConstraint -> A view is normally positioned by 4 constraints, 2 vertical and 2 horizontal - I think you should remove the last two (center constraints) - That may be your problem.
Try to refactor your code...

Programmatic Autolayout changes behaviour of a UIView

I have a controller where I add a subview programmatically. With the configuration of the subview I add autolayout constraints programmatically. Everthing is working except that the view doesn't react on touches if I add the constraints and even the set backgroundcolor is not displayed.
The buttonView should be displayed in the lower right corner of my parent view.
Any ideas what could be wrong?
Here is how I add my constraints:
private func configureAutolayoutConstraints(buttonView: UIView, parentView: UIView){
buttonView.translatesAutoresizingMaskIntoConstraints = false
let bottomConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Bottom, relatedBy: .Equal, toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: -130)
parentView.addConstraint(bottomConstraint)
let trailingConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Trailing, relatedBy: .Equal, toItem: parentView, attribute: .Trailing, multiplier: 1.0, constant: -90)
parentView.addConstraint(trailingConstraint)
}
Autolayout engine needs at least 4 constraints to determine the frame of view. You have applied bottom and trailing constraints only. You need either width+height OR leading+top constraint to make it work.

How to add a constraint programmatically so that the height of a view just fits the bottom of its last subview?

I have constraint code that lays out a number of UILabels (4+) vertically from top to bottom inside a container view (a regular UIView). I now want my container view to be sized so that its height matches the bottom of that last label that I've added.
I have tried this:
let constraintPanelHeight = NSLayoutConstraint(item: cell.panelOptions,
attribute: .Height, relatedBy: .Equal, toItem: priorLabel!,
attribute: .Bottom, multiplier: 1, constant: 0)
cell.contentView.addConstraint(constraintPanelHeight)
but this generates an Invalid pairing of layout attributes error since I'm matching .Height of one view with .Bottom of a subview (I'm guess that's why).
How can I auto-size my containing view like this?
I don't know what your trying to achieve there exactly. If you want to pin the one cell to the bottom of the other, you'll have to use attribute .Top and another constraint for the height. For example: you have 10 labels in your view, then set your height to one tenth of the superview:
let heightConstraint = NSLayoutConstraint(item: cell.panelOptions, attribute: .Height, relatedBy: .Equal, toItem: superview, attribute: .Height, multiplier: 0.1, constant: 0)
But since iOS9 there's also the UIStackView which makes laying out subviews in a view (vertically or horizontally) very easy. Have a look at that if you want to spread your labels evenly in your superview.
If you are fixed number of views the easiest and most readable way is the use the visual layout format and constraints like this:
NSLayoutConstraint.constraints
WithVisualFormat("V:|[view1][view2][view3]|",
options: NSLayoutFormatOptions.AlignAllCenterX,
metrics: nil,
views: ["view1": view1, "view2": view2,"view3": view3])
If you want to add a variable number the principle is the same but achieved with a loop. You shouldn't set the height of anything. Let the intrinsicContentSize of each label size the container from the inside. You might need to set the priority of contentCompressionResistance of each label to 1000 just to be sure the labels don't get squashed. Keep in mind the you will need horizontal constraints as well but they should be simpler to work out.
And here's the version for a variable number of subviews:
var prevView : UIView?
for view in views{
container.addSubview(view)
//
// Add horizontal constraints for each view to fit the container
// exclude for simplicity
// Add vertical constraints
if prevView == nil{
container.addConstraint(NSLayoutConstraint(item: container,
attribute: .Top,
relatedBy: .Equal,
toItem: view,
attribute: .Top,
multiplier: 1, constant: 0)
)
} else if view == views.last!{
container.addConstraint(NSLayoutConstraint(item: container,
attribute: .Bottom,
relatedBy: .Equal,
toItem: view,
attribute: .Bottom,
multiplier: 1,
constant: 0))
} else if let prev = prevView {
container.addConstraint(NSLayoutConstraint(item: prev,
attribute: .Bottom,
relatedBy: .Equal,
toItem: view,
attribute: .Top,
multiplier: 1,
constant: 0))
}
prevView = view
}

Determining width of a UIView using autolayout - SWIFT

Alright so in the interface builder (Main.storyboard), I have a containerView:UIView embedded in a UIScrollView. Within in the containerView I want to create additional UIView's to hold blocks of content such as a header, body, etc. The reason for doing it like this, is so that the content can scroll vertically but not horizontally.
My goal is to use autolayout to create these different UIView's. As of right now the containerView automatically adjusts it's width depending on the screen size of the device being used, as to prevent horizontal scrolling. It does this using an IBOutlet I created for the width constraint. It currently looks like so:
#IBOutlet var containerView: UIView!
#IBOutlet var containerViewWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
//Prevents horizontal scrolling
containerViewWidthConstraint.constant = self.view.frame.size.width
createHeader()
}
Then I created a function called createheader{} which pins a headerView:UIView at the top of the containerView, and 8 points from either edge of the containerView:
func createHeader() {
//Create header
let headerView = UIView()
headerView.backgroundColor = UIColor.blueColor()
self.containerView.addSubview(headerView)
//Create header constraints
let leftMargin = NSLayoutConstraint(item: headerView, attribute: .Leading, relatedBy: .Equal, toItem: containerView, attribute: .Leading, multiplier: 1.0, constant: 8)
let rightMargin = NSLayoutConstraint(item: containerView, attribute: .Trailing, relatedBy: .Equal, toItem: headerView, attribute: .Trailing, multiplier: 1.0, constant: 8)
let topMargin = NSLayoutConstraint(item: headerView, attribute: .Top, relatedBy: .Equal, toItem: containerView, attribute: .Top, multiplier: 1.0, constant: 70)
let heightConstraint = NSLayoutConstraint(item: headerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 160)
//Activate header constraints
headerView.setTranslatesAutoresizingMaskIntoConstraints(false)
NSLayoutConstraint.activateConstraints([leftMargin,rightMargin,topMargin,heightConstraint])
println(headerView.frame.size.width)
}
Now since the size of the content inside the headerView will be dependent on the screen size of the device being used, I want to be able to create functions that size the width of the content depending on the size of the width of the headerView itself. However every time I try to grab the width of the headerView using:
println(headerView.frame.size.width)
It returns a value of zero, which is obviously not the case because it is still creating a blue-background headerView according to the constraints above.
Why is SWIFT not recognizing that the headerView has a width? And how can I grab the width of the headerView?
After installing constraints you need to call layoutIfNeeded if you want to update the frames immediately.
func createHeader() {
//Create header
let headerView = UIView()
headerView.backgroundColor = UIColor.blueColor()
self.containerView.addSubview(headerView)
...
//Activate header constraints
headerView.setTranslatesAutoresizingMaskIntoConstraints(false)
NSLayoutConstraint.activateConstraints([leftMargin,rightMargin,topMargin,heightConstraint])
self.containerView.layoutIfNeeded() // Updates the frames
println(headerView.frame.size.width) // Will output the correct width
}
Note that this will happen automatically on the next iteration of the UI loop which is, however, not helpful to you when you want to see the effects immediately.

Resources