Download - Swift, via "swift-2048"

Transcript
Page 1: Swift, via "swift-2048"

Swift (via swift-2048)

Austin Zheng

Page 2: Swift, via "swift-2048"

Who am I?

• My name is Austin Zheng

• I work at LinkedIn as an iOS developer

• Before that I wrote firmware for an embedded systems startup in Redwood City

Page 3: Swift, via "swift-2048"

What Is 2048?• iOS clone of web game “2048” by

Gabriele Cirulli

• In turn based on iOS game “Threes” by Asher Vollmer

• Slide in a direction to combine like tiles

• 2+2 = 4, 4+4 = 8

• Make a ‘2048’ tile, or fill up the board and lose

Page 4: Swift, via "swift-2048"

(2048 demo)

Page 5: Swift, via "swift-2048"

Architecture(very high level)

ViewGame Logic (Model)

View ControllerActions

View Commands

(forwarded to view)

Backing Store

Page 6: Swift, via "swift-2048"

Backing Store

Page 7: Swift, via "swift-2048"

Backing Store (Old)

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

Page 8: Swift, via "swift-2048"

Backing Storestruct 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 boardArray[row*dimension + col] } set { boardArray[row*dimension + col] = newValue } } }

Page 9: Swift, via "swift-2048"

Structs

• Like classes, they can have properties and methods.

• Unlike classes, structs can’t inherit from other structs.

• Unlike classes, structs are value types

Page 10: Swift, via "swift-2048"

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

Page 11: Swift, via "swift-2048"

Subscripts

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]

Page 12: Swift, via "swift-2048"

What, exactly, are we storing?

Page 13: Swift, via "swift-2048"

TileModel (Old)

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

Page 14: Swift, via "swift-2048"

TileObjectenum TileObject { case Empty case Tile(value: Int) } !let anEmptyTile = TileObject.Empty let eightTile = TileObject.Tile(value: 8) let anotherTile = TileObject.Tile(value: 2)

Page 15: Swift, via "swift-2048"

Swift Enums

• They can do everything C or Objective-C enums can…

• They can also do everything structs in Swift can do - methods and properties…

• Optionally, you can have an enum value store associated data. (variants, tagged unions, sum types, case classes)

Page 16: Swift, via "swift-2048"

Game Logic

Page 17: Swift, via "swift-2048"

22

2

22

2

2

Page 18: Swift, via "swift-2048"

22

2

22

2

2

Page 19: Swift, via "swift-2048"

22

2

22

2

2

Page 20: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 21: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 22: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 23: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 24: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 25: Swift, via "swift-2048"

func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) -> [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } ! for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }

Page 26: Swift, via "swift-2048"

A single row…

• Condense the row to remove any space - some might move, some might stay still

• Collapse two adjacent tiles of equal value into a single tile with double the value

• Convert our intermediate representation into ‘Actions’ that the view layer can easily act upon

Page 27: Swift, via "swift-2048"

Tracking changes?• We want to know when a tile is moved, when it

stays still, when it’s combined

• Let’s posit an ActionToken

• An ActionToken lives in an array. Its position in the array is the final position of the tile it represents

• An ActionToken also tracks the state of the tile or tiles that undertook the action it describes

Page 28: Swift, via "swift-2048"

ActionToken (old)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

Page 29: Swift, via "swift-2048"

ActionToken

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) }

Page 30: Swift, via "swift-2048"

Game Logic

• Condense - remove spaces between tiles

42

2 4

Page 31: Swift, via "swift-2048"

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; }

Page 32: Swift, via "swift-2048"

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; }

Page 33: Swift, via "swift-2048"

Swift ‘switch’• At its most basic, works like the C or Objective-C

switch statement

• But it can do far more!

• One example: take the values out of an enum

• Cases can be qualified by ‘where’ clauses

• Has to be comprehensive, and no default fallthrough

Page 34: Swift, via "swift-2048"

Game Logic

• Collapse - perform necessary merges

4 84

4 2 82

Page 35: Swift, via "swift-2048"

func collapse(group: [ActionToken]) -> [ActionToken] { func quiescentTileStillQuiescent(inputPosition: Int, outputLength: Int, originalPosition: Int) -> Bool { return (inputPosition == outputLength) && (originalPosition == inputPosition) } ! var tokenBuffer = [ActionToken]() var skipNext = false for (idx, token) in enumerate(group) { if skipNext { skipNext = false continue } switch token { case .SingleCombine: assert(false, "Cannot have single combine token in input") case .DoubleCombine: assert(false, "Cannot have double combine token in input") case let .NoAction(s, v) where (idx < group.count-1 && v == group[idx+1].getValue() && quiescentTileStillQuiescent(idx, tokenBuffer.count, s)): let nv = v + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.SingleCombine(source: next.getSource(), value: nv)) case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()): let next = group[idx+1] let nv = t.getValue() + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.DoubleCombine(source: t.getSource(), second: next.getSource(), value: nv)) case let .NoAction(s, v) where !quiescentTileStillQuiescent(idx, tokenBuffer.count, s): tokenBuffer.append(ActionToken.Move(source: s, value: v)) case let .NoAction(s, v): tokenBuffer.append(ActionToken.NoAction(source: s, value: v)) case let .Move(s, v): tokenBuffer.append(ActionToken.Move(source: s, value: v)) default: break } } return tokenBuffer }

Page 36: Swift, via "swift-2048"

Game Logic

• Convert - create ‘move orders’ for the view

enum MoveOrder { case SingleMoveOrder(source: Int, destination: Int, value: Int, wasMerge: Bool) case DoubleMoveOrder(firstSource: Int, secondSource: Int, destination: Int, value: Int) }

Page 37: Swift, via "swift-2048"

func convert(group: [ActionToken]) -> [MoveOrder] { var moveBuffer = [MoveOrder]() for (idx, t) in enumerate(group) { switch t { case let .Move(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: false)) case let .SingleCombine(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: true)) case let .DoubleCombine(s1, s2, v): moveBuffer.append(MoveOrder.DoubleMoveOrder(firstSource: s1, secondSource: s2, destination: idx, value: v)) default: break } } return moveBuffer }

Page 38: Swift, via "swift-2048"

Views

Page 39: Swift, via "swift-2048"

func insertTile(pos: (Int, Int), value: Int) { let (row, col) = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) ! addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }

Page 40: Swift, via "swift-2048"

func insertTile(pos: (Int, Int), value: Int) { let (row, col) = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) ! addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }

Page 41: Swift, via "swift-2048"

func insertTile(pos: (Int, Int), value: Int) { let (row, col) = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) ! addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }

Page 42: Swift, via "swift-2048"

func insertTile(pos: (Int, Int), value: Int) { let (row, col) = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) ! addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }

Page 43: Swift, via "swift-2048"

Selectors

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]; }]; }

Page 44: Swift, via "swift-2048"

Selectors

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]; }]; }

Page 45: Swift, via "swift-2048"

Selectors

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]; }]; }

Page 46: Swift, via "swift-2048"

Selectors

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]; }]; }

Page 47: Swift, via "swift-2048"

Selectors in Swiftlet upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:")) upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe)

@objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }

Page 48: Swift, via "swift-2048"

Selectors in Swiftlet upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:")) upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe)

@objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }

Page 49: Swift, via "swift-2048"

Selectors in Swiftlet upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:")) upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe)

@objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }

Page 50: Swift, via "swift-2048"

Questions?

Page 51: Swift, via "swift-2048"