Cocorico
Cocorico

Reputation: 2347

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?

Upvotes: 5

Views: 3899

Answers (3)

Rob
Rob

Reputation: 438467

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

}

Upvotes: 0

Rob
Rob

Reputation: 438467

Three approaches leap out at me:

  1. 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]];
    
  2. 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)]|"
    
  3. 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.

Upvotes: 4

memmons
memmons

Reputation: 40502

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

Upvotes: 0

Related Questions