【iOS】#2 羽球計分板App|挑戰純程式碼 + Auto Layout(使用SnapKit)
Published in
15 min readJan 10, 2024
製作動機:有鑒於每週固定會和朋友一起打羽球,當然要來做款羽球計分的 App 啦!不僅自己用得到,還可以跟打球的朋友炫耀一番(? 因為計分板畫面相對單純,決定挑戰用純程式碼 Auto Layout 來刻畫面,那廢話不多說,我們開始吧~
純程式碼 Auto Layout (使用SnapKit)
使用 SPM 安裝 SnapKit
參考下方連結:
在專案中 import SnapKit
import SnapKit
開始刻畫面
- 將畫面上所需的元件準備起來:
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)
}
進行到這邊,主要畫面大功告成👏
實做功能
- 點擊分數 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() {...}
}