In the last post, I built out a front-end so that we can see the game board for Tetris. This involved creating a very basic ViewModel that displays a static board. Now, I need to flesh out the Model side.

Create the Model

The model is supposed to contain the state of the app. This means the board, Tetrominos, and anything else we may want to save. We’ll need to go back through and update the ViewModel to use this state, but let’s design out the model first.

We’ll copy the numRows, numColumns and the gameBoard down into the model. However, instead of using [[TetrisGameSquare]] for the gameBoard, we’ll make a new struct. This structure will contain an optional enum for the type of Tetromino that occupied that location.

Next, we’ll copy the initializer down from the ViewModel. We still need to initialize the gameBoard array, but we need to replace the TetrisGameSquare with nil.

The last thing we need to do is create a new method for the squareClicked logic. I’ll make a new function, called toggleBlock, and this will do the toggling for the spot.

class TetrisGameModel: ObservableObject { var numRows: Int var numColumns: Int @Published var gameBoard: [[TetrisGameBlock]] init(numRows: Int = 23, numColumns: Int = 10) { self.numRows = numRows self.numColumns = numColumns gameBoard = Array(repeating: Array(repeating: TetrisGameBlock(pieceType: nil), count: numRows), count: numColumns) } func toggleBlock(row: Int, column: Int) { print("Column: \(column), Row: \(row)") if gameBoard[column][row].blockType == nil { gameBoard[column][row].blockType = BlockType.allCases.randomElement() } else { gameBoard[column][row].blockType = nil } } } struct TetrisGameBlock { var blockType: BlockType? } enum BlockType: CaseIterable{ case s, z, i, j, l, t, o }

I made the PieceType enum conform to CaseIterable so I could take advantage of the randomElement method. This makes it very easy to get a random type for that enum.

Next up, we need to change the ViewModel to use this new object.

Update the ViewModel

The ViewModel should really only serve to store information about the View, and translate the Model to something the View can use. What’s nice about Swift is that it lets you use computed properties, so we can have our properties get automatically updated when a change happens in our model. What ends up happening is we can make most properties be a passthrough for our Model.

Passing the number of rows and columns through is simple, but what about the board? This requires a little bit of thought, but it’s not too hard. We’ll need some kind of mapping from the TetrisGameBlock to a TetrisGameSquare, and we can encapsulate this inside a function. Then, we can just call gameboard.map (and map on those elements) to map all the pieces using this function.

We also need to use that new toggleBlock function to update our clicked square. We can just use this as a passthrough as well.

class TetrisGameViewModel: ObservableObject { @Published var tetrisGameModel = TetrisGameModel() var numRows: Int { tetrisGameModel.numRows } var numColumns: Int { tetrisGameModel.numColumns } var gameBoard: [[TetrisGameSquare]] { tetrisGameModel.gameBoard.map { $0.map(convertToSquare)} } func convertToSquare(block: TetrisGameBlock?) -> TetrisGameSquare { return TetrisGameSquare(color: getColorForBlock(blockType: block?.blockType)) } func getColorForBlock(blockType: BlockType?) -> Color { switch blockType { case .i: return Color.tetrisLightBlue case .l: return Color.tetrisOrange case .j: return Color.tetrisDarkBlue case .o: return Color.tetrisYellow case .t: return Color.tetrisPurple case .z: return Color.tetrisRed case .s: return Color.tetrisGreen case .none: return Color.tetrisBlack } } func squareClicked(row: Int, column: Int) { tetrisGameModel.toggleBlock(row: row, column: column) } } struct TetrisGameSquare { var color: Color }

Let’s test this out!

Hmm, that didn’t work 🤔

Nesting Published Objects

As it turns out, nested published objects don’t work well SwiftUI. Luckily, published objects are built on Combine’s Publisher, and we can manually do some linking. To do this, we just need to create a simple subscriber for the publisher that call’s objectWillChange.send().

class TetrisGameViewModel: ObservableObject { ... var anyCancellable: AnyCancellable? = nil ... init() { anyCancellable = tetrisGameModel.objectWillChange.sink { self.objectWillChange.send() } } ... }

Much Better!

Now for a Tetromino

Now it’s time to place some Tetrominos on our board. The Tetromino will not live on the board, but it will be its own separate object. This will make it much easier to track and to move.

For us to understand what our Tetromino object should look like, we should first understand how rotations are going to work. With the exception for the i block and the o block, all blocks have a central block that they rotate around. For o, we can just ignore rotation. For i, we can pick pick an arbitrary block to be this center.

From here, we can start to create the Tetromino object. I’ll give the Tetromino 3 properties:

origin: Where this center block is relative to the board

blocks: What block there are relative to the origin

blockType: What shape this Tetromino is

struct Tetromino { var origin: BlockLocation var blockType: BlockType func getBlocks() -> [BlockLocation] { [ // I block for now BlockLocation(row: 0, column: -1), BlockLocation(row: 0, column: 0), BlockLocation(row: 0, column: 1), BlockLocation(row: 0, column: 2), ] } } struct BlockLocation { var row: Int var column: Int }

I’ve hard coded this to be an I block, and we’ll explore how this will work for other block in the future. Let’s add this to our TetrisGameModel and TetrisGameViewModel. For the ViewModel, we’ll have to draw the Tetromino over the gameBoard before we can return it.

class TetrisGameModel: ObservableObject { ... @Published var currentTetromino: Tetromino? = Tetromino(origin: BlockLocation(row: 22, column: 4), blockType: .i) ... }

class TetrisGameViewModel: ObservableObject { ... var gameBoard: [[TetrisGameSquare]] { var board = tetrisGameModel.gameBoard.map { $0.map(convertToSquare)} if let tetromino = tetrisGameModel.currentTetromino { for location in tetromino.getBlocks() { board[location.column + tetromino.origin.column][location.row + tetromino.origin.row] = TetrisGameSquare(color: getColorForBlock(blockType: tetromino.blockType)) } } return board } ... }

Voila! We’ve now just drawn our first Tetromino!

What Next?

Now that we have both the front-end and the back-end done, we can start doing the fun stuff! In the next post, I think we can finally make the rest of the Tetrominos and have them fall.

Feel free to check out my Github Repo, or watch my tutorial about this on Youtube:

Thanks for reading!

– SchwiftyUI