Change NSLayoutConstraint within UIView Subclass - ios

I am trying to affect the NSLayoutConstraint within my UIView subclass. However where ever I put the code it doesn't seem to change the autolayout constraint. I have used NSLayoutConstraint many times before but for some reason cannot seem to reference it in the subclass.
My constraint is
self.ripHeight.constant
Connected by
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *ripHeight;
I have tried it in the init and awakeFromNib methods as below
- (id)init {
self = [super init];
if (self) {
//self.ripHeight.constant = 100;
}
return self;
}
-(void)awakeFromNib {
[super awakeFromNib];
// 2 . Change Height of Ripll Container to suit device - bitch
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
//Create Nib Frame
CGRect frameRect = self.frame;
frameRect.size.width = screenWidth;
frameRect.size.height = screenWidth * 1.3333;
self.frame = frameRect;
self.ripHeight.constant = 100;
}
I am loading the view in by
[self.ripContainer addSubview:customView];

Try to update layout manually once you have changed the constraint :
-(void)awakeFromNib {
[super awakeFromNib];
//...
//your code for constraint
[self.superview setNeedsLayout];
}
- setNeedsLayout
Invalidates the current layout of the receiver and triggers a layout
update during the next update cycle.
Discussion
Call this method on your application’s main thread when you
want to adjust the layout of a view’s subviews. This method makes a
note of the request and returns immediately. Because this method does
not force an immediate update, but instead waits for the next update
cycle, you can use it to invalidate the layout of multiple views
before any of those views are updated. This behavior allows you to
consolidate all of your layout updates to one update cycle, which is
usually better for performance.

Possible approach to resolve this type of issue is using awakeAfterUsingCoder. It works as a placeholder view. Load your view here instead of in ViewController. Also set layout constraints right in the XIB.
For more information see this link.

Related

intrinsicContentSize causes infinite loop in special case

I made a container view i call SimpleStackView. The idea is simple, any subviews are stacked on top of eachother. The width of a SimpleStackView determines the width of its subviews, and the height of a SimpleStackView is determined by the height of its subviews.
I do it in layoutSubviews where i call sizeThatFits on each subview and layout them on top of eachother using the returned heights. The sum of those heights also determine what is returned from both the sizeThatFits override and intrinsicContentSize override of SimpleStackView.
I support iOS 7 so i cant use UIStackView.
I use AutoLayout to layout most things in my app. My SimpleStackView works fine in many places where its laid out using AutoLayout next to other views (i rely on its intrinsicContentSize to define its height, no height constraints), except in one case where a SimpleStackView is put in the contentView of a UITableViewCell in a UITableView. In that one case, an infinite loop is triggered. Im not an AutoLayout guru. I might be missing something about how intrinsicContetSizes are used inside AutoLayout? What could be the case of this? How do i use intrinsicContentSize properly so it works correctly in all cases?
The code of SimpleStackView is relatively short; here's the full class implementation:
#implementation SimpleStackView
#synthesize rowSpacing=_rowSpacing;
- (void)layoutSubviews
{
[super layoutSubviews];
[self sizeToFit];
[self invalidateIntrinsicContentSize];
CGFloat nextRowTop = 0;
for (UIView *view in self.subviews)
{
CGSize size = [view sizeThatFits:CGSizeMake(self.bounds.size.width, view.bounds.size.height)];
view.frame = CGRectMake(0, nextRowTop, self.bounds.size.width, size.height);
nextRowTop += view.frame.size.height + self.rowSpacing;
}
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat sumOfHeights = 0;
for (UIView *view in self.subviews) {
sumOfHeights += [view sizeThatFits:CGSizeMake(size.width, view.bounds.size.height)].height;
}
CGFloat sumOfRowSpacings = MAX(0, (int)self.subviews.count - 1) * self.rowSpacing;
return CGSizeMake(size.width, sumOfHeights + sumOfRowSpacings);
}
- (CGSize)intrinsicContentSize
{
CGFloat intrinsicHeight = [self sizeThatFits:self.bounds.size].height;
return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicHeight);
}
// i tried this to fix the infinite loop; didnt work was still stuck in infinite loop
//- (void)setFrame:(CGRect)frame
//{
// CGRect frameBefore = self.frame;
// [super setFrame:frame];
// if (NO == CGRectEqualToRect(frameBefore, frame))
// [self invalidateIntrinsicContentSize];
//}
#end
edit: I forgot to mention; the UITableCellView that causes the infinite loop has an unbroken chain of constraints from the top of contentView to bottom of contentView. The infinite loop stops happening when i remove one of the constraints to break the chain. I'd like to keep the constraints, they are there to compress a multiline UILabel when row height is small (which is set in the UITableViewDelegate's heightForRowAtIndexPath).

Where should I put AutoLayout code?

