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’sframe
. - 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 theUIDynamicAnimator
class and aUISnapBehavior
. - 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
andUISnapBehavior
classes. - The flipping over of cards. This animation was written with the transition API in the
UIView
class.
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.
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() } }
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) } }
/// 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.