UIImageViews embed in UIScrollView in code w/ auto layout on - ios

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.

Related

iOS Objective C ScrollView

I am trying to implement a UIScrollView and load it with images from an array of images in Xcode using objective-C, each image in the UIScrollView must be full screen both in portrait and in the landscape mode.I have been able to make it work in portrait mode but not in landscape mode. It should be fullscreen in all iOS device sizes. Below is the code I have written so far. I have UIScrollView in my storyboard, a button and a label. Any answer or pointing to a tutorial that implements this will be appreciated. Thanks in advance.
CGRect screen = [[UIScreen mainScreen] bounds];
CGFloat widthInPixel = screen.size.width;
CGFloat heightInPixel = screen.size.height;
float increaseAmount = widthInPixel;
self.imageScrollView.contentMode = UIViewContentModeScaleAspectFit;
self.imageScrollView.pagingEnabled = YES;
[self.imageScrollView setAlwaysBounceVertical:NO];
[self.imageScrollView setAlwaysBounceHorizontal:NO];
imageViews = [[NSMutableArray alloc] init];
self.imageScrollView.clipsToBounds = YES;
NSInteger imageNumbers = [self.images count];
UIImageView *image;
for(NSInteger i = 0; i < imageNumbers; i++) {
CGFloat xOrigin = i * self.view.frame.size.width;
image = [[UIImageView alloc] initWithFrame:
CGRectMake(xOrigin, 0,
widthInPixel,
self.imageScrollView.frame.size.height)];
image.contentMode = UIViewContentModeScaleAspectFit;
image.clipsToBounds = YES;
image.image = self.images[i];
[image setAutoresizingMask:
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight];
[self.imageScrollView addSubview:image];
}
self.imageScrollView.contentSize = CGSizeMake(image.frame.size.width *
imageNumbers,
self.imageScrollView.frame.size.height);
You really should learn how to use auto-layout and constraints. Use your favorite search engine and search for ios auto layout tutorial ... you'll find plenty of material.
Edit:
Scroll offset is an inherent issue when rotating a scroll view with paging enabled. See the edit below for an implementation of viewWillTransitionToSize.
But, to give you an idea, this will do what you want, including auto-resizing on device rotation:
//
// ViewController.m
// ScrollingImages
//
// Created by Don Mag on 7/19/18.
//
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet UIScrollView *theScrollView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSArray *images = #[#"a", #"b", #"c", #"d", #"e"];
[_theScrollView setPagingEnabled:YES];
[_theScrollView setAlwaysBounceVertical:NO];
[_theScrollView setAlwaysBounceHorizontal:NO];
// we'll use this to hold the most recently added view
UIImageView *prevImageView = nil;
for (int i = 0; i < images.count; i++) {
// create an image view with named image from array
UIImageView *v = [[UIImageView alloc] initWithImage:[UIImage imageNamed:images[i]]];
// we want to use auto-layout
v.translatesAutoresizingMaskIntoConstraints = NO;
// we want aspect-fit
v.contentMode = UIViewContentModeScaleAspectFit;
// add it to the scroll view
[_theScrollView addSubview:v];
// set width and height constraints equal to the scroll view
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0] setActive:YES];
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0] setActive:YES];
if (i == 0) { // if it's the first image
// add top constraint
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0] setActive:YES];
// and leading constraint
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0] setActive:YES];
} else {
// constrain leading to previous image view trailing
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:prevImageView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0] setActive:YES];
// and top to previous image view top
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:prevImageView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0] setActive:YES];
}
if (i == images.count - 1) { // if it's the last image
// add trailing constraint
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0] setActive:YES];
// and bottom constraint
[[NSLayoutConstraint
constraintWithItem:v
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:_theScrollView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0] setActive:YES];
}
// reference to most recently added view
prevImageView = v;
}
}
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// execute before rotation
// get the "index" of the current image in the scroll view
NSUInteger idx = (unsigned)(_theScrollView.contentOffset.x / _theScrollView.frame.size.width);
[coordinator animateAlongsideTransition:^(id _Nonnull context) {
// execute during rotation
// update the scroll view's contentOffset, based on the "index"
self.theScrollView.contentOffset = CGPointMake(idx * self.theScrollView.frame.size.width, 0);
} completion:^(id _Nonnull context) {
// execute after rotation (if additional code wanted)
}];
}
#end
You can download a working example project here: https://github.com/DonMag/ScrollingImages

