Conway's Game of Life in Swift
Conway’s Game of Life is a fun simulation game, and we’re going to code it in Swift! Based on 3 simple rules, we’ll see which of the pixels makes it to the next generation. It’s great coding practice, perfect for a Sunday afternoon.
Ready? Let’s go.
- What’s Game of Life?
- Example Code
- How Life Works in Swift
- Getting Started: The Grid
- Coding The Glider Factory
- Drawing The Grid with GridView
- Creating The Game of Life Environment
- Which Cells Stay Alive?
- Computing The Next Generation
- Automating with a Timer
- Run Conway’s Game of Life!
- Further Reading
What’s Game of Life?
Game of Life is a cellular automaton invented by British mathematician John Conway (1937-2020). It’s a simulation that defines simple rules about how a population (of pixels!) evolves after creating an initial setup.
That sounds boring, but it’s absolutely fascinating. Check this out:
(Image: Lucas Vieira, CC BY SA 3.0)
What you see here is Gosper’s glider gun. It’s a configuration that produces gliders, the tiny spaceship-like things that shoot out the middle. Based on 3 simple rules, and an initial setup, this “game” continues indefinitely.
The Game of Life takes place on a 2-dimensional grid of cells, for example, like a pixel image. Each cell can be alive or dead. Every generation of the game, you determine which cells live on to the next generation. This happens based on the alive/dead state of the 8 neighbors of a cell, and 3 rules.
These rules, to be exact:
- A live cell with 2 or 3 live neighbours survives.
- A dead cell with 3 live neighbours becomes a live cell.
- All other live cells die in the next generation, and all other dead cells stay dead.
You could say that a cell stays alive if it has a few cells around it (1). New cells are “born” when there are 3 cells around it (2). All else is lost (3).
Here’s an example of how that works for the gliders you’ve seen before.
You’re looking at the starting configuration of a glider. The neighbors of the center cell are highlighted. Will this pixel survive to the next generation? Count the number of alive neighbors, and see for yourself! (The state of the other cells is also shown in the second image.)
What about a blinker? It’s a simple configuration of 3 cells, that switches between a horizontal and vertical line. It’s stable, so it’ll continue blinking forever.
Why, though? The center cell will always stay alive. The 2 cells at the ends alternate between horizontal and vertical, because they’ll always have 3 neighbors. Intriguing, right?
That’s not all…
- The Game of Life can simulate a Turing machine; it’s Turing complete. You can essentially simulate every possible algorithm in the Game of Life. You can, theoretically, create an initial state for Life that produces the digits of Pi. You can create Game of Life in itself. Your imagination will run out of ideas before you’ve exhausted Game of Life!
- A concept within Game of Life is whether a pattern of cells stabilizes in a given number of generations. Many patterns will stay chaotic for a long time, until stabilizing. Thanks to the halting problem, a common rule (or challenge) in computation theory, no algorithm exists that can predict if a later pattern will appear. You can literally keep playing the Game of Life indefinitely. It’s inevitable!
- Game of Life is fascinating, and pretty crazy. A quick search online shows you plenty of videos of intricate, chaotic configurations that produce the most astounding patterns. You don’t have to go crazy to get some neat patterns though; with a simple initial configuration, you get gliders, spaceships, blinkers, pulsars, loafs, boats, and so on.
Author’s Note: John Horton Conway (age 82) died on April 11, 2020 from complications of COVID-19. His invaluable contributions to mathematics, game theory and computer science go far beyond my comprehension. At the same time, I’m infinitely mesmerized by the simple nature of Game of Life.
Example Code
You can get the example code for this tutorial on this GitHub repository. You’ll find 3 projects:
- Game of Life with arrays: A playground with the code in this tutorial
-
Game of Life with Sets: An alternative implementation based on
Set
- Game of Life Xcode project: An iOS app with better performance on iPhone
In this tutorial we’ll create the implementation that uses arrays, because it performs better in an Xcode playground. The Game of Life implementation that uses Set
is quite elegant, but due to heavy object create/destroy it performs poorly in an Xcode playground.
The code in this tutorial was inspired by Conway’s Game of Life on Wikipedia, The Game of Life with Functional Swift by Colin Eberhardt, and Conway’s Game of Life on Rosetta Code.
How Life Works in Swift
Before we begin, let’s discuss the structure of the code we’re about to write. From a birds-eye view, this Game of Life implementation has 2 components:
- The
Grid
struct, which represents the Game of Life’s cells in a 2D array. It’s responsible for calculating the next generation. - The
GridView
view (UIView), which will draw theGrid
on screen. It simply iterates over the cells, drawing black pixels if a cell is alive.
We’ll also create a Factory
struct, which has static functions that produce a Game of Life pattern. With a bit of X/Y wizardry, we’re going to add those patterns to the grid so you can create your initial setup easily.
Let’s get to it!
Getting Started: The Grid
Start your project by creating an empty playground in Xcode. We’ll start with a clean slate – exciting!
Then, add the following code at the top of the playground:
import UIKit
import PlaygroundSupport
We’re importing UIKit
for the UIView
type, and PlaygroundSupport
so we can run the playground indefinitely.
Next, add the following code:
struct Grid
{
var size = (width: 50, height: 50)
var cells:[[Int]]
}
This Grid
struct is the data structure for the cells in Game of Life. It’ll house the functions that will compute a new generation, for example.
We’ve added 2 properties, size
and cells
. The type of size
is (Int, Int)
, which is a tuple. We’ve named the two values in the tuple width
and height
. They’re the size of the grid, so now we’ve got a grid of 50×50 cells.
The type of cells
is [[Int]]
. This is an array of arrays of integers, or rather, a 2-dimensional array of integers. You can picture this as a 2D X/Y grid of 1’s and 0’s. We can get to the state of each cell with cells[x][y]
†.
Finally, add the following initializer to the Grid
struct:
init() {
self.cells = Array(repeating: Array(repeating: 0, count: size.height), count: size.width)
}
What’s going on here? The above code will initialize the cells
property with a 2D array of zeroes. The resulting array will have a size of width
by height
. It’s an empty grid of cells; the empty beginnings of the Game of Life.
If you look closely, you’ll see that we’re making 2 calls to Array(repeating:count:)
. The inner call will repeat 0
for size.height
times, i.e. a row of zeroes. The outer call will repeat that inner Array(···)
for size.width
times, i.e. a row of rows of zeroes.
†: For the sake of simplicity, we’re using the cells
grid as cells[x][y]
and call that an X/Y grid. A smart reader will now point out that, as is, the indices in the cells
array will correspond to the Y coordinate and the indices for cells[y]
will correspond to the X coordinate. This means that if you print out the values in cells
, you’ll see an Y/X grid. If that bothers you, feel free to transpose the array!
Coding The Glider Factory
OK, next up, the Factory
struct. This component will have some hard-coded cell patterns that we can insert into the Grid
struct. Its API allows you to quickly create some neat initial configurations for Game of Life without coding every 1 and 0 by hand.
Add the following code to your playground:
struct Factory
{
static func glider() -> [[Int]]
{
return [
[0, 1, 0],
[0, 0, 1],
[1, 1, 1],
]
}
static func blinker() -> [[Int]]
{
return [
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
]
}
static func random(size: Int) -> [[Int]]
{
var cells = Array(repeating: Array(repeating: 0, count: size), count: size)
let odds = 1.0 / 5.0
for x in 0..<size {
for y in 0..<size {
if Double.random(in: 0..<1) < odds {
cells[x][y] = 1
}
}
}
return cells
}
}
You can find the complete Factory struct here. It includes Gosper’s glider gun, which was too big to include here. Keep in mind the grid is Y/X.
Here’s how the Factory
struct works. We’ve got a static function glider()
and blinker()
, which both return a 2D array of type [[Int]]
. This is essentially a small version of a grid with cells.
The random()
function creates a random 2D grid of alive/dead cells, in a square grid of cells with size size
. First, an empty 2D array is created. Then, we’re looping over the X/Y cells in the grid. For each cell, we’re doing a random dice throw that has a 1 in 5 chance of producing a 1 (alive cell).
Next up, we’re going to need a way to add these patterns from Factory
to the Grid
. Let’s code a function for that!
Add the following function to the Grid
(!) struct:
mutating func insertCells(_ insertedCells: [[Int]], at start: (x: Int, y: Int))
{
for x in 0..<insertedCells.count {
for y in 0..<insertedCells[x].count {
let xd = x + start.x
let yd = y + start.y
if xd >= 0 && yd >= 0 && xd < size.width && yd < size.height {
cells[xd][yd] = insertedCells[x][y]
}
}
}
}
Let’s take a look at how that works. This function has 4 important aspects:
- The function is called
insertCells(_:at:)
. You can insert a 2D array, i.e. a grid of cells, at astart.x
andstart.y
coordinate. - Inside the function, we’re looping over the 2D
insertedCells
array. We’re looking at each single cell in the 2D array. - Inside the loop, we’re first calculating the destination coordinate
xd
andyd
. We do this by offsetting the 2D coordinate a cell with thestart.x
andstart.y
of the starting point. - Finally, we’re checking if the destination
xd
andyd
are within the bounds ofcells
. If so, we’re adding the cell’s value frominsertedCells
to thecells
property of the grid.
See how this works? We’re essentially taking the 2D array – the (small) pattern – and add that to the (big) grid for Game of Life. An added benefit is the starting point, for example, you can add a glider in the middle of the grid by providing a value for start.x
and start.y
.
Drawing The Grid with GridView
Now that some code is in place for the grid, it’s time to draw that grid on screen. We’re going to do so by defining GridView
, which is a subclass of the UIView
type. You can put this view in any UIKit-based app.
First, add the following code to the playground:
class GridView: UIView
{
var grid = Grid()
}
This is the GridView
class, which is a subclass of UIView
. It has one property called grid
of type Grid
. This is the struct we defined earlier; we’re essentially tacking that data structure onto the GridView
view.
Next up, add the following function to the GridView
class:
override func draw(_ rect: CGRect)
{
}
This draw(_:)
function is part of UIView
, and we’re overriding it here with our own implementation. It’s called every time that the view needs to be (re)drawn. Whatever we “draw” in this function is shown in the view, so that’s a perfect hook into drawing the contents of the grid (in pixels).
Here’s how the drawing is going to work:
- Get the graphics context, i.e. the “canvas” we’re going to draw onto
- Clear the canvas, so we’re starting with a clean slate
- Fill the canvas with a white background
- Determine the size of a cell in pixels, based on the grid and view size
- Loop over each X/Y coordinate in the cell grid, and if the cell is, draw a black rectangle on the canvas at the corresponding coordinate
Let’s go!
Setting Up The Canvas
First, add the following code to the draw(_:)
function:
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.clear(CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height))
Here’s what’s happening:
- First, we get a reference to the graphics context and assign it to
context
. When that fails, the function returns and exits execution. - Then, we’re clearing the graphics context. Everything that’s on there is removed. We’re doing so within the rectangle
(0, 0, width, height)
.
Filling White Background
Next, add this code to the draw(_:)
function:
context.setFillColor(UIColor.white.cgColor)
context.addRect(CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height))
context.fillPath()
That does this:
- Set the fill color to white, i.e. grab the white paint bucket
- Define a rectangle that has the same size as the view
- Fill this rectangle with the white color
We now have drawn a completely white view.
Drawing The Cells
Before we can draw the Game of Life’s cells on screen, we’ll need to determine size of a cell in pixels. For example, our grid is 50×50 cells, and the view’s size could be 400×400 points (pixels†), so that means 1 cell is 8×8 pixels in size.
Add the following code to the function:
let cellSize = (width: bounds.width / CGFloat(grid.size.width), height: bounds.height / CGFloat(grid.size.height))
The cellSize
constant is a tuple with width
and height
values. Both are calculated by dividing the width of the view by width.grid
, and view height divided by grid.height
respectively. The view is divided by the grid, and now we have an individual cell size of cellSize.width
× cellSize.height
pixels.
†: Technically, iOS apps use the concept of “points” to account for screen densities (DPI) between different iPhone/iPad devices. In this tutorial, you can regard points and pixels to be synonymous. Learn more here: 1x, 2x and 3x Image Scaling on iOS Explained
Then, add the following code. It’ll set the fill color to black:
context.setFillColor(UIColor.black.cgColor)
Finally, add the following code to the draw(_:)
function:
for x in 0..<grid.size.width {
for y in 0..<grid.size.height {
if grid.cells[x][y] == 1 {
context.addRect(CGRect(x: CGFloat(x) * cellSize.width, y: CGFloat(y) * cellSize.height, width: cellSize.width, height: cellSize.height))
context.fillPath()
}
}
}
Let’s take a closer look at that code. First, the 2 nested for loops. You’re going to see more of those! How do they work?
You already know that grid
includes a cells
property that is a 2D array of integer values. Its size is determined by grid.size.width
and grid.size.height
. When both are 50
, the grid.cells
array has size 50×50 = 2500 cells.
Each of those cells has a coordinate. This coordinate space lies between (0, 0) and (49, 49). We can reach every cell in the grid by their X/Y coordinates between those values. This means looping from 0 to 49, and in each of those loops, looping from 0 to 49 again.
(0, 0) (0, 1) (0, 2) (0, 3) (0, 4) ··· (0, 48) (0, 49)
(1, 0) (1, 1) (1, 2) (1, 3) (1, 4) ··· (0, 48) (0, 49)
···
(49, 0) (49, 1) ···
See how that works? In Swift, we express both loops with a range. For example, for x in 0..<grid.size.width
. That means: Loop from zero until 50, not including 50. Within the loop, we have access to the current value of x
. Combine that with another loop for y in ···
, and you’ve got X/Y coordinates for each cell in the grid.
What’s going on inside the loop? Here’s what:
- Check if the value of the cell at that coordinate is
1
, because otherwise we don’t have to paint it black (it’s white already) - Add a rectangle at the corresponding coordinate in the view, i.e. multiply X/Y in the grid to X/Y in the view based on
cellSize
- Fill the rectangle with a black color
Awesome!
Creating The Game of Life Environment
So far, we’ve created the Grid
with cells, created a Factory
for cell patterns (like a glider), and created the GridView
that’ll draw the Game of Life grid on screen. Let’s put that code to use!
Add the following code to your playground, at the bottom of the code, so below everything else:
PlaygroundPage.current.needsIndefiniteExecution = true
let gridView = GridView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = gridView
gridView.grid.insertCells(Factory.glider(), at: (x: 2, y: 2))
gridView.grid.insertCells(Factory.glider(), at: (x: 10, y: 10))
gridView.grid.insertCells(Factory.blinker(), at: (x: 5, y: 10))
gridView.grid.insertCells(Factory.random(size: 20), at: (x: 20, y: 20))
gridView.setNeedsDisplay()
Here’s what the code does:
- Enable infinite execution for this playground; this means the playground won’t stop executing at the end of the code, so we’ll be able to use timers and async programming. (We’ll need this setting for later.)
- Create a
GridView
instance of 400×400 points, and assign that to theliveView
component of the playground. When set, this grid will now show up in the playground’s Live View. You can show/hide it with Option + Command + Enter. - With
insertCells(_:at:)
we’re adding a bunch of preset Game of Life patterns to the grid. You’re looking at a bunch of gliders, a blinker, and some random dots. Feel free to add some more! (Only within the (0, 0, 50, 50) rectangle.) - Finally,
setNeedsDisplay()
will ping thegridView
that it needs to be redrawn. This will invoke thedraw(_:)
function, which will draw the contents ofgridView.grid.cells
on screen.
Here’s what you should see on your screen now:
Which Cells Stay Alive?
In the next section, we’re going to compute the next Game of Life generation by looping over each cell and checking if they’re alive or dead. But before we can do so, we’ll need to code a function that determines if an individual cell survives to the next generation. Let’s code that!
First, add the following function to the Grid
(!) struct:
func staysAlive(_ x: Int, _ y: Int, isAlive: Bool) -> Bool
{
}
The staysAlive(_:_:isAlive:)
function determines if a cell at grid coordinate (x, y)
stays alive in the next generation. It will return true
for alive, and false
for dead.
The isAlive
parameter, of type Bool
, is used to indicate that the cell is alive in the current generation. This status is important for determining if the cell stays alive in the next generation.
Inside the staysAlive(···)
function, we’re going to have to determine if a cell stays alive. As we’ve discussed at the beginning of this tutorial, we’ll use 3 rules to determine a cell’s fate:
- A live cell with 2 or 3 live neighbours survives.
- A dead cell with 3 live neighbours becomes a live cell.
- All other live cells die in the next generation, and all other dead cells stay dead.
The algorithm we use for this simpler than you think – it only has 2 components! We first count the number of alive neighbors, and then take a decision on that number and the state of isAlive
. Easy-peasy!
First, add the following code to the function:
var count = 0
let pairs = [
[-1,-1], [0,-1], [1,-1],
[-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]
]
The count
variable is used to keep track of the number of alive neighbors. It starts at zero, of course.
The pairs
constant is a 2D array with relative X/Y coordinates. It’s essentially a matrix of X/Y pairs. Imagine a cell, and then imagine placing this 3×3 matrix on top of it.
Each of the 8 neighbors around the cell correspond to an item in the pairs
array. For example, (-1, -1) is the cell in the top-left corner relative to the center cell. (We’re using some formatting to make this code easier to read.)
Next, we’re going to loop over the pairs. Add this code to the function:
for pair in pairs
{
let xd = x + pair[0]
let yd = y + pair[1]
}
Looks familiar? As we’re looping over the pairs
array, we’re taking the X and Y values, pair[0]
and pair[1]
respectively, and add those to the x
and y
parameters of the staysAlive()
function.
An example:
- We’re determining if the cell at
(3, 3)
should stay alive - Looping over
pairs
, we find the relative coordinate(-1, -1)
- This corresponds to absolute coordinate
(2, 2)
because(2, 2) == (3 + -1, 3 + -1) == (3 - 1, 3 - 1)
. (Remember, plus and minus is minus!)
Next, add the following code inside the for in
loop, below the existing code:
if xd >= 0 && yd >= 0 &&
xd < size.width && yd < size.height &&
cells[xd][yd] == 1 {
count += 1
}
What’s going on here? You’re looking at 4 steps:
- With the relative coordinates
xd
andyd
set, check if they’re greater than or equal to zero, i.e. inside the bounds of the grid - Check if they’re smaller than the
width
andheight
of the grid, i.e. within the bounds of the grid - Check if the cell at
cells[xd][yd]
, i.e. the neighbor cell, is alive – its value is1
if it’s alive, and0
if it’s dead - If all that is true, increase
count
with 1, because we’ve found an alive neighbor cell!
Let’s do a quick recap now. We’re trying to find out if a given cell in the grid should stay alive in the next generation. We know its coordinate, so by using a matrix of cells around that coordinate, we’re checking their status. Looping over each of those neighboring cells, we check if they’re alive. If a neighbor is alive, we increase count
by 1.
Finally, add the following code to the staysAlive()
function, outside the for in
loop, below the existing code:
if isAlive && (count == 2 || count == 3) {
return true
} else if !isAlive && count == 3 {
return true
}
return false
Ah, what’s that!? This looks like the rules for Game of Life, right? Who knew that could be so simple…
- If the cell that we’re checking is currently alive, and it’s alive neighbors
count
is either 2 or 3, the current cell stays alive. - If the cell that we’re checking is not alive, and it has 3 alive neighbors, then the current cell stays/becomes alive.
- Anything else? Sorry, you’re dead!
Awesome! This concludes the work on the function staysAlive()
. We’re ready to apply that to the grid now, and calculate the next generation.
Computing The Next Generation
When you break up a problem into smaller sub-problems, and solve those, the “bigger” problem becomes easier to solve. That’s one of the miracles of computer programming. We’ve done all this work, only to make the core of Game of Life – computing the next generation – easier to code. Let’s get to it!
Add the following code to the Grid
struct:
mutating func generation()
{
var nextCells = Array(repeating: Array(repeating: 0, count: size.height), count: size.width)
for x in 0..<size.width {
for y in 0..<size.height {
if staysAlive(x, y, isAlive: cells[x][y] == 1) {
nextCells[x][y] = 1
}
}
}
cells = nextCells
}
Here’s what’s going on in the code:
- Create an empty 2D array
nextCells
with a bunch of zeroes of sizewidth
×height
. This is effectively the same as what we’re doing in theinit()
function ofGrid
. We’re starting the next generation with an empty grid. - Loop over the grid with an inner and outer loop. The outer loop runs from
0
tosize.width
(not including), and the inner loop from0
tosize.height
(not including). Just as before, this gives us access to all grid cell coordinates(x, y)
between the bounds of the grid. - Use the
staysAlive(_:_:isAlive:)
function to determine if a cell should survive to the next generation. Be mindful of the parameters here! We’re providing thex
andy
of the current cell, and the status of the current cell withcells[x][y]
. If the cell is alive,cells[x][y]
equals1
. - Then, on the innermost line in the loop, if
staysAlive()
returnstrue
, set the same(x, y)
coordinate onnextCells
to1
. This cell is alive in the next generation. Yay! - Finally, overwrite
cells
withnextCells
. This is the grid of the next generation, so the current generation is discarded.
What else is there to say about this function!? We’re looping over the grid, calculating every cell’s dead/alive status, and commit the next generation to the cells
property of the grid. Awesome!
Automating with a Timer
Last but not least, we’ll need some code to put all this together. We’ve created the Grid
, the GridView
, and some code to compute the next generations. You can essentially put that in a loop, and let it run forever.
That’s exactly what we’re going to do! Add the following code to the playground, below the existing code:
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .milliseconds(500))
timer.setEventHandler(handler: {
gridView.grid.generation()
DispatchQueue.main.async {
gridView.setNeedsDisplay()
}
})
timer.activate()
This code creates a timer that repeats some code every 500 milliseconds. You can see we’re calling the generation()
function on the grid, and then call setNeedsDisplay()
to redraw the view. On the last line, we’re activating the timer.
A problem with making Game of Life work is that the computation needs to take place in a serial queue. You can only calculate one generation, and then the next, and the next, and so on. What doesn’t work is a concurrent or parallel process.
The pace of the computation is also important, especially in an Xcode playground. The computation can potentially slow down as more cells are present in the grid, or when your Mac is doing something else. That’s why we’re only firing the generation()
function once every 0.5 seconds.
Why didn’t we use simpler Timer component here? That Timer
component uses a runloop to do work, and it works asynchronously. The DispatchSourceTimer
that makeTimerSource()
returns uses the default serial background queue, so we’re guaranteed that the work happens serially. Two generations cannot overlap, so to speak.
Inside the handler of the timer, after calling generation()
, we’re jumping to the main thread and schedule setNeedsDisplay()
, i.e. a redraw, there. This must happen asynchronously, but that also means that the computation in generation()
can potentially run faster than the view can update. We’re avoiding this by setting a reasonable pace (500 ms) for the timer.
Quick Note: In the example code, I’ve included an example iOS app that you can run on your iPhone. Rendering views on an iPhone is much faster than doing the same in an Xcode playground. I’ve seen good performance with firing the timer every 50 milliseconds or so. That means you can simulate more generations in less time!
Run Conway’s Game of Life!
That’s it! Fire up your Xcode playground or iPhone app and see Conway’s Game of Life come to life. Awesome!
Further Reading
Want to learn more? Check out these resources: