How to calculate UIViewController height before adding view to view tree? - ios

I need to measure the height of a simple autolayout based VC for a given width. For example a simple UIViewController with only one label which is positioned using leading, trailing, top and bottom constraints to the VCs root view. The VC should not have a fixed size but automatically adapt to the label content.
This is only an example, of course the VC could have other content which is influences the size.
How can I calculate the VCs size for a given width and label content without adding it to the view hierarchy?
Background:
I am using a third party FormSheet control which allows to easily show any ViewController as form sheet with different styles, transitions, etc. The only downside is, that one has to specify a fixed sheet size before the VC is presented.
While this works great for VCs with "static" content / fixed sizes even a label with different texts for different languages might break the design.
Thus I am look for something like this:
ContentViewController *contentVC = [ContentViewController new];
CGRect contentBounds = [SomeClass calculateAutoLayoutHeightForView:contentVC.view withFixedWidth:500];
[ThirPartyFormSheetController presentController:contentVC withSize:contentBounds];
How can this be done?

Given a width, you can use systemLayoutSizeFittingSize:UILayoutFittingCompressedSize to determine what the height will be after auto-layout does its work.
Assuming constraints in the view-to-show are set up correctly:
CGFloat w = 500.0;
[loadedView.widthAnchor constraintEqualToConstant:w].active = YES;
// caluclate the size using FittingCompressedSize
CGSize contentBounds = [loadedView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
Here is a simple example (only need to assign the ViewController class to a view controller in Storyboard... no IBOutlets needed). Lots of comments in the code should make everything clear:
//
// ViewController.h
// Created by Don Mag on 4/8/19.
//
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
//
// ViewController.m
// Created by Don Mag on 4/8/19.
//
#import "ViewController.h"
#import "FormViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// add a button we'll use to show the form VC
UIButton *b = [UIButton new];
b.translatesAutoresizingMaskIntoConstraints = NO;
[b setTitle:#"Show Form" forState:UIControlStateNormal];
[b setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[b setBackgroundColor:[UIColor redColor]];
[self.view addSubview:b];
[NSLayoutConstraint activateConstraints:
#[
[b.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20.0],
[b.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:0.75],
[b.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor]
]
];
[b addTarget:self action:#selector(loadAndShowForm:) forControlEvents:UIControlEventTouchUpInside];
}
- (void) loadAndShowForm:(id)sender {
// instantiate the form view controller
FormViewController *vc = [FormViewController new];
// get a reference to its view
UIView *v = vc.view;
// use auto-layout
v.translatesAutoresizingMaskIntoConstraints = NO;
// set the label text in the form view
vc.topLabel.text = #"This is a bunch of text for the TOP label in the Form VC";
vc.bottomLabel.text = #"This is a bunch of text for the BOTTOM label in the Form VC. It's enough text to cause a few lines of word-wrap, assuming we're running on an iPhone.";
// specify a width for the form view
// we'll use width of current view minus 60 (30-pts on each side)
CGFloat w = self.view.frame.size.width - 60.0;
[v.widthAnchor constraintEqualToConstant:w].active = YES;
// caluclate the size using FittingCompressedSize
CGSize contentBounds = [v systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
// because we set the width constraint, we now have the "compressed" height
//[ThirdPartyFormSheetController presentController:contentVC withSize:contentBounds];
// debugging from here down
NSLog(#"Auto-layout resulting size: %#", [NSValue valueWithCGSize:contentBounds]);
// set the height for the form view
[v.heightAnchor constraintEqualToConstant:contentBounds.height].active = YES;
// add it to the view, so we can confirm the height calculation
[self.view addSubview:v];
// center it on the view
[NSLayoutConstraint activateConstraints:
#[
[v.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[v.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
]
];
}
#end
//
// FormViewController.h
// Created by Don Mag on 4/8/19.
//
#import <UIKit/UIKit.h>
#interface FormViewController : UIViewController
#property (strong, nonatomic) UILabel *topLabel;
#property (strong, nonatomic) UITextField *theTextField;
#property (strong, nonatomic) UILabel *bottomLabel;
#end
//
// FormViewController.m
// Created by Don Mag on 4/8/19.
//
#import "FormViewController.h"
#interface FormViewController ()
#end
#implementation FormViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
// create a multi-line "top label"
_topLabel = [UILabel new];
_topLabel.backgroundColor = [UIColor cyanColor];
_topLabel.text = #"Hello Top Label";
_topLabel.numberOfLines = 0;
// create a text field
_theTextField = [UITextField new];
_theTextField.backgroundColor = [UIColor greenColor]; // just to make it easy to see
_theTextField.borderStyle = UITextBorderStyleRoundedRect;
_theTextField.text = #"The Text Field";
// create a multi-line "bottom label"
_bottomLabel = [UILabel new];
_bottomLabel.backgroundColor = [UIColor cyanColor];
_bottomLabel.text = #"Hello Bottom Label";
_bottomLabel.numberOfLines = 0;
// we're using auto-layout and constraints
_topLabel.translatesAutoresizingMaskIntoConstraints = NO;
_theTextField.translatesAutoresizingMaskIntoConstraints = NO;
_bottomLabel.translatesAutoresizingMaskIntoConstraints = NO;
// add to view
[self.view addSubview:_topLabel];
[self.view addSubview:_theTextField];
[self.view addSubview:_bottomLabel];
// these elements and constraints will define the height of the content
[NSLayoutConstraint activateConstraints:
#[
// constrain top label leading, trailing and top to top of view, all at 20-pts
[_topLabel.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:20.0],
[_topLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0],
[_topLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0],
// constrain text field leading and trailing, and top to bottom of top label, all at 20-pts
[_theTextField.topAnchor constraintEqualToAnchor:_topLabel.bottomAnchor constant:20.0],
[_theTextField.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0],
[_theTextField.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0],
// constrain bottom label leading, trailing and top to bottom of text field, all at 20-pts
[_bottomLabel.topAnchor constraintEqualToAnchor:_theTextField.bottomAnchor constant:20.0],
[_bottomLabel.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-20.0],
[_bottomLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0],
// AND constrain bottom label to bottom of view at 20-pts
[_bottomLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0]
]
];
}
#end
The result (adding the loaded VC's view as a subview - see the comments in the code):
and with more text to show the automatic height calculation:
If you change the amount of text for the labels (set in ViewController.m), you will see that the height is calculated correctly.

Swift:
If all you need is to calculate height on the basis of label's text, you can use this solution
https://stackoverflow.com/a/25187891/7848711

Related

UIStackView change item spacing as stack view changes sizes

Overview
I have a stack view that has multiple circle views in it. The circle views could be images (like profile pictures) or anything. These views should be able to overlap if the size of the stack view is too small for the subviews. And the views should spread out if the stack view is too big for the subviews. Also, subviews can be added or removed dynamically, even if the size of the stack view doesn't change.
For example, in the following image the top stack view has these circle views that are overlapping and everything is working fine there (the frame is exactly the size of the subviews views). But then, looking at the second stack view, after adding a few more views, the first view gets compressed. But what I want to happen is for all of the views to overlap a bit more and to not compress any of the views.
Question
What is the best way to implement this behavior? Should I override layoutSubviews, like I am proposing in the next section, or is there a better way to implement this? Again, I just want the views to either spread out, if the stack view is too large for them, or for them to overlap each other, if the stack view is too narrow. And the stack view can change size at any time and also arranged subviews can be added or removed at any time, and all of those things should cause the view spacing to be recalculated.
Proposed Solution
I was considering overriding the layoutSubviews method of the stack view and then somehow measuring all of the views, adding those widths together, and then the spacing that is currently present (I guess go through each of the arranged subviews and see what the spacing is for that subview). So it would be negative spacing for overlap or positive spacing if the items are actually spread out. Then, I would compare that width with the frame in layoutSubviews and if it was too wide, then I would decrease the spacing. Otherwise, if the views did not take up the full stack view, then I would increase their spacing.
Here is my code and the proposed algorithm in layoutSubviews.
Code
MyShelf.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
#interface MyShelf : UIStackView
#property (assign, nonatomic) CGSize itemSize;
#property (assign, nonatomic) MyShelfItemShape itemShape;
#property (strong, nonatomic) UIColor *itemBorderColor;
#property (assign, nonatomic) CGFloat itemBorderWidth;
#property (assign, nonatomic) CGFloat preferredMinimumSpacing;
#property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
#end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
#interface MyShelf ()
#property (strong, nonatomic) UIStackView *stackView;
#end
#implementation MyShelf
#pragma mark - Initializing the View
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
self.spacing = -10;
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.distribution = UIStackViewDistributionFillProportionally;
self.itemSize = CGSizeZero;
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
}
- (void)layoutSubviews {
//if the new frame is different from the old frame
//if the size of the items in the stack view is too large, reduce the spacing down to a minimum of preferredMinimumSpacing
//else if the size of the items in the stack view is too small, increase the spacing up to a maximum of preferredMaximumSpacing
//otherwise keep the spacing as-is
[super layoutSubviews];
}
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:#[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
if (animated) {
//prepare the view to be initially hidden so it can be animated in
view.alpha = 0.0;
view.hidden = YES;
[super insertArrangedSubview:view atIndex:stackIndex];
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 1.0; view.hidden = NO; }
completion:nil];
} else {
[super insertArrangedSubview:view atIndex:stackIndex];
}
[self reorderArrangedSubviews];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.arrangedSubviews.count animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 0.0; view.hidden = YES; }
completion:^(BOOL finished) { [super removeArrangedSubview:view]; }];
} else {
[super removeArrangedSubview:view];
}
}
- (void)reorderArrangedSubviews {
for (__kindof UIView *arrangedSubview in self.arrangedSubviews) {
[self sendSubviewToBack:arrangedSubview];
}
}
#end
Requirements
If the view is a fixed width
For this case, the view that contains these circle subviews is a fixed width. It could be that it has a width constraint that specifies the number of points wide it is or it could be constrained by other views such that its width is predetermined.
In this case, the subviews should be arranged next to each other until they can no longer fit in the frame, and at which point they start to collapse (negative spacing between items).
If the view is a flexible width
For this case, the view that contains the circular subviews doesn't have a width specified. Instead, its width is determined by the width of the contents. So it should keep growing up until it can no longer grow, and at which point, then the subviews start to overlap.
The general idea is to use centerX constraints on your circle views - I'll call them ShelfItem, and constrain them to an "invisible positioning view."
The reason to do that, is because when the item's centerX is on the leading edge (or trailing edge) half of it will extend to the left or right of the positioning view.
Think about dividing a width into equal parts (all values are in %)...
If we have 3 items, we need 2 equal parts. To get the percentage spacing, we use 1.0 / (numItems - 1):
With 4 items, we need 3 equal parts:
With 5 items, we need 4 equal parts:
And with 6 items, we need 5 equal parts:
So, by making the "item" views subviews of the "positioning" view, we can loop through and set their centerX constraints like this:
UIView *thisItem;
CGFloat pct = 1.0 / (CGFloat)([subviews count] - 1);
for (int i = 0; i < subviews.count; i++) {
thisItem = subviews[i];
CGFloat thisPCT = pct * i;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
c.active = YES;
}
It's not quite that simple though...
First, auto-layout doesn't like a multiplier: 0.0 ... so the left-most item needs to have centerX equal to positioning view Leading.
The second thing is that your layout requires the item views to be left-aligned when there is enough room, not evenly spaced.
To accomplish that, we'll make each item view's centerX lessThanOrEqualTo the previous item's centerX + itemWidth... and we'll give the "percentage" constraints a less-than-required priority.
So, each time we add (or remove) an item, we'll call a method to update the centerX constraints... it will look about like this:
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
The last task is to add a "framing" view that will match the bounds of the laid-out item views.
Here's a complete example...
ShelfItem.h - a simple round view with a label
#import <UIKit/UIKit.h>
#interface ShelfItem : UIView
#property (strong, nonatomic) UILabel *label;
#end
ShelfItem.m
#import "ShelfItem.h"
#implementation ShelfItem
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
self.backgroundColor = UIColor.whiteColor;
_label = [UILabel new];
_label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight];
_label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_label];
[_label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES;
[_label.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES;
self.layer.borderColor = UIColor.blueColor.CGColor;
self.layer.borderWidth = 1.0;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.bounds.size.height * 0.5;
}
#end
ShelfView.h - our view that does all the work
#import <UIKit/UIKit.h>
#interface ShelfView : UIView
- (void)addItem:(NSInteger)n;
- (void)removeItem;
#end
ShelfView.m
#import "ShelfView.h"
#import "ShelfItem.h"
#interface ShelfView () {
UIView *positionView;
UIView *framingView;
CGFloat itemWidth;
NSLayoutConstraint *framingViewTrailingConstraint;
}
#end
#implementation ShelfView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
itemWidth = 60.0;
// framingView will match the bounds of the items
// it will not be their superView, but will look like it
framingView = [UIView new];
framingView.translatesAutoresizingMaskIntoConstraints = NO;
framingView.backgroundColor = UIColor.systemYellowColor;
[self addSubview:framingView];
// positionView is used for the item position constraints
// but is not seen
positionView = [UIView new];
positionView.translatesAutoresizingMaskIntoConstraints = NO;
positionView.backgroundColor = UIColor.clearColor;
[self addSubview:positionView];
// initialize framingView trailing constraint -- it will be updated in updatePositions
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:positionView.leadingAnchor];
framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
[NSLayoutConstraint activateConstraints:#[
// positioning view is at vertical center with no height
[positionView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[positionView.heightAnchor constraintEqualToConstant:0.0],
// leading and trailing are 1/2 the item width
[positionView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:itemWidth * 0.5],
[positionView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-itemWidth * 0.5],
// framing view leading is at positioning view leading minus 1/2 item width
[framingView.leadingAnchor constraintEqualToAnchor:positionView.leadingAnchor constant:-itemWidth * 0.5],
// constrained top and bottom
[framingView.topAnchor constraintEqualToAnchor:self.topAnchor],
[framingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
]];
}
- (void)updatePositions {
if ([positionView.subviews count] == 0) {
// no items, so all we have to do is update the framing view
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:self.leadingAnchor];
framingViewTrailingConstraint.active = YES;
return;
}
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
// update the trailing anchor of the framing view to the last shelf item
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:thisItem.trailingAnchor];
framingViewTrailingConstraint.active = YES;
}
- (void)addItem:(NSInteger)n {
// create a new shelf item
ShelfItem *v = [ShelfItem new];
v.translatesAutoresizingMaskIntoConstraints = NO;
v.label.text = [NSString stringWithFormat:#"%ld", (long)n];
// add it as a subview of positionView
// at index Zero (so it will be underneath existing items)
[positionView insertSubview:v atIndex:0];
// width and height
[v.widthAnchor constraintEqualToConstant:itemWidth].active = YES;
[v.heightAnchor constraintEqualToAnchor:v.widthAnchor].active = YES;
// vertically centered on positionView
[v.centerYAnchor constraintEqualToAnchor:positionView.centerYAnchor constant:0.0].active = YES;
// update all shelf items
[self updatePositions];
}
- (void)removeItem {
// remove the last-added item
[positionView.subviews[0] removeFromSuperview];
// update all shelf items
[self updatePositions];
}
#end
ViewController.h - controller with two ShelfViews and Add / Remove buttons:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
ViewController.m
#import "ViewController.h"
#import "ShelfView.h"
#interface ViewController ()
{
ShelfView *shelfViewA;
ShelfView *shelfViewB;
NSInteger counter;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
counter = 0;
// top shelf view has systemYellow background, so we see its
// full width all the time
shelfViewA = [ShelfView new];
shelfViewA.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewA.backgroundColor = UIColor.systemYellowColor;
[self.view addSubview:shelfViewA];
// second shelf view has clear background, so we only see its
// framing view width when items are added
shelfViewB = [ShelfView new];
shelfViewB.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewB.backgroundColor = UIColor.clearColor;
[self.view addSubview:shelfViewB];
UIButton *addBtn = [UIButton new];
addBtn.translatesAutoresizingMaskIntoConstraints = NO;
addBtn.backgroundColor = UIColor.systemGreenColor;
[addBtn setTitle:#"Add" forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[addBtn addTarget:self action:#selector(addTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:addBtn];
UIButton *removeBtn = [UIButton new];
removeBtn.translatesAutoresizingMaskIntoConstraints = NO;
removeBtn.backgroundColor = UIColor.systemGreenColor;
[removeBtn setTitle:#"Remove" forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[removeBtn addTarget:self action:#selector(removeTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:removeBtn];
UILabel *info = [UILabel new];
info.translatesAutoresizingMaskIntoConstraints = NO;
info.backgroundColor = [UIColor colorWithWhite:0.90 alpha:1.0];
info.textAlignment = NSTextAlignmentCenter;
info.numberOfLines = 0;
info.text = #"Shelf View Width\n60-pts on each side.";
[self.view addSubview:info];
// respect safeArea
UILayoutGuide *g = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:#[
[shelfViewA.topAnchor constraintEqualToAnchor:g.topAnchor constant:60.0],
[shelfViewA.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewA.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewA.heightAnchor constraintEqualToConstant:60.0],
[info.topAnchor constraintEqualToAnchor:shelfViewA.bottomAnchor constant:8.0],
[info.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[info.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.topAnchor constraintEqualToAnchor:info.bottomAnchor constant:8.0],
[shelfViewB.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewB.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.heightAnchor constraintEqualToConstant:60.0],
[addBtn.topAnchor constraintEqualToAnchor:shelfViewB.bottomAnchor constant:20.0],
[addBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[addBtn.widthAnchor constraintEqualToConstant:200.0],
[removeBtn.topAnchor constraintEqualToAnchor:addBtn.bottomAnchor constant:20.0],
[removeBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[removeBtn.widthAnchor constraintEqualToConstant:200.0],
]];
}
- (void)addTapped {
++counter;
[shelfViewA addItem:counter];
[shelfViewB addItem:counter];
}
- (void)removeTapped {
if (counter > 0) {
--counter;
[shelfViewA removeItem];
[shelfViewB removeItem];
}
}
#end
Running that gives us this - note the "top" shelf view shows its frame, the "bottom" shelf view only shows the "framing view":
and when the view changes size, such as on device rotation, we don't have to do anything ... auto-layout handles it for us:
You can simply make it by adjust distribution attribute.
self.distribution = UIStackViewDistributionEqualCentering;
What's more, UIStackView

iOS Header StackView with correct alignment?

I am trying to implement a UIView which will be a header. Note I am only focused on implementing the header (as shown below) in this question.
Here is how it is supposed to be look like (the HEADER is in yellow)
Basically, the header UIView should have a UIBUTTON all the way to the left and a UILabel exactly in the middle, nothing on the right
The problem I am having is how I will make the UIView for this.
My idea was have a main horizontal UIStackView, but if I put a UILabel and UIButton into it, how can I (in code) align it in the way I've described? I am unable to use the UI Builder for this, but have to lay it out in Objective C code.
#interface HeaderView : UIView
#implementation HeaderView {
UIStackView mainHorizontalStackView;
UIButton leftButton;
UILabel middleLabel;
}
-(instanceType) initializer(){
mainHorizontalStackView = ... //alloc
leftButton = ...
middleLabel = ...
// how do I set up the constraints to make it fit the desired setup?
}
This is a very basic example of creating your custom view:
HeaderView.h
//
// HeaderView.h
// Created by Don Mag on 4/7/20.
//
#import <UIKit/UIKit.h>
#interface HeaderView : UIView
#end
HeadView.m
//
// HeaderView.m
// Created by Don Mag on 4/7/20.
//
#import "HeaderView.h"
#interface HeaderView ()
#property (strong, nonatomic) UIButton *leftButton;
#property (strong, nonatomic) UILabel *centeredLabel;
#end
#implementation HeaderView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// default background color
self.backgroundColor = [UIColor colorWithRed:1.0 green:0.95 blue:0.8 alpha:1.0];
self.layer.borderColor = [UIColor brownColor].CGColor;
self.layer.borderWidth = 1.0;
self.layer.cornerRadius = 8.0;
_leftButton = [UIButton new];
[_leftButton setTitle:#"BUTTON" forState:UIControlStateNormal];
[_leftButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
_leftButton.translatesAutoresizingMaskIntoConstraints = NO;
_centeredLabel = [UILabel new];
_centeredLabel.text = #"LABEL";
_centeredLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_leftButton];
[self addSubview:_centeredLabel];
// adjust constant values if "padding" from edges desired
[NSLayoutConstraint activateConstraints:#[
// constrain button to left, top, bottom
[_leftButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:4.0],
[_leftButton.topAnchor constraintEqualToAnchor:self.topAnchor constant:8.0],
[_leftButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-8.0],
// constrain label centered horizontally in view, centered vertically to button
[_centeredLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_centeredLabel.centerYAnchor constraintEqualToAnchor:_leftButton.centerYAnchor],
]];
}
return self;
}
#end
TestViewController.h
//
// TestViewController.h
// Created by Don Mag on 4/7/20.
//
#import <UIKit/UIKit.h>
#interface TestViewController : UIViewController
#end
TestViewController.m
//
// TestViewController.m
// Created by Don Mag on 4/7/20.
//
#import "TestViewController.h"
#import "HeaderView.h"
#interface TestViewController ()
#end
#implementation FirstViewController
- (void)viewDidLoad {
[super viewDidLoad];
HeaderView *v = [HeaderView new];
v.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:v];
// respect safe area
UILayoutGuide *g = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:#[
// constrain header view top / leading / trailing to self.view (safe area)
// adjust constant values if "padding" from edges desired
[v.topAnchor constraintEqualToAnchor:g.topAnchor constant:0.0],
[v.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:8.0],
[v.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-8.0],
]];
}
#end
Result:

