Episode #136

Clipping Paths

24 minutes
Published on September 11, 2014

This video is only available to subscribers. Get access to this video and 573 others.

In this episode we'll attempt to create the board for the game Connect Four. We'll leverage what we've learned about auto layout and create the connect four board constraints, then we'll draw the view. We have to draw it filled with a bunch of holes, so that we can see objects passing behind it. Using Core Graphics and clipping paths we can accomplish this effect.

Episode Links

Setting up the Layout of the Board

Our board needs to be set up in two places. First, the class itself for its minimum size and aspect ratio:

// GameBoardView.m
- (id)initWithRows:(NSInteger)rows columns:(NSInteger)columns {
    self = [super init];
    if (self) {
        self.rows = rows;
        self.columns = columns;
        self.translatesAutoresizingMaskIntoConstraints = NO;
        self.opaque = NO;
    }

    return self;
}

+ (BOOL)requiresConstraintBasedLayout {
    return YES;
}

- (void)updateConstraints {
    [super updateConstraints];

    CGFloat aspectRatio = self.columns / (CGFloat)self.rows;
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self
                                                     attribute:NSLayoutAttributeHeight
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:self
                                                     attribute:NSLayoutAttributeWidth
                                                    multiplier:1/aspectRatio
                                                      constant:0]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[self(%gt;=200)]"
                                                                options:0
                                                                metrics:nil
                                                                  views:NSDictionaryOfVariableBindings(self)]];
}

Note that we indicate that we require constraint based layout, and we make sure to turn off translatesAutoResizingMasksIntoConstraints.

Then in the containing view controller, we specify the rest of the constraints:

// ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    self.gameBoardView = [[GameBoardView alloc] initWithRows:6 columns:7];
    [self.view addSubview:self.gameBoardView];

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[_gameBoardView]-|"
                                                                      options:0
                                                                      metrics:nil
                                                                        views:NSDictionaryOfVariableBindings(_gameBoardView)]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.gameBoardView
                                                           attribute:NSLayoutAttributeCenterY
                                                           relatedBy:NSLayoutRelationEqual
                                                              toItem:self.view
                                                           attribute:NSLayoutAttributeCenterY
                                                          multiplier:1.0
                                                            constant:0]];

}

Drawing the Board with Holes

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);

    CGContextBeginPath(context);
    CGContextAddRect(context, self.bounds);

    CGRect innerRect = CGRectInset(self.bounds, PADDING, PADDING);
    CGFloat squareSize = innerRect.size.width / self.columns;
    CGContextTranslateCTM(context, PADDING, PADDING);
    for (int y = 0; y < self.rows; y++) {
        for (int x = 0; x < self.columns; x++) {
            CGFloat holeSize = squareSize - PADDING * 2;
            CGRect holeRect = CGRectMake(PADDING, PADDING, holeSize, holeSize);
            CGContextAddEllipseInRect(context, holeRect);
            CGContextTranslateCTM(context, squareSize, 0);
        }
        CGContextTranslateCTM(context, - innerRect.size.width, squareSize);
    }

    CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.822 green:0.822 blue:0.000 alpha:1.000].CGColor);
    CGContextEOFillPath(context);

    CGContextRestoreGState(context);
}

Here we use CGContextEOFillPath to fill our background but not the holes. Take a look at the links above for an explanation of how this works.

Dropping Pieces Behind the Board

Next we can test out our effect by adding a piece behind the board when the user taps on it and dropping it past the bottom of the screen:

  // viewDidLoad
    [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
                                                           action:@selector(spawnPiece:)]];


- (void)spawnPiece:(UITapGestureRecognizer *)tap {
    CGSize size = [self.gameBoardView pieceSize];
    CGPoint tapLocation = [tap locationInView:self.view];
    CGPoint point = CGPointMake(tapLocation.x - size.width / 2.0f, tapLocation.y - size.height / 2.0f);

    CGRect frame;
    frame.size = size;
    frame.origin = point;

    PieceView *piece = [[PieceView alloc] initWithFrame:frame pieceColor:PieceColorRed];
    [self.view insertSubview:piece belowSubview:self.gameBoardView];

    [UIView animateWithDuration:0.75
                          delay:0
                        options:UIViewAnimationOptionCurveEaseIn
                     animations:^{
                         CGRect newFrame = frame;
                         newFrame.origin.y = self.view.bounds.size.height;
                         piece.frame = newFrame;
                     } completion:^(BOOL finished) {
                         [piece removeFromSuperview];
                     }];
}