There are two classes in my code, one is NameCell, which contains a simple UILabel with text. The second one is NameValueCell, which inherits from that class, but adds also the property UIView *valueView.
One layout constraint needs to be altered. I'm looking for a way to override:
H:|[nameView]| - nameView should occupy full width in NameCell
with
H:|[nameView][valueView(==nameView)]| - nameView to valueView width ratio should be 1:1 in NameValueCell
What is the best practice out there for overriding NSLayoutConstraint? I have to stick to inheritance in my code because my application requires many different UITableViewCell specializations.
NameCell.h:
#interface NameCell : UITableViewCell
#property (nonatomic, retain) IBOutlet UIView *nameView;
#property (nonatomic, retain) IBOutlet UILabel *nameLabel;
#end
NameValueCell.h:
#interface NameValueCell : NameCell
#property (nonatomic, retain) IBOutlet UIView *valueView;
#end
NameCell.m:
#implementation NameCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
UIView *nameView = [[UIView alloc] init];
self.nameView = nameView;
[self.contentView addSubview:self.nameView];
UILabel *nameLabel = [[UILabel alloc] init];
self.nameLabel = nameLabel;
[self.nameView addSubview:self.nameLabel];
NSDictionary *views = NSDictionaryOfVariableBindings(nameView, nameLabel);
NSArray *constraints;
// The constraint that should be overridden
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[nameView]|"
options: 0
metrics:nil
views:views];
[self.contentView addConstraints:constraints];
}
return self;
}
#end
NameValueCell.m:
#implementation NameValueCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
NSString *reuseID = reuseIdentifier;
UIView *valueView = [[UIView alloc] init];
self.valueView = valueView;
[self.contentView addSubview:self.valueView];
NSDictionary *views = #{
#"nameView": self.nameView,
#"nameLabel": self.nameLabel,
#"valueView": self.valueView
};
NSArray *constraints;
// The overriding constraint
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[nameView][valueView(==nameView)]|"
options: 0
metrics:nil
views:views];
[self.contentView addConstraints:constraints];
}
return self;
}
#end
The subclass wants to augment the superclass behavior, for example, by adding additional subviews; but it also wants to override the superclass when it comes to creating constraints. To do both, the best approach is to factor out the view creating and the constraint creating code, then in the subclass, control whether we're augmenting or overriding by calling super selectively.
First, factor out...
// in NameCell.m initWithStyle
// super initWithStyle..., if (self) { ...
[self addCustomSubviews];
[self addCustomConstraints];
In NameCell, these new methods should be implemented exactly as you have them inline in the question, but in the subclass: (1) don't implement init at all, allowing the superclass init to call the factored code, and (2) override the factored code as follows...
// NameValueCell.m
- (void)addCustomSubviews {
// augmenting super here, so call super...
[super addCustomSubviews];
// add the value view
UIView *valueView = [[UIView alloc] init];
// and so on
}
- (void)addCustomConstraints {
// overriding super here, so don't call super, just add constraints
NSDictionary *views = #{
// and so in
}
In a less fussy but less clear alternative, you could just leave your inits as they are, but in the subclass init, remove the constraints that were just created in the super...
// in NameValueCell.m, in the initWithStyle method, before creating constraints
[self removeConstraints:self.constraints]; // then ...
NSDictionary *views = #{ // and so on...
I wouldn't call this alternative a best (or even good) practice, but I think it should work.
First: don't add constraints; activate them. It's far simpler and less error-prone.
Okay, then. Just keep a reference to the constraints that might need replacing in an NSArray instance variable:
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[nameView]|"
options: 0
metrics:nil
views:views];
self.removeableConstraints = constraints; // an instance property
[NSLayoutConstraint activateConstraints: constraints];
Now all the subclass has to do is deactivate self.removeableConstraints and activate its substitute constraints.
[NSLayoutConstraint deactivateConstraints: self.removeableConstraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[nameView][valueView(==nameView)]|"
options: 0
metrics:nil
views:views];
[NSLayoutConstraint activateConstraints: constraints];
That is the general pattern for swapping out constraints, and there is no reason that the class/subclass relationship here shouldn't use it.
Related
Using Interface Builder, I have built a really long ScrollView filled with Custom UIViews, regular UIViews, StackViews, UILabels, UIButtons, etc.
For some of the Custom UIViews, if they do not have any data, then I want to replace them with a UILabel that says "No Data Available" and I want to be able to set the margins and center the text of that UILabel.
What's the best/easiest way to do this programmatically in my ViewController given that all the views are arranged using interface builder?
Thanks for your help in advance!
You can do this by adding a UILabel, with some simple constraints, over the views you want to cover instead of inside them if you want to ensure you aren't messing with controls you don't, well, control.
I set up a simple test app to show how this method can work
This has a stack view with some images in it, a text view, and a button to trigger the sample.
You should be able to apply this method to your views as you determine in your code that you have no data to show, and want to show the placeholder, but in my example I've set up an IBOutletCollection that has both the stack view and the text view in it, and am running this on both views when the button is pressed.
All you need to do is provide the placeholder text and the view you want to replace to this method
/// This method will hide a view and put a placeholder label in that view's superview, centered in the target view's frame.
- (void)showPlaceholderText:(NSString *)placeholder forView:(UIView *)view
{
// Build the placeholder with the same frame as the target view
UILabel *placeholderLabel = [[UILabel alloc] initWithFrame:view.frame];
placeholderLabel.textAlignment = NSTextAlignmentCenter;
placeholderLabel.text = placeholder;
placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO;
// Hide the target view
view.hidden = YES;
// Put our placeholder into the superview, overtop the target view
[view.superview addSubview:placeholderLabel];
// Set up some constraints to ensure the placeholder label stays positioned correctly
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeRight multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f]];
}
The constraints added to the placeholder should keep it positioned correctly, through rotation or any other layout activity in the view.
One idea is, instead of replacing the custom views with labels, give them an "noData" mode where they present the right thing if there's no data...
// CustomView.h
#interface CustomView : UIView
#property(assign,nonatomic) BOOL noData;
#end
// CustomView.m
#interface CustomView ()
#property(weak,nonatomic) UILabel *noDataLabel;
#end
- (void)setNoData:(BOOL)noData {
_noData = noData;
self.noDataLabel.alpha = (noData)? 1.0 : 0.0;
}
- (UILabel *)noDataLabel {
if (!_noDataLabel) {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:self.bounds];
noDataLabel.backgroundColor = self.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self addSubview:noDataLabel];
_noDataLabel = noDataLabel;
}
return _noDataLabel;
}
EDIT
If you want to treat the custom views as untouchable, you can handle the state in the view controller that contains them, but it's a little awkward because we need to solve the problem of associating the noData label with the subview. Something like this can work...
// in the view controller that contains the views that should be covered with labels
#interface ViewController ()
#property(weak,nonatomic) NSMutableArray *noDataViews;
#end
// initialize noDataViews early, like in viewDidLoad
_noDataViews = [#[] mutableCopy];
The array noDataViews can contain dictionaries. The dictionary will contain the view that has noData (this can be an instance of your third-party custom view), and a UILabel intended to cover it.
- (void)setView:(UIView *)view hasNoData:(BOOL)noData {
// find the dictionary corresponding to view
NSDictionary *dictionary;
for (NSDictionary *d in self.noDataViews) {
if (d[#"view"] == view) {
dictionary = d;
break;
}
}
// if it doesn't exist, insert it
if (!dictionary) {
UILabel *label = [self labelToCover:view];
dictionary = #{ #"view":view, #"label":label };
[self.noDataViews addObject:dictionary];
}
// get the label
UILabel *label = dictionary[#"label"];
label.alpha = (noData)? 1.0 : 0.0;
}
// create a label that will cover the passed view, add it as a subview and return it
- (UILabel *)labelToCover:(UIView *)view {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:view.frame];
noDataLabel.backgroundColor = view.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self.view addSubview:noDataLabel];
return noDataLabel;
}
Depending on how often the views change state to the noData state, you might want to clean up the dictionaries, removing those whose label's alpha == 0.0.
- (void)releaseNoDataViews {
NSMutableArray *removeThese = [#[] mutableCopy];
// work out which ones to remove
for (NSDictionary *d in self.noDataViews) {
UILabel *label = d[#"label"];
if (label.alpha == 0.0) {
[removeThese addObject:d];
}
}
for (NSDictionary *d in removeThese) {
UILabel *label = d[#"label"];
[label removeFromSuperview];
[self.noDataViews removeObject:d];
}
}
This a little verbose because by keeping our hands off the custom views, we put the logic to change how they look (cover them) in the view controller.
Maybe a better idea that keeps hands off the custom views is to wrap them in a containing view that does the additional work adding the noData state.
For example, say CustomView comes from the third party. Create a class called CustomViewWrapper that contains the CustomView as a child and adds the noData behavior outlined above. Instead of painting CustomViews in IB, paint CustomViewWrappers....
// CustomViewWrapper.h
#class CustomView;
#interface CustomViewWrapper : UIView
#property(assign,nonatomic) BOOL noData;
#end
// CustomViewWrapper.m
#import "CustomView.h"
#interface CustomViewWrapper ()
#property(weak,nonatomic) CustomView *customView;
#property(weak,nonatomic) UILabel *noDataLabel;
#end
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecorder];
if (self) {
CustomView *customView = [[CustomView alloc] init];
[self addSubView:customView];
_customView = customView;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.customView.frame = self.bounds;
}
- (void)setNoData:(BOOL)noData {
_noData = noData;
self.noDataLabel.alpha = (noData)? 1.0 : 0.0;
}
- (UILabel *)noDataLabel {
if (!_noDataLabel) {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:self.bounds];
noDataLabel.backgroundColor = self.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self addSubview:noDataLabel];
_noDataLabel = noDataLabel;
}
return _noDataLabel;
}
I have created MyCustomView.xib/h/m: which extends UIView class. Then in my Main.storyboard, put UIView object, changed the class to MyCustomView and linked to MainController.h. So, MainController contains reference to MyCustomView instance.
For loading from xib, in MyCustomView I do the following:
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
if (self.subviews.count == 0) {
[self commonInit];
}
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (void) stretchToSuperView:(UIView*)view
{
view.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *bindings = NSDictionaryOfVariableBindings(view);
NSString *formatTemplate = #"%#:|[view]|";
for (NSString * axis in #[#"H",#"V"]) {
NSString * format = [NSString stringWithFormat:formatTemplate,axis];
NSArray * constraints = [NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:nil views:bindings];
[view.superview addConstraints:constraints];
}
}
- (void)commonInit
{
MyCustomView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
[self stretchToSuperView:views.firstObject];
}
This works quite well, until I want to declare delegate in MyCustomView in order to notify MainController to any change(button click, etc). So, my ManController conforms MyCustomViewDelegate and implements methods.
EDIT 1 setting delegate
//MainViewController.m file
#interface MainViewController ()
#property (strong, nonatomic) IBOutlet MyCustomView *customView;
#end
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.customView.delegate = self;
}
The problem here is that delegate becomes nil and I don't understand the reason, so don't know what's the mistake.
Edit 2 I think somehow I have 2 different instances of MyCustomView.
I have added new property in MyCustomView:
#interface MyCustomView : UIView
#property (weak, nonatomic) IBOutlet UIButton *firstItem;
#property(nonatomic, weak)id <MyCustomViewDelegate> delegate;
// this is test property
#property(nonatomic, assign)int testProperty;
#end
And when I set this property in viewDidLoad and then click to first button, I see that testProperty has value 0. So, this could mean something wrong with IBOutlet MyCustomView *customView.
You are correct, that you have two view objects. The one that you added to the storyboard and the one you created via loadNibNamed.
Bottom line, loadNibNamed will create the view for you (assuming that you've specified MyCustomView as the base class for the view specified in the NIB; you can leave NIB's "File owner" blank). You can then write a convenience method in MyCustomView to instantiate the NIB-based view:
+ (instancetype)myCustomViewWithDelegate:(id<MyCustomViewDelegate>)delegate {
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:delegate options:nil];
MyCustomView *view = array.firstObject;
NSAssert([view isKindOfClass:[MyCustomView class]], #"Base class of NIB's top level view is not MyCustomView");
view.delegate = delegate;
return view;
}
Then, rather than specifying the view on the storyboard, you must instantiate and add it to the view hierarchy programmatically, e.g.:
- (void)viewDidLoad {
[super viewDidLoad];
MyCustomView *view = [MyCustomView myCustomViewWithDelegate:self]; // since `self.myCustomView` should be `weak`, let's hold the view in local variable
[self.view addSubview:view];
self.myCustomView = view; // then set the property after the view is safely added to view hierarchy
NSDictionary *views = #{#"myCustomView" : self.myCustomView};
self.myCustomView.translatesAutoresizingMaskIntoConstraints = false;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[myCustomView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[myCustomView]|" options:0 metrics:nil views:views]];
// ...
}
Obviously, when adding a view programmatically, you have to specify its frame and autoresizingMask, or use constraints like shown above.
Unfortunately, I can't make a simple comment, so I have to ask, and give advices as 'answer'.
First of all, I don't see where do you set the delegate to your MainController, but I think in the viewDidLoad() of the controller.
Second thing. It's really important how do you setup your MyCustomView.xib, because you create a brand new object MyCustomView, and its properties will be unavailable from the controller.
MyCustomView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
Without knowing the setup of the xib, and setting method of the delegate, here is my guess what could help you:
In MyCustomView.xib set the class of the rootView to UIView.
In MyCustomView.xib set the class of the File's Owner to MyCustomView
Create your subview as a UIView.
UIView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
Set the delegate of the myCustomView object in your controller.
self.myCustomView.delegate = self;
I hope it will help, if not, then please extend your question with the delegate setter code block, and your xib setup method. (class of file's owner, class of root view etc)
I have a custom table view cell, with an additional text field text view.
I've added in autolayout constraints. The cell's text label layout seems to be fine in portrait mode, but when rotated, the text label moves to the center of the cell(behind the text view), it is until I tap and type into the text view, the label gets adjusted to the right location.
.h
#interface MTTextViewCell : UITableViewCell
- (id)initWithLabelTextForView:(NSString *)labelTextForView;
#property (nonatomic, strong) UITextView *cellTextView;
#end
.m
#import "MTTextViewCell.h"
#implementation MTTextViewCell
- (id)initWithLabelTextForView:(NSString *)labelTextForView {
self = [super init];
if(self) {
//1. Setup existing cell elements.
self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.textLabel.text = labelTextForView;
self.selectionStyle = UITableViewCellSelectionStyleNone;
//2. Setup custom cell elements.
_cellTextView = [[UITextView alloc] init];
_cellTextView.translatesAutoresizingMaskIntoConstraints = NO;
_cellTextView.backgroundColor = [UIColor grayColor];
[self.contentView addSubview:_cellTextView];
self.autoresizesSubviews = YES;
//Local variables for the purposes of autolayout language and macros.
UIView *alTextLabel = self.textLabel;
UIView *alCellTextView = _cellTextView;
NSDictionary *alViewDictionary = NSDictionaryOfVariableBindings(alTextLabel, alCellTextView);
//Horizontal layout constraints.
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[alCellTextView]-|"
options:0
metrics:nil
views:alViewDictionary]];
//Vertical layout constraints.
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[alTextLabel(20.0)]-[alCellTextView]-|"
options:NSLayoutFormatAlignAllLeft
metrics:nil
views:alViewDictionary]];
}
return self;
}
This also sporadically happens to a similar cell using a text field, as
well as a segmented control.
See screencap:
I am on Xcode Version 6.4, iOS SDK 8.4
Any suggestions or pointers is appreciated! Thanks.
check that the heights add up to the maximum height of the phone in landscape mode. It's probably adding to greater than the height now
I'm currently manually positioning my UI elements, which gets annoying if I update the text or add elements as I have to recalculate all the positions each time.
Is there a way using autolayout to size and positions the elements, for example as follows, no matter how much text the labels contain?
UILabel (multiline, variable height)
[20px gap]
UILabel (multiline, variable height)
[20px gap]
UIButton
Yes, it will look something like this
#implementation MyClass {
NSDictionary *_viewsDictionary;
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// First create your controls - you can just use CGRectZero
_label1 = [[UILabel alloc] initWithFrame:CGRectZero];
_label1 setText:#"Some text";
_label2 = [[UILabel alloc] initWithFrame:CGRectZero];
_label2 setText:#"Some text 2";
_button = [[UIButton alloc] initWithFrame:CGRectZero];
// Then just add them to your as a sub view
[self addSubview:self.currentBalanceLabel];
[self addSubview:_nameLabel];
[self addSubview:_button];
// Put them in an NSDictionary - this is a macro and will be used when setting up the contraints below
_viewsDictionary = NSDictionaryOfVariableBindings(_nameLabel, _currentBalanceLabel,_button);
// This tells the view to run update contraints
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
}
- (void)updateConstraints
{
[super updateConstraints];
// Using the _viewsDictionary, we must tell always tell how all the controls will be setup both
// horizontally and vertically. In this case wear are going to tell the label to take the entire width
// The rest of the vies will be aligned on the left below
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label1]-|"
options:0
metrics:nil
views:_viewsDictionary];
//Add the contraints
[self addConstraints:constraints];
// Next setup the vertical contraints. This is what you asked about spefically, label - 20 - label - 20 - button
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[_labelq]-20-[_label2]-20-[_button]"
options: NSLayoutFormatAlignAllLeft
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
}
#end
I'm implementing a custom container which is pretty similar to UINavigationController except for it does not hold the whole controller stack. It has a UINavigationBar which is constrained to the container controller's topLayoutGuide, which happens to be 20px off the top, which is OK.
When I add a child view controller and put its view into the hierarchy I want its topLayoutGuide seen in IB and used for laying out the child view controller's view's subviews to appear at the bottom of my navigation bar. There is a note of what is to be done in the relevant documentation:
The value of this property is, specifically, the value of the length
property of the object returned when you query this property. This
value is constrained by either the view controller or by its enclosing
container view controller (such as a navigation or tab bar
controller), as follows:
A view controller not within a container view controller constrains this property to indicate the bottom of the status bar, if visible,
or else to indicate the top edge of the view controller's view.
A view controller within a container view controller does not set this property's value. Instead, the container view controller
constrains the value to indicate:
The bottom of the navigation bar, if a navigation bar is visible
The bottom of the status bar, if only a status bar is visible
The top edge of the view controller’s view, if neither a status bar nor navigation bar is visible
But I don't quite understand how to "constrain it's value" since both the topLayoutGuide and it's length properties are readonly.
I've tried this code for adding a child view controller:
[self addChildViewController:gamePhaseController];
UIView *gamePhaseControllerView = gamePhaseController.view;
gamePhaseControllerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentContainer addSubview:gamePhaseControllerView];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-0-[gamePhaseControllerView]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(gamePhaseControllerView)];
NSLayoutConstraint *topLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.topLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.navigationBar
attribute:NSLayoutAttributeBottom
multiplier:1 constant:0];
NSLayoutConstraint *bottomLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.bottomLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
multiplier:1 constant:0];
[self.view addConstraint:topLayoutGuideConstraint];
[self.view addConstraint:bottomLayoutGuideConstraint];
[self.contentContainer addConstraints:horizontalConstraints];
[gamePhaseController didMoveToParentViewController:self];
_contentController = gamePhaseController;
In the IB I specify "Under Top Bars" and "Under Bottom Bars" for the gamePhaseController. One of the views is specifically constrained to the top layout guide, anyway on the device it appears to be 20px off the the bottom of the container's navigation bar...
What is the right way of implementing a custom container controller with this behavior?
As far as I have been able to tell after hours of debugging, the layout guides are readonly, and derived from the private classes used for constraints based layout. Overriding the accessors does nothing (even though they are called), and it's all just craptastically annoying.
(UPDATE: now available as cocoapod, see https://github.com/stefreak/TTLayoutSupport)
A working solution is to remove apple's layout constraints and add your own constraints. I made a little category for this.
Here is the code - but I suggest the cocoapod. It's got unit tests and is more likely to be up to date.
//
// UIViewController+TTLayoutSupport.h
//
// Created by Steffen on 17.09.14.
//
#import <UIKit/UIKit.h>
#interface UIViewController (TTLayoutSupport)
#property (assign, nonatomic) CGFloat tt_bottomLayoutGuideLength;
#property (assign, nonatomic) CGFloat tt_topLayoutGuideLength;
#end
-
#import "UIViewController+TTLayoutSupport.h"
#import "TTLayoutSupportConstraint.h"
#import <objc/runtime.h>
#interface UIViewController (TTLayoutSupportPrivate)
// recorded apple's `UILayoutSupportConstraint` objects for topLayoutGuide
#property (nonatomic, strong) NSArray *tt_recordedTopLayoutSupportConstraints;
// recorded apple's `UILayoutSupportConstraint` objects for bottomLayoutGuide
#property (nonatomic, strong) NSArray *tt_recordedBottomLayoutSupportConstraints;
// custom layout constraint that has been added to control the topLayoutGuide
#property (nonatomic, strong) TTLayoutSupportConstraint *tt_topConstraint;
// custom layout constraint that has been added to control the bottomLayoutGuide
#property (nonatomic, strong) TTLayoutSupportConstraint *tt_bottomConstraint;
// this is for NSNotificationCenter unsubscription (we can't override dealloc in a category)
#property (nonatomic, strong) id tt_observer;
#end
#implementation UIViewController (TTLayoutSupport)
- (CGFloat)tt_topLayoutGuideLength
{
return self.tt_topConstraint ? self.tt_topConstraint.constant : self.topLayoutGuide.length;
}
- (void)setTt_topLayoutGuideLength:(CGFloat)length
{
[self tt_ensureCustomTopConstraint];
self.tt_topConstraint.constant = length;
[self tt_updateInsets:YES];
}
- (CGFloat)tt_bottomLayoutGuideLength
{
return self.tt_bottomConstraint ? self.tt_bottomConstraint.constant : self.bottomLayoutGuide.length;
}
- (void)setTt_bottomLayoutGuideLength:(CGFloat)length
{
[self tt_ensureCustomBottomConstraint];
self.tt_bottomConstraint.constant = length;
[self tt_updateInsets:NO];
}
- (void)tt_ensureCustomTopConstraint
{
if (self.tt_topConstraint) {
// already created
return;
}
// recording does not work if view has never been accessed
__unused UIView *view = self.view;
// if topLayoutGuide has never been accessed it may not exist yet
__unused id<UILayoutSupport> topLayoutGuide = self.topLayoutGuide;
self.tt_recordedTopLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.topLayoutGuide];
NSAssert(self.tt_recordedTopLayoutSupportConstraints.count, #"Failed to record topLayoutGuide constraints. Is the controller's view added to the view hierarchy?");
[self.view removeConstraints:self.tt_recordedTopLayoutSupportConstraints];
NSArray *constraints =
[TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
topLayoutGuide:self.topLayoutGuide];
// todo: less hacky?
self.tt_topConstraint = [constraints firstObject];
[self.view addConstraints:constraints];
// this fixes a problem with iOS7.1 (GH issue #2), where the contentInset
// of a scrollView is overridden by the system after interface rotation
// this should be safe to do on iOS8 too, even if the problem does not exist there.
__weak typeof(self) weakSelf = self;
self.tt_observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
__strong typeof(self) self = weakSelf;
[self tt_updateInsets:NO];
}];
}
- (void)tt_ensureCustomBottomConstraint
{
if (self.tt_bottomConstraint) {
// already created
return;
}
// recording does not work if view has never been accessed
__unused UIView *view = self.view;
// if bottomLayoutGuide has never been accessed it may not exist yet
__unused id<UILayoutSupport> bottomLayoutGuide = self.bottomLayoutGuide;
self.tt_recordedBottomLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.bottomLayoutGuide];
NSAssert(self.tt_recordedBottomLayoutSupportConstraints.count, #"Failed to record bottomLayoutGuide constraints. Is the controller's view added to the view hierarchy?");
[self.view removeConstraints:self.tt_recordedBottomLayoutSupportConstraints];
NSArray *constraints =
[TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
bottomLayoutGuide:self.bottomLayoutGuide];
// todo: less hacky?
self.tt_bottomConstraint = [constraints firstObject];
[self.view addConstraints:constraints];
}
- (NSArray *)findLayoutSupportConstraintsFor:(id<UILayoutSupport>)layoutGuide
{
NSMutableArray *recordedLayoutConstraints = [[NSMutableArray alloc] init];
for (NSLayoutConstraint *constraint in self.view.constraints) {
// I think an equality check is the fastest check we can make here
// member check is to distinguish accidentally created constraints from _UILayoutSupportConstraints
if (constraint.firstItem == layoutGuide && ![constraint isMemberOfClass:[NSLayoutConstraint class]]) {
[recordedLayoutConstraints addObject:constraint];
}
}
return recordedLayoutConstraints;
}
- (void)tt_updateInsets:(BOOL)adjustsScrollPosition
{
// don't update scroll view insets if developer didn't want it
if (!self.automaticallyAdjustsScrollViewInsets) {
return;
}
UIScrollView *scrollView;
if ([self respondsToSelector:#selector(tableView)]) {
scrollView = ((UITableViewController *)self).tableView;
} else if ([self respondsToSelector:#selector(collectionView)]) {
scrollView = ((UICollectionViewController *)self).collectionView;
} else {
scrollView = (UIScrollView *)self.view;
}
if ([scrollView isKindOfClass:[UIScrollView class]]) {
CGPoint previousContentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + scrollView.contentInset.top);
UIEdgeInsets insets = UIEdgeInsetsMake(self.tt_topLayoutGuideLength, 0, self.tt_bottomLayoutGuideLength, 0);
scrollView.contentInset = insets;
scrollView.scrollIndicatorInsets = insets;
if (adjustsScrollPosition && previousContentOffset.y == 0) {
scrollView.contentOffset = CGPointMake(previousContentOffset.x, -scrollView.contentInset.top);
}
}
}
#end
#implementation UIViewController (TTLayoutSupportPrivate)
- (NSLayoutConstraint *)tt_topConstraint
{
return objc_getAssociatedObject(self, #selector(tt_topConstraint));
}
- (void)setTt_topConstraint:(NSLayoutConstraint *)constraint
{
objc_setAssociatedObject(self, #selector(tt_topConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSLayoutConstraint *)tt_bottomConstraint
{
return objc_getAssociatedObject(self, #selector(tt_bottomConstraint));
}
- (void)setTt_bottomConstraint:(NSLayoutConstraint *)constraint
{
objc_setAssociatedObject(self, #selector(tt_bottomConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)tt_recordedTopLayoutSupportConstraints
{
return objc_getAssociatedObject(self, #selector(tt_recordedTopLayoutSupportConstraints));
}
- (void)setTt_recordedTopLayoutSupportConstraints:(NSArray *)constraints
{
objc_setAssociatedObject(self, #selector(tt_recordedTopLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)tt_recordedBottomLayoutSupportConstraints
{
return objc_getAssociatedObject(self, #selector(tt_recordedBottomLayoutSupportConstraints));
}
- (void)setTt_recordedBottomLayoutSupportConstraints:(NSArray *)constraints
{
objc_setAssociatedObject(self, #selector(tt_recordedBottomLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)setTt_observer:(id)tt_observer
{
objc_setAssociatedObject(self, #selector(tt_observer), tt_observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)tt_observer
{
return objc_getAssociatedObject(self, #selector(tt_observer));
}
-
//
// TTLayoutSupportConstraint.h
//
// Created by Steffen on 17.09.14.
//
#import <UIKit/UIKit.h>
#interface TTLayoutSupportConstraint : NSLayoutConstraint
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide;
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide;
#end
-
//
// TTLayoutSupportConstraint.m
//
// Created by Steffen on 17.09.14.
//
#import "TTLayoutSupportConstraint.h"
#implementation TTLayoutSupportConstraint
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide
{
return #[
[TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0],
[TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0],
];
}
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide
{
return #[
[TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0],
[TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
];
}
#end
I think they mean you should constrain the layout guides using autolayout, i.e. an NSLayoutConstraint object, instead of manually setting the length property. The length property is made available for classes that choose not to use autolayout, but it seems with custom container view controllers you do not have this choice.
I assume the best practice is make the priority of the constraint in the container view controller that "sets" the value of the length property to UILayoutPriorityRequired.
I'm not sure what layout attribute you would bind, either NSLayoutAttributeHeight or NSLayoutAttributeBottom probably.
In the parent view controller
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
for (UIViewController * childViewController in self.childViewControllers) {
// Pass the layouts to the child
if ([childViewController isKindOfClass:[MyCustomViewController class]]) {
[(MyCustomViewController *)childViewController parentTopLayoutGuideLength:self.topLayoutGuide.length parentBottomLayoutGuideLength:self.bottomLayoutGuide.length];
}
}
}
and than pass the values to the children, you can have a custom class as in my example, a protocol, or you can maybe access the scroll view from the child's hierarchy