I'm using PureLayout to implement AutoLayout of subviews in a UIView. But I don't know the best practice of organizing the code.
Should I put the AutoLayout related code in the init of the UIView, or the overridden methods such as updateConstraints and layoutSubviews?
For example, I want to create a subclass of UIView called PHView, and for any phview, there is a subview called centerView, it is always at the center of phview, and width/height is 0.3*phview's width/height.
https://www.dropbox.com/s/jaljggnymxliu1e/IMG_3178.jpg
#import "PHView.h"
#import "Masonry.h"
#interface PHView()
#property (nonatomic, assign) BOOL didUpdateConstraints;
#property (nonatomic, strong) UIView *centerView;
#end
#implementation PHView
- (instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [UIColor redColor];
self.translatesAutoresizingMaskIntoConstraints = NO;
}
return self;
}
- (UIView *)centerView {
if (!_centerView) {
_centerView = [UIView new];
_centerView.backgroundColor = [UIColor yellowColor];
[self addSubview:_centerView];
}
return _centerView;
}
-(void)updateConstraints {
if (!_didUpdateConstraints) {
_didUpdateConstraints = YES;
[self.centerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.mas_centerX);
make.centerY.equalTo(self.mas_centerY);
make.width.equalTo(self.mas_width).multipliedBy(0.3);
make.height.equalTo(self.mas_height).multipliedBy(0.3);
}];
}
[super updateConstraints];
}
#end
'didUpdateConstraints' aims to indicate you have added constraints, so you will only add constraints once.
in UIViewController:make phview top bottom left right 20 to the margin.
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
PHView *myView = [PHView new];
[self.view addSubview:myView];
[myView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(20, 20, 20, 20));
}];
}
You should add constraints when you are sure that view has been added to its superview. Basically, you should do it in superview's class any point after addSubview: is called.
To answer your questions:
1- in init methods, can you be sure of that view has been added as subview to a superview? it wouldn't be safe to assume that. maybe you can add constraints in init method of superview
2- layoutSubviews is in where autolayout code actually works. you can't add constraints in layoutSubviews. already playing with autolayout constraints are not cheap, therefore you should add/remove them as few as possible, doing so in a method that is called multiple times (i.e. layoutSubviews) is not the best practice.
Mechanism of autolayout is going to inner view from outer view, so subviews do not actually concern about constraints. it is superview's responsibility
Hope this helps you by understanding controller’s view hierarchy
How View Controllers Participate in the View Layout Process
The view controller’s view is resized to the new size.
If autolayout is not in use, the views are resized according to their autoresizing masks.
The view controller’s viewWillLayoutSubviews method is called.
The view’s layoutSubviews method is called. If autolayout is used to configure the view hierarchy, it updates the layout constraints by executing the following steps:
a.The view controller’s updateViewConstraints method is called.
b.The UIViewController class’s implementation of the updateViewConstraints method calls the view’s updateConstraints method.
c. After the layout constraints are updated, a new layout is calculated and the views are repositioned.
The view controller’s viewDidLayoutSubviews method is called.
Please refer this for more details

Dynamically resize UICollectionView's supplementary view (containing multiline UILabel's)

Inside a UICollectionView's supplementary view (header), I have a multiline label that I want to truncate to 3 lines.
When the user taps anywhere on the header (supplementary) view, I want to switch the UILabel to 0 lines so all text displays, and grow the collectionView's supplementary view's height accordingly (preferably animated). Here's what happens after you tap the header:
Here's my code so far:
// MyHeaderReusableView.m
// my gesture recognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
// force -layoutSubviews to run again
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
[self layoutIfNeeded];
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
NSLog(#"height: %#", #(height));
}
When I log height at the end of layoutSubviews, its value is 149 while the label is truncated and numberOfLines is set to 3. After tapping the headerView, setting numberOfLines to 0, and forcing a layout pass, height then gets recorded as 163.5. Great!
The only problem is that the entire headerView doesn't grow, and the cells don't get pushed down.
How can I dynamically change the height of my collectionView's supplementary view (preferably animated)?
I'm aware of UICollectionViewFlowLayout's headerReferenceSize and collectionView:layout:referenceSizeForHeaderInSection: but not quite sure how I'd use them in this situation.
I got something working, but I'll admit, it feels kludgy. I feel like this could be accomplished with the standard CollectionView (and associated elements) API + hooking into standard layout/display invalidation, but I just couldn't get it working.
The only thing that would resize my headerView was setting my collection view's flow layout's headerReferenceSize. Unfortunately, I can't access my collection view or it's flow layout from my instance of UICollectionReusableView, so I had to create a delegate method to pass the correct height back.
Here's what I have now:
// in MyHeaderReusableView.m
//
// my UITapGestureRecognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
if (self.resizeDelegate) {
[self.resizeDelegate wanderlistDetailHeaderDidResize:self.frame.size];
}
}
// in my viewController subclass which owns the UICollectionView:
- (void)wanderlistDetailHeaderDidResize:(CGSize)newSize
{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// this is the key line
flowLayout.headerReferenceSize = newSize;
// this doesn't look beautiful but it's the best i can do for now. I would love for just the bottom of the frame to animate down, but instead, all the contents in the header (the top labels) have a crossfade effect applied.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView layoutIfNeeded];
}];
}
Like I said, not the solution I was looking for, but a working solution nonetheless.
I ran into the same issue than you, so I was just wondering: did you ever get a solution without the crossfade effect that you mention in the code sample?. My approach was pretty much the same, so I get the same problem. One additional comment though: I managed to implement the solution without the need for delegation: What I did was from "MyHeaderReusableView.m" You can reference the UICollectionView (and therefore, the UICollectionViewLayout) by:
//from MyHeaderReusableView.m
if ([self.superview isKindOfClass:UICollectionView.class]) {
//get collectionView reference
UICollectionView * collectionView = (UICollectionView*)self.superview;
//layout
UICollectionViewFlowLayout * layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
//... perform the header size change
}

