Since Apple announced SwiftUI, I’ve been trying to figure out something to put in practice what I’ve learned so far and also to challenge myself to go even further (as well as to put some Clean Code and testing in practice).
Guess what? 🙃
One of these days I caught myself playing the 2048 game for a couple of hours on my way to Rio de Janeiro 🏖️ when I suddenly had this idea:
I'm gonna remake this game from scratch with SwiftUI and Combine and share the result with the community!
So, the goal of this article is to list down my findings and some tips that I found throughout the development of the 2048 game.
I hope you find it somehow useful!
Here is the Github repository so you can follow along with the reading:
2048 is a single-player sliding block puzzle game designed by Italian web developer Gabriele Cirulli. The game’s objective is to slide numbered tiles on a grid to combine them to create a tile with the number 2048. However, one can continue to play the game after reaching the goal, creating tiles with larger numbers. — (Wikipedia)
Before diving in
When I am facing a challenge and I'm not sure where to start from, I usually write down a simple list of the things I know I'm gonna face. It will help you to identify what are the gaps of knowledge you must fill.
For this game, these are the "macro" steps I got from this initial investigation:
Dealing with a matrix:
Behind the scenes, the heart of this game is a matrix manipulation. It's been a while since I wrote a matrix in Swift so I decided to give it special attention in order to get familiar with ways to manipulate it, write Sequence extensions, write tests, and fail fast if it was the case!
Game logic and state:
To create the bridge in between the entity that would work with the matrix and the User Interface that is going to receive user inputs to perform these manipulations, my thought was:
In order to play with Combine and Swift UI, once the game engine is ready, I'm gonna figure out a way for the view to listen for the game state (the board itself, game over conditions, scores, etc), managed by some sort of ViewModel (or whatever entity it is supposed to be).
From, creating Tiles, listening to game state, open/close multiple screens, breakdown Views into files or extensions, and, with some luck figure out how animations work, this would be a huge thing to play around and to learn more about how SwiftUI works along with Combine and some other fun things.
And here's where we start…
#1 Multiple ways to initialize a matrix
Of course, there are several ways of doing this kind of thing. If you know any other way to initialize a matrix-like that, please feel free to leave a comment below!
Now that we have a matrix (and by the way, extra tip: matrix is a typealias for [[Int]]), we must have a way to transform it, according to the game rules.
This is where the GameEngine was born. I decided that this entity was going to be responsible just for dealing with matrix operations like:
It basically exchanges the row with the column.
If you have it like [i,j], on a nested for loop, i goes to j and j goes to i.
A simple map that reverts each row: [1,2,3,4] -> [4,3,2,1]
A combination of :
- Slide tiles to the right-hand side
- Combine tiles which values are equal
- Slide it again
This is how a "push up" operation looks like step-by-step:
While developing the GameEngine, I noticed that I ended up with lots of functions that were receiving either the matrix or one of its rows, as the input and returning a transformed matrix or row, as the output. And here's where the tip 2 comes in handy.
#2 Custom Operators
Imagine if you could add a new operator like a plus(+) or minus(-)?
This is where the custom operator joins the party 🎉!
Depending on the direction I wanted to push the tiles to (left, right, up or down), I had do call the respective transformation functions in a certain order and even chained up, like:
- Rotate the matrix
- Flip the matrix
- Combine the values
- Flip back the matrix
- Rotate back the matrix
Since I'm always returning the transformed matrix, the first option I had was to go with the simple "dot notation / concat", like:
Because the logic behind this set of functions is that we are always combining rows to one side, that’s why depending on the direction we’re moving the tiles across the board, we have to manipulate, operate and manipulate it back to the original state.
So at this moment, I realized that I could have a custom operator to help out representing all the transformation chain in a simple way:
By creating the custom operator (|>), I got the ability to represent the chaining of operations in a very "instructional" list, where anyone could read it like a simple set of instructions:
Notice how easy it is to read how the code is transforming the matrix.
Of course, the GameEngine was created fully with Test Driven Development, the main ingredient of my refactoring strategy.
Because I had enough coverage, I had the freedom to experiment a lot of different ways to clean up the code, test out different approaches and so on.
Here are the tests that I used to drive the implementation of the GameEngine:
Here's what we have so far:
- 4x4 Matrix
- GameEngine responsible for transforming matrices (basic game operations)
So now it is time to talk about dealing with User Interface!
#3 Handling app state with Combine and SwiftUI
Every time the board changes, we have to update the UI as well, and here's where SwiftUI and Combine demonstrated how well they work together.
I created an entity called GameViewModel conforming to ObservableObject, and GameView to observe GameViewModel changes by having an instance of it marked with the property wrapper ObservedObject. Therefore, any changes on GameViewModel state makes the GameView invalidate its UI and re-render it.
#4 Subviews separation with extensions
During the development of some of the UI elements, one thing that bothers me is the amount of code inside the body property.
Depending on the complexity of interface you're about to implement, it tends to get huge and so hard to figure out what is the piece of code you're supposed to be looking at.
So for this project, I decided to experiment moving the small pieces of code out of the body property and extracted them to an extension, with a very simple and small scope, as you can see below:
Yet some of these Views were moved to separate SwiftUI files as they were reused in other parts of the app (or getting way to big for a computed property).
How about you? How are you handling these small parts of UI inside your Views? Please do not forget to leave a comment below 😊
#5 Subclassing gestures
One thing that also bothers me is how we instantiate simple swipe gestures, especially when you have to add four of these all at the same time: up, down, left, right.
To do so, I would have to instantiate four UISwipeGestureRecornizers with their targets and their functions, setup the swipe gesture direction and then add it to the desired view.
To make my life and also the reader's life easier, I subclassed the UISwipeGestureRecognizer, making the swipe gesture setup a lot easier and readable.
As you can see in the image below, we're creating a Target that is responsible for triggering the action we passed in on the Swipe class init.
Zero rocket science 🚀
And here's how I managed to use it:
Once the user was able to swipe and play the game, I decided that it was time to automate some tests!
#6 UITest Robots!
For this project, I thought It was a nice opportunity to test out the Testing Robots Pattern, something that I've never tried before.
This pattern basically consists on creating classes to abstract your app's behavior enough to read your UITests like a step by step script, while making it possible to reuse these robots in a lot of different use cases.
Because 2048 game is a very simple app, I created a single robot that knows how to do some stuff, like pushing tiles around, tapping on buttons, checking scores, etc.
Here's the GameRobot:
For me, among others, one the benefits of this pattern, is the element mapping inside it.
Then you can have simple instructions using these mapped elements, always returning Self and discarding the result
because of the reason for returning Self at the end of each one of these functions, you are going to be able to chain up the instructions:
I'll leave the original content on Testing Robots below.
And here we are!
Unfortunately, I wasn't able to put enough sweat to make animations work like I managed to, but it is on the Todo list!
If you want to contribute with the 2048 game with SwiftUI, please feel free to open a Pull Request!
I'll be more than happy to get your feedback and your collaboration to improve this project together!
If you read until here, I'd like to thank you for your patience and invite you to follow me up on the social media (Twitter, Instagram, etc): @caiobzen