Please note, this blog entry is from a previous course. You might want to check out the current one.
Add a section (or sections) to your UICollectionView to show “found matches.” In other words, the user can scroll down in the UICollectionView (below the game) and see all of the matches they’ve found so far in the current game. The actual cards should appear (perhaps miniaturized, perhaps not, up to you). You will likely want to create a new UICollectionViewCell with 2 or 3 instances of a custom UIView subclass (that you’ve already written) as subviews and maybe some nice adornment.
The following solution will add two additional sections to the collection views. One to show the found matches, and one for the title e.g. “Matches Found:” (which could actually be implemented as header as well) …
Create a new property in the generic view-controller class to hold the matched cards and initialize it lazily:
@property (strong, nonatomic) NSMutableArray *matchedCards; // of Card ... - (NSMutableArray *)matchedCards { if (!_matchedCards) { _matchedCards = [[NSMutableArray alloc] init]; } return _matchedCards; }
The array is filled after a card has been flipped an resulted in a match. To do this loop over all cards and check if is unplayable. At the same time create two arrays with indexes of positions in the collections view where cards should be deleted (from the fist section) or matched cards should bee added (to the third section). At the end perform a batch update on the collection view. There check also if the matched section (third section) was empty before and will now be filled. In that case add the title to the second section. Note that before the code checked only if the tap occurred on a valid cell (and did nothing when a space was tapped). Now it is necessary to limit valid taps also to the first section:
- (IBAction)flipCard:(UITapGestureRecognizer *)gesture { ... if (indexPath && (indexPath.section == 0)) { ... NSMutableArray *deleteIndexPaths = [[NSMutableArray alloc] init]; NSMutableArray *matchedIndexPaths = [[NSMutableArray alloc] init]; for (int i = self.game.numberOfCards - 1; i >= 0; i--) { Card *card = [self.game cardAtIndex:i]; if (card.isUnplayable) { if (![self.matchedCards containsObject:card]) { [matchedIndexPaths addObject:[NSIndexPath indexPathForItem:[self.matchedCards count] inSection:2]]; [self.matchedCards addObject:card]; } if (self.removeUnplayableCards) { [self.game removeCardAtIndex:i]; [deleteIndexPaths addObject:[NSIndexPath indexPathForItem:i inSection:0]]; } } } [self.cardCollectionView performBatchUpdates:^{ if ([deleteIndexPaths count]) { [self.cardCollectionView deleteItemsAtIndexPaths:deleteIndexPaths]; } if ([matchedIndexPaths count]) { [self.cardCollectionView insertItemsAtIndexPaths:matchedIndexPaths]; if ([self.matchedCards count] == self.numberOfMatchingCards) { [self.cardCollectionView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:1]]]; } } } completion:nil]; ... } }
The array needs to be deleted when the deal button gets pressed:
- (IBAction)dealButtonPressed:(UIButton *)sender { ... self.matchedCards = nil; ... }
The code for the data source of the collection code needs to be adjusted for the three sections. First set the number of sections to three. Note that it is possible to add sections later on but there can occur race conditions trying to add a section and later on animate the insertion of the first cell when your data source already knows about the first cell when it adds the section …
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 3; }
The number of cells per section needs now to be specific to the section. For the first section it equals to the number of “playable” cards. The second sections contains one or no cell depending on if there are matches to display. For the third section the number of cells equals to the number of matched cards:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (section == 2) return [self.matchedCards count]; if (section == 1) return [self.matchedCards count] ? 1 : 0; return self.game.numberOfCards; }
Filling a cell needs also depend on the section. Note that the updateCell: API method needs to be changed to add the section information in form of the index path. The first section stays unchained, the third uses the matched cards. The title section uses a new cell (drag it out in both game controllers in storyboard and do not forget to set their reuse identifers) and adds a label as title (which could also be done in storyboard):
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 2) { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"PlayingCard" forIndexPath:indexPath]; [self updateCell:cell usingCard:self.matchedCards[indexPath.item] atIndexPath:indexPath]; return cell; } if (indexPath.section == 1) { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"HeaderCell" forIndexPath:indexPath]; UILabel *textLabel = [[UILabel alloc] initWithFrame:cell.bounds]; textLabel.text = @"matched cards:"; textLabel.textColor = [UIColor blackColor]; textLabel.backgroundColor = [UIColor clearColor]; textLabel.font = [UIFont fontWithName:@"System Bold" size:20.0]; [cell addSubview:textLabel]; return cell; } UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"PlayingCard" forIndexPath:indexPath]; Card *card = [self.game cardAtIndex:indexPath.item]; [self updateCell:cell usingCard:card atIndexPath:indexPath]; return cell; }
As mentioned above the updateCell: methods needs to be changed slightly:
// GameViewController.h - (void)updateCell:(UICollectionViewCell *)cell usingCard:(Card *)card atIndexPath:(NSIndexPath *)indexPath; // GameViewController.m - (void)updateCell:(UICollectionViewCell *)cell usingCard:(Card *)card atIndexPath:(NSIndexPath *)indexPath { // abstract }
… and upateUI will use this changed API:
- (void)updateUI { for (UICollectionViewCell *cell in [self.cardCollectionView visibleCells]) { NSIndexPath *indexPath = [self.cardCollectionView indexPathForCell:cell]; if (indexPath.section == 2) { [self updateCell:cell usingCard:self.matchedCards[indexPath.item] atIndexPath:indexPath]; } else { [self updateCell:cell usingCard:[self.game cardAtIndex:indexPath.item] atIndexPath:indexPath]; } } ... }
One last change in the general view-controller class: Currently the size of the cells is taken from storyboard. We would like different sizes depending on the current section. Therefore we declare the class as delegate of the collection view:
@interface GameViewController () <..., UICollectionViewDelegateFlowLayout> ... - (void)viewDidLoad { ... self.cardCollectionView.delegate = self; ... }
The card-matching-game-view-controller class needs to adjust for the changed API. In the matched-cards section it will ignore the face-up setting and will always be face up and will not change the transparency:
- (void)updateCell:(UICollectionViewCell *)cell usingCard:(Card *)card atIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[PlayingCardCollectionViewCell class]]) { PlayingCardView *playingCardView = ((PlayingCardCollectionViewCell *)cell).playingCardView; if ([card isKindOfClass:[PlayingCard class]]) { ... playingCardView.faceUp = !indexPath.section ? playingCard.isFaceUp : YES; playingCardView.alpha = playingCard.isUnplayable && !indexPath.section ? 0.3 : 1.0; } } }
… and will change the size of the cells as well at its spacing depending on the section:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 2) return CGSizeMake(56, 72); if (indexPath.section == 1) return CGSizeMake(150, 20); return CGSizeMake(70, 90); } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { if (section == 1) return UIEdgeInsetsMake(10, 10, 0, 0); return UIEdgeInsetsMake(0, 0, 0, 0); }
The set-game-view-controller class will always be face down and also ignore the transparency in the matched-cards section:
- (void)updateCell:(UICollectionViewCell *)cell usingCard:(Card *)card atIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[SetCardCollectionViewCell class]]) { SetCardView *setCardView = ((SetCardCollectionViewCell *)cell).setCardView; if ([card isKindOfClass:[SetCard class]]) { ... setCardView.faceUp = !indexPath.section ? setCard.isFaceUp : NO; setCardView.alpha = setCard.isUnplayable && !indexPath.section ? 0.3 : 1.0; } } }
… and also changes the sizes and spacing of the cells:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 2) return CGSizeMake(40, 40); if (indexPath.section == 1) return CGSizeMake(150, 20); return CGSizeMake(65, 65); } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { if (section == 1) return UIEdgeInsetsMake(10, 10, 0, 0); return UIEdgeInsetsMake(10, 10, 10, 10); }
The complete code is available on github.