Friday, August 5, 2016

Rapid, Interactive Prototyping With Xcode Playgrounds_part1


Introduction

Since their introduction in Xcode 6 alongside Swift, to their current iteration in Xcode 7.3.1, playgrounds have come a long way. With new features and better stability, they are evolving into a viable tool for rapid prototyping or quickly hacking together a proof of concept.

As a developer, sometimes you have a flash of inspiration in the form of an interesting idea for an app and you want to quickly code up a prototype that represents the bare essence of your idea. Or you just want to verify your understanding of how some piece of UIKit code will behave. If you are like me, you would rather avoid the hassle and mental overhead of creating an Xcode project and having to deal with a myriad of factors, such as device types and resolutions, and build settings. These decisions can be deferred until after you have made up your mind that the core idea is worth pursuing.

In this tutorial, we create a card-based memory game all within the confines of a playground. It is a common, well-known game, so no credit for originality there. The game consists of eight pairs of identical cards (so a total of 16 cards) placed face down in a 4x4 grid.

The player needs to flip two cards whose faces are briefly revealed and then quickly turned back over. The objective of the game is for the player to try and remember the positions of the cards and uncover identical pairs, which are then removed from the game. The game is over when the grid is cleared.

The game is touch-based and incorporates simple view animations. You learn how you can make modifications to your app and see the result of your changes live.

1. Getting Started

Fire up Xcode and select New > Playground... from Xcode's File menu. Give the playground a name, such as MemoryGameXCPTut, set Platform to iOS, and save the playground. I am using Xcode 7.3.1 for this tutorial.


Finding Your Way Around the Playground

Let's spend some time familiarizing ourselves with the playground interface. Feel free to skim this section if you are already familiar with playgrounds.

A playground can have multiple pages, each associated with its own live view and its own sources/resources folders. We won't be using multiple pages in this tutorial. Playgrounds support markup formatting that allows you to add rich text to a playground and link between playground pages.

The first thing you see after creating a playground is the playground source editor. This is where you write code, which has an immediate effect on the live view. One of the ways to toggle the (dis)appearance of the Project Navigator is using the shortcut Command-0. In the Project Navigator, you can see two folders, Sources and Resources.

Sources

In the Sources folder, you can add auxiliary code in one or more Swift files, such as custom classes, view controllers, and views. Even though the bulk of the code that defines your prototype's logic goes there, it is auxiliary in the sense that it is tucked in the background when you are viewing your app live.

The advantage of putting the auxiliary code in the Sources folder is that it is automatically compiled every time you modify and save the file. This way, you get faster feedback in the live view from changes made in the playground.  Back in the playground, you are able to access public properties and methods that you expose in the auxiliary code affecting how your app behaves.

Resources

You can add external resources, such as images, in the Resources folder.
In this tutorial, you frequently need to jump between a Swift file we create in the Sources folder and the playground file (technically also a Swift file, except you won't refer to it by its file name). We also make use of the Assistant Editor in the tutorial, having it display the Timeline, to view the live output side-by-side with the playground code. Any changes you make in the playground are reflected instantly (well, within a few seconds) in the live output. You are also able to touch-interact with the live view and its user interface elements. To ensure you can do all this, take a quick glance at the figure below.


Corresponding to the green numbers I have added to the figure:
  1. This button hides the Assistant Editor so that only the main editor is visible.
  2. This button reveals the Assistant Editor. The Assistant Editor is visible on the right of the main editor. This editor can help by showing us relevant files, such as the counterpart of the file in the main editor.
  3. From left to right, these two buttons are respectively used to toggle the appearance of the Project Navigator and the debug console. In the console, we can inspect the output of print statements among other things.
  4. The jump bar at the top of the main editor can also be used to navigate to a particular file. Clicking the project name twice brings you back to the playground. Alternatively, you can also use the Project Navigator.
Sometimes, when viewing the playground, you need to ensure that the Assistant Editor is displaying the Timeline instead of some other file. The below figure shows how to do this. In the Assistant Editor, select Timeline, the counterpart of the playground, instead of Manual, which allows you to show any file in the Assistant Editor.



When you are editing a source file from the Sources folder, as its counterpart, the Assistant Editor shows the interface of your code, that is, declarations and function prototypes without their implementations. I prefer to hide the Assistant Editor when I am working on a file in the Sources folder and only expose the Assistant Editor in the playground to see the live view.

To access the special abilities of playgrounds, you need to import the XCPlayground module.
  1. import XCPlayground
You set the liveView property of the currentPage of the XCPlaygroundPage object to an object that conforms to the  XCPlaygroundLiveViewable protocol. This can be a custom class or it can be a UIView or UIViewController instance.

Adding Files to the Sources/Resources Folder

I have added a few images we can work with in this tutorial. Download the images, extract the archive, and add the images in the Images folder to the Resources folder of the playground in the Project Navigator.

Make sure to drag just the images so that each image file resides in the Resources folder, not in Resources/Images.

Delete the code in the playground. Right-click the Sources folder and select New File from the menu. Set the name of the file to Game.swift.


2. Writing Helper Classes & Methods

Add the following code to Game.swift. Make sure you save the file after every code addition.
  1. import UIKit
  2. import XCPlayground
  3. import GameplayKit // (1)
  4.  
  5. public extension UIImage { // (2)
  6.     public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
  7.         let rect = CGRect(origin: .zero, size: size)
  8.         UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
  9.         color.setFill()
  10.         UIRectFill(rect)
  11.         let image = UIGraphicsGetImageFromCurrentImageContext()
  12.         UIGraphicsEndImageContext()
  13.          
  14.         guard let cgImage = image.CGImage else { return nil }
  15.         self.init(CGImage: cgImage)
  16.     }
  17. }
  18.  
  19. let cardWidth = CGFloat(120) // (3)
  20. let cardHeight = CGFloat(141)
  21.  
  22. public class Card: UIImageView { // (4)
  23.     public let x: Int
  24.     public let y: Int
  25.     public init(image: UIImage?, x: Int, y: Int) {
  26.         self.x = x
  27.         self.y = y
  28.         super.init(image: image)
  29.         self.backgroundColor = .grayColor()
  30.         self.layer.cornerRadius = 10.0
  31.         self.userInteractionEnabled = true
  32.     }
  33.      
  34.     required public init?(coder aDecoder: NSCoder) {
  35.         fatalError("init(coder:) has not been implemented")
  36.     }
  37. }
