Animated Set (CS193p fall of 2017, assignment IV solution)

This project is comprised of two parts: A Set game and a Concentration game, both presented by a UITabBarViewController. The Set game written in this app is a game based on the Set one, which is a popular card game (I’ve used this video to get its business logic). The Concentration is a simple game based on matches of card pairs, which are then removed from the table. All these projects were developed based on the previous assignments (I, II and III).

The fourth assignment required the student to add animations to the following events of the Set game:

  • All cards are smoothly rearranged in the grid, this is done using UIViewPropertyAnimator to animate each card’s frame.
  • All cards are dealt one by one at the beginning of the game and after the discovery of a match. Each card moves from the deck to its appropriate position in the cards grid. As per the requirements, two cards mustn’t be dealt at the same time. The animation uses UIViewPropertyAnimator combined with the UIDynamicAnimator class and a UISnapBehavior.
  • In the case of a match, cards are removed from the grid and put into the matched deck. This removal animation was also written using the UIViewPropertyAnimator, UIDynamicAnimator and UISnapBehavior classes.
  • The flipping over of cards. This animation was written with the transition API in the UIView class.
The usage of a UITabBarController to embed the Set and Concentration controllers was also required, and the Concentration tab also had to use a UISplitViewController, with a theme chooser as the master controller and the game’s controller as the detail one.
 Assignment IV project storyboard

Main challenges

I had some troubles before getting the deal animation working fine, it conflicted with the rearrangement of cards, which uses a UIViewPropertyAnimator to animate each card’s frame to its correct position in the grid. This happened mainly because of both animations modifying the same properties at the same time:

override func layoutSubviews() {
  super.layoutSubviews()
    
  // Only updates the buttons frames if the centered rect has changed,
  // This will occur when orientation changes.
  // This check will prevent frame changes while
  // the dynamic animator is doing it's job.
  if grid.frame != gridRect {
    updateViewsFrames()
  }
}

The deal animation can be triggered by the addition of new cards to the container and can also be independently called by using the dealCardsWithAnimation method in the container view:

/// Adds new buttons to the UI.
/// - Parameter byAmount: The number of buttons to be added.
/// - Parameter animated: Bool indicating if the addition should be animated.
func addButtons(byAmount numberOfButtons: Int = 3, animated: Bool = false) {
  guard isPerformingDealAnimation == false else { return }
    
  let cardButtons = makeButtons(byAmount: numberOfButtons)
    
  for button in cardButtons {
    // Each button is hidden and face down by default.
    button.alpha = 0
    button.isFaceUp = false
      
    addSubview(button)
    buttons.append(button)
  }
    
  grid.cellCount += cardButtons.count
  grid.frame = gridRect
    
  if animated {
    dealCardsWithAnimation()
  }
}
I also had troubles with the device orientation changes while the cards were being dealt. In order to solve this issue, I had to cancel any running animations and rearrange the cards after the transition was done. In the controllers I had the following code:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  // In case the controller is still being presented and the
  // views haven't been instantiated.
  guard containerView != nil else { return }
    
  coordinator.animate(alongsideTransition: { _ in
    self.containerView.prepareForRotation()
  }) { _ in
    self.containerView.updateViewsFrames(withAnimation: true)
  }
}
 And in the container view:
/// Prepares the container for the device's rotation event.
/// Stops any running deal animations and respositions all the views.
func prepareForRotation() {
  animator.removeAllBehaviors()
    
  // Invalidates all scheduled deal animations.
  scheduledDealAnimations?.forEach { timer in
    if timer.isValid {
      timer.invalidate()
    }
  }
    
  positioningAnimator?.stopAnimation(true)
    
  for button in buttons {
    button.transform = .identity
    button.setNeedsDisplay()
  }
    
  isPerformingDealAnimation = false
}

Improvements

There’s some code that still needs refactoring. Also, none of the extra credit requirements regarding the Set game were executed, these are going to be my next steps.

The source code for this solution can be found in my Github repository.

Leave a comment