Building 2048 swift cover?fm=jpg&fl=progressive&q=75&w=300

Lessons Learned Building “2048” in Swift

The second Silicon Valley Swift Language User Group meetup on July 24th featured Austin Zheng. This talk focused on Austin’s experience in porting the popular game 2048 from Objective-C into Swift. In his talk, Austin begins with a quick demonstration of the game 2048, before delving into concepts essential to building games, including the backing store, game logic, and views.


Architecture (6:40)

The architecture of the game 2048 is actually a very simple model of view controller architecture. The game logic stands in as the model and is responsible for tracking the state of the game. The views show the player the game board, tiles, and any remaining elements of the game. Actions from the player are picked up by the view controller and sent to the game logic, which in turn creates a chain of commands to update the view.

Backing Store (7:57)

The goal of the backing store is to keep track of the game state. In the Objective-C version, this was done with a mutable array with sixteen slots.

@interface F3HGameModel ()
@property (nonatomic, strong) NSMutableArray *gameState;
@property (nonatomic) NSUInteger dimension;
//...
@end

In the Swift version, a custom data structure called SquareGameboard models the game board as a square two dimensional array.

struct SquareGameboard<T> {
  var boardArray: [T]; let dimension: Int

  init(dimension d: Int, initialValue: T) {
    dimension = d
    boardArray = [T](count:d*d, repeatedValue:initialValue)
  }

  subscript(row: Int, col: Int) -> T {
    get { 
      return boardArrayprow*dimension + col]
    }
    set {
      boardArray[row*dimension + col] = newValue
    }
  }
}

Structs (9:11)

SquareGameboard is declared as a struct, which are like C structs but more powerful. They can have methods, members, and properties to store data. They can also be conformed to protocols (known in Java as interfaces), which are a list of methods or properties that a class can choose to implement. Unlike classes, structs are value types and can’t inherit from other structs.

Get more development news like this

Generics (11:12)

Generics are a way for statically typed languages to create methods that work the same way with different types of data. The stand-in type allows the compiler to initialize whatever type of SquareGameboard in 2048, whether it be an array of bools, strings, or in this case, ints.

struct SquareGameboard<T> {
  let dimension: Int
  var boardArray: [T] 
  
  init(dimension d: Int, initialValue: T) { 
    dimension = d 
    boardArray = [T](count:d*d, repeatedValue:initialValue) 
  } 
}

Subscripts (12:41)

Subscripts are denoted by the square bracket notation “[ ]”, which are used in many other languages to access arrays. In Swift, they aren’t reserved for array or dictionary access, but can be defined for any class to do anything the user wants. This subscript looks like a function as it takes two arguments, but also looks like a property with its getter and setter. The getter is a basic function that takes in two arguments and returns a value of type T, while the setter takes in one more argument and doesn’t return anything.

subscript(row: Int, col: Int) -> T { 
  get { 
    return boardArray[row*dimension + col] 
  } 
  set { 
    boardArray[row*dimension + col] = newValue 
  } 
}

gameboard[x, y] = TileObject.Empty

let someTile = gameboard[x, y]

TileModel/Object (14:28)

In Objective-C, the TileModel class represented a tile and held information about a given tile in its properties “empty” and “value”.

// This is an Objective-C class which represents a tile 
@interface F3HTileModel : NSObject
@property (nonatomic) BOOL empty; 
@property (nonatomic) NSUInteger value;
@end

In Swift, an enum provides cases which can store associative values. This provides an elegant alternative to creating tiles for every value, which can become troublesome when changing tile values.

enum TileObject { 
  case Empty 
  case Tile(value: Int) 
}

let anEmptyTile = TileObject.Empty
let eightTile = TileObject.Tile(value: 8)
let anotherTile = TileObject.Tile(value: 2)

Swift Enums (16:37)

Enums in Swift can be thought of as structs that have additional functionality. They can have methods, properties, and even subscripts. Associated values provide a good way to organize and encapsulate data in Swift.

Game Logic (17:45)

In 2048, any swipe will need to update each column/row. A for loop iterates through four times and updates each column or row. Tiles that comprise each column/row will be different for each iteration, so a new function is created to get coordinates for the tiles. These coordinates are transformed into the actual tiles on the gameboard using the “map” function, which is a higher-order function that applies the same function to each item in a sequence and returns the transformed sequence. Afterwards, the actual computation to change the tiles happens. In this process, the three possible changes include condensing the row, collapsing adjacent tiles, and converting player input into “move commands”.

Nested functions are very useful in this program in helping encapsulate helper functions that don’t need to be used elsewhere in the program. The functions that are nested within have access to anything else in the parent function.

ActionToken (22:37)

In order to track if tile is moved or whether it should combine, an ActionToken is created to represent a tile. It encapsulates information about tiles that had to change to create new tile. In the Objective-C implementation, the ActionToken is C enum that has 4 cases and a small data object to wrap some properties about a tile.

typedef enum {
  F3HMergeTileModeNoAction = 0,
  F3HMergeTileModeMove, 
  F3HMergeTileModeSingleCombine, 
  F3HMergeTileModeDoubleCombine 
} F3HMergeTileMode; 

@interface F3HMergeTile : NSObject
@property (nonatomic) F3HMergeTileMode mode; 
@property (nonatomic) NSInteger originalIndexA; 
@property (nonatomic) NSInteger originalIndexB; 
@property (nonatomic) NSInteger value; 

