Save 37% off PRO during our Black Friday Sale! »

芝加哥大學的圈圈叉叉_App.pdf

 芝加哥大學的圈圈叉叉_App.pdf

現場操作練習,製作芝加哥大學的 iOS App 作業,圈圈叉叉 App。
使用 UIBezierPath 繪製圈圈叉叉的井字格線。
使用 UIPanGestureRecognizer 移動圈圈 & 叉叉。
使用 UIViewPropertyAnimator 製作動畫效果。
CGRect 的 function intersection(_:)。
MVC 架構,設計 model 類別 Grid。
CABasicAnimation 的動畫效果。

Transcript

  1. 新⼿手的 iOS App 練功坊 2 芝加哥⼤大學的圈圈叉叉 App 彼得潘 http://makeiosapp2.strikingly.com

  2. 彼得潘簡介 App程式設計入⾨門:iPhone.iPad 彼得潘的 Swift 程式設計入⾨門 正職: 作家 副業: 專欄欄作家,⼯工程師,講師,顧問,家教,App評審, App接案,企業包班,創業家,iOS

    APP ⾦金金牌擺渡⼈人 iOS App⼯工程師/外包廠商的⾯面試鑑賞師 http://apppeterpan.strikingly.com
  3. 叫我彼得潘,Peter,Deeplove, ⿁鬼塚,Swift⼩小王⼦子,情歌王⼦子 莫叫我老師 http://bit.ly/2xe9eOG

  4. 芝加哥⼤大學 iOS 課程 MPCS 51030 http://uchicago.mobi

  5. 學習製作更更好的 iOS App 開發投影片 https://bit.ly/2UVnqHy

  6. 圈圈叉叉 App (Tic Tac Toe) http://uchicago.mobi/sessions/session4/ 原來來要完成⼀一個作業,真的不簡單,很多細節 ! 比想像中花時間,因此待會給⼤大家練習的時間比較少, 建議有興趣的可以回家重頭做⼀一次,

    然後 po 在 medium https://bit.ly/2mJdBjE https://en.wikipedia.org/wiki/Tic-tac-toe
  7. 製作 App 畫⾯面

  8. iPhone 8 的 App 畫⾯面 不考慮 Auto Layout

  9. 製作 App 的井字畫⾯面 利利⽤用 UIBezierPath 在 GridView畫井字 加入 9 個

    view 對應井字的九個格⼦子,連結 outlet collection
  10. 加入 9 個 view 對應井字的九個格 ⼦子,連結 outlet collection 芝加哥⼤大學的作業提⽰示

  11. 運⽤用 UIBezierPath 繪製各種形狀狀 https://bit.ly/2qOJEhF 在 playground demo

  12. demo: playground 畫三⾓角形 import UIKit let path = UIBezierPath() path.move(to:

    CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: 100, y: 0)) path.addLine(to: CGPoint(x: 100, y: 100)) path.close()
  13. 將 view 變成任意形狀狀的三種⽅方法 • https://bit.ly/2EiDVZ1 • ⽅方法1: 利利⽤用 mask •

    ⽅方法2: addSublayer • ⽅方法3: ⾃自訂 UI 元件類別,定義它的 function draw
  14. demo ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形

  15. demo ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形

  16. demo ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形 class RectangleView

  17. demo 畫線 ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形 class RectangleView:

    UIView { // Only override draw() if you perform custom drawing. // An empty implementation adversely affects performance during animation. override func draw(_ rect: CGRect) { // Drawing code let path = UIBezierPath() path.move(to: CGPoint(x: 50, y: 50)) path.addLine(to: CGPoint(x: 190, y: 50)) path.addLine(to: CGPoint(x: 190, y: 90)) path.addLine(to: CGPoint(x: 50, y: 90)) path.close() UIColor.gray.setStroke() path.lineWidth = 10 path.stroke() } }
  18. demo ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形 path.move(to: CGPoint(x: 50,

    y: 50)) path.addLine(to: CGPoint(x: 190, y: 50)) path.lineWidth = 10 從 (50, 50) 畫到 (190, 50), 線的寬度為 10, 線的中⼼心點對到 Y 座標 50, 因此線的 Y 座標從 45 開始 ( 在 2x 解析度,45 pt = 90 px )
  19. demo ⾃自訂 UI 元件類別,定義 function draw 畫長⽅方形 path.move(to: CGPoint(x: 50,

    y: 50)) path.addLine(to: CGPoint(x: 190, y: 50)) path.addLine(to: CGPoint(x: 190, y: 90)) path.addLine(to: CGPoint(x: 50, y: 90)) path.close() path.lineWidth = 10 從 (50, 90) 畫到 (50, 50), 線的寬度為 10, 線的中⼼心點對到 X 座標 50, 因此線的 X 座標從 45 開始 ( 在 2x 解析度,45 pt = 90 px )
  20. demo 畫線 + 填滿 class RectangleView: UIView { override func

    draw(_ rect: CGRect) { // Drawing code let path = UIBezierPath() path.move(to: CGPoint(x: 50, y: 50)) path.addLine(to: CGPoint(x: 190, y: 50)) path.addLine(to: CGPoint(x: 190, y: 90)) path.addLine(to: CGPoint(x: 50, y: 90)) path.close() UIColor.gray.setStroke() path.lineWidth = 10 path.stroke() UIColor.red.setFill() path.fill() } }
  21. demo 畫線 + 填滿 將 (50, 50), (190, 50), (190,

    90), (50,90) 代表的長⽅方形填滿, 因此寬度 10 的灰線只看到寬度 5。 path.move(to: CGPoint(x: 50, y: 50)) path.addLine(to: CGPoint(x: 190, y: 50)) path.addLine(to: CGPoint(x: 190, y: 90)) path.addLine(to: CGPoint(x: 50, y: 90)) path.close()
  22. 從程式製作國旗圖案 https://bit.ly/2NilWDh

  23. 製作圈圈叉叉的 Grid View

  24. 新增 GridView

  25. class 設為 GridView

  26. Grid View 跟左右邊界間距 10 格⼦子每⼀一格 110 * 110,格⼦子間的間距 12.5 Your

    nine views do not need to be subviews of the grid view.
  27. demo 連結 outlet collection https://bit.ly/2AiR6aq 將畫⾯面上多個元件變成 array 的 outlet collection

    使⽤用 TicTacToe1.zip 製作
  28. 利利⽤用 UIBezierPath 在 GridView 畫井字 override func draw(_ rect: CGRect)

    { // Drawing code let path = UIBezierPath() let squareWidth = 110 let lineWidth = 12.5 var y = squareWidth + lineWidth / 2 path.move(to: CGPoint(x: 0, y: y)) path.addLine(to: CGPoint(x: rect.width, y: y))
  29. 利利⽤用 UIBezierPath 在 GridView 畫井字 class GridView: UIView { override

    func draw(_ rect: CGRect) { // Drawing code let path = UIBezierPath() let squareWidth: CGFloat = 110 let lineWidth: CGFloat = 12.5 var y = squareWidth + lineWidth / 2 path.move(to: CGPoint(x: 0, y: y)) path.addLine(to: CGPoint(x: rect.width, y: y)) y += squareWidth + lineWidth path.move(to: CGPoint(x: 0, y: y)) path.addLine(to: CGPoint(x: rect.width, y: y)) var x = squareWidth + lineWidth / 2 path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x, y: rect.height)) x += squareWidth + lineWidth path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x, y: rect.height)) path.lineWidth = lineWidth UIColor.purple.setStroke() path.stroke() } }
  30. 練習 利利⽤用 UIBezierPath 在 GridView 畫井字 download TicTacToe2.zip 練習 https://bit.ly/2IDEnp1

  31. 加入 O & X 的 label 在 O & X

    的 label 加入 UIPanGestureRecognizer 在 O label 加上 gesture 就可以了了, 之後從程式控制 gesture 要綁到 O 或 X label
  32. O & X 的 label alpha 設為 0.5 User Interaction

    設為 false 之後再從程式設定輪輪到玩家玩時,alpha 設為 1, user interaction 設為 true
  33. 利利⽤用 Info View 顯⽰示遊戲說明

  34. 利利⽤用 Info View 顯⽰示遊戲結果

  35. 加入 InfoView Y 設為 -150

  36. 在 awakeFromNib 設定 InfoView 顯⽰示的樣⼦子 override func awakeFromNib() { super.awakeFromNib()

    layer.cornerRadius = 10 layer.borderWidth = 5 layer.borderColor = UIColor.white.cgColor } InfoView
  37. 連接 Info View 裡 label 的 outlet @IBOutlet weak var

    textLabel: UILabel! 要連結 custom view 裡元件的 outlet 時, 只能⼿手動從程式輸入再拉線 (例例外: cell 裡的元件可以直接拉線到程式) InfoView
  38. Info Button 點選 ok 後,InfoView 往下移動離開畫⾯面,並在離開畫⾯面後回到畫⾯面的上⽅方 點選 info button,InfoView 從螢幕上⽅方往下移動進入畫⾯面的中間

  39. 連接 info view 的 outlet @IBOutlet weak var infoView: InfoView!

    ViewController
  40. 連接 Info Button 的 action @IBAction func infoButtonTapped(_ sender: Any)

    { } ViewController
  41. 利利⽤用 UIViewPropertyAnimator 製作顯⽰示 info view 的動畫效果 func show(text: String) {

    textLabel.text = text superview?.bringSubviewToFront(self) let animator = UIViewPropertyAnimator(duration: 1, curve: .easeIn) { self.center = self.superview!.center } animator.startAnimation() } InfoView
  42. 點擊 info button , 顯⽰示遊戲說明 @IBAction func infoButtonTapped(_ sender: Any)

    { infoView.show(text: "Get 3 in a row to win!") } ViewController
  43. 按下 info view 的 button InfoView 往下移動離開畫⾯面,並在離開畫⾯面後回到畫⾯面的上⽅方 當 info view

    顯⽰示的是遊戲結果時,關掉 info view 後,
 ⽤用動畫淡出畫⾯面上的 O & X,開始新⼀一輪輪的遊戲
  44. 利利⽤用 UIViewPropertyAnimator 製作關掉 info view 的動畫效果 func close() { let

    animator = UIViewPropertyAnimator(duration: 1, curve: .easeIn) { self.frame.origin.y = self.superview!.frame.maxY } animator.addCompletion { (_) in self.frame.origin.y = -self.frame.height } animator.startAnimation() } InfoView
  45. 按下 info view 的 button 時, 關掉 info view @IBAction

    func closeInfoView(_ sender: Any) { infoView.close() // 當 info view 顯⽰示的是遊戲結果時,關掉 info view 後,
 ⽤用動畫淡出畫⾯面上的 O & X,開始新⼀一輪輪的遊戲 } ViewController
  46. 練習 download TicTacToe3.zip 練習 https://bit.ly/2NjKvBh

  47. 遊戲開始

  48. 連接 O & X label 的 outlet @IBOutlet weak var

    xLabel: UILabel! @IBOutlet weak var oLabel: UILabel! ViewController
  49. 輪輪到 O & X 開始時, 顯⽰示旋轉動畫 func takeTurn(label: UILabel) {

    let rotateAnimator = UIViewPropertyAnimator(duration: 2, curve: .easeIn) { label.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4) label.alpha = 0.75 } let backAnimator = UIViewPropertyAnimator(duration: 2, curve: .easeOut) { label.transform = CGAffineTransform.identity label.alpha = 1 } backAnimator.addCompletion { (_) in label.isUserInteractionEnabled = true self.view.bringSubviewToFront(label) } rotateAnimator.addCompletion { (_) in backAnimator.startAnimation() } rotateAnimator.startAnimation() } ViewController
  50. 畫⾯面出現後,從 O 開始玩 override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated)

    takeTurn(label: oLabel) } ViewController
  51. 移動 O & X

  52. 利利⽤用 UIPanGestureRecognizer 移動元件

  53. translation(in:) https://apple.co/2SNx5ng func translation(in view: UIView?) -> CGPoint

  54. 觀察 translation @IBAction func movePiece(_ sender: UIPanGestureRecognizer) { let translation

    = sender.translation(in: view) print(translation) } translation.x & translation.y 代表移動的距離 UIPanGestureRecognizer 連接的 IBAction,拖曳時觸發
  55. OK 嗎 ? @IBAction func movePiece(_ sender: UIPanGestureRecognizer) { guard

    let label = sender.view as? UILabel else { return } let translation = sender.translation(in: view) label.frame.origin = CGPoint(x: label.frame.minX + translation.x, y: label.frame.minY + translation.y) }
  56. ⾶飛到外太空了了 ! @IBAction func movePiece(_ sender: UIPanGestureRecognizer) { guard let

    label = sender.view as? UILabel else { return } let translation = sender.translation(in: view) label.frame.origin = CGPoint(x: label.frame.minX + translation.x, y: label.frame.minY + translation.y) }
  57. ⽅方法 1: 調整 origin var oLabelStartLocation = CGPoint.zero var xLabelStartLocation

    = CGPoint.zero override func viewDidLoad() { super.viewDidLoad() oLabelStartLocation = oLabel.frame.origin xLabelStartLocation = xLabel.frame.origin } ViewController
  58. ⽅方法 1: 調整 origin @IBAction func movePiece(_ sender: UIPanGestureRecognizer) {

    guard let label = sender.view as? UILabel else { return } let translation = sender.translation(in: view) if label == oLabel { label.frame.origin = CGPoint(x: oLabelStartLocation.x + translation.x, y: oLabelStartLocation.y + translation.y) } else { label.frame.origin = CGPoint(x: xLabelStartLocation.x + translation.x, y: xLabelStartLocation.y + translation.y) } } ViewController
  59. ⽅方法 2: 調整 center ViewController var oLabelStartCenter = CGPoint.zero var

    xLabelStartCenter = CGPoint.zero override func viewDidLoad() { super.viewDidLoad() oLabelStartCenter = oLabel.center xLabelStartCenter = xLabel.center }
  60. ⽅方法 2: 調整 center @IBAction func movePiece(_ sender: UIPanGestureRecognizer) {

    guard let label = sender.view as? UILabel else { return } let translation = sender.translation(in: view) if label == oLabel { label.center = CGPoint(x: oLabelStartCenter.x + translation.x, y: oLabelStartCenter.y + translation.y) } else { label.center = CGPoint(x: xLabelStartCenter.x + translation.x, y: xLabelStartCenter.y + translation.y) } } ViewController
  61. ⽅方法 3: 將 translation 歸 0 不⽤用先儲存 label ⼀一開始的位置 @IBAction

    func movePiece(_ sender: UIPanGestureRecognizer) { guard let label = sender.view as? UILabel else { return } let translation = sender.translation(in: view) label.center = CGPoint(x: label.center.x + translation.x, y: label.center.y + translation.y) sender.setTranslation(.zero, in: view) } ViewController
  62. ⼿手放開時,如果 O & X 在格⼦子之外或放在 已被佔據的格⼦子,O & X 將回到原來來的位置 利利⽤用

    UIView 的 animation 實現動畫
  63. ⽅方法 4: 調整 transform 移動 O & X func pieceBackToStartLocation(label:

    UILabel) { UIView.animate(withDuration: 0.5) { label.transform = .identity } } @IBAction func movePiece(_ sender: UIPanGestureRecognizer) { guard let label = sender.view as? UILabel else { return } if sender.state == .ended { pieceBackToStartLocation(label: label) } else { let translation = sender.translation(in: view) label.transform = CGAffineTransform(translationX: translation.x, y: translation.y) } } ViewController
  64. 拖曳 O & X 判斷停在哪⼀一格

  65. func intersects(_ rect2: CGRect) -> Bool https://apple.co/2BNbn86 Returns whether two

    rectangles intersect.
  66. 拖曳 O & X 停在哪⼀一格 @IBAction func movePiece(_ sender: UIPanGestureRecognizer)

    { guard let label = sender.view as? UILabel else { return } if sender.state == .ended { let targetSquare = squares.first { (square) -> Bool in return label.frame.intersects(square.frame) } if let targetSquare = targetSquare { placePiece(label, on: targetSquare) } else { pieceBackToStartLocation(label: label) } } else { let translation = sender.translation(in: view) label.transform = CGAffineTransform(translationX: translation.x, y: translation.y) } } ViewController
  67. 將 O& X 放到格⼦子上 func placePiece(_ label: UILabel, on square:

    UIView) { UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: [], animations: { label.transform = .identity label.center = square.center }) { (_) in } } ViewController
  68. 拖曳 O & X 停在哪⼀一格 let targetSquare = squares.first {

    (square) -> Bool in return label.frame.intersects(square.frame) } 這樣真的 ok 嗎 ?
  69. 當 O & X 停在多個格⼦子時,判 斷哪個格⼦子重疊的區塊比較⼤大

  70. func intersection(_ r2: CGRect) -> CGRect https://apple.co/2IsaTdA Returns the intersection

    of two rectangles.
  71. @IBAction func movePiece(_ sender: UIPanGestureRecognizer) { guard let label =

    sender.view as? UILabel else { return } if sender.state == .ended { var maxIntersectionArea: CGFloat = 0 var targetSquare: UIView? for square in squares { let intersectionFrame = square.frame.intersection(label.frame) let area = intersectionFrame.width * intersectionFrame.height if area > maxIntersectionArea { maxIntersectionArea = area targetSquare = square } } if let targetSquare = targetSquare { placePiece(label, on: targetSquare) } else { pieceBackToStartLocation(label: label) } } else { let translation = sender.translation(in: view) label.transform = CGAffineTransform(translationX: translation.x, y: translation.y) } }
  72. 完成這⼀一輪輪,換下⼀一個玩家 func placePiece(_ label: UILabel, on square: UIView) { var

    originalPieceCenter = CGPoint.zero UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: [], animations: { label.transform = .identity originalPieceCenter = label.center label.center = square.center }) { (_) in self.finishCurrentTurn(label: label, originalPieceCenter: originalPieceCenter) } } ViewController
  73. 完成這⼀一輪輪,換下⼀一個玩家 func finishCurrentTurn(label: UILabel, originalPieceCenter: CGPoint) { let newLabel =

    createPieceLabel(label: label) newLabel.center = originalPieceCenter view.addSubview(newLabel) if label == xLabel { xLabel = newLabel takeTurn(label: oLabel) } else { oLabel = newLabel takeTurn(label: xLabel) } }
  74. 產⽣生新的 piece func createPieceLabel(label: UILabel) -> UILabel { let newLabel

    = UILabel(frame: label.frame) newLabel.text = label.text newLabel.font = label.font newLabel.backgroundColor = label.backgroundColor newLabel.textColor = label.textColor newLabel.textAlignment = label.textAlignment newLabel.alpha = 0.5 newLabel.isUserInteractionEnabled = false return newLabel } ViewController
  75. 將 gesture recognizer 加到下⼀一個 玩家的 label ViewController @IBOutlet var gestureRecognizer:

    UIPanGestureRecognizer! 連接 gesture 的 outlet
  76. 將 gesture recognizer 加到下⼀一個 玩家的 label func takeTurn(label: UILabel) {

    let rotateAnimator = UIViewPropertyAnimator(duration: 2, curve: .easeIn) { label.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4) label.alpha = 0.75 } let backAnimator = UIViewPropertyAnimator(duration: 2, curve: .easeOut) { label.transform = CGAffineTransform.identity label.alpha = 1 } backAnimator.addCompletion { (_) in label.isUserInteractionEnabled = true label.addGestureRecognizer(self.gestureRecognizer) self.view.bringSubviewToFront(label) } rotateAnimator.addCompletion { (_) in backAnimator.startAnimation() } rotateAnimator.startAnimation() } ViewController
  77. 練習: 定義 function movePiece download TicTacToe4.zip 練習 https://bit.ly/2GA4KdB

  78. 偵測格⼦子已被佔據 & 判斷輸贏

  79. Grid model

  80. class Grid { enum Piece { case o case x

    } let squareCount = 9 lazy var squares = [Piece?](repeating: nil, count: squareCount) let lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]] var winner: Piece? { var winner: Piece? for line in lines { if let firstPiece = squares[line[0]] { let result = line.allSatisfy { (index) -> Bool in return squares[index] == firstPiece } if result { winner = firstPiece break } } } return winner } Grid
  81. 0 1 2 3 4 5 6 7 8

  82. allSatisfy https://bit.ly/2XezC8l

  83. var isFull: Bool { return squares.indices.allSatisfy { (index) -> Bool

    in return isSquareEmpty(index: index) == false } } var isTie: Bool { if isFull, winner == nil { return true } else { return false } } func isSquareEmpty(index: Int) -> Bool { return squares[index] == nil } func occupy(piece: Piece, on index: Int) { squares[index] = piece } func clear() { squares = [Piece?](repeating: nil, count: squareCount) } Grid
  84. ViewController

  85. func placePiece(_ label: UILabel, on square: UIView, index: Int) {

    var originalPieceCenter = CGPoint.zero UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: [], animations: { label.transform = .identity originalPieceCenter = label.center label.center = square.center }) { (_) in self.finishCurrentTurn(label: label, index: index, originalPieceCenter: originalPieceCenter) } } ViewController
  86. func finishCurrentTurn(label: UILabel, index: Int, originalPieceCenter: CGPoint) { occupyPieces.append(label) let

    newLabel = createPieceLabel(label: label) newLabel.center = originalPieceCenter view.addSubview(newLabel) if label == xLabel { self.grid.occupy(piece: .x, on: index) xLabel = newLabel takeTurn(label: oLabel) } else { self.grid.occupy(piece: .o, on: index) oLabel = newLabel takeTurn(label: xLabel) } } ViewController var occupyPieces = [UILabel]()
  87. 顯⽰示結果

  88. func finishCurrentTurn(label: UILabel, index: Int, originalPieceCenter: CGPoint) { occupyPieces.append(label) let

    newLabel = createPieceLabel(label: label) newLabel.center = originalPieceCenter view.addSubview(newLabel) let nextLabel: UILabel if label == xLabel { self.grid.occupy(piece: .x, on: index) xLabel = newLabel nextLabel = oLabel } else { self.grid.occupy(piece: .o, on: index) oLabel = newLabel nextLabel = xLabel } if let winner = grid.winner { if winner == Grid.Piece.o { infoView.show(text: "Congratulations, O wins!") } else { infoView.show(text: "Congratulations, X wins!") } } else if grid.isTie { infoView.show(text: "Tie") } else { takeTurn(label: nextLabel) } } ViewController
  89. 當 info view 顯⽰示的是遊戲結果時,關掉 info view 後,⽤用動畫淡出畫⾯面上的 O & X,

    開始新⼀一輪輪的遊戲 @IBAction func closeInfoView(_ sender: Any) { infoView.close() if grid.winner != nil || grid.isTie { newGame() } } ViewController
  90. 新⼀一輪輪的遊戲 func newGame() { grid.clear() UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 2, delay: 1, options:

    [], animations: { self.occupyPieces.forEach { (piece) in piece.alpha = 0 } }) { (_) in self.occupyPieces.forEach { (piece) in piece.removeFromSuperview() } self.occupyPieces.removeAll() self.takeTurn(label: self.oLabel) } }
  91. 完整版下載連結 https://github.com/AppPeterPan/TicTacToe TicTacToe5.zip

  92. 利利⽤用 CABasicAnimation 實現 連線動畫

  93. 兩兩條線 其實不太可能,除非對⼿手很笨

  94. 取得 winner 連線的位置 var winnerLines: [[Int]] { var winnerLines =

    [[Int]]() for line in lines { if let firstPiece = squares[line[0]] { let result = line.allSatisfy { (index) -> Bool in return squares[index] == firstPiece } if result { winnerLines.append(line) } } } return winnerLines }
  95. None
  96. func showWinLineAnimation(startPoint: CGPoint, endPoint: CGPoint) { let path = UIBezierPath()

    path.move(to: startPoint) path.addLine(to: endPoint) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath shapeLayer.strokeColor = UIColor.red.cgColor shapeLayer.lineWidth = 10 shapeLayer.lineCap = .round CATransaction.begin() CATransaction.setAnimationDuration(3) let strokeAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd)) strokeAnimation.fromValue = 0 strokeAnimation.toValue = 1 CATransaction.setCompletionBlock { shapeLayer.removeFromSuperlayer() if let winner = self.grid.winner { if winner == Grid.Piece.o { self.infoView.show(text: "Congratulations, O wins!") } else { self.infoView.show(text: "Congratulations, X wins!") } } } shapeLayer.add(strokeAnimation, forKey: nil) view.layer.addSublayer(shapeLayer) CATransaction.commit() }
  97. 連線動畫版下載連結 https://github.com/AppPeterPan/TicTacToeWithLineAnimation TicTacToe6.zip

  98. 相關教學資源 • FB粉絲團: 愛瘋⼀一切為蘋果的彼得潘
 http://www.facebook.com/iphone.peterpan • 個⼈人網站
 http://apppeterpan.strikingly.com • medium:

    彼得潘的App Neverland
 https://medium.com/@apppeterpan • GitBook: 彼得潘的iOS App開發便便利利貼
 https://www.gitbook.com/book/apppeterpan/iosappcodestickies/ • FB社團: 彼得潘的蘋果App開發教室
 https://www.facebook.com/groups/peterpanappclass/
  99. 相關教學資源 • SlideShare
 http://www.slideshare.net/deeplovepan • email: apppeterpan@gmail.com • 彼得潘的 SWIFT

    iOS APP office hour
 http://swiftiosappofficehour.strikingly.com • FB: https://www.facebook.com/deeplove.pan • LINE: deeplovepeterpan
 如果沒加我朋友,但是有設定阻擋訊息的,會看不到我傳的訊 息。 line 官⽅方 IG