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
I am encountering a strange behaviour when using UITextView with a long text. In particular, when setting the maximumNumberOfLines of its textContainer to 0, the content disappears completely.
I have created a small test project to be sure there was nothing strange in my code that was causing it, and you can see it in the screenshots. In my project I have a UIViewController that contains a UIScrollView, that contains a vertical UIStackView. The stack view contains a UITextView (the red title in the screenshots), another stackview containing a label, text view, button, and then other text views.
When the button is clicked I am setting maximumNumberOfLines to 0 (before it was 2), and the content just disappears. I've tried with and without animation and I have the same result.
The disappearing seems to be related to the height of the final text, as if I use a smaller font, the content disappears only after setting much more text.
Any ideas why it could be happening?
For completeness, I am using Xamarin.iOS, and here
is my ViewController.
The Content disappears because your text is too large for a UIView object to show. According to this post we know that, actually UIView has a maximum height and width limited by the amount of memory they consume.
In my test if we don't set too much characters to the textView(remove some textView.Text += textView.Text;), the content will show. And I also tested that on UILabel, so does it. Because they all inherit from UIView.
If you do want to show so many strings, try to enable the textView's ScrollEnabled. Do not let the textView's Bounds exceed the maximum limit. You can try to add the Height and Width Constraints when you want to expand the textView:
var textViewContraints = new NSLayoutConstraint[]
{
NSLayoutConstraint.Create(textView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, 700),
NSLayoutConstraint.Create(textView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, 500)
};
UIView.AnimateNotify(1.2d, () =>
{
if (!buttonExpanded)
{
textView.ScrollEnabled = true;
textView.TextContainer.MaximumNumberOfLines = 0;
textView.TextContainer.LineBreakMode = UILineBreakMode.WordWrap;
textView.SizeToFit();
textView.AddConstraints(textViewContraints);
expandButton.Transform = CGAffineTransform.MakeRotation((nfloat)(Math.PI / 2.0f));
textView.Text = "r" + textStr;
textView.Text = textView.Text.Substring(1);
}
else
{
textView.ScrollEnabled = false;
foreach (NSLayoutConstraint constraint in textView.Constraints)
{
textView.RemoveConstraint(constraint);
}
textView.TextContainer.MaximumNumberOfLines = 3;
textView.TextContainer.LineBreakMode = UILineBreakMode.WordWrap;
expandButton.Transform = CGAffineTransform.MakeRotation(0f);
textView.Text = textStr;
}
scrollView.LayoutIfNeeded();
buttonExpanded = !buttonExpanded;
}, null);
I have a scroller at the bottom of my view controller which contains buttons. Buttons are not always active, they change as the user adds them to his favourites. Which means I have to decide on the x position of the buttons programmatically.
What I have done is created outlets of all the buttons from the view controller in the storyboard to the script and I am attempting to access them like this (simplified code):
var Items = [String : Bool]
#IBOutlet weak var item1: UIButton!
...
if Items[item] == true {
item1.isEnabled = true
\\ here comes a line of code where i should position item1's x position but I haven't managed to find the right way
}
The question is how to change the x position of UIButton programmatically, and are there better ways to handle the problem? I am using Xcode 8 and swift 3
EDIT :
Method for constraints:
func activeConstraint(element: UIButton, constant: Int) {
let widthConstraint100 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 100)
let widthConstraint0 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 0)
if constant == 0 {
element.removeConstraint(widthConstraint100)
element.addConstraint(widthConstraint0)
} else {
element.removeConstraint(widthConstraint0)
element.addConstraint(widthConstraint100)
}
element.updateConstraints()
}
Example of call to the function:
if Items[item] == true {
activeConstraint(element: item, constant: 100)
} else {
activeConstraint(element: item, constant: 0)
}
EDIT 2:
Although I have seen many questions here referring UIStackView (like this one), none of the solutions worked for me. I have an horizontal scroller at the bottom of the screen with many buttons that get activated or deactivated programmatically with their width constraint set to 0 or 100. Here is the final result that works in my case (XCode 8.0) :
Hierarchy:
image
UIScrollView HAS to have one element: View (lets call it Container View) that holds all the contents of the scroller. Container view needs to have 4 constraints - top, bottom, leading and trailing set towards superView (UIScrollView). (preferably all 4 set to 0). Also set vertical constraint to center horizontally from Container view to superView - UIScrollView.
StackView holds all the buttons - it needs to have all 4 constraints (top, bottom, leading, trailing) set towards superView which is Container View.
All buttons need to have their width constraint set to a constant of desire.
I have tried to make a UILabel that is a certain width using preferredMaxLayoutWidth but no matter what I do it won't work. Can you help me? I have tries so many different combinations to make it work.
#IBAction func addBottomTextButton(sender: AnyObject) {
if addBottomTextField.text.isEmpty == false {
let halfScreenWidth = screenSize.width * 0.5
let bottomScreenPosition = screenSize.width
memeBottomText = addBottomTextField.text
fontName = "Impact"
let memeBottomTextCaps = memeBottomText.uppercaseString // --> THIS IS A STRING!
labelBottom.text = memeBottomTextCaps
labelBottom.textColor = UIColor.blackColor()
labelBottom.textAlignment = .Center
labelBottom.font = UIFont(name: fontName, size: 32.0)
labelBottom.sizeToFit()
labelBottom.userInteractionEnabled = true
labelBottom.adjustsFontSizeToFitWidth = true
labelBottom.numberOfLines = 1
labelBottom.preferredMaxLayoutWidth = screenSize.width
labelBottom.setTranslatesAutoresizingMaskIntoConstraints(true)
var r = CGFloat(halfScreenWidth)
var s = CGFloat(bottomScreenPosition)
labelBottom.center = CGPoint(x: r, y: s)
self.view.addSubview(labelBottom)
self.view.addConstraint(NSLayoutConstraint(item: labelBottom, attribute:
NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: labelBottom,
attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0))
dismissKeyboard()
}
}
Judging by your code I'd say your problem was you haven't got your constraints setup correctly and you're mixing using NSLayoutConstraints with setting the position using center and setting the size using sizeToFit.
Firstly, in the constraint you've setup you're relating labelBottom (the item argument) to itself (the toItem argument). I'm not exactly sure what you were trying to achieve with that? I'd recommend having a look at some tutorials on AutoLayout if you're unfamiliar with its concepts. Here's a good one: http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1
Secondly, just a small point, on the line let memeBottomTextCaps = memeBottomText.uppercaseString you've written // --> THIS IS A STRING. An easier way to remind yourself of the variable type when looking back at your code could be to use: let memeBottomTextCaps: String = memeBottomText.uppercaseString.
Thirdly, preferredMaxLayoutWidth isn't used to set the width of a UILabel - that's what the frame is for (or NSLayoutConstraints if you're using AutoLayout).
Lets get on with it!
Here's an example of how to create a label that is pinned to the bottom edge of its container view and is not allowed to be wider than it's container: (Keep in mind that all this can be done in IB)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
// 1.
label.setTranslatesAutoresizingMaskIntoConstraints(false)
// 2.
label.text = // Put your text here.
// 3.
self.view.addSubview(label)
// 4.
let pinToBottomConstraint = NSLayoutConstraint(item: label,
attribute: .Bottom,
relatedBy: .Equal,
toItem: self.view,
attribute: .Bottom,
multiplier: 1.0,
constant: -8.0)
// 5.
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("|-8-[label]-8-|",
options: .DirectionLeadingToTrailing,
metrics: nil,
views: ["label" : label])
// 6.
self.view.addConstraint(pinToBottomConstraint)
self.view.addConstraints(horizontalConstraints)
}
}
The following referrers to the commented numbers in the code above.
1. You need to set setTranslatesAutoresizingMaskIntoConstraints to false to stop constraints being created that would otherwise conflict with the constraints we're going to create later. Here's the what Apple have to say about it:
Because the autoresizing mask naturally gives rise to constraints that fully specify a view’s position, any view that you wish to apply more flexible constraints to must be set to ignore its autoresizing mask using this method. You should call this method yourself for programmatically created views. Views created using a tool that allows setting constraints should have this set already.
2. You need to make sure you put your own text here, otherwise the code won't run.
3. The label must be added to the view hierarchy before adding constraints between it and it's superview! Otherwise, in this case, you'll get a runtime error saying:
Unable to parse constraint format:
Unable to interpret '|' character, because the related view doesn't have a superview
|-8-[label]-8-|
This is due to our horizontalConstraints needing to know the label's superview (the superview is denoted by the "|") but the label doesn't have a superview.
4. The pinToBottomConstraint constraint does what it says. The constant of -8 just specifies that I want the label to be 8 points from the bottom of its container view.
We don't need to create a constraint to specify the label's size - that's an intrinsic property of the UILabel which is determined, for example, by the number of lines and font.
5. The horiontalConstraints are created using Visual Format Language. Here's a good tutorial: http://code.tutsplus.com/tutorials/introduction-to-the-visual-format-language--cms-22715 Basically, "|-8-[label]-8-|" creates constraints to pin the left and right edges of the label to the left and right edges of its superview.
6. Finally add the constraints!
This is what it looks like:
I hope that answers your question.
I think the property only work for multiline situation.
// Support for constraint-based layout (auto layout)
// If nonzero, this is used when determining -intrinsicContentSize for multiline labels
#available(iOS 6.0, *)
open var preferredMaxLayoutWidth: CGFloat
And it indeed true after my test. So, we need set multiline. It will work.
titleLabel.numberOfLines = 0
I don't why Apple limit it to only multiline. In fact, we often need to set max width on label easily by one property.
Finally, if we want set max width , we need set max constaint, like the following
if device.isNew == "1" {
self.title.mas_updateConstraints { (make) in
make?.width.lessThanOrEqualTo()(163.w)
}
self.newTag.isHidden = false
} else {
self.newTag.isHidden = true
self.title.mas_updateConstraints { (make) in
make?.width.lessThanOrEqualTo()(207.w)
}
}
QUESTION:
I'll simplify the question but keep my original for reference...
I am modifying the priority of existing constraints, but the result only changes the position of one of the UIScrollView subviews. All remaining subviews maintain their original size and position, yet it looks like I am incurring a layout pass on all subviews below the one I am modifying constraints on. So, why is ViewWillLayoutSubviews and UpdateViewConstraints being called on things that haven't changed?
[Original question]
See the details below. What is the cause of the scrolling hesitation seen in the included screencasts and how can I fix it?
BACKGROUND:
I’ve built an accordion style list control that hosts the views of several child UIViewControllers, each paired with a header view to enable the user to toggle visibility of its content view. I’ve created this list control using a UIScrollView with auto layout. I’ve become very familiar with the intricacies of auto layout with a UIScrollView but admit that I am pretty new to auto layout in general. I’ve relied heavily on Apple’s documentation and related blog posts from the community:
Apple's Documentation
Relevant StackOverflow questions
among many, many others.
I’ve implemented this control so that the header views can be floated above other UIScrollView content. Very much like the section views of a grouped UITableView, they will stick to the top of the UIScrollView as the user scrolls down to view more content. Incidentally, I originally built this using a UITableView, but the way it manages visible cells caused scrolling performance issues of its own.
PROBLEM:
I’m having some performance issues when scrolling content. I’ve done some troubleshooting, and I’ve found that when the “floating header” feature is disabled, scrolling performance is pretty good (although there is still some hesitation on expanding/collapsing a section which may have the same cause as my scrolling performance issue). But when this feature is enabled, scrolling hesitates as each header view is floated. I’ve included a screencast of my prototype running on my iPod Touch 5.
Screencast of prototype running on iPod Touch 5
It’s a very minor hesitation, but this prototype has significantly less complex content views. The final project shows hesitation of up to about a second.
DETAILS:
The prototype has been built using Xamarin, but I'm proficient in Objective-C if that's how you want to answer. Here’s how I’ve set up my constraints to support this feature. I’ve done this in a Reload() method that modifies the UIScrollView subviews.
UIView previousContent = null;
for (var sectionIdx = 0; sectionIdx < this.Source.NumberOfSections (this); sectionIdx++) {
var vwHeader = this.Source.GetViewForHeader (this, sectionIdx);
var vwContent = this.Source.GetViewForSection (this, sectionIdx);
this.scrollView.AddSubview (vwHeader);
this.scrollView.AddSubview (vwContent);
this.scrollView.BringSubviewToFront (vwHeader);
var headerHeight = this.Source.GetHeightForHeader (this, sectionIdx);
var isSectionCollapsed = this.Source.GetIsSectionCollapsed (this, sectionIdx);
// This will never change, so set constraint priority to Required (1000)
var headerHeightConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, headerHeight);
headerHeightConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (headerHeightConstraint);
// This constraint is used to handle visibility of a section.
// This is updated in UpdateConstraints.
var contentZeroHeightConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, 0.0f);
if (isSectionCollapsed)
contentZeroHeightConstraint.Priority = (float)UILayoutPriority.Required - 1.0f;
else
contentZeroHeightConstraint.Priority = (float)UILayoutPriority.DefaultLow;
this.AddConstraint (contentZeroHeightConstraint);
// Set initial state of dictionary that keeps track of all inline and floating header constraints
if (!this.inlineConstraints.ContainsKey (sectionIdx))
this.inlineConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ());
this.inlineConstraints [sectionIdx].Clear ();
if (!this.floatConstraints.ContainsKey (sectionIdx))
this.floatConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ());
this.floatConstraints [sectionIdx].Clear ();
// If this is the first section, pin top edges to the scrollview, not the previous sibling.
if (previousContent == null) {
// Pin the top edge of the header view to the top edge of the scrollview.
var headerTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, 0.0f);
headerTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerTopToScrollViewTopConstraint);
this.AddConstraint (headerTopToScrollViewTopConstraint);
// Also pin the top edge of the content view to the top edge of the scrollview, with a padding of header height.
// This is done to minimize constraints that need to be modified when a header is floated.
// May be safely changed to pin to the bottom edge of the header view.
var contentTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, headerHeight);
contentTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentTopToScrollViewTopConstraint);
} else {
// Pin the top edge of the header view to the bottom edge of the previous content view.
var previousContentBottomToHeaderTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwHeader, NSLayoutAttribute.Top, 1.0f, 0.0f);
previousContentBottomToHeaderTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (previousContentBottomToHeaderTopConstraint);
this.AddConstraint (previousContentBottomToHeaderTopConstraint);
// Also pin the top edge of the content view to the bottom edge of the previous content view.
// This is done to minimize constraints that need to be modified when a header is floated.
// May be safely changed to pin to the bottom edge of the header view.
var previousContentBottomToContentTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwContent, NSLayoutAttribute.Top, 1.0f, -headerHeight);
previousContentBottomToContentTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (previousContentBottomToContentTopConstraint);
}
// If this is the last section, pin the bottom edge of the content view to the bottom edge of the scrollview.
if (sectionIdx == this.Source.NumberOfSections (this) - 1) {
var contentBottomToScrollViewBottomConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Bottom, 1.0f, 0.0f);
contentBottomToScrollViewBottomConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentBottomToScrollViewBottomConstraint);
}
// Pin the leading edge of the header view to the leading edge of the scrollview.
var headerLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f);
headerLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerLeadingToScrollViewLeadingConstraint);
this.AddConstraint (headerLeadingToScrollViewLeadingConstraint);
// Pin the leading edge of the content view to the leading edge of the scrollview.
var contentLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f);
contentLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentLeadingToScrollViewLeadingConstraint);
// Pin the trailing edge of the header view to the trailing edge of the scrollview.
var headerTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
headerTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerTrailingToScrollViewTrailingConstraint);
this.AddConstraint (headerTrailingToScrollViewTrailingConstraint);
// Pin the trailing edge of the content view to the trailing edge of the scrollview.
var contentTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
contentTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentTrailingToScrollViewTrailingConstraint);
// Add a width constraint to set header width to scrollview width.
var headerWidthConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f);
headerWidthConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (headerWidthConstraint);
// Add a width constraint to set content width to scrollview width.
var contentWidthConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f);
contentWidthConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (contentWidthConstraint);
// Add a lower priority constraint to pin the leading edge of the header view to the leading edge of the parent of the scrollview.
var floatHeaderLeadingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this, NSLayoutAttribute.Leading, 1.0f, 0.0f);
floatHeaderLeadingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderLeadingEdgeConstraint);
this.AddConstraint (floatHeaderLeadingEdgeConstraint);
// Add a lower priority constraint to pin the top edge of the header view to the top edge of the parent of the scrollview.
var floatHeaderTopEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1.0f, 0.0f);
floatHeaderTopEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderTopEdgeConstraint);
this.AddConstraint (floatHeaderTopEdgeConstraint);
// Add a lower priority constraint to pin the trailing edge of the header view to the trailing edge of the parent of the scrollview.
var floatHeaderTrailingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
floatHeaderTrailingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderTrailingEdgeConstraint);
this.AddConstraint (floatHeaderTrailingEdgeConstraint);
previousContent = vwContent;
}
All content in a UIScrollView needs leading, top, trailing and bottom edge constraints, so that the UIScrollView can determine its ContentSize, so I have done that. As you can see, I’ve added the floating header constraints, even though at execution time no headers should float. I’ve given them a lower priority so that they’re not applied by default. I’ve done the same with a content height constraint for a collapsed section. I’ve done this so that I don’t have to add/remove constraints to float a header or collapse a section, I just need to modify constraint priorities. I don't know if that's good practice but I thought it might help avoid unnecessary layout passes.
I’m keeping track of the constraints that apply to both inline and floating headers. When it’s determined that a header should be floated, I lower the priority of the relevant inline header constraints to DefaultLow and increase the priority of the relevant floating header constraints to DefaultHigh. I do that in an event handler for the UIScrollView’s Scrolled event. I determine which section is occupying the space at ContentOffset and float its header. I’m keeping track of the last index of the header that’s been floated, just to avoid inlining something that doesn’t need to be inlined.
private int lastFloatHeaderIdx = -1;
private void scrolled (object sender, EventArgs e) {
// Restore the code below to see the scroll hesitation from what I think are unnecessary calls to ViewWillLayoutSubviews and UpdateViewConstraints
// How can I achieve this behavior without incurring the unnecessary expense?
if (this.Source != null) {
for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) {
var headerHeight = this.Source.GetHeightForHeader (this, idx);
var vwContent = this.Source.GetViewForSection (this, idx);
var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height));
var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f);
if (sectionFrame.IntersectsWith (scrollContent)) {
this.floatHeader (idx);
} else if (idx > this.lastFloatHeaderIdx) { // This is an unnecessary optimization. Appears to have no effect.
var inlines = this.inlineConstraints [idx];
if (inlines.Count > 0 && inlines [0].Priority < (float)UILayoutPriority.DefaultHigh) { // This is also an unnecessary optimization. Appears to have no effect.
this.inlineHeader (idx);
}
}
}
}
}
I’ve done some additional troubleshooting by added logging to the ViewWillLayoutSubviews and UpdateViewConstraints of the child UIViewControllers, and I can see that when a header is floated, a layout pass is done on the previous content view and all views below it. I believe this is the cause of the hesitation. I don’t think its a coincidence that the layout pass includes the previous content. To float the header, I have to deprioritize the constraint pinning its top edge to the bottom of the previous content view and increase the priority on the constraint pinning its top edge to the top edge of the UIScrollView.
But since the size and position of the content views inside the UIScrollView don’t change, I don’t think I should be incurring a layout pass on anything. And, I’ve found that sometimes I don’t. For example, if I flick to quickly scroll to the bottom, the headers are floated one after the other as expected, but no layout passes occur — at least not until the scroll velocity slows. I’ve included a screencast of my prototype running in the simulator, with console output.
Screencast of prototype running in the simulator with console output
I’ve also included a link to the source.
Archive of source
While I think you'd probably be better served addressing your mentioned performance problems via UITableView rather than reinventing UITableView, there are definitely some places here that look suspicious. You should first run your code through Instruments to see where the real problems are. Trying to optimize without spending some time profiling is usually a goose-chase.
But still, let's look at some parts of your loop. Loops are often where are problems are.
for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) {
var headerHeight = this.Source.GetHeightForHeader (this, idx);
var vwContent = this.Source.GetViewForSection (this, idx);
var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height));
var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f);
This is calling a lot of functions repeatedly that you shouldn't need to. NumberOfSections should only be called once. GetHeightForHeader had better be very cheap, or else you should cache its results in an array. Similarly GetViewForSection. If that isn't a simple array lookup, you should turn it into one. You're also generating scrollContent for every section, but it's always the same.
Finally, I would give a strong look at floatHeader and inlineHeader. Make sure that these already know their exact values and don't have to calculate a lot of stuff. Your loop should do nothing but find what view has a range of Y coordinates that overlap the current Y coordinate (you don't need a full IntersectsWith, just the Y coordinate), and then adjust either 1 or 2 view's Y coordinate (the current floating view, or the previous floating view and the new one). You shouldn't need anything else going on here.
But step one is to run it through Instruments and see what jumps out.