【iOS】#2 羽球計分板App|挑戰純程式碼 + Auto Layout(使用SnapKit)

製作動機:有鑒於每週固定會和朋友一起打羽球,當然要來做款羽球計分的 App 啦!不僅自己用得到,還可以跟打球的朋友炫耀一番(? 因為計分板畫面相對單純,決定挑戰用純程式碼 Auto Layout 來刻畫面,那廢話不多說,我們開始吧~

純程式碼 Auto Layout (使用SnapKit)

使用 SPM 安裝 SnapKit

參考下方連結:

在專案中 import SnapKit

import SnapKit

開始刻畫面

  1. 將畫面上所需的元件準備起來:
let redView = UIView()
let blueView = UIView()
let redScoreButton = UIButton()
let blueScoreButton = UIButton()
var redScore = 0
var blueScore = 0
let redUndoButton = UIButton()
let blueUndoButton = UIButton()
let changeSide = UIButton()

2. 將畫面分成左右紅藍兩邊,這邊先進行左半邊紅隊的部分

以下刻畫面都在 viewDidLoad 中進行

  • 將 redView 背景色設為紅色
redView.backgroundColor = .red
  • 使用 Auto Layout 前記得先把 redView 加進 view 中
view.addSubview(redView)
  • 使用 SnapKit 的定位方式將 redView 對齊至畫面最左邊,高度撐滿畫面,寬度設置為畫面的一半
redView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.height.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
}
  • 用同樣的方法完成右半邊藍隊的部分,右半邊對齊至 trailing,如下:
blueView.backgroundColor = .blue
view.addSubview(blueView)
blueView.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.height.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
}

目前得到的畫面

3. 加入分數按鈕

  • 將 redScoreButton 的文字設為 redScore (開始為 0)
  • 設置文字大小
  • redScoreButton 對齊至 redView 的中心,並將 size 設為等同於 redView,這樣只要點選到紅色範圍,就等同於有點選到 redScoreButton
redScoreButton.setTitle("\(redScore)", for: .normal)
redScoreButton.titleLabel?.font = .systemFont(ofSize: 150)
redView.addSubview(redScoreButton)

redScoreButton.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalToSuperview()
}

藍色分數用同樣方式完成後得到以下畫面

4. 加入 undo 按鈕

  • 使用 UIColor 設置有透明度的 Button
  • 使用 UIButton.layer.cornerRadius 設置圓角
  • redUndoButton 設置定位及按鈕寬高,對齊 bottom 時用 inset 往內推一段空間,視覺上比較舒服
redUndoButton.backgroundColor = UIColor(red: 50/255, green: 50/255, blue: 50/255, alpha: 0.5)
redUndoButton.setTitle("undo", for: .normal)
redUndoButton.layer.cornerRadius = 12
redView.addSubview(redUndoButton)

redUndoButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(56)
make.height.equalTo(35)
make.width.equalTo(100)
}

另一邊 blueUndoButton 用同樣方式完成後得到以下畫面

5. 加入 changeSide 按鈕,方式和 undo 按鈕差不多,直接看以下程式碼:

changeSide.backgroundColor = UIColor(red: 50/255, green: 50/255, blue: 50/255, alpha: 0.5)
changeSide.setTitle("Change Side", for: .normal)
changeSide.layer.cornerRadius = 12
view.addSubview(changeSide)

changeSide.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(56)
make.height.equalTo(35)
make.width.equalTo(150)
}

進行到這邊,主要畫面大功告成👏

實做功能

  1. 點擊分數 Button,分數加一
  • 在 viewDidLoad 中綁定按鈕功能