Update the preferredMaxLayoutWidth for a multiline UILabel with unknown size (Auto Layout)

I have a custom view class which inherits from UIView. This class has an UILabel as its subview. In the init-function of this custom view class I set up everything needed like this:
//h-file
#import <UIKit/UIKit.h>
#interface MyCustomView : UIView
#property (strong, nonatomic) UILabel *myLabel;
#end
//m-file
#implementation MyCustomView
#synthesize myLabel = _myLabel;
- (id)init
{
self = [super init];
if (self) {
_myLabel = [UILabel new];
if(_textView){
_myLabel.highlightedTextColor = [UIColor whiteColor];
_myLabel.translatesAutoresizingMaskIntoConstraints = NO;
_myLabel.lineBreakMode = NSLineBreakByWordWrapping;
_myLabel.numberOfLines = 0;
_myLabel.backgroundColor = [UIColor clearColor];
[self addSubview:_myLabel];
}
}
return self;
}
#end
I also set up a bunch of constraints to manage padding inside my custom view - furthermore there are constraints to layout multiple MyCustomView-instances for both vertical and horizontal axis as well.
To get a multilined label output I have to set the preferredMaxLayoutWidth-property of the UILabel myLabel. The width depends on the free space available. At http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html I read, that I can let Auto Layout calculate the width first and set it as preferredMaxLayoutWidth after the frame of the MyCustomView-instance (the label inside is single lined at this moment) has been set.
If I put the following function into the MyCustomView, the label still has a single line of text:
- (void)layoutSubviews
{
[super layoutSubviews];
float width = _myLabel.frame.size.width;
_myLabel.preferredMaxLayoutWidth = width;
[super layoutSubviews];
}
If I set the preferredMaxLayoutWidth to an explicit value inside the init-function, the label is multilined.
Does anybody know what I am doing wrong here?
Thanks in advance!
Without seeing all the constrains you have setup for your custom view, and the superview that contains it, it's really hard to determine the problem, I suggest you to print out all the view frames of the entire view hierarchy starting from the view controller's view at viewDidLayoutSubviews and determine if the label and its superviews have correct frame set.
I have an encountered similar issues with dynamic label size and scroll view so I created a prototype here, might be useful to you too: https://github.com/briandotnet/AutoLayoutScrollViewExperiment

Best way to redraw a custom view when orientation changes

I have a custom view:
-(id)initWithFrame:(CGRect)frame
{
CGRect frameRect = CGRectMake(0, NAVIGATION_BAR_HEIGHT , frame.size.width, 4 * ROW_HEIGHT + NAVIGATION_BAR_HEIGHT + MESSAGE_BODY_PADDING);
self = [super initWithFrame:frameRect];
if (self) {
_selectionViewWidth = &frame.size.width;
[self initView];
}
return self;
}
-(void)initView
{
CGRect sectionSize = CGRectMake(0, 0 , *(_selectionViewWidth), ROW_HEIGHT * 4);
_selectionView = [[UIView alloc] initWithFrame:sectionSize];
[_selectionView setBackgroundColor:[UIColor clearColor]];
That I use in a View Controller the next way:
_mailAttributesView = [[MailAttributesView alloc]initWithFrame:self.view.frame];
_mailAttributesView.delegate = self;
[self.view addSubview:_mailAttributesView];
So when orientation changes from P to L I have the next problem:
What's the best way to get orientation change callback and redraw my custom view?
You likely need to override your UIView layoutSubviews method and proceed to manually layout your subviews (looks like to/from/cc/subject controls) there.
Or, you could better configure your subview spring/struts (or autolayout constraints) for automatic layout. You could do this in code or via a nib or storyboard.
EDIT: additional info since you seem not to be getting layoutSubviews on orientation change.
My guess is that the viewcontroller-view isn't resizing/repositioning your MailAttributes view either.
It's also not clear when/where you add your MailAttributesView to the veiwcontroller view. If you're doing it in viewDidLoad your viewcontroller view may or may not have a valid frame size (depending if it was loaded from a nib or not). It's best not to depend on the viewcontroller-view frame for layout purposes in viewDidLoad.
Rather, layout any viewcontroller-view subviews in viewWillLayoutSubviews. There your viewcontroller-view frame will be set.
Others may point out that you can set your autoresizingFlags in viewDidLoad for any subviews, but there are gotcha's with this. Primarily if the parent view has zero size, and your subviews are to be inset but have springs/struts defined to glue them to the parent view edges.
The best solution overall IMO is to setup autolayout constraints for everything contained in your viewcontroller view, on down.

Resources