I have to show a popover on iPhone screen with multiple "Switch" controls. And to add and remove subviews on/from popover view with switch on/off actions respectively. For better illustration of the situation see below
images.
The above popover view first appears when user taps on a button. The popover has to stay always at the center of the screen and initially add contact switch will be in off condition. When turned on the below subviews has to be added on popover while keeping the popover in center of the screen and increasing the height of popover as per subviews.
And just like the above the popover view has to grow again in height with adding two more subviews when "Add mail" switch will be "ON". And finally look like this,
That's it. I am using auto-layout through out my application and this is where I am perplexed. I know I can remove the popovers and one more new each time but that seems to be kind of novice option. So is there any simple way to add subviews and expand its superview dynamically with auto-layout ? I've seen many questions with UILabel and working with respect to it's intrinsic content size but still unable to get any idea with this particular situation. Any help will be appreciated. Happy coding.
This can be accomplished with plain layout constraints without having to manually constrain the height of the container view, and then update the constant of that constraint.
The way to do this, is to constrain the container view's height based on the bottom of the bottom most subview.
Then put a reference to this constraint within your view controller.
now you can write something like the following view controller, which will add a new subview at the bottom of the container view, and automatically update the container view's height.
#import "ViewController.h"
#interface ViewController ()
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
#property (weak, nonatomic) IBOutlet UIButton *addButton;
#property (weak, nonatomic) IBOutlet UIView *containerView;
#property (nonatomic, weak) UIView *lastView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.lastView = self.addButton;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)addButtonTapped:(id)sender {
UIView *newView = [[UIView alloc] initWithFrame:CGRectZero];
newView.translatesAutoresizingMaskIntoConstraints = NO;
newView.backgroundColor = [UIColor redColor];
[newView addConstraint:[NSLayoutConstraint constraintWithItem:newView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:35]];
[self.containerView addSubview:newView];
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[lastView]-(14)-[newView]"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:#{#"lastView" : self.lastView, #"newView" : newView}]];
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(10)-[newView]-(10)-|"
options:NSLayoutFormatAlignmentMask
metrics:nil
views:#{#"newView":newView}]];
[self.containerView removeConstraint:self.bottomConstraint];
self.bottomConstraint = [NSLayoutConstraint constraintWithItem:self.containerView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:newView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:14];
[self.containerView addConstraint:self.bottomConstraint];
self.lastView = newView;
}
#end
Add this all together, and you should get the following behavior.
You can outlet height constraint of the view, and then set value accordingly to elements.
Related
I am confused with the autolayout on xib/nib. I am comfortable with storyboard autolayout.
What I am trying to do is:
1. I have a storyboard with one view controller with one UIView called borderView. It is constrained correctly.It is resized correctly for iphone 5 and 6. Here is the screenshot of the storyboard:
This is the borderView in iphone 5: (I am trying to add the nib view as a subview to this border view)
2. I created a nib with UIView in it with the dimensions 300 x 300. I want this view to be added to the borderVIew in my ViewController. HEre is the screenshot for my nib.
Note: I didnt gave any height or width constraints anywhere. I just gave leading ,traliing,top and bottom.
and I am trying to add the nib to my borderview as follows:
This is the method in my viewController:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[[NSBundle mainBundle] loadNibNamed:#"AlertView" owner:self options:nil];
[self.borderView addSubview:self.myAlertViewFromNib];
}
and the result in the iphone 5 is:
As you can see, the nib view is not aligned in the center of the screen (as the border view is aligned center to the screen).
I have no idea of how to give constraints to nib itself. Can anyone please tell me if it is possible to do it in xcode or do I need to give the constraints programmatically?
You want to add the AlertView as a center aligned in the view controller
Rather than setting size from the xib just create the perfect constrained XIB view and define macros for sizes as below.
Step 1 have some macros for size
#define kAlertViewWidth 300
#define kAlertViewHeight 300
Step 2 in #interface .Please create on property like this in the viewcontroller
#property (nonatomic, retain) IBOutlet UIView *myViewFromNib;
Getter Method For the property
-(UIView *)myViewFromNib
{
if (!_myViewFromNib) {
_myViewFromNib = [[[NSBundle mainBundle] loadNibNamed:#"AlertView" owner:self options:nil] objectAtIndex:0];
_myViewFromNib.translatesAutoresizingMaskIntoConstraints = NO;
}
return _myViewFromNib;
}
//Step 3 in viewDidLoad method
-(void)viewDidLoad{
[self setUpConstraints];
}
Method which will add the required constraints
-(void)setUpConstraints
{
\[self.view addSubview:self.myViewFromNib\];
\[NSLayoutConstraint activateConstraints:#\[\[NSLayoutConstraint constraintWithItem:self.myViewFromNib attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0\],
\[NSLayoutConstraint constraintWithItem:self.myViewFromNib attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0\],
\[NSLayoutConstraint constraintWithItem:self.myViewFromNib attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0
constant:kAlertViewWidth\],
\[NSLayoutConstraint constraintWithItem:self.myViewFromNib attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0
constant:kAlertViewHeight\]\]\];
}
If you have no choice but to do it this way try setting the centre manually. i.e. yourView.frame.x = super.frame.size.width/2 - yourView.frame.size.width/2. This should centre it horizontally. If it won't let you, then you'd have to add in the constraints after adding in to subview.
Instead of a xib and a storyboard, use a storyboard and inside your borderview add another view (with everything you have in the xib) and constrain that view within your borderview.
Use the vertically center and horizontally center constraints to center the inner view. If you need to change the constraints later, create an outlet for them in your header file and adjust the priority.
If you still want to use a xib within a view, try using a third party library like Masonry to adjust your xib.
The new NSLayoutConstraint methods activateConstraints: and deactivateConstraints: don't appear to work correctly with IB-created constraints (they do work correctly for code-created constraints). I created a simple test app with one button that has two sets of constraints. One set, which is installed, has centerX and centerY constraints, and the other set, which is uninstalled, has top and left constraints (constant 10). The button method switches these constraints sets. Here is the code,
#interface ViewController ()
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *uninstalledConstraints;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *installedConstraints;
#end
#implementation ViewController
- (IBAction)switchconstraints:(UIButton *)sender {
[NSLayoutConstraint deactivateConstraints:self.installedConstraints];
[NSLayoutConstraint activateConstraints:self.uninstalledConstraints];
}
-(void)viewWillLayoutSubviews {
NSLog(#"installed: %# uninstalled: %#", ((NSLayoutConstraint *)self.installedConstraints[0]).active ? #"Active" : #"Inactive", ((NSLayoutConstraint *)self.uninstalledConstraints[0]).active ? #"Active" : #"Inactive");
}
When the app launches, the button is in the correct, centered position defined by its installed constraints. After I do the activation/inactivation in the button's action method, the button moves to its new position correctly, but when I rotate the view to landscape, it moves back to its initially defined position (though a log still shows the newly activated set as being active). When I rotate back to portrait, the button stays in its initial position (centered in the screen), and now the log shows that initial set of constraints as active, and the ones I activated, as inactive.
The question is, is this a bug, or are these methods not supposed to work in this way with IB defined constraints?
The problem is that you are doing something incoherent with "uninstalled" constraints in the storyboard. They are there but not there. "Uninstalled" constraints are for use only with size classes! You use them if you are going to let Xcode swap constraints for you automatically on rotation. Xcode can't cope with what you're doing. But if you create the second set of constraints in code, everything will work fine.
So, do this. Delete the two "uninstalled" constraints, and delete the uninstalledConstraints outlet. Now replace your entire view controller code with this:
#property (strong, nonatomic) NSMutableArray *c1;
#property (strong, nonatomic) NSMutableArray *c2;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *installedConstraints;
#property (weak,nonatomic) IBOutlet UIButton *button;
#end
#implementation ViewController {
BOOL did;
}
- (void)viewDidLayoutSubviews {
NSLog(#"did");
if (!did) {
did = YES;
self.c1 = [self.installedConstraints mutableCopy];
self.c2 = [NSMutableArray new];
[self.c2 addObject:
[NSLayoutConstraint constraintWithItem:self.button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTopMargin multiplier:1 constant:30]];
[self.c2 addObject:
[NSLayoutConstraint constraintWithItem:self.button attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeadingMargin multiplier:1 constant:30]];
}
}
- (IBAction)switchconstraints:(UIButton *)sender {
[NSLayoutConstraint deactivateConstraints:self.c1];
[NSLayoutConstraint activateConstraints:self.c2];
NSMutableArray* temp = self.c1;
self.c1 = self.c2;
self.c2 = temp;
}
Now repeatedly press the button. As you see, it jumps between the two positions. Now rotate the app; the button stays where it is.
I faced a similar situation.
I created two sets of NSLayoutConstraints in interface builder. Each set for one case. One set was "installed" the other not.
When I switch the case the coresponding set of layout constraint was activated and the other was deacticated. An as described in the Qusteion rotating forth and back does not work properly.
Is solved this by installing both sets in interface builder. And to get rid of the warnings I used a slighlt lower priority (999) for the second set.
This worked for me.
Btw: Strang i used the "installed/not installed" aproach on another viewcontroller, their it worked.
In the not working case the viewcontroller was embedded in a containerview, perhaps that was the reason.
You can use this workflow, even though it’s clear that this is not a supported use case currently for the folks at Apple.
The trick is to not uninstall constraints (as #matt also pointed out), but to deactivate them in the viewDidLoad() of your UIViewController subclass, before layout occurs (and your conflicting constraints cause a problem).
I’m using that method now and it works perfectly. Ideally, of course, we’d have the ability to visually create groups of constraints and use them like keyframe simply by activating and deactivating them (with free animations in between) :)
I would like the red button to be animated towards the leading position of the second button :
Some examples showed how to change the "constant" with numbers, but I would like to put automatically at the leading position of the second button.
I tried this, but the red button does not move, the animations log is correctly called though :
- (void)updateConstr{
NSLayoutConstraint *newLeading = [NSLayoutConstraint
constraintWithItem:self.redB
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.secondButton
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0f];
self.leadingConstraint = newLeading;//is an IBOutlet pointing on the constraint (see the image)
[self.redB setNeedsUpdateConstraints];
[UIView animateWithDuration:0.5 animations:^{
[self.redB layoutIfNeeded];
NSLog(#"animations");//is called, but the red button does not move
}];
}
- (IBAction)firstAction:(id)sender { //after a click on button "one"
NSLog(#"firstAction");
[self updateConstr];
}
This must do it:
- (void)updateConstr{
NSLayoutConstraint *newLeading = [NSLayoutConstraint
constraintWithItem:self.redB
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.secondButton
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0f];
[self.redB.superview removeConstraint: self.leadingConstraint];
[self.redB.superview addConstraint: newLeading];
self.leadingConstraint = newLeading;
[UIView animateWithDuration:0.5 animations:^{
[self.redB layoutIfNeeded];
}];
}
What I typically do in this situation is the following.
Add two constraints to your scene. One where it's aligned left between the button and the "one" label. The second, where it's aligned left between the button and the "second" label (i.e. both values will be 0). These constraints will initially conflict with one another, and that's fine.
Add IBOutlets to your view controller for the NSLayoutConstraints and assign the two constraints we've created to those IBOutlets.
Set the constraint priority on your initial condition to 999 (i.e. constraint on left align to "one" should be 999). Set the constraint priority on the destination constraint to 998 (i.e. constraint on left align between button to "second" is 998). You'll now see that these constraints will no longer conflict. This is because the priority on one constraint overrides the other.
You may see where this is headed now. So when you want to animate the button between the constraints, swap the priorities and animate!
Code:
#interface MyViewController ()
#property (nonatomic, weak) NSLayoutConstraint* constraint0;
#property (nonatomic, weak) NSLayoutConstraint* constraint1;
#end
- (void)someMethodWhereIWantToAnimate
{
NSInteger temp = constraint0.priority;
constraint0.priority = constraint1.priority;
constraint1.priority = temp;
[UIView animateWithDuration:1.0 animations:^{
// Simplest is to use the main ViewController's view
[self.view layoutIfNeeded];
}];
}
I have a view with 2 container views: one main one on top and one at the bottom.
When the app launches, the bottom one is hidden via a frame that goes beyond the screen height. The top one in the meantime occupies the entire app window.
When I decide to show that bottom container, I want the top container to decrease in height and the view of the controller in that main container to be impacted as well.
I tried to add a constraint programmatically and used layoutIfNeeded but nothing worked.
I'm new to this. I don't necessarily want the best answer but how I should approach this.
Thanks!!!!
-(void)showBottom {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.bottomContainer attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.mainContainer attribute:NSLayoutAttributeTop multiplier:1.0f constant:49.0f];
[self.view addConstraint:constraint];
}
You can try pinning objects with a Top Space to Superview constraint and animating it.
// .h
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *topConstraint;
// .m
[UIView animateWithDuration:1.0 animations:^{
self.topConstraint.constant = 0;
[self.nView layoutIfNeeded];
}];
I am configuring a custom UITableViewCell using a prototype cell in a Storyboard. However, all the UILabels (and other UI elements) do not seem to be added to the cell's contentView, instead being added to the UITableViewCell view directly. This creates issues when the cell is put into editing mode, as the content is not automatically shifted/indented (which it would do, if they were inside the contentView).
Is there any way to add the UI elements to the contentView when laying out the cell using Interface Builder/Storyboard/prototype cells? The only way I have found is to create everything in code and use [cell.contentView addSubView:labelOne] which wouldn't be great, as it is much easier to layout the cell graphically.
On further investigation (viewing the subview hierarchy of the cell) Interface Builder does place subviews within the cell's contentView, it just doesn't look like it.
The root cause of the issue was iOS 6 autolayout. When the cell is placed into editing mode (and indented) the contentView is also indented, so it stands to reason that all subviews within the contentView will move (indent) by virtue of being within the contentView. However, all the autolayout constraints applied by Interface Builder seem to be relative to the UITableViewCell itself, rather than the contentView. This means that even though the contentView indents, the subviews contained within do not - the constraints take charge.
For example, when I placed a UILabel into the cell (and positioned it 10 points from the left-hand side of the cell) IB automatically applied a constraint "Horizontal Space (10)". However, this constraint is relative to the UITableViewCell NOT the contentView. This means that when the cell is indented, and the contentView moves, the label stays put as it is complying with the constraint to remain 10 points from the left-hand side of the UITableViewCell.
Unfortunately (as far as I am aware) there is no way to remove these IB created constraints from within IB itself, so here is how I solved the problem.
Within the UITableViewCell subclass for the cell, I created an IBOutlet for that constraint called cellLabelHSpaceConstraint. You also need an IBOutlet for the label itself, which I called cellLabel. I then implemented the -awakeFromNib method as per below:
- (void)awakeFromNib {
// -------------------------------------------------------------------
// We need to create our own constraint which is effective against the
// contentView, so the UI elements indent when the cell is put into
// editing mode
// -------------------------------------------------------------------
// Remove the IB added horizontal constraint, as that's effective
// against the cell not the contentView
[self removeConstraint:self.cellLabelHSpaceConstraint];
// Create a dictionary to represent the view being positioned
NSDictionary *labelViewDictionary = NSDictionaryOfVariableBindings(_cellLabel);
// Create the new constraint
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-10-[_cellLabel]" options:0 metrics:nil views:labelViewDictionary];
// Add the constraint against the contentView
[self.contentView addConstraints:constraints];
}
In summary, the above will remove the horizontal spacing constraint which IB automatically added (as is effective against the UITableViewCell rather than the contentView) and we then define and add our own constraint to the contentView.
In my case, all the other UILabels in the cell were positioned based upon the position of the cellLabel so when I fixed up the constraint/positioning of this element all the others followed suit and positioned correctly. However, if you have a more complex layout then you may need to do this for other subviews as well.
As mentioned, XCode's Interface Builder is hiding the UITableViewCell's contentView. In reality, all UI elements added to the UITableViewCell are in fact subviews of the contentView.
For the moment, it IB is not doing the same magic for layout constraints, meaning that they are all expressed at UITableViewCell level.
A workaround is in a subclass's awakeFromNib to move all NSAutoLayoutConstrains from UITableViewCell to it's contentView and express them in terms of the contentView :
-(void)awakeFromNib{
[super awakeFromNib];
for(NSLayoutConstraint *cellConstraint in self.constraints){
[self removeConstraint:cellConstraint];
id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
NSLayoutConstraint* contentViewConstraint =
[NSLayoutConstraint constraintWithItem:firstItem
attribute:cellConstraint.firstAttribute
relatedBy:cellConstraint.relation
toItem:seccondItem
attribute:cellConstraint.secondAttribute
multiplier:cellConstraint.multiplier
constant:cellConstraint.constant];
[self.contentView addConstraint:contentViewConstraint];
}
}
Here is a subclass, based on other answers ideas, I'm going to base my custom cells on:
#interface FixedTableViewCell ()
- (void)initFixedTableViewCell;
#end
#interface FixedTableViewCell : UITableViewCell
#end
#implementation FixedTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (nil != (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
[self initFixedTableViewCell];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self initFixedTableViewCell];
}
- (void)initFixedTableViewCell {
for (NSInteger i = self.constraints.count - 1; i >= 0; i--) {
NSLayoutConstraint *constraint = [self.constraints objectAtIndex:i];
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
BOOL shouldMoveToContentView = YES;
if ([firstItem isDescendantOfView:self.contentView]) {
if (NO == [secondItem isDescendantOfView:self.contentView]) {
secondItem = self.contentView;
}
}
else if ([secondItem isDescendantOfView:self.contentView]) {
if (NO == [firstItem isDescendantOfView:self.contentView]) {
firstItem = self.contentView;
}
}
else {
shouldMoveToContentView = NO;
}
if (shouldMoveToContentView) {
[self removeConstraint:constraint];
NSLayoutConstraint *contentViewConstraint = [NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
[self.contentView addConstraint:contentViewConstraint];
}
}
}
#end
An alternative to subclassing is to revise the constraints in cellForRowAtIndexPath.
Embed all the content of the cell inside a container view. Then point the leading and trailing constraints to the cell.contentView rather than the table view cell.
UIView *containerView = [cell viewWithTag:999];
UIView *contentView = [cell contentView];
//remove existing leading and trailing constraints
for(NSLayoutConstraint *c in [cell constraints]){
if(c.firstItem==containerView && (c.firstAttribute==NSLayoutAttributeLeading || c.firstAttribute==NSLayoutAttributeTrailing)){
[cell removeConstraint:c];
}
}
NSLayoutConstraint *trailing = [NSLayoutConstraint
constraintWithItem:containerView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:contentView
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
NSLayoutConstraint *leading = [NSLayoutConstraint
constraintWithItem:containerView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:contentView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
[cell addConstraint:trailing];
[cell addConstraint:leading];
I think this is fixed in iOS 7 beta 3 making the workarounds unnecessary from that point on (but probably harmless as in most cases they will become empty operations).
Based on the code by Skoota (I am a beginner, don't know much of what you did, but excellent work) my suggestion is to put all your stuff in an edge-to-edge container view and add the following:
In the cell's header file, I have the following IBOutlets:
#property (weak, nonatomic) IBOutlet UIView *container;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *leftConstrain;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *rightConstrain;
In the implementation file, I have the following in awakeFromNib:
// Remove the IB added horizontal constraint, as that's effective gainst the cell not the contentView
[self removeConstraint:self.leftConstrain];
[self removeConstraint:self.rightConstrain];
// Create a dictionary to represent the view being positioned
NSDictionary *containerViewDictionary = NSDictionaryOfVariableBindings(_container);
// Create the new left constraint (0 spacing because of the edge-to-edge view 'container')
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-0-[_container]" options:0 metrics:nil views:containerViewDictionary];
// Add the left constraint against the contentView
[self.contentView addConstraints:constraints];
// Create the new constraint right (will fix the 'Delete' button as well)
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[_container]-0-|" options:0 metrics:nil views:containerViewDictionary];
// Add the right constraint against the contentView
[self.contentView addConstraints:constraints];
Again, the above was made possible by Skoota. Thanks!!! Al credits go to him.