redScoreButton.addTarget(self, action: #selector(redScorePressed(_:)), for: .touchUpInside)
blueScoreButton.addTarget(self, action: #selector(blueScorePressed(_:)), for: .touchUpInside)
  • 實作 func,點擊 redScoreButton / blueScoreButton 時,redScore / blueScore 往上加一,並且更新 Button 文字
@objc func redScorePressed(_ sender: UIButton) {
redScore += 1
redScoreButton.setTitle("\(redScore)", for: .normal)
}

@objc func blueScorePressed(_ sender: UIButton) {
blueScore += 1
blueScoreButton.setTitle("\(blueScore)", for: .normal)
}

2. 點擊 undoButton,分數減一

  • 與加分大同小異,直接看以下程式碼:
//在viewDidLoad中綁定按鈕功能
redUndoButton.addTarget(self, action: #selector(redUndoPressed(_:)), for: .touchUpInside)
blueUndoButton.addTarget(self, action: #selector(blueUndoPressed(_:)), for: .touchUpInside)

//分數大於0時才執行減1的動作
@objc func redUndoPressed(_ sender: UIButton) {
if redScore > 0 {
redScore -= 1
redScoreButton.setTitle("\(redScore)", for: .normal)
}
}

@objc func blueUndoPressed(_ sender: UIButton) {
if blueScore > 0 {
blueScore -= 1
blueScoreButton.setTitle("\(blueScore)", for: .normal)
}
}

3. 判斷勝利條件,顯示獲勝者

  • 羽球計分一局為二十一分,一局滿二十分會進入平局(Duece),一方必須連續得兩分才能拿下勝利
  • 做一個名為 isWiner 的 function,傳入兩個參數(自己的分數、對方的分數),回傳一個 Bool 值
  • 使用 guard else 語法,確保得分的一方分數大於或等於勝利分數(21),並且超過對方兩分即勝利
private func isWiner(senderScore: Int, otherScore: Int) -> Bool {
let winScore = 21
guard senderScore >= winScore else {
return false
}
return senderScore - otherScore >= 2
}
  • 點擊分數 Button 功能中加入 if isWiner 判斷是否獲勝
  • 獲勝方將按鈕文字改為 Win!,並呼叫 showRestart() (此 function 在下一步驟解說)
@objc func redScorePressed(_ sender: UIButton) {
redScore += 1
redScoreButton.setTitle("\(redScore)", for: .normal)
if isWiner(senderScore: redScore, otherScore: blueScore) {
redScoreButton.setTitle("Win!", for: .normal)
showRestart()
}
}

@objc func blueScorePressed(_ sender: UIButton) {
blueScore += 1
blueScoreButton.setTitle("\(blueScore)", for: .normal)
if isWiner(senderScore: blueScore, otherScore: redScore) {
blueScoreButton.setTitle("Win!", for: .normal)
showRestart()
}
}

4. 比賽結束時顯示 Restart 按鈕,點擊後分數歸零

  • 進行 Restart 按鈕相關設置
  • 使用 UIButton.layer.borderWidth / .borderColor 設置邊框
let restart = UIButton()

//在viewDidLoad中刻畫面
restart.backgroundColor = UIColor(red: 50/255, green: 50/255, blue: 50/255, alpha: 0.8)
restart.setTitle("RESTART", for: .normal)
restart.titleLabel?.font = .systemFont(ofSize: 28)
restart.layer.cornerRadius = 80
restart.layer.borderWidth = 2
restart.layer.borderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1).cgColor
  • 實作 showRestart function,目的為比賽結束時再將 Restart 按鈕加入畫面(在上一步驟 if isWiner 判斷結果為 true 時呼叫)
func showRestart() {
view.addSubview(restart)
restart.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(160)
}
restart.isHidden = false
}
  • 實作分數歸零功能,歸零後隱藏 Restart 按鈕
//在viewDidLoad中綁定按鈕功能
restart.addTarget(self, action: #selector(restartPressed(_:)), for: .touchUpInside)

//按下restart後兩邊分數歸零,並隱藏restart按鈕
@objc func restartPressed(_ sender: UIButton) {
redScore = 0
redScoreButton.setTitle("\(redScore)", for: .normal)
blueScore = 0
blueScoreButton.setTitle("\(blueScore)", for: .normal)
restart.isHidden = true
}

5. 點擊 ChangeSide 按鈕,分數對調

  • 創建新的常數儲存原分數後,Button 文字對調,原分數的值也對調
//在viewDidLoad中綁定按鈕功能
changeSide.addTarget(self, action: #selector(changeSide(_:)), for: .touchUpInside)

@objc func changeSide(_ sender: UIButton) {
let redScore = redScore
let blueScore = blueScore
redScoreButton.setTitle("\(blueScore)", for: .normal)
blueScoreButton.setTitle("\(redScore)", for: .normal)
self.redScore = blueScore
self.blueScore = redScore
}

以上, App 功能實作完成👏

整理程式碼

最後來把 UI 相關的程式碼整理一下

  • 將各個元件設置包進 configXXX 函式中
  • 再將所有 configXXX 函式包進 configUI function
  • 在 viewDidLoad 中只需要呼叫 configUI() 即可完成畫面設置
override func viewDidLoad() {
super.viewDidLoad()
configUI()

//綁定各按鈕function...
}

private extension ViewController {
func configUI() {
configRedView()
configBlueView()
configRedScoreButton()
configBlueScoreButton()
configRedUndoButton()
configBlueUndoButton()
configRestart()
configChangeSide()
}

func configRedView() {
redView.backgroundColor = .red
view.addSubview(redView)
redView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.height.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
}
}

func configBlueView() {...}

func configRedScoreButton() {...}

func configBlueScoreButton() {...}

func configRedUndoButton() {...}

func configBlueUndoButton() {...}

func configRestart() {...}

func configChangeSide() {...}
}

完整 APP 操作展示:

--

--