I am doing this and I am curious whether it is the best way, or a dumb way!
I have a bunch of 40 pixel wide images, each one is like a Scrabble tile. My app wants to display some and center them on the screen. Only it don't know how many there are going to be! Could be between 3 and 10.
So I think best thing is if I count how many, multiple by 40, so I know how many pixels wide the whole thing will be, and then let's pretend it's 280 pixels - I will create a 280 px wide UIView, stick all the tiles in there, and then use Autolayout to center that UIView on the device.
That way if user rotates device, no problem!
Is this the best way? Also I am going to need to let the user drag the tiles out of that UIView and into another place on screen. Will that be possible?
Three approaches leap out at me:
I think your solution of using a container view is perfectly fine. But, you don't have to mess around with determining the size of the images. You can just define the relation between the container and the image views, and it will resize the container to conform to the intrinsic size of the image views (or if you explicitly define the size of the image views, that's fine, too). And you can then center the container (and not give it any explicit width/height constraints):
// create container
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor clearColor];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:containerView];
// create image views
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"1.png"]];
imageView1.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView1];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"2.png"]];
imageView2.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView2];
NSDictionary *views = NSDictionaryOfVariableBindings(containerView, imageView1, imageView2);
// define the container in relation to the two image views
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageView1]-[imageView2]|" options:0 metrics:nil views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[imageView1]-|" options:0 metrics:nil views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[imageView2]-|" options:0 metrics:nil views:views]];
// center the container
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
Another common solution with constraints is to create two extra UIView objects (sometimes called "spacer views"), for which you'll specify a background color of [UIColor clearColor], and put them on the left and right of your image views, and define them to go to the margins of the superview, and define the right view to be the same width of the left view. While I'm sure you're building your constraints as you're going along, if we were going to write the visual format language (VFL) for two imageviews to be centered on the screen, it might look like:
#"H:|[leftView][imageView1]-[imageView2][rightView(==leftView)]|"
Alternatively, you could eliminate the need for the container view or the two spacer views on the left and right by creating NSLayoutAttributeCenterX constraints using constraintWithItem, and specifying multiplier for the various image views so that they're spaced the way you want. While this technique eliminates the need for these two spacer views, I also think it's a little less intuitive.
But it might look like:
[imageViewArray enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:NSLayoutAttributeCenterX
multiplier:2.0 * (idx + 1) / ([imageViewArray count] + 1)
constant:0];
[view.superview addConstraint:constraint];
}];
This admittedly employs a slightly different spacing of the image views, but in some scenarios it's fine.
Personally, I'd lean towards the first approach, but any of these work.
If you have a grid layout your best solution is to use the UICollectionView. This is a highly customizable class that can be configured for almost any grid layout requirements.
I've yet to find a better introduction to what UICollectionView can do than the WWDC 2012 videos:
WWDC 2012 Session 205: Introducing Collection Views by Olivier Gutknecht and Luke Hiesterman
WWDC 2012 Session 219: Advanced Collection Views and Building Custom Layouts by Luke the Hiesterman
A good web based tutorial from Ray Wenderlich is here:
http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12
By the way, I notice that you asked a second question at the conclusion of your question, namely how to drag the image views out of your container.
Let's assume that you've done the constraints as you've suggested in your question, with the tiles being in a container view that you've centered on your main view (see option 1 of my other answer). You would presumably write a gesture recognizer handler, that would, as you start dragging, remove the tile from the container's list of tiles and then animate the updating of the constraints accordingly:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint originalCenter;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// move the gesture.view out of its container, and up to the self.view, so that as the container
// resizes, this view we're dragging doesn't move in the process, too
originalCenter = [self.view convertPoint:gesture.view.center fromView:gesture.view.superview];
[self.view addSubview:gesture.view];
gesture.view.center = originalCenter;
// now update the constraints for the views still left in the container
[self removeContainerTileConstraints];
[self.tiles removeObject:gesture.view];
[self createContainerTileConstraints];
[UIView animateWithDuration:0.5 animations:^{
[self.containerView layoutIfNeeded];
}];
}
CGPoint translate = [gesture translationInView:gesture.view];
gesture.view.center = CGPointMake(originalCenter.x + translate.x, originalCenter.y + translate.y);
if (gesture.state == UIGestureRecognizerStateEnded)
{
// do whatever you want when you drop your tile, presumably changing
// the superview of the tile to be whatever view you dropped it on
// and then adding whatever constraints you need to make sure it's
// placed in the right location.
}
}
This will gracefully animate the tiles (and, invisibly, their container view) to reflect that you dragged a tile out of the container.
Just for context, I'll show you how I created the container and the tiles to be used with the above gesture recognizer handler. Let's say that you had an NSMutableArray, called tiles, of your Scrabble-style tiles that were inside your container. You could then create the container, the tiles, and attach a gesture recognizer to each tile like so:
// create the container
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor lightGrayColor];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:containerView];
self.containerView = containerView; // save this for future reference
// center the container (change this to place it whereever you want it)
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
// create the tiles (in my case, three random images), populating an array of `tiles` that
// will specify which tiles the container will have constraints added
self.tiles = [NSMutableArray array];
NSArray *imageNames = #[#"1.png", #"2.png", #"3.png"];
for (NSString *imageName in imageNames)
{
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageName]];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[imageView addGestureRecognizer:pan];
imageView.userInteractionEnabled = YES;
[self.tiles addObject:imageView];
}
// add the tile constraints
[self createContainerTileConstraints];
And you'd obviously need these utility methods:
- (void)removeContainerTileConstraints
{
NSMutableArray *constraintsToRemove = [NSMutableArray array];
// build an array of constraints associated with the tiles
for (NSLayoutConstraint *constraint in self.containerView.constraints)
{
if ([self.tiles indexOfObject:constraint.firstItem] != NSNotFound ||
[self.tiles indexOfObject:constraint.secondItem] != NSNotFound)
{
[constraintsToRemove addObject:constraint];
}
}
// now remove them
[self.containerView removeConstraints:constraintsToRemove];
}
- (void)createContainerTileConstraints
{
[self.tiles enumerateObjectsUsingBlock:^(UIView *tile, NSUInteger idx, BOOL *stop) {
// set leading constraint
if (idx == 0)
{
// if first tile, set the leading constraint to its superview
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:tile.superview
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0]];
}
else
{
// if not first tile, set the leading constraint to the prior tile
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.tiles[idx - 1]
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:10.0]];
}
// set vertical constraints
NSDictionary *views = NSDictionaryOfVariableBindings(tile);
[tile.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[tile]|" options:0 metrics:nil views:views]];
}];
// set the last tile's trailing constraint to its superview
UIView *tile = [self.tiles lastObject];
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:tile.superview
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0]];
}
Related
What i want to achieve is set buttons/images Horizontally Centered with main view, including its width set to 75% of the screen width.
I want to fit like 7 such images/buttons vertically on the screen (line-by-line).
I am using the following code, which is working perfect:
UIImageView *l1 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[l1 setImage:[UIImage imageNamed:#"level-1"]];
[l1 setContentMode:UIViewContentModeScaleAspectFit];
l1.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:l1];
NSLayoutConstraint *c1 = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:20.f
];
[self.view addConstraint:c1];
NSLayoutConstraint *c1b = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.f
];
[self.view addConstraint:c1b];
NSLayoutConstraint *c1c = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.75f
constant:0.f
];
[self.view addConstraint:c1c];
Instead of repeating the same code for different images, i want to use some iteration process and increment the image name [UIImage imageNamed:#"level-xxx"]] and bind top position to the bottom position of the last added item.
How it would be possible? Thx
iOS 9 introduces a new class called UIStackView that allows you to do this :
https://www.hackingwithswift.com/read/31/2/uistackview-by-example
There are even back-ports of this to iOS 7 on Github.
Some people might downvote me for this but sometimes , when it comes to autolayout I just say , $##& this I'll write the layout code myself. And this is one of those cases (especially when UIStackView is not available).
First create all your views and store them in an array in viewDidLoad.
Override viewDidLayoutSubviews on your view controller and loop through the array , set the frames and lay them out one by one.
-(void)viewDidLayoutSubviews
{
NSArray* imageList = self.imageViewList
// calculate it if necessary
CGPoint startPoint = CGPointMake(200,200)
CGFloat spacing = 50
for(UIView *view in imageList) {
view.center = startPoint
startPoint = CGPointMake(startPoint.x , startPoint.y + spacing)
}
}
This is way more obvious , way easier to debug than a mess of constraints created in code.
This is my first time using UIScrollViews with a pure Autolayout approach. This is what the view hierarchy looks like
view
-scrollview
--view1
--view2
--view3
scrollview should contain view1|view2|view3 in that order.
I set the scrollviews width, height, centerx and bottom space to superview. The view1, view2 and view3 that are created all have their width and height constraints setup in their updateConstraints method. Additionally, some constraints are provided in code. What is the reason this scrollview is not scrolling from left to right? I have read literally all of the guides I can find online about creating and adding subviews to a UIScrollView programmatically with auto layout. I found some mention about having to provide four different constraints, leading, trailing, top and bottom for each view added as a subview to the scrollview. Are these the only NSLayoutAttributes that one can specify? How do attributes such as NSLayoutAttribueLeft or NSLayoutAttribueRight relate? I have read documentation on Apples website as well, specifically https://developer.apple.com/library/ios/technotes/tn2154/_index.html. I am attaching the setup I currently have. Everything is done via code.
- (void)viewDidLoad
{
[super viewDidLoad];
self.dataSource = #[ [[PCCGenericRating alloc] initWithTitle:#"Easiness"
andMessage:#"WHAT A JOKERRRR"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]],
[[PCCGenericRating alloc] initWithTitle:#"Joker"
andMessage:#"WHAT A JOKERRRR"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]],
[[PCCGenericRating alloc] initWithTitle:#"Difficulty"
andMessage:#"YOu are not difficult at all"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]]
];
[self initView];
}
- (void)initView {
CGFloat navigationBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
CGFloat heightDifference = navigationBarHeight + statusBarHeight;
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.delegate = self;
[self.scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
self.scrollView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.scrollView];
//setup constraints
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:0.0f]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1.0f
constant:-heightDifference]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0]];
[self.dataSource enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
PCCGenericRating *rating = (PCCGenericRating *)obj;
PCCGenericRatingView *ratingView = [self createViewWithRating:rating];
[self.scrollView addSubview:ratingView];
int multiplier = (idx == 0) ? 1 : (int) (idx + 1) ;
[self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:ratingView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterX
multiplier:multiplier
constant:0.0f]];
[self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:ratingView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]];
}];
}
- (PCCGenericRatingView *)createViewWithRating:(PCCGenericRating *)rating {
PCCGenericRatingView *view = [PCCGenericRatingView genericRatingViewWithTitle:rating.title andMessage:rating.message];
return view;
}
Upon printing out the scrollview constraints, they look okay to me:
po self.scrollView.constraints
<__NSArrayM 0x115b051f0>(
<NSLayoutConstraint:0x1145d9290 PCCGenericRatingView:0x114579880.centerX == UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145d9410 PCCGenericRatingView:0x114579880.centerY == UIScrollView:0x11458d4b0.centerY>,
<NSLayoutConstraint:0x1145d9dd0 PCCGenericRatingView:0x1145d9560.centerX == 2*UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145d9e40 PCCGenericRatingView:0x1145d9560.centerY == UIScrollView:0x11458d4b0.centerY>,
<NSLayoutConstraint:0x1145da6b0 PCCGenericRatingView:0x1145d9e90.centerX == 3*UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145da730 PCCGenericRatingView:0x1145d9e90.centerY == UIScrollView:0x11458d4b0.centerY>
)
Here is a screenshot of what it looks like:
I find it odd that the last element in the datasource is the first view controller showing up in the scrollview, when it should be the last view. It also doesn't scroll left to right as it should.
Make sure your top_constraint for the view1 and bottom_constraint for view3 will be as per your scrollView's constraints. Otherwise scrollview's contentSize: {0, 0}.
Wherever you are printing your constraints, try printing scrollview.contentSize, it will likely be 0,0 and that is where your problem is. As far as I know, and as you mentioned in your post, you have to explicitly set the subviews of a scrollview to the scrollviews top bottom left and right constraints. Setting these automatically sets the contentSize of the scrollview which will enable it to scroll. It looks like you are only setting centerX and centerY constraints which will not set the scrollviews contentSize to what you need.
Try setting these programatically (this is pseudocode but you get the idea):
view1.topConstraint = scrollView.topConstraint
view1.leftConstraint = scrollView.leftConstraint
view3.bottomConstraint = scrollView.bottomConstraint
view3.rightConstraint = scrollView.rightConstraint
If you set all of those correctly, your scrollview will scroll properly. Just remember to check the contentsize, and if the contentsize is 0,0 then your constraints aren't properly set up.
I have a UIView that holds a UIScrollView. The UIScrollView contains an MKMapView subview and a placeholder subview right below it. I would like to pin the MKMapView to the top of the screen and allow the placeholder subview to slide over it and cover it up.
Apple says it's now possible to accomplish this with Autolayout but it doesn't seem to be working for me. The code below displays the UIScrollView and it's subviews properly, but the map still scrolls along with everything else. Am I missing something really obvious?
https://developer.apple.com/LIBRARY/ios/technotes/tn2154/_index.html
Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.
UIView .m file:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Create scroll view and add to view
UIScrollView *scrollView = [[UIScrollView alloc] init];
[scrollView setBackgroundColor: [UIColor clearColor]];
[scrollView setShowsVerticalScrollIndicator:NO];
[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:scrollView];
// Create map view and add to scroll view
mapView = [[MKMapView alloc] init];
mapView.showsPointsOfInterest = NO;
mapView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:mapView];
// Create a placeholder image to scroll over map view
UIImageView *randomPlaceholderStuff = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"stuff.png"]];
[dividingLine setTranslatesAutoresizingMaskIntoConstraints:NO];
[scrollView addSubview:dividingLine];
// Layouts
NSDictionary *viewArranging = NSDictionaryOfVariableBindings(scrollView, tourMap, randomPlaceholderStuff);
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|"
options:0
metrics:0
views:viewArranging]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|"
options:0
metrics:0
views:viewArranging]];
UIView *referenceSuperView = scrollView.superview;
[referenceSuperView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[mapView(320)]|"
options:0
metrics:0
views:viewArranging]];
[referenceSuperView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[mapView(320)]"
options:0
metrics:0
views:viewArranging]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-334-[randomPlaceholderStuff]|"
options:0
metrics:0
views:viewArranging]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[randomPlaceholderStuff(320)]|"
options:0
metrics:0
views:viewArranging]];
}
return self;
}
#end
Edit:
jrturton's answer was spot on. Here's the code that ended up working:
[self addConstraint:[NSLayoutConstraint constraintWithItem:mapView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView.superview attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:mapView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:scrollView.superview attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:mapView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:320.0]];
You can't do what you're trying with visual format language. The | character will always be interpreted as the view's direct superview, so you're not creating the constraints you think you are.
You need to use the longer method (constraintWithItem...) to create individual constraints, where item1 is your floating view, and item2 is the scroll view's superview.
You can add floating sub view over UIScrollView by adding/moving subview to super view of scrollview like:
Place/set your button over scroll view (not inside scroll view) as shown here in this snapshot. And also set button constraints (position) with respect to super view of your scrollview.
Here is ref. snapshot of hierarchy of position of each view over each-other.
Part of the screen I'm building includes a section with n views. I'm generating these views on the fly in code--they're simple UIView subclasses.
I'm using AutoLayout constraints for this screen, and I'd like each view to automatically position itself 15px or so below the view above it.
In Xcode it's possible to create a spacing to nearest neighbor constraint, which seems to do exactly what I want.
However, I can't seem to find any examples that show how to create this in code.
Is it possible to create a "spacing to nearest neighbor" constraint programmatically?
I was actually doing some personal exercises of auto-layout and animations when I stumbled over your question and decided to extend my a small demo, which you can download here.
If I have understood you correctly below piece of code can be used for inspiration. With a small effort it could be extended with removal of views too and also with dynamic height of the views.
Please note my code includes a solution with and without animation - the latter is of course more simple.
#import "Demo2ViewController.h"
#interface Demo2ViewController ()
{
NSMutableArray *_viewList;
NSDictionary *_metrics;
}
#end
#implementation Demo2ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[[self view] setBackgroundColor:[UIColor colorWithRed:.95 green:.95 blue:.95 alpha:1.0]];
_metrics = #{#"height": #30, // height of the views being added
#"space": #15}; // space between two views
// the first view
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectNull];
_viewList = [[NSMutableArray alloc] initWithObjects:textView, nil];
textView.text = [NSString stringWithFormat:#"view: %lu", (unsigned long)[_viewList count]];
// a button to add more views
UIButton *buttonAddView = [[UIButton alloc] initWithFrame:CGRectNull];
[buttonAddView setTitle:#"add new view" forState:UIControlStateNormal];
[buttonAddView setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[buttonAddView addTarget:self action:#selector(buttonPushed:) forControlEvents:UIControlEventTouchDown];
NSDictionary *subviews = NSDictionaryOfVariableBindings(textView, buttonAddView);
for (id view in [subviews allValues]) {
[[self view] addSubview:view];
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
}
// initial constraints
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textView]-|" options:0 metrics:nil views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[buttonAddView]-|" options:0 metrics:nil views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textView(==height)]" options:0 metrics:_metrics views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[buttonAddView]-|" options:0 metrics:nil views:subviews]];
}
-(void)buttonPushed:(UIButton*)button
{
UITextView *prevView = [_viewList lastObject]; // get reference to previous view
// create a new view
UITextView *newView = [[UITextView alloc] initWithFrame:CGRectNull];
[[self view] addSubview:newView];
[newView setTranslatesAutoresizingMaskIntoConstraints:NO];
[_viewList addObject:newView];
newView.text = [NSString stringWithFormat:#"view: %lu", (unsigned long)[_viewList count]];
NSDictionary *subviews = NSDictionaryOfVariableBindings(prevView, newView);
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[newView]-|" options:0 metrics:nil views:subviews]];
#if 0
// without animation
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView]-space-[newView(==height)]" options:0 metrics:_metrics views:subviews]];
[[self view] layoutIfNeeded];
#else
// with animation
// to begin with the new view gets zero height and no space to previous view
NSArray *tempConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView][newView(==0)]" options:0 metrics:nil views:subviews];
[[self view] addConstraints:tempConstraints];
[[self view] layoutIfNeeded]; // to ensure zero height is the starting point for the animation
[newView setAlpha:0.0f]; // starting point for fade-in
[UIView animateWithDuration:0.25f animations:^{
[[self view] removeConstraints:tempConstraints]; // remove zero height constraint
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView]-space-[newView(==height)]" options:0 metrics:_metrics views:subviews]]; // add final constraints
[newView setAlpha:1.0f]; // fade-in
[[self view] layoutIfNeeded];
}];
#endif
}
#end
You can create a constraint dictionary and a constraint string and apply them programmatically.
Adding items and keys to the dictionary is trivial so I won't deal with that. Just remember every view in the constraint system must be in the dictionary.
Creating the format string is the interesting bit. Presumably you want to add your views under as certain view, say it has the NSString key topView. The first part of the format string looks like
NSString *constraintBase = [NSString stringWithFormat:#"V:topView"];
For each view you want to add, you add to that string
NSString *constraintString = [constraintBase stringByAppendingString:[NSString stringWithFormat:#"-15-%#", viewDictionaryKey]];
Finally apply constraintString as usual, a constructed visual constraint format string.
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:constraintString options:0 metrics:nil views:viewsToConstrain]];
Here the view's which are generated programatically are added to a parent view.
When a view is created its constraints should be added to it.
But the constraints must mapped with the previous view, and so we need to identify the previous view (last view in the parent view)
NSArray *subViewList = [_vwParentView subviews];
UIView *lastView;
if (subViewList.count > 0) {
lastView = [subViewList lastObject];
}
The above code will help to find the last created view.
When Views are created programatically and added as sub views, the views will added as stack for parrentView and hence the view created at last will be the last object in the subViewList array
Note: Assuming that a separate view is assigned as parent view with no subviews initially.
UIView *contentView = [[UIView alloc]init];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:1.0 constant:initialWidth];
[contentView addConstraint:widthConstraint];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:1.0 constant:initialHeight];
[contentView addConstraint:heightConstraint];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[_vwParentView addSubview:contentView];
NSLayoutConstraint *gapMaintainTopConstraint;
if (lastView == nil) {
gapMaintainTopConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_vwParentView attribute:NSLayoutAttributeTop multiplier:1.0 constant:15];
}
else
{
gapMaintainTopConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:lastView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:15];
}
[_vwParentView addConstraint:gapMaintainTopConstraint];
In case to change the size of the added view in future, it should be achieved by changing its widthConstraint or heightConstraint, only then the constraint which is associated to it (to maintain specific gap) will work. Size should not be changed using frames thereafter.
Meaning - constraint based views should be handled using constraints only.
It is possible to create a "spacing to nearest neighbour" constraint only after the created view is added as a sub view,.
The translatesAutoresizingMaskIntoConstraints property of the created view should be disabled, so that there won't be any conflict of constraints when there is any change in size (of the created view) in future.
I am currently having trouble with my iOS app and Autolayout. I want to let the user scroll photos from Tumblr in a UIScrollView (similar to Photos app, when scrolling your library). So when my ImageViewController gets on screen, it has an array of posts but no data for the images yet. My problem is the following :
In my scrollView, I want to add as many UIImageViewS as needed, and I would like them to be all the same size. How should I do it? I tried many (probably bad designed :-/) ways, like initwithframe: with auto layout on and keeping a reference to them in a NSMutableArray...
My goal now is to add them to my scrollView in viewDidLoad and have correct constraints set.
Thanks for your help and sorry for my poor English
EDIT
OK I solved my problem: I used a nice scrollView and set its constraints with auto layout in viewWillAppear
Here is the code for those interested (sorry for layout) :
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self downloadPhotos];
[self.scrollView setContentOffset:CGPointMake(self.selectedImageIndex * self.scrollView.frame.size.width, 0) animated:NO];
// Add UIImageViewS to self.scrollView with constraints and so on...
NSMutableDictionary *viewsDictionnary = [[NSMutableDictionary alloc] init];
NSMutableString *imageViewsString = [[NSMutableString alloc] init];
NSMutableArray *imageViews = [[NSMutableArray alloc] init];
for (int i = 0; i < self.fetchedResultsController.fetchedObjects.count; i++) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.scrollView.bounds];
imageView.image = [UIImage imageNamed:#"placeholder_imageView"];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:imageView];
[imageViews addObject:imageView];
[viewsDictionnary setObject:imageView forKey:[NSString stringWithFormat:#"imageView%d", i]];
[imageViewsString appendString:[NSString stringWithFormat:#"[imageView%d]", i]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0]];
}
self.imageViews = imageViews;
NSString *horizontal = [NSString stringWithFormat:#"H:|%#|", imageViewsString];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:horizontal options:0 metrics:0 views:viewsDictionnary]];
[self.scrollView setContentOffset:CGPointMake(self.selectedImageIndex * self.scrollView.frame.size.width, 0) animated:NO];
}
I recommend not using a scrollView but using a collectionView instead (iOS 6 and above). You will probably insist that you need to use iOS 5 or iOS 4. Make things easy for yourself, collectionViews make memory management easy and if you plan on loading a lot of photos in view, just go with collection views. You can then create a collectionViewCell which will have a UIImageView. The UIImageView will be only one size, you can then set its image scaling propery to scale to fit so no matter what shape or size, the images will fit in the image view and not be distorted. Do some research on colletion views, heck, there probably is a tutorial you can use to load images from the internet and display them using a collection view.
-(void)viewDidLoad
{
[super viewDidLoad];
float posX = 0.0 , poxY = 0.0;
float sizeWidth = 100 , sizeHeight = 200;
float scrollContentWidth = 320.0;
scrollview.showsHorizontalScrollIndicator=YES;
scrollview.scrollEnabled=YES;
scrollview.userInteractionEnabled=YES;
self.scrollView.pagingEnabled = TRUE;
for(int i=0;i<[imageArray count];i++)
{
NSString *imageURL = [imageArray objectAtIndex:i];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(posX,posY,sizeWidth,sizeHeight)];
[imageView setImage:[UIImage imageWithContentsOfFile:imageURL]];
[self.scrollView addSubview:imageView];
[imageView addConstraint:
[NSLayoutConstraint constraintWithItem:imageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:imageView
attribute:NSLayoutAttributeWidth
multiplier:1
constant:100]];
[imageView addConstraint:
[NSLayoutConstraint constraintWithItem:imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:imageView
attribute:NSLayoutAttributeHeight
multiplier:1
constant:200]];
[self.scrollView setContentSize:CGSizeMake(480.0,scrollContentWidth)];
scrollContentWidth=scrollContentWidth+320.0;
posX = posX + 320.0;
}
}
Hope it will help you.