Conway's Game of Life in Swift

Written by LearnAppMaking on December 18 2020 in App Development, iOS, Play With Code, Swift

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.

  1. What’s Game of Life?
  2. Example Code
  3. How Life Works in Swift
  4. Getting Started: The Grid
  5. Coding The Glider Factory
  6. Drawing The Grid with GridView
  7. Creating The Game of Life Environment
  8. Which Cells Stay Alive?
  9. Computing The Next Generation
  10. Automating with a Timer
  11. Run Conway’s Game of Life!
  12. 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:

Gosper's glider gun, Game of Life

(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:

  1. A live cell with 2 or 3 live neighbours survives.
  2. A dead cell with 3 live neighbours becomes a live cell.
  3. 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.

Game of Life, Glider

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.

Game of Life, Blinker

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:

  1. Game of Life with arrays: A playground with the code in this tutorial
  2. Game of Life with Sets: An alternative implementation based on Set
  3. 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:

  1. The Grid struct, which represents the Game of Life’s cells in a 2D array. It’s responsible for calculating the next generation.
  2. The GridView view (UIView), which will draw the Grid 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:

  1. The function is called insertCells(_:at:). You can insert a 2D array, i.e. a grid of cells, at a start.x and start.y coordinate.
  2. Inside the function, we’re looping over the 2D insertedCells array. We’re looking at each single cell in the 2D array.
  3. Inside the loop, we’re first calculating the destination coordinate xd and yd. We do this by offsetting the 2D coordinate a cell with the start.x and start.y of the starting point.
  4. Finally, we’re checking if the destination xd and yd are within the bounds of cells. If so, we’re adding the cell’s value from insertedCells to the cells 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:

  1. Get the graphics context, i.e. the “canvas” we’re going to draw onto
  2. Clear the canvas, so we’re starting with a clean slate
  3. Fill the canvas with a white background
  4. Determine the size of a cell in pixels, based on the grid and view size
  5. 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:

  1. Set the fill color to white, i.e. grab the white paint bucket
  2. Define a rectangle that has the same size as the view
  3. 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:

  1. 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)
  2. 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
  3. 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:

  1. 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.)
  2. Create a GridView instance of 400×400 points, and assign that to the liveView 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.
  3. 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.)
  4. Finally, setNeedsDisplay() will ping the gridView that it needs to be redrawn. This will invoke the draw(_:) function, which will draw the contents of gridView.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:

  1. A live cell with 2 or 3 live neighbours survives.
  2. A dead cell with 3 live neighbours becomes a live cell.
  3. 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:

  1. With the relative coordinates xd and yd set, check if they’re greater than or equal to zero, i.e. inside the bounds of the grid
  2. Check if they’re smaller than the width and height of the grid, i.e. within the bounds of the grid
  3. Check if the cell at cells[xd][yd], i.e. the neighbor cell, is alive – its value is 1 if it’s alive, and 0 if it’s dead
  4. 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…

  1. 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.
  2. If the cell that we’re checking is not alive, and it has 3 alive neighbors, then the current cell stays/becomes alive.
  3. 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:

  1. Create an empty 2D array nextCells with a bunch of zeroes of size width × height. This is effectively the same as what we’re doing in the init() function of Grid. We’re starting the next generation with an empty grid.
  2. Loop over the grid with an inner and outer loop. The outer loop runs from 0 to size.width (not including), and the inner loop from 0 to size.height (not including). Just as before, this gives us access to all grid cell coordinates (x, y) between the bounds of the grid.
  3. 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 the x and y of the current cell, and the status of the current cell with cells[x][y]. If the cell is alive, cells[x][y] equals 1.
  4. Then, on the innermost line in the loop, if staysAlive() returns true, set the same (x, y) coordinate on nextCells to 1. This cell is alive in the next generation. Yay!
  5. Finally, overwrite cells with nextCells. 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!

Game of Life

Further Reading

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.