Using a custom UITableView containing an image view cell with UITableViewAutomaticDimension

In my new app, I am trying to adopt AutoLayout throughout. In one of my table views, I have an image view and a label. If I do this in my controller's viewDidLoad: method
self.tableView.rowHeight = UITableViewAutomaticDimension;
the row is not high enough to accommodate the image. (I have added fixed width and height constraints to the image view).
If I remove the statement, then the height of the cell is the height as set in IB.
Is there a method I have to implement in my UITableCell class to tell the AutoLayout system my minimum height requirement? Or did I do something wrong?
Well, I could never figure this out using IB. However, I did get success adding the constraints manually, so I thought it worth sharing my complete solution. (Remember: an image view, a label, dynamic text). I want my image to always be 80 by 80, no matter how big the content gets.
Enjoy.
-(id)initWithCoder:(NSCoder *)aDecoder {
if ( !(self = [super initWithCoder:aDecoder]) ) return nil;
// TODO: Add an imageview and populate it
_titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0,0,300,20)];
_titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
_titleLabel.numberOfLines = 0;
[self.contentView addSubview:_titleLabel];
_albumArtImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
[_albumArtImageView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.contentView addSubview:_albumArtImageView];
NSMutableArray* constraints = [NSMutableArray new];
UIView* contentView = self.contentView;
[_titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
// _titleLabel to contentView
[constraints addObject:[NSLayoutConstraint
constraintWithItem:_titleLabel
attribute:NSLayoutAttributeFirstBaseline
relatedBy:NSLayoutRelationEqual
toItem:contentView
attribute:NSLayoutAttributeTop
// as font grow, space from top grows
multiplier:1.8
constant:30]];
// similarly for bottom
[constraints addObject:[NSLayoutConstraint
constraintWithItem:contentView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:_titleLabel
attribute:NSLayoutAttributeLastBaseline
multiplier:1.3
constant:8]];
[constraints addObject:[NSLayoutConstraint
constraintWithItem:contentView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:nil
attribute:0
multiplier:1.0
constant:80]];
// Now the imageView
[constraints addObject:[NSLayoutConstraint
constraintWithItem:_albumArtImageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:0
multiplier:1.0
constant:80]];
[constraints addObject:[NSLayoutConstraint
constraintWithItem:_albumArtImageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:0
multiplier:1.0
constant:80]];
// for horizontal
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-2-[_albumArtImageView]-5-[_titleLabel]-15-|"
options:0 metrics:nil views:NSDictionaryOfVariableBindings(_titleLabel,_albumArtImageView) ]];
[self.contentView addConstraints:constraints];
return self;
}

iOS - Pure AutoLayout and UIScrollView not scrolling

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.

How to add constraints to items with random positions

I have an ios app that I add a variable number (between 2 and 10) labels to in randomly generated positions. It's all done programmatically. This is how the location of the labels is determined.
int width = self.view.frame.size.width - 200;
int height = self.view.frame.size.height - 200;
newFrame.origin.x = arc4random() % width;
newFrame.origin.y = 80 + arc4random() % (height-80);
All of the labels are added to an array, self.viewLabels, after they are created and added to the view, otherwise there's no permanent reference to them because they are created in a loop
while (numViews < (numLabels)){
CustomLabel *timer = [[CustomLabel alloc] init];;
....
It works fine, except when I turn the app to landscape view. Some of the labels disappear that were at the bottom of the portrait view. I'm looking into adding constraints programmatically, and I understand the first step is to add the elements that need to be constrained to this dictionary
NSDictionary *views = NSDictionaryOfVariableBindings(button, button2);
Since I only have reference to these labels in the array self.viewLabels, I'm trying to figure out if there's a way I can get the labels in that dictionary. I tried to use the iterator to create unique names for the labels
for (int i = 0; i < [self.viewLabels count]; i++){
CustomLabel * label[i] = self.viewLabels[i];
}
That doesn't work, and even if it did, I can't figure out how to add them to the dictionary. And even if I got them in a dictionary, how to add constraints to items that have random positions in the view?
Can you suggest a strategy I could use in this situation?
Update
If it's impossible to add constraints after I've randomly generated positions, is it possible to do something when I create the positions to ensure they will all be visible in both landscape and portrait?
Update 2- based on the first answer by #rdelmar, I've tried the code below (i.e. adding labels without frames and then adding constraints after they are added to the view). However none of the labels are appearing on screen. You can see how my code was before by the lines I've commented out. I had previously added labels in random locations...
while (numViews < (numLabels)){
CustomLabel *label = [[CustomLabel alloc] init];;
//
// label.frame = CGRectMake(0, 0, 150, 50); //removed the frame
label.text = #"blah";
// newFrame = label.frame;
// int width = self.view.frame.size.width - 200;
// int height = self.view.frame.size.height - 200;
// newFrame.origin.x = arc4random() % width;
// newFrame.origin.y = 80 + arc4random() % (height-80);
// label.frame = newFrame;
[label setFont:[UIFont systemFontOfSize:50]];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
tgr.numberOfTapsRequired = 1;
tgr.numberOfTouchesRequired = 1;
[label addGestureRecognizer:tgr];
label.userInteractionEnabled = YES;
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:label];
[self.gameClocks addObject: label];
numViews += 1;
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1
constant:0]];
Try thinking about this in a different way -- you don't add constraints to views with randomly generated positions, you create random constraints that result in the views having random positions. So, when you create the views, you don't give them any frame. You create the label, add it to the subview, then add the constraints. If you want the labels to be visible in both portrait and landscape, it would be best to use the multiplier rather than the constant values of the constraints so the position is relative to the size of the view (not a constant distance from some edge). To do this, you would use constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:, rather than the visual format language, so you don't have to worry about the views dictionary. When you use the multiplier, you have to use the right edge or the bottom edge of the superview, since those have non-zero values (while the top and left side do not).
After Edit:
This is one way to do it. I create random locations by passing in a random number between 0 and 1 to the multiplier coefficient. To keep the labels inside the view, I pin either the label's left side or right side depending on whether the multiplier value would result in the label being close to the left side or right side of the superview (same with top or bottom). I am also making the height and width of the label relative to the size of the view, so the labels are shorter but wider in landscape.
#interface ViewController ()
#property (strong,nonatomic) NSMutableArray *labelArray;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.labelArray = [NSMutableArray new];
while (self.labelArray.count <10) {
UILabel *label = [UILabel new];
label.backgroundColor = [UIColor orangeColor];
[self.view addSubview:label];
[self.labelArray addObject:label];
[self createConstraintsForRanomPositions:label];
}
for (int i = 0; i< self.labelArray.count; i++) {
[self.labelArray[i] setText:[NSString stringWithFormat:#"Label %d", i]];
}
}
-(void)createConstraintsForRanomPositions:(UIView *) view {
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
CGFloat rightMultiplier = arc4random_uniform(100)/ 100.0;
CGFloat bottomMultiplier = arc4random_uniform(100)/ 100.0;
NSLayoutConstraint *con1;
if (bottomMultiplier <= .2) {
con1 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:bottomMultiplier constant:0];
}else{
con1 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:bottomMultiplier constant:0];
}
NSLayoutConstraint *con2;
if (rightMultiplier <= .2) {
con2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:0 toItem:self.view attribute:NSLayoutAttributeRight multiplier:rightMultiplier constant:0];
}else{
con2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:0 toItem:self.view attribute:NSLayoutAttributeRight multiplier:rightMultiplier constant:0];
}
NSLayoutConstraint *con3 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth relatedBy:0 toItem:self.view attribute:NSLayoutAttributeWidth multiplier:.2 constant:0];
NSLayoutConstraint *con4 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:0 toItem:self.view attribute:NSLayoutAttributeHeight multiplier:.1 constant:0];
[self.view addConstraints:#[con1, con2, con3, con4]];
[self.view layoutIfNeeded]; // this is needed, otherwise the frames are all {{0,0}, {0,0}} in the following forloop
for (UIView *placedView in self.labelArray) { // rejects any label that overlaps with any other
if (![placedView isEqual:view] && CGRectIntersectsRect(CGRectInset(view.frame, -2, -2), placedView.frame)) {
[view removeFromSuperview];
[self.labelArray removeObject:view];
break;
}
}
}

Best way to center some images using iOS autolayout

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

Resources