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 4×4 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:
- This button hides the Assistant Editor so that only the main editor is visible.
- 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.
- 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.
- 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.
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.
import UIKit import XCPlayground import GameplayKit // (1) public extension UIImage { // (2) public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image.CGImage else { return nil } self.init(CGImage: cgImage) } } let cardWidth = CGFloat(120) // (3) let cardHeight = CGFloat(141) public class Card: UIImageView { // (4) public let x: Int public let y: Int public init(image: UIImage?, x: Int, y: Int) { self.x = x self.y = y super.init(image: image) self.backgroundColor = .grayColor() self.layer.cornerRadius = 10.0 self.userInteractionEnabled = true } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
I have added a few numbered comments to explain some sections of the implementation:
- In addition to
UIKit
andXCPlayground
, we are also importingGamePlayKit
. This framework includes a convenient method that will help us implement a method to randomly shuffle an array. - This extension on
UIImage
allows us, with the help ofUIKit
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. - The
cardHeight
andcardWidth
constants represent the card image sizes based on which we will compute other sizes. - The
Card
class, inheriting fromUIImageView
, represents a card. Even though we set a few properties in theCard
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 propertiesx
andy
to remember their position in the grid.
3. View Controller
Add the following code to Game.swift, immediately after the previous code:
public class GameController: UIViewController { // (1): public variables so we can manipulate them in the playground public var padding = CGFloat(20)/* { didSet { resetGrid() } } */ public var backImage: UIImage = UIImage( color: .redColor(), size: CGSize(width: cardWidth, height: cardHeight))! // (2): computed properties var viewWidth: CGFloat { get { return 4 * cardWidth + 5 * padding } } var viewHeight: CGFloat { get { return 4 * cardHeight + 5 * padding } } var shuffledNumbers = [Int]() // stores shuffled card numbers // var firstCard: Card? // uncomment later public init() { super.init(nibName: nil, bundle: nil) preferredContentSize = CGSize(width: viewWidth, height: viewHeight) shuffle() setupGrid() // uncomment later: // let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:))) // view.addGestureRecognizer(tap) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func loadView() { view = UIView() view.backgroundColor = .blueColor() view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) } // (3): Using GameplayKit API to generate a shuffling of the array [1, 1, 2, 2, ..., 8, 8] func shuffle() { let numbers = (1...8).flatMap{[$0, $0]} shuffledNumbers = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(numbers) as! [Int] } // (4): Convert from card position on grid to index in the shuffled card numbers array func cardNumberAt(x: Int, _ y: Int) -> Int { assert(0 <= x && x < 4 && 0 <= y && y < 4) return shuffledNumbers[4 * x + y] } // (5): Position of card's center in superview func centerOfCardAt(x: Int, _ y: Int) -> CGPoint { assert(0 <= x && x < 4 && 0 <= y && y < 4) let (w, h) = (cardWidth + padding, cardHeight + padding) return CGPoint( x: CGFloat(x) * w + w/2 + padding/2, y: CGFloat(y) * h + h/2 + padding/2) } // (6): setup the subviews func setupGrid() { for i in 0..<4 { for j in 0..<4 { let n = cardNumberAt(i, j) let card = Card(image: UIImage(named: String(n)), x: i, y: j) card.tag = n card.center = centerOfCardAt(i, j) view.addSubview(card) } } } // (7): reset grid /* func resetGrid() { view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) for v in view.subviews { if let card = v as? Card { card.center = centerOfCardAt(card.x, card.y) } } } */ override public func viewDidAppear(animated: Bool) { for v in view.subviews { if let card = v as? Card { // (8): failable casting UIView.transitionWithView( card, duration: 1.0, options: .TransitionFlipFromLeft, animations: { card.image = self.backImage }, completion: nil) } } } }
- The two properties,
padding
andbackImage
, are declaredpublic
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. - 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 theviewHeight
calculation. - 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 afor
-loop to generate the array. Using methods from theGamePlayKit
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 of1
inshuffledArray
corresponds to 1.png). - We wrote a method that maps the location of a card on the 4×4 grid to its location in the
shuffledNumbers
array of length 16. The factor4
in the arithmetic calculation reflects the fact that we have four cards per row. - 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. - The
setupGrid()
method is called during the view controller’s initialization. It lays out the 4×4Card
grid. It also assigns each card’s identity based on theshuffledNumbers
array and stores it in thetag
property inherited from the card’s base class,UIView
. In the game logic, we compare thetag
values to figure out whether two cards match or not. This rather rudimentary modeling scheme serves well enough for our current needs. - 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. - 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 theCard
class, (checked through theas?
failable downcasting operator) the body of theif
-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 howUIView
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:
import XCPlayground import UIKit let gc = GameController() XCPlaygroundPage.currentPage.liveView = gc
Make sure the timeline is visible. The view controller’s view should spring to life and show us a 4×4 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.
4. Modifying Variables in the Playground
Let’s now change the back faces of the cards from solid red to an image, specifically b.png in the Resources folder. Add the following line to the bottom of the playground.
gc.backImage = UIImage(named: "b")!
After a second or two, you’ll see that the back sides of the cards have changed from plain red to a cartoon hand.
Let’s now try to alter the padding
property, which we assigned a default value of 20 in Game.swift. The space between the cards should increase as a result. Add the following line to the bottom of the playground:
gc.padding = 75
Wait for the live view to refresh and see that … nothing has changed.
5. A Brief Detour
To understand what is going on, you have to keep in mind that entities, such as view controllers and their associated views, have a complex life cycle. We are going to focus on the latter, that is, views. Creating and updating of a view controller’s view is a multistage process. At specific points in the life cycle of the view, notifications are issued to the UIViewController
, informing it of what is going on. More importantly, the programmer can hook into these notifications by inserting code to direct and customize this process.
The loadView()
and viewDidAppear(_:)
methods are two methods we used to hook into the view life cycle. This topic is somewhat involved and beyond the scope of this discussion, but what matters to us is that the code in the playground, after the assignment of the view controller as the playground’s liveView
, is executed some time between the call to viewWillAppear(_:)
and the call to viewDidAppear(_:)
. You can verify this by modifying some property in the playground and add print statements to these two methods to display the value of this property.
The issue with the value of padding
not having the expected visual effect is that, by that time, the view and its subviews have already been laid out. Keep in mind that, whenever you make a change to the code, the playground is rerun from the beginning. In that sense, this issue isn’t specific to playgrounds. Even if you were developing code to run on the simulator or on a physical device, often times you would need to write additional code to ensure that the change in a property’s value has the desired effect on the view’s appearance or content.
You might ask why we were able to change the value of the backImage
property and see the result without doing anything special. Observe that the backImage
property is actually used for the first time in viewDidAppear(_:)
, by which time it has already picked up its new value.
6. Observing Properties and Taking Action
Our way to deal with this situation will be to monitor changes to the value of padding
and resize/reposition the view and subviews. Fortunately, this is easy to do with Swift’s handy property observing feature. Start by uncommenting the code for the resetGrid()
method in Game.swift:
// (7): reset grid func resetGrid() { view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) for v in view.subviews { if let card = v as? Card { card.center = centerOfCardAt(card.x, card.y) } } }
This method recomputes the position of the view’s frame and that of each Card
object based on the new values of viewWidth
and viewHeight
. Recall that these properties are computed based on the value of padding
, which has just been modified.
Also, modify the code for padding
to use the didSet
observer whose body, as the name indicates, executes whenever we set the value of padding
:
// (1): public variables so we can manipulate them in the playground public var padding = CGFloat(20) { didSet { resetGrid() } }
The resetGrid()
method kicks in and the view is refreshed to reflect the new spacing. You can verify this in the playground.
It appears we were able to fix things quite easily. In reality, when I first decided I wanted to be able to interact with the padding
property, I had to go back and make changes to the code in Game.swift. For example, I had to abstract out the Card
center calculation in a separate function (centerOfCardAt(_:_:)
) to cleanly and independently (re)compute the positions of the cards whenever they needed to be laid out.
Making computed properties for viewWidth
and viewHeight
also helped. While this kind of rewrite is something you should be prepared for as a trade-off of not doing much upfront design, it can be reduced with some forethought and experience.
7. Game Logic & Touch Interaction
It is now time to implement the game’s logic and enable ourselves to
interact with it through touch. Begin by uncommenting the firstCard
property declaration in the GameController
class:
var firstCard: Card?
Recall that the logic of the game involves revealing two cards, one after the other. This variable keeps track of whether a card flip performed by the player is the first of the two or not.
Add the following method to the bottom
of the GameController
class, before the terminating curly brace:
func handleTap(gr: UITapGestureRecognizer) { let v = view.hitTest(gr.locationInView(view), withEvent: nil)! if let card = v as? Card { UIView.transitionWithView( card, duration: 0.5, options: .TransitionFlipFromLeft, animations: {card.image = UIImage(named: String(card.tag))}) { // trailing completion handler: _ in card.userInteractionEnabled = false if let pCard = self.firstCard { if pCard.tag == card.tag { UIView.animateWithDuration( 0.5, animations: {card.alpha = 0.0}, completion: {_ in card.removeFromSuperview()}) UIView.animateWithDuration( 0.5, animations: {pCard.alpha = 0.0}, completion: {_ in pCard.removeFromSuperview()}) } else { UIView.transitionWithView( card, duration: 0.5, options: .TransitionFlipFromLeft, animations: {card.image = self.backImage}) { _ in card.userInteractionEnabled = true } UIView.transitionWithView( pCard, duration: 0.5, options: .TransitionFlipFromLeft, animations: {pCard.image = self.backImage}) { _ in pCard.userInteractionEnabled = true } } self.firstCard = nil } else { self.firstCard = card } } } }
That is a lengthy method. That is because it packs all the required touch handling, game logic as well as associated animations in one method. Let’s see how this method does its work:
- First, there is a check to ensure that the user actually touched a
Card
instance. This is the sameas?
construct that we used earlier. - If the user did touch a
Card
instance, we flip it over using an animation similar to the one we implemented earlier. The only new aspect is that we use the completion handler, which executes after the animation completes, to temporarily disable touch interactions for that particular card by setting theuserInteractionEnabled
property of the card. This prevents the player from flipping over the same card. Note the_ in
construct that is used several times in this method. This is just to say that we want to ignore theBool
parameter that the completion handler takes. - We execute code based on whether the
firstCard
has been assigned a non-nil value using optional binding, Swift’s familiarif let
construct. - If
firstCard
is non-nil, then this was the second card of the sequence that the player turned over. We now need to compare the face of this card with the previous one (by comparing thetag
values) to see whether we got a match or not. If we did, we animate the cards fading out (by setting theiralpha
to 0). We also remove these cards from the view. If the tags are not equal, meaning the cards don’t match, we simply flip them back facing down and setuserInteractionEnabled
totrue
so that the user can select them again. - Based on the current value of
firstCard
, we set it to eithernil
or to the present card. This is how we switch the code’s behavior between two successive touches.
Finally, uncomment the following two statements in the GameController
‘s initializer that adds a tap gesture recognizer to the view. When the tap gesture recognizer detects a tap, the handleTap()
method is invoked:
let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:))) view.addGestureRecognizer(tap)
Head back to the playground’s timeline and play the memory game. Feel free to decrease the large padding
we assigned a bit earlier.
The code in handleTap(_:)
is pretty much the unembellished version of what I wrote the first time. One might raise the objection that, as a single method, it does too much. Or that the code isn’t object-oriented enough and that the card flipping logic and animations should be neatly abstracted away into methods of the Card
class. While these objections aren’t invalid per se, remember that quick prototyping is the focus of this tutorial and since we did not foresee any need to interact with this part of the code in the playground, we could afford to be a bit more “hack-ish”.
Once we have something working and we decide we want to pursue the idea further, we would certainly have to give consideration to code refactoring. In other words, first make it work, then make it fast/elegant/pretty/…
8. Touch Handling In the Playground
While the main part of the tutorial is now over, as an interesting aside, I want to show you how we can write touch handling code directly in the playground. We will first add a method to the GameController
class that allows us to peek at the faces of the cards. Add the following code to the GameController
class, immediately after the handleTap(_:)
method:
public func quickPeek() { for v in view.subviews { if let card = v as? Card { card.userInteractionEnabled = false UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image = UIImage(named: String(card.tag))}) { _ in UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image = self.backImage}) { _ in card.userInteractionEnabled = true } } } } }
Suppose we want the ability to activate or deactivate this “quick peek” feature from within the playground. One way to do this would be to create a public Bool
property in the GameController
class that we could set in the playground. And of course, we would have to write a gesture handler in the GameController
class, activated by a different gesture, that would invoke quickPeek()
.
Another way would be to write the gesture handling code directly in the playground. An advantage of doing it this way is that we could incorporate some custom code in addition to calling quickPeek()
. This is what we will do next. Add the following code to the bottom of the playground:
class LPGR { static var counter = 0 @objc static func longPressed(lp: UILongPressGestureRecognizer) { if lp.state == .Began { gc.quickPeek() counter += 1 print("You peeked (counter) time(s).") } } } let longPress = UILongPressGestureRecognizer(target: LPGR.self, action: #selector(LPGR.longPressed)) longPress.minimumPressDuration = 2.0 gc.view.addGestureRecognizer(longPress)
To activate the quick peek feature, we will use a long press gesture, that is, the player holds their finger on the screen for a certain amount of time. We use two seconds as the threshold.
For handling the gesture, we create a class, LPGR
(long press gesture recognizer abbreviated), with a static
variable property, counter
, to keep track of how many times we peeked, and a static
method longPressed(_:)
to handle the gesture.
By using the static
qualifier, we can avoid having to create an LPGR
instance because the entities declared static are associated with the LPGR
type (class) rather than with a particular instance.
Apart from that, there is no particular advantage to this approach. For complicated reasons, we need to mark the method as @objc
to keep the compiler happy. Note the use of LPGR.self
to refer to the type of the object. Also note that in the gesture handler, we check if the state
of the gesture is .Began
. This is because the long press gesture is continuous, that is, the handler would execute repeatedly as long as the user kept their finger on the screen. Since we only want the code to execute once per finger press, we do it when the gesture is first recognized.
The incrementing counter is the custom code that we introduced, which doesn’t rely on functionality provided by the GameController
class. You can view the output of the print(_:)
function (after having peeked a couple of times) in the console at the bottom.
Conclusion
Hopefully, this tutorial has demonstrated an interesting example of rapid, interactive prototyping in Xcode playgrounds. Apart from the reasons for using playgrounds I mentioned earlier, you could think up other scenarios where they could be useful. For instance:
- demonstrating prototype functionality to clients and letting them choose options and make customizations with live feedback and without having to dig into the nitty-gritty details of the code.
- developing simulations, such as for physics, where students can play with some parameter values and observe how the simulation is affected. In fact, Apple has released an impressive playground that showcases their interactivity and the incorporation of physics via the
UIDynamics
API. I encourage you to check it out.
When using playgrounds for demonstration/teaching purposes such as these, you will probably want to make liberal use of the markup capabilities of playgrounds for rich text and navigation.
The Xcode team seems committed to improving playgrounds as new versions of the IDE are rolled out. The breaking news is that Xcode 8, currently in beta, will feature playgrounds for iPad. But, obviously, playgrounds are not meant to substitute the full blown Xcode IDE and the need to test on actual devices when developing a complete, functional app. Ultimately, they are just a tool to be used when it makes sense, but a very useful one.