How do you add a subview to UIWindow and prevent it from disappearing when transitioning using UINavigationController?

I am trying to add a "Floating Button" on top of a scrollview with a lot of subviews. When the button is clicked, UINavigation controller prompts a new viewController to appear. When I dismiss the controller, to go back to the original viewController, I want the button to still be there!
To not deal with constraints conflicts, my solution was to add the UIButton to the UIWindow.
[UIApplication.sharedApplication.keyWindow addSubview:_myButton];
Everything works well. But the problem is when I dismiss the new controller to come back to the controller containing the button:
[self dismissViewControllerAnimated:YES completion:nil];
The button is not immediately "there". Only once the animation finishes (the viewController goes fully down), does the button "re-appear". If I got rid of animation by setting:
[self dismissViewControllerAnimated:NO completion:nil];
Everything works well! The button is IMMEDIATELY there. But I really want to keep this animation.
I was wondering if there was a way to have the button STAY on a viewController, or at the very least appear before the animation fully completes to make it seem like an actual floating button on the screen.
If this is not possible, is there another approach to building floating buttons natively on iOS, without installing any additional pods?
Thanks!
Here is a very simple example.
add a scroll view to the main view
add content to the scroll view
add a button to the main view -- not to the scroll view
constrain the button at the top-right of the scroll view
add a touch up action for the button to present another view controller
Because we are adding the button as a sibling of the scroll view (not as a subview), it will "float" in front of the scroll view, and the scroll view's contents will scroll behind it.
When we present - and then dismiss - another VC, the "floating button" won't be going anywhere, so it will still be visible when we return to the main VC.
Initial view:
Scroll view content scrolling "behind" the button:
Code to see it in action:
ViewController.h
//
// ViewController.h
// OCSept2019
//
// Created by Don Mag on 9/12/19.
//
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
ViewController.c
//
// ViewController.m
// OCSept2019
//
// Created by Don Mag on 9/12/19.
//
#import "ViewController.h"
#import "AnotherViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// instantiate a scroll view
UIScrollView *scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor cyanColor];
// instantiate a stack view
UIStackView *stackView = [UIStackView new];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisVertical;
stackView.alignment = UIStackViewAlignmentFill;
stackView.distribution = UIStackViewDistributionFill;
stackView.spacing = 40.0;
// add 20 labels as arrangeed subview of the stack view
for (int i = 0; i < 20; i++) {
UILabel *v = [UILabel new];
v.backgroundColor = [UIColor yellowColor];
v.text = [NSString stringWithFormat:#"This is label %ld", (long)i + 1];
v.textAlignment = NSTextAlignmentCenter;
[stackView addArrangedSubview:v];
}
// add the stack view as a subview of the scroll view
[scrollView addSubview:stackView];
// add the scroll view as a subview of self.view
[self.view addSubview:scrollView];
[NSLayoutConstraint activateConstraints:
#[
// constrain the scroll view to all 4 sides of self.view (safe area), with 20-pts "padding"
[scrollView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant: 20.0],
[scrollView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant: -20.0],
[scrollView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor constant: 20.0],
[scrollView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor constant: -20.0],
// constrain the stack view to all 4 sides of the scroll view, with
// 8-pts left and right "padding"
// 40-pts top and bottom "padding"
[stackView.topAnchor constraintEqualToAnchor:scrollView.topAnchor constant: 40.0],
[stackView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor constant: -40.0],
[stackView.leadingAnchor constraintEqualToAnchor:scrollView.leadingAnchor constant: 8.0],
[stackView.trailingAnchor constraintEqualToAnchor:scrollView.trailingAnchor constant: -8.0],
// constrain stack view width equal to scroll view width - 16 (to account for 8-pt padding on each side)
[stackView.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor constant:-16.0],
]
];
// instantiate a button with Red background
UIButton *b = [UIButton new];
b.translatesAutoresizingMaskIntoConstraints = NO;
[b setTitle:#"Tap to Present Another VC" forState:UIControlStateNormal];
b.backgroundColor = [UIColor redColor];
[b setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
// add the button as a subview of self.view
// this will place it *in front of* the scroll view
[self.view addSubview:b];
// constrain it at upper-right corner of the scroll view
[NSLayoutConstraint activateConstraints:
#[
[b.topAnchor constraintEqualToAnchor:scrollView.topAnchor constant:8.0],
[b.trailingAnchor constraintEqualToAnchor:scrollView.trailingAnchor constant:-8.0],
]
];
// add a touch up inside action
[b addTarget:self action:#selector(btnTap) forControlEvents:UIControlEventTouchUpInside];
}
- (void) btnTap {
// instantiate another view controller
AnotherViewController *vc = [AnotherViewController new];
// present it
[self presentViewController:vc animated:YES completion:nil];
}
#end
AnotherViewController.h
//
// AnotherViewController.h
// OCSept2019
//
// Created by Don Mag on 9/12/19.
//
#import <UIKit/UIKit.h>
#interface AnotherViewController : UIViewController
#end
AnotherViewController.m
//
// AnotherViewController.m
// OCSept2019
//
// Created by Don Mag on 9/12/19.
//
#import "AnotherViewController.h"
#interface AnotherViewController ()
#end
#implementation AnotherViewController
- (void)viewDidLoad {
[super viewDidLoad];
// green background
self.view.backgroundColor = [UIColor greenColor];
// instantiate a button with Blue background
UIButton *b = [UIButton new];
b.translatesAutoresizingMaskIntoConstraints = NO;
[b setTitle:#"Tap to Dismiss" forState:UIControlStateNormal];
b.backgroundColor = [UIColor blueColor];
[b setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
// add it as a subview of self.view
[self.view addSubview:b];
// constrain it centered X and Y
[NSLayoutConstraint activateConstraints:
#[
[b.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[b.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
]
];
// add a touch up inside action
[b addTarget:self action:#selector(btnTap) forControlEvents:UIControlEventTouchUpInside];
}
- (void) btnTap {
// dismiss this view controller
[self dismissViewControllerAnimated:YES completion:nil];
}
#end

Resolve ambiguity with content hugging priority

Important: this question is not about adding/removing/modifying constraints!
I need help with understanding why the following layout is ambiguous:
UIView (rootView)
| UIView (topView)
| UIView (bottomView)
Constraints setup: V:|[topView][bottomView]|, and content hugging priority of bottomView is higher than the content hugging of the topView.
I would understand it if both views would have the same content hugging priority, but since the value is higher on bottomView I expect that it should resist more growing larger.
Below I paste the code you can use in a "Single View App" Xcode project template:
//
// ViewController.m
//
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) UIView* rootView;
#property (strong, nonatomic) UIView* topView;
#property (strong, nonatomic) UIView* bottomView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupViewHierarchy];
[self setupIssueIrrelevantConstraints];
[self setupIssueRelevantConstraints];
[self bumpVerticalContentHuggingPriorityOfView:self.bottomView];
}
- (void)setupViewHierarchy {
self.view.backgroundColor = [UIColor lightGrayColor];
self.rootView = [UIView new];
self.rootView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.rootView];
self.topView = [UIView new];
self.topView.translatesAutoresizingMaskIntoConstraints = NO;
self.topView.backgroundColor = [UIColor greenColor];
[self.rootView addSubview:self.topView];
self.bottomView = [UIView new];
self.bottomView.translatesAutoresizingMaskIntoConstraints = NO;
self.bottomView.backgroundColor = [UIColor blueColor];
[self.rootView addSubview:self.bottomView];
}
- (void)setupIssueIrrelevantConstraints {
[self.rootView.widthAnchor constraintEqualToConstant:200.0].active = YES;
[self.rootView.heightAnchor constraintEqualToConstant:200.0].active = YES;
[self.rootView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[self.rootView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
[self.topView.leftAnchor constraintEqualToAnchor:self.rootView.leftAnchor].active = YES;
[self.topView.rightAnchor constraintEqualToAnchor:self.rootView.rightAnchor].active = YES;
[self.bottomView.leftAnchor constraintEqualToAnchor:self.rootView.leftAnchor].active = YES;
[self.bottomView.rightAnchor constraintEqualToAnchor:self.rootView.rightAnchor].active = YES;
}
- (void)setupIssueRelevantConstraints {
[self.topView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES;
[self.bottomView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor].active = YES;
[self.topView.bottomAnchor constraintEqualToAnchor:self.bottomView.topAnchor].active = YES;
}
- (void)bumpVerticalContentHuggingPriorityOfView:(UIView*)view {
UILayoutPriority contentHuggingPriority = [view contentHuggingPriorityForAxis:UILayoutConstraintAxisVertical];
contentHuggingPriority++;
[view setContentHuggingPriority:contentHuggingPriority
forAxis:UILayoutConstraintAxisVertical];
}
#end
I know what ambiguous layout is, and no more constraints are required to resolve that layout. I expect that bottomView height will be equal to 0 because since its content hugging priority is larger it should resist more growing than topView.
The trouble is that you have a misconception of what content hugging is. It is about content.
Content hugging is relevant only when a view has an intrinsic content size, as does a label or a button.
https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize
Content hugging is the priority with which, in the face of other constraints, a view should obey the dictates of its intrinsic content size. That’s all it is.
But your views do not have any intrinsic content size; there is no content to hug. Therefore your content hugging settings are meaningless and ignored.
Authors solution:
Thank you very much #matt. Your answer did help me. I was aware of everything you wrote, except that "having no intrinsic content size" does not mean that its equal to {0, 0} but {-1, -1}, and that is a difference.
To sum things up my layout does work as expected with a subtle change. I got it to work by replacing UIView with ZeroIntrinsicSizeView which looks like this:
#interface ZeroIntrinsicSizeView : UIView
#end
#implementation ZeroIntrinsicSizeView
- (CGSize)intrinsicContentSize {
return CGSizeZero;
}
#end

Replace custom UIViews arranged by Interface Builder with UILabels programmatically

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;
}

Resources