+ (instancetype)mergeTile;
@end

The Swift reimplementation uses enums to store only relevant data for each case individually.

enum ActionToken { 
  case NoAction(source: Int, value: Int) 
  case Move(source: Int, value: Int) 
  case SingleCombine(source: Int, value: Int) 
  case DoubleCombine(source: Int, second: Int, value: Int) 
}

Switches (25:30)

A Swift ‘switch’ statement works basically like the C or Objective-C version, but has additional functionality. One example is that it take enum values with associated data to lift the data out. Switch statements also support “where” guards to evaluate a case only if it meets a certain condition. However, switch statements must be comprehensive and have no default fallthrough. Switches are used in the condense, collapse, and convert changes in the game logic.

func condense(group: [TileObject]) -> [ActionToken] { 
  var tokenBuffer = [ActionToken]() 
  for (idx, tile) in enumerate(group) { 
    switch tile { 
    case let .Tile(value) where tokenBuffer.count == idx: 
      tokenBuffer.append(ActionToken.NoAction(source: idx, value: value)) 
    case let .Tile(value): 
      tokenBuffer.append(ActionToken.Move(source: idx, value: value)) 
    default: 
      break
    } 
  } 
 return tokenBuffer; 
}

Views (28:45)

View programming in Swift has different challenges than in business logic, but isn’t that much different from Objective-C. UI programming is more dependent on the UIKit and Apple libraries. For example, when 2048 inserts tiles, the program uses UIView.animateWithDuration after creating the animation.

Selectors (32:02)

Selectors are less straight-forward. In order to register swipes, UISwipeGestureRecognizer is called and given a target (self) and a selector, which tells the Gesture Recognizer what object should respond and what method it should call.Objective-C uses message passing, where the C function objc_messageSend takes messages and sends them to the destination, and so the method called on the object is resolved at runtime.

Swift instead uses compile time message dispatch, but can still interoperate with Objective-C code in two primary ways. One is by creating a class that inherits from an NSObject and thus runs using Objective-C runtime. Another method is to use an attribute that basically tells the compiler to make a method compatible with Objective-C.

UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] 
  initWithTarget:self action:@selector(upButtonTapped)];

upSwipe.numberOfTouchesRequired = 1; 
upSwipe.direction = UISwipeGestureRecognizerDirectionUp; 
[self.view addGestureRecognizer:upSwipe];

- (void)upButtonTapped { 
  [self.model performMoveInDirection:F3HMoveDirectionUp
    completionBlock:^(BOOL changed) { 
      if (changed) [self followUp]; }];
}

Q&A (36:40)

Q: The Swift language is still evolving, did you have to update your code significantly at any point?
Austin: I didn’t face any major semantic issues. There were a couple syntactic changes that broke my code, like the changes to the half closed range from “..” to “..<” and the declaration of the array type.

Q: Have you noticed any big difference between Objective-C and Swift in terms of things like designing a UI element?
Austin: In terms of UI kit compatibility, I don’t think the paradigms are that different. I think it will be based more on where Apple takes Cocoa where they might evolve it or make it more Swift friendly, or maybe keep it the same way and retain backwards compatibility.

Q: Have you noticed any differences or backwards compatibility issues between running iOS 7 or iOS 8?
Austin: No, I haven’t noticed any obvious bugs. If you target iOS 7, the Swift runtime library is embedded with your application so you can still run a Swift app.

Q: What are your thoughts about your productivity and using Swift versus Objective-C?
Austin: It really depends on preference, and there are features that might help but could also be abused. Sometimes I’ve wished I could use type collections, better pattern matching, or enums to encapsulate data. Some people are more comfortable with the dynamic nature of some languages, but I think a big benefit of Swift might be error checking, with non nullable types and optionals.

Q: Have you noticed any differences in teaching Swift versus any other languages? Are there any surprising mistakes or misunderstandings that people learning Swift have encountered?
Austin: Playgrounds were really helpful from the didactic standpoint. I used Playgrounds to do live coding during the class, so if we wanted to change something or see why something did or didn’t work, we could quickly see what happened. In terms of surprising features, I think the old array behaviour for mutable arrays surprises people, but there isn’t anything that causes too much trouble. Custom operators were a little bit confusing too because they are defined differently from everything else.

Q: About 2048, if you have 3 numbers in a column and swipe down, which two combine?
Austin: The game is setup to combine the tiles closest to the end, so if you’re going from right to left and you have “2 2 2”, the tiles that are closest to the left would combine. That would have been interesting, using a custom operator to do all these tile combination operations.

Q: What do you think will be added to Swift in the next couple of years?
Austin: One thing I see as a problem is that you can have an enum inside of an enum, which crashes. There will probably be better support for enums and tying them less into struct semantics. It would be interesting to have better backwards compatibility with C. I’d really like to see sets, which are missing from Swift for some reason.

Q: Do you have any idea when LinkedIn might start using Swift for development?
Austin: We have a core mobile team at LinkedIn as well as some other groups who are working on mobile projects. Some of them have really enthusiastically adopted Swift, but my group, which works on the main application, is being more conservative. We’re going to be waiting until Swift comes out of beta in the fall, then go from there.


About the content

This content has been published here with the express permission of the author.

Austin Zheng

Austin writes software and tries to spend his time learning new things. Sometimes he’ll write about these new things on his blog.

4 design patterns for a RESTless mobile integration »

close