I have added a few numbered comments to explain some sections of the implementation:
  1. In addition to UIKit and XCPlayground, we are also importing GamePlayKit. This framework includes a convenient method that will help us implement a method to randomly shuffle an array.
  2. This extension on UIImage allows us, with the help of UIKit methods, to make images with a solid color of any size we want. We will use this to set the initial background image of the playing cards.
  3. The cardHeight and cardWidth constants represent the card image sizes based on which we will compute other sizes.
  4. The Card class, inheriting from UIImageView, represents a card. Even though we set a few properties in the Card class, the main purpose of creating this class is to help us identify and iterate over the subviews that correspond to playing cards in the game. The cards also have properties x and y to remember their position in the grid.
3. View Controller

Add the following code to Game.swift, immediately after the previous code:
  1. public class GameController: UIViewController {  
  2.     // (1): public variables so we can manipulate them in the playground
  3.     public var padding = CGFloat(20)/* {
  4.         didSet {
  5.             resetGrid()
  6.         }
  7.     } */   
  8.     public var backImage: UIImage = UIImage(
  9.         color: .redColor(),
  10.         size: CGSize(width: cardWidth, height: cardHeight))!     
  11.     // (2): computed properties
  12.     var viewWidth: CGFloat {
  13.         get {
  14.             return 4 * cardWidth + 5 * padding
  15.         }
  16.     }     
  17.     var viewHeight: CGFloat {
  18.         get {
  19.             return 4 * cardHeight + 5 * padding
  20.         }
  21.     }     
  22.     var shuffledNumbers = [Int]() // stores shuffled card numbers     
  23.     // var firstCard: Card? // uncomment later       
  24.     public init() {
  25.         super.init(nibName: nil, bundle: nil)
  26.         preferredContentSize = CGSize(width: viewWidth, height: viewHeight)
  27.         shuffle()
  28.         setupGrid()
  29.         // uncomment later:
  30.         // let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:)))
  31.         // view.addGestureRecognizer(tap)              
  32.     }     
  33.     required public init?(coder aDecoder: NSCoder) {
  34.         fatalError("init(coder:) has not been implemented")
  35.     }
  36.      
  37.     public override func loadView() {
  38.         view = UIView()
  39.         view.backgroundColor = .blueColor()
  40.         view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
  41.     }     
  42.     // (3): Using GameplayKit API to generate a shuffling of the array [1, 1, 2, 2, ..., 8, 8]
  43.     func shuffle() {
  44.         let numbers = (1...8).flatMap{[$0, $0]}
  45.         shuffledNumbers =
  46.             GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(numbers) as! [Int]         
  47.     }     
  48.     // (4): Convert from card position on grid to index in the shuffled card numbers array
  49.     func cardNumberAt(x: Int, _ y: Int) -> Int {
  50.         assert(0 <= x && x < 4 && 0 <= y && y < 4)         
  51.         return shuffledNumbers[4 * x + y]
  52.     }
  53.     // (5): Position of card's center in superview
  54.     func centerOfCardAt(x: Int, _ y: Int) -> CGPoint {
  55.         assert(0 <= x && x < 4 && 0 <= y && y < 4)
  56.         let (w, h) = (cardWidth + padding, cardHeight + padding)
  57.         return CGPoint(
  58.             x: CGFloat(x) * w + w/2 + padding/2,
  59.             y: CGFloat(y) * h + h/2 + padding/2)       
  60.     }     
  61.     // (6): setup the subviews
  62.     func setupGrid() {
  63.         for i in 0..<4 {
  64.             for j in 0..<4 {
  65.                 let n = cardNumberAt(i, j)
  66.                 let card = Card(image: UIImage(named: String(n)), x: i, y: j)
  67.                 card.tag = n
  68.                 card.center = centerOfCardAt(i, j)
  69.                 view.addSubview(card)
  70.             }
  71.         }
  72.     }     
  73.     // (7): reset grid
  74. /*
  75.     func resetGrid() {
  76.         view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
  77.         for v in view.subviews {
  78.             if let card = v as? Card {
  79.                 card.center = centerOfCardAt(card.x, card.y)
  80.             }
  81.         }         
  82.     }
  83. */   
  84.     override public func viewDidAppear(animated: Bool) {
  85.         for v in view.subviews {
  86.             if let card = v as? Card {     // (8): failable casting
  87.                 UIView.transitionWithView(
  88.                     card,
  89.                     duration: 1.0,
  90.                     options: .TransitionFlipFromLeft,
  91.                     animations: {
  92.                         card.image =  self.backImage
  93.                     }, completion: nil)
  94.             }
  95.         }
  96.     }
  97. }
  1. The two properties, padding and backImage, are declared public so that we can access them in the playground later. They represent the blank space surrounding the cards on the grid and the image displayed on the back of each card respectively. Note that both properties have been given initial values, representing a padding of 20 and a solid red color for the card's non-face side image. You can ignore the commented-out code for now.
  2. We are calculating the desired width and height of the views by means of computed properties. To understand the viewWidth calculation, remember that there are four cards in each row and we also need to take the padding of each card into account. The same idea applies to the viewHeight calculation.
  3. The code (1...8).flatMap{[$0, $0]} is a concise way of producing the array [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8]. If you aren't familiar with functional programming, you could also write a for-loop to generate the array. Using methods from the GamePlayKit framework, we shuffle the numbers in the array.  The numbers correspond to the eight pairs of cards. Each number represents the card image of the same name (for example, a value of 1 in shuffledArray corresponds to 1.png).
  4. We wrote a method that maps the location of a card on the 4x4 grid to its location in the shuffledNumbers array of length 16. The factor 4 in the arithmetic calculation reflects the fact that we have four cards per row.
  5. We also have a method that figures out the position of a card (its center property) in the grid based on card dimensions and padding.
  6. The setupGrid() method is called during the view controller's initialization. It lays out the 4x4 Card grid. It also assigns each card's identity based on the shuffledNumbers array and stores it in the tag property inherited from the card's base class, UIView. In the game logic, we compare the tag values to figure out whether two cards match or not. This rather rudimentary modeling scheme serves well enough for our current needs.
  7. This currently unused piece of code will help us reposition the cards in case the padding changes. Remember that we declared the padding property as a public property so we can access it in the playground.
  8. The code in viewDidAppear(_:) runs immediately after the view controller's view becomes visible. We iterate through the view's subviews and, if the subview is an instance of the Card class, (checked through the as? failable downcasting operator) the body of the if-statement defines the transition to perform. This is where we will change the image being displayed on the cards, from the cartoon image defining the face of each card to the (common) backImage of all the cards. This transition is accompanied by a left-to-right flip animation giving the appearance of the cards being physically turned over. If you aren't familiar with how UIView animations work, this may look a bit odd. Even though we added each card's animation sequentially in a loop, the animations are batched into a single animation transaction and executed concurrently, that is, the cards flip together.
Revisit the playground and replace any text in the editor with the following:
  1. import XCPlayground
  2. import UIKit
  3. let gc = GameController()
  4. XCPlaygroundPage.currentPage.liveView = gc
Make sure the timeline is visible. The view controller's view should spring to life and show us a 4x4 grid of cards with cute cartoons animals that flip over to show us the back of the cards. Right now, we can't do much with this view because we haven't programmed any interaction into it yet. But it's definitely a start.

Written by Akiel Khan
If you found this post interesting, follow and support us.
Suggest for you:

iOS 10 Message Extension App Reskinning without Coding

iOS 10 Projects: Build Amazing Apps with Apple's Newest iOS

iOS 10 & Swift 3: From Beginner to Paid Professional

The Complete iOS 10 Developer - Build Real Apps with Swift 3

Swift 3 and iOS 10 The Final Course Learn to Code like a Pro

Master iOS 9 - 10 - Xcode 8 and Swift 3


No comments:

Post a Comment