Related
This is how my screen is supposed to look like. It has a UIToolBar at the bottom and its contains a UIBarButtonItem. It was working fine before before iOS 11.
But in iOS 11, the UIBarButtonItem is showing out its place. It is showing in the status bar. In the simulator it is working fine, and it shows inside the toolbar. But when I run it on an iPhone 6s running iOS 11. It's showing like in the screen below.
Here's the code..
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
UIBarButtonItem[] bArray = {
getSetAsHomeButton(apiCall, this, 0),
new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)
};
SetToolbarItems(bArray, true);
}
public static UIBarButtonItem getSetAsHomeButton(ICommonApiCall commonApiCall, UIViewController controller, int status)
{
var view = new UIButton ();
view.Layer.CornerRadius = 8;
view.BackgroundColor = UIColor.White;
view.TranslatesAutoresizingMaskIntoConstraints = false;
view.WidthAnchor.ConstraintEqualTo(108).Active = true;
view.HeightAnchor.ConstraintEqualTo(32).Active = true;
var HomeIcon = new UILabel (new RectangleF (0, 0, 21, 21));
HomeIcon.Font = FontAwesome.Font (22);
HomeIcon.Text = FontAwesome.FAHome;
HomeIcon.TextColor = PlatformConstants.PrimaryColor;
var HomeLabel = new UILabel (new RectangleF (25, 0, 64, 44));
HomeLabel.Text = "Set as default";
HomeLabel.Font = UIFont.FromName (PlatformConstants.PrimaryFont + "-Medium", 11);
HomeLabel.TextColor = PlatformConstants.PrimaryColor;
HomeLabel.Lines = 0;
HomeLabel.SizeToFit ();
view.AddSubview (HomeIcon);
view.AddSubview (HomeLabel);
HomeIcon.TranslatesAutoresizingMaskIntoConstraints = false;
HomeLabel.TranslatesAutoresizingMaskIntoConstraints = false;
var hSpaceLeading = NSLayoutConstraint.Create (HomeIcon, NSLayoutAttribute.Leading,
NSLayoutRelation.Equal, view, NSLayoutAttribute.Leading, 1, 6);
view.AddConstraint (hSpaceLeading);
var HomeIconCenterY = NSLayoutConstraint.Create (HomeIcon, NSLayoutAttribute.CenterY,
NSLayoutRelation.Equal, view, NSLayoutAttribute.CenterY, 1, 0);
view.AddConstraint (HomeIconCenterY);
var HomeLabelCenterY = NSLayoutConstraint.Create (HomeLabel, NSLayoutAttribute.CenterY,
NSLayoutRelation.Equal, view, NSLayoutAttribute.CenterY, 1, 0);
view.AddConstraint (HomeLabelCenterY);
var hSpace = NSLayoutConstraint.Create (HomeLabel, NSLayoutAttribute.Left,
NSLayoutRelation.Equal, HomeIcon, NSLayoutAttribute.Right, 1, 4);
view.AddConstraint (hSpace);
var HomeLabelWidth = NSLayoutConstraint.Create (HomeLabel, NSLayoutAttribute.Width,
NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 80);
view.AddConstraint (HomeLabelWidth);
view.LayoutIfNeeded ();
view.TouchUpInside += delegate {
SetAsHome (commonApiCall, controller, status, view).ContinueWith (t => Console.WriteLine (t.Exception),
TaskContinuationOptions.OnlyOnFaulted);
};
return new UIBarButtonItem (view);
}
Any help is appreciated. Thank you.
Since iOS Auto Layout has been introduced to the UINavigationBar and UIToolbar elements. So the best way to fix your problem is to add the proper constraints.
Here you can find some more help and details on the issue:
https://forums.developer.apple.com/thread/80075
Hope this solves your problem.
In my xamarin iOS app I have a list view, I want to show the loading icon when it is being loading. But, I do not want to disable the whole page. I want the user to still go back if they wish to, using back navigation.
I used this as a reference.
So, I'm trying to set the CGRect Frame, to leave the navigation on the top active and disable the rest of the page during loading status.
I am using something like this: new CGRect(30, 0,0, 0) to leave 30 units from the top but it doesnt work. Can anyone help me out to have a frame to leave just navigation bar and cover rest of the page?
AutoLayout is a much better way to accomplish this task than using frames. Unless you need to target REALLY, REALLY old versions of iOS, AutoLayout is generally the way to go.
I'm assuming you are using a UIViewController and not a UITableViewController. This is an important distinction, because the UITableViewController only allows you to add to the UITableView which makes this task much more challenging.
Add this AddAnOverlay method to your UIViewController class, then call it whenever you want to display an overlay. You will probably need to put overlay in an instance variable so that you can remove it later. Remove it by calling overlay.RemoveFromSuperview() and you're done.
void AddAnOverlay()
{
var overlay = new UIView();
overlay.BackgroundColor = UIColor.Black.ColorWithAlpha(0.45f); // or whatever colo
overlay.TranslatesAutoresizingMaskIntoConstraints = false;
var label = new UILabel();
label.TranslatesAutoresizingMaskIntoConstraints = false;
label.Text = "Loading Fantastic Things!";
var spinner = new UIActivityIndicatorView();
spinner.TranslatesAutoresizingMaskIntoConstraints = false;
spinner.StartAnimating();
overlay.AddSubview(spinner);
overlay.AddConstraint(NSLayoutConstraint.Create(overlay, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, spinner, NSLayoutAttribute.CenterX, 1, 0));
overlay.AddConstraint(NSLayoutConstraint.Create(overlay, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, spinner, NSLayoutAttribute.CenterY, 1, 0));
overlay.AddSubview(label);
// can adjust space between by changing -30 to whatever
overlay.AddConstraint(NSLayoutConstraint.Create(spinner, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, label, NSLayoutAttribute.Top, 1, -30));
overlay.AddConstraint(NSLayoutConstraint.Create(overlay, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, label, NSLayoutAttribute.CenterX, 1, 0));
View.AddSubview(overlay);
View.AddConstraint(NSLayoutConstraint.Create(TopLayoutGuide, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, overlay, NSLayoutAttribute.Top, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(View, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, overlay, NSLayoutAttribute.CenterX, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(View, NSLayoutAttribute.Width, NSLayoutRelation.Equal, overlay, NSLayoutAttribute.Width, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(BottomLayoutGuide, NSLayoutAttribute.Top, NSLayoutRelation.Equal, overlay, NSLayoutAttribute.Bottom, 1, 0));
}
Notice the TopLayoutGuide and BottomLayoutGuide in the NSLayoutConstraint. These represent the top and bottom of your current view controller, so they can be used to size things so they don't hide navigation bars, tab bars, etc.
You have to add the overlay to the table view, which leads you to a centering problem. For this there are some options: recalculate the position of the overlay when scrolling, add an additional layer behind the table view which you reference with autolayout for positioning, ...
I'm using autolayout and you should avoid giving actual numbers, but it is also possible using bounds. Also be aware of if the user navigates away you have to cancel the task!
UPDATE
Now Solved The problem was that when I was updating the bottomConstraint I was setting the Constant to the bottom padding property. Sounds reasonable but of course the Constant should have been set to 0 - BottomPadding. That explains why the bottom of the text was being not visible, it was being constrained beyond its clipping container.
I have a simple UIView custom control called PaddedLabel that wraps (not inherits) a UILabel
The view hierarchy is
PaddedLabel -> UILabel
When the constraints on the UILabel have their constants updated the outer View does not chnage height. It is as if the outer UIView is seeing only the Height of the Label as the Height it needs rather than the Height of the Label plus constants. This is how it looks
In UpdateConstraints I add some constraints and if there is a Text value I set the Constant on the Constraint to the value I want for padding else I set the Constant to 0.
public override void UpdateConstraints()
{
base.UpdateConstraints();
if (this.constraintsApplied == false)
{
this.leftConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1.0f, this.LeftPadding);
this.AddConstraint(this.leftConstraint);
this.rightConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Right, NSLayoutRelation.Equal, this, NSLayoutAttribute.Right, 1.0f, 0 - this.RightPadding);
this.AddConstraint(this.rightConstraint);
this.topConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1.0f, this.TopPadding);
this.AddConstraint(this.topConstraint);
this.bottomConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this, NSLayoutAttribute.Bottom, 1.0f, 0 - this.BottomPadding);
this.AddConstraint(this.bottomConstraint);
this.constraintsApplied = true;
}
if (this.Text.HasValue())
{
this.topConstraint.Constant = this.TopPadding;
// The following code was the problem.
// It should have been 0 - this.BottomPadding Now corrected
// this.bottomConstraint.Constant = this.BottomPadding;</del>
this.bottomConstraint.Constant = 0 - this.BottomPadding;
}
else
{
this.topConstraint.Constant = 0;
this.bottomConstraint.Constant = 0;
}
}
When the Text property is set I set the Text property on the inner UILabel and call SetNeedsUpdateConstraints
public string Text
{
get
{
return this.text;
}
set
{
if (this.text == value)
{
return;
}
this.text = value;
this.nestedLabel.Text = value;
this.SetNeedsUpdateConstraints();
}
}
If you want the PaddedLabel view to expand and fit around the inside UILabel, change the bottom constraint. You want to tie the bottom of PaddedLabel to the bottom of the UILabel, so as the UILabel grows it makes PaddedLabel expand! The way it is now, you're telling the UILabel to squish itself inside of the PaddedLabel view.
Reverse the bottomConstraint and you should be set.
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.
I started using Xamarin because I wanted to stay in the Visual Studio 2013 environment and not have to learn a new environment. Anyway, I'm going to paste my controller's code below and hopefully somebody is smarter than me (an almost certainty) and can get me back on track.
I just discovered AutoLayout. It seems to me that AutoLayout understanding is critical to speeding up development. However, I'm not finding a lot of information for using AutoLayout with pure C# in Visual Studio 2013. Perhaps I'm just not looking in the right places.
Anyway, Let's start this new discussion with a simple controller that uses AutoLayout TOTALLY in C# without using any .nib or Interface Builder stuff at all. And without using Xamarin Studio. Just ALL done in Visual Studio 2013.
Here are the requirements:
Make a UIViewController that will facilitate implementation of apple's iAD.
Basically, we want to place an iAD banner at the bottom of the screen taking up the full width.
We will put the view above the iAD banner and have it fill the rest of the screen.
The banner view may disappear from time to time if no ADs are present, so we need to handle that.
We need to handle when the device rotates to accommodate new orientation.
We need to handle different devices. iPod, iPad, iPhone, version 4 and 5
This should be trivial, but I've been banging my head on the keyboard for 2 days trying to get this to work. Any recommendations, examples, or ideas would be GREATLY HELPFUL. Remember, we want to ONLY use C# in Visual Studio and not use Interface Builder at all. Here is my non working attempt:
Using the code below, I end up with the AdBanner off of the screen below the InternalView. Also, the Internal View is longer than the screen and only half the screen width. What's going on here? Do I need to turn on the AutoLayout Feature somewhere? Can I do it in C# code or is it hiding somewhere in Project Settings?
using System;
using MonoTouch.iAd;
using MonoTouch.UIKit;
namespace ADayBDayiOS
{
public class ADViewController : UIViewController
{
private UIView InternalView { get; set; }
private ADBannerView AdView { get; set; }
public override void ViewDidLoad()
{
base.ViewDidLoad();
InternalView = new UIView{BackgroundColor=UIColor.Blue};
//This is apple's standard ADBannerView
AdView = new ADBannerView(ADAdType.Banner) {Hidden = true};
AdView.FailedToReceiveAd += HandleFailedToReceiveAd;
AdView.AdLoaded += HandleAdLoaded;
View.BackgroundColor = UIColor.Clear;
//I'm pretty sure that we need these three lines
View.TranslatesAutoresizingMaskIntoConstraints = false;
InternalView.TranslatesAutoresizingMaskIntoConstraints = false;
AdView.TranslatesAutoresizingMaskIntoConstraints = false;
View.AddSubview(InternalView);
View.AddSubview(AdView);
Resize();
}
public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation)
{
base.DidRotate(fromInterfaceOrientation);
Resize();
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
Resize();
}
private void Resize()
{
//Remove all constraints, and reset them...
View.RemoveConstraints(View.Constraints);
if (AdView == null || AdView.Hidden)
{//Fill up the entire screen with our InternalView
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, View, NSLayoutAttribute.Width, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, View, NSLayoutAttribute.Bottom, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, View, NSLayoutAttribute.Top, 1, 0));
}
else
{//Put banner ad at the bottom of the screen and fill the rest of the screen with our InternalView
View.AddConstraint(NSLayoutConstraint.Create(AdView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, View, NSLayoutAttribute.Width, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, View, NSLayoutAttribute.Width, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(AdView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, View, NSLayoutAttribute.Bottom, 1, 0));
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, View, NSLayoutAttribute.Bottom, 1, AdView.Bounds.Height));
View.AddConstraint(NSLayoutConstraint.Create(InternalView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, View, NSLayoutAttribute.Top, 1, 0));
}
}
/// <summary>
/// Shows the AdView when a new Ad loads
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleAdLoaded(object sender, EventArgs e)
{
if (AdView == null)
return;
AdView.Hidden = false;
Resize();
}
/// <summary>
/// Hides the AdView when no ads are available
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleFailedToReceiveAd(object sender, AdErrorEventArgs e)
{
if (AdView == null)
return;
AdView.Hidden = true;
Resize();
}
}
}
Creating Constraints in code for AutoLayout manually with exposed iOS methods is a tedious process.
AutoLayout has a lot of benefits and using it to achieve stuff like orientation changes and general layout's is kinda a no-brainer. To achieve what you require with just VS2013, I'd suggest having a look at
FluentLayouts
It's made by the author that created MVVMCross and it has quite some documentation to get you started.
Blog Post
Youtube Vid Tutorial
Essentially with it you can write constraints like:
View.AddConstraints(
button.AtTopOf(View).Plus(vPadding),
button.AtRightOf(View).Minus(hPadding),
button.Width().EqualTo(ButtonWidth),
text.AtLeftOf(View, hPadding),
text.ToLeftOf(button, hPadding),
text.WithSameTop(button)
);
So for your case,
you'd prolly want to have the ad banner view pinned to the Top of the superview with pins for left and right of the superview as well. Prolly add a fixed height if you need to. Pinning to the left and right of the superview will then cater for when the device orientation changes and scale the width accordingly. Top position would be catered with the top pin and the fixed height for the height of the banner.
AutoLayout by itself asks you for the X,Y position of an element and for the element to know it's desired size. Some controls like buttons have an implicit size so you wouldn't need to set this width/height explicitly. However things like a plain UIView does not. So you would have to specify their size with constraints too.
Finally having a tool like FluentLayouts helps us create constraints much much easier, but the fundamentals of what AutoLayouts is and how to use it is just general knowledge on the topic which you might actually be better off visiting apple docs for or some tutorials like these. Yes it shows it in XCode but it also explains the topic which we need to understand regardless. That site also has articles about constraints from code that explains the nitty gritty about constants and multipliers and sorts with constraints that are worth a read through. Once you understand the concepts and what you can do with it, then pick a tool like fluent-layouts and your requirements should fall into place well.
Frank Krueger has an elegant solution to this problem which you can read about here: http://praeclarum.org/post/45690317491/easy-layout-a-dsl-for-nslayoutconstraint. The code is available here: https://gist.github.com/praeclarum/5175100
Just add the class to your iOS project and you can write code like this:
void LayoutWithEase ()
{
View.ConstrainLayout (() =>
button.Frame.Width == ButtonWidth &&
button.Frame.Right == View.Frame.Right - HPadding &&
button.Frame.Top == View.Frame.Top + VPadding &&
text.Frame.Left == View.Frame.Left + HPadding &&
text.Frame.Right == button.Frame.Left - HPadding &&
text.Frame.Top == button.Frame.Top
);
}
I found that the following works pretty well for a simple controller that is displaying an iAd AdBannerView along with a regular view. The code below also calls the 'Resize' method for all subviews that are of type 'AdView'
public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation)
{
Resize();
base.DidRotate(fromInterfaceOrientation);
}
public override void ViewDidAppear(bool animated)
{
Resize();
base.ViewDidAppear(animated);
}
private void Resize()
{
try
{
if (AdBannerView.Hidden)
{
InternalView.Frame = new RectangleF(0, 20, View.Bounds.Width, View.Bounds.Height);
InternalView.Frame = new RectangleF(0, 20, View.Bounds.Width, View.Bounds.Height);
}
else
{
InternalView.Frame = new RectangleF(0, 20, View.Bounds.Width,
View.Bounds.Height - AdBannerView.Bounds.Height);
AdBannerView.Frame = new RectangleF(0, InternalView.Bounds.Height, View.Bounds.Width,
AdBannerView.Bounds.Height);
InternalView.Frame = new RectangleF(0, 20, View.Bounds.Width,
View.Bounds.Height - AdBannerView.Bounds.Height);
AdBannerView.Frame = new RectangleF(0, InternalView.Bounds.Height, View.Bounds.Width,
AdBannerView.Bounds.Height);
}
foreach (UIView view in View.Subviews)
{
var adView = view as AdView;
if (adView != null)
{
adView.Resize();
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
}