#19_亂數遊戲之一-猜骰子
賭俠說:一二三,六點小
此次作業來自
這次做的骰子遊戲是小時候看很多的賭 X 系列裡常出現的博弈,就是猜骰盅裡的骰子點數和,只是有層級之分,通常是三面一樣的大於非三面一樣的點數和,比方說骰盅裡的是三個六( 豹子 ),那就必須壓在三個六上面才算贏,壓 ”大” 還是算輸
而 ”大” 跟 “小” 的區分是骰盅裡的每個骰子最大點數和除以 2 作區分,比方說 6 + 6 + 5= 17 那就是大,簡短的說就是 ”小” 的範圍就是骰子點數和 1 ~ 9,”大” 就是 10 ~ 18 這樣
經歷上一次的作業,這次決定試試完全使用 code 不用 storyboard 試試,有一些前置步驟參考如以下
經歷之前的作業練習現在能稍微在腦中簡單構思畫面和排版,最一開始當然是先要知道主題是什麼然後開始想像畫面,接著善用 UIView 套住 UIStackView 然後再用 UIStackView 去修改水平或縱向去套住各種按鈕或是 ImageView 或 小 View 等等的,但 AutoLayout 的概念一定要有,應該是說 UIStackView 跟 AutoLayout 的理解需要先在 Storyboard 有個大概理解,且一定要先在最一開始構想時能有清楚的流程,不然很容易一直要回想 XD,我自己是這樣 😂
程式碼 ( 因為不熟悉 SceneDelegate,所以這裡的問題紀錄稍多 )
// SceneDelegate.swift
// randomGames
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// 確保 scene 是 UIWindowScene 的實例
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
// 創建一個 UIWindow 並將其與 windowScene 關聯
//let window = UIWindow(windowScene: windowScene)
// 創建 UITabBarController
let tabBarController = UITabBarController()
tabBarController.tabBar.tintColor = .white
//tabBarController.tabBar.unselectedItemTintColor = .lightGray
//tabBarController.tabBar.barTintColor = .black
tabBarController.tabBar.barStyle = .black
// 創建 ViewViewController 並為其設置 UITabBarItem
let dicesVC = dicesViewViewController()
dicesVC.tabBarItem = UITabBarItem(title: "Dices", image: UIImage(systemName: "dice.fill"), tag: 0)
let wheelVC = wheelViewController()
wheelVC.tabBarItem = UITabBarItem(title: "Wheel", image: UIImage(systemName: "circle.dashed.inset.fill"), tag: 1)
let rpsVC = rockPaperScissorsViewController()
rpsVC.tabBarItem = UITabBarItem(title: "RPS", image: UIImage(systemName: "hand.raised.fingers.spread.fill"), tag: 2)
let tttVC = ticTacToeViewController()
tttVC.tabBarItem = UITabBarItem(title: "TTT", image: UIImage(systemName: "grid"), tag: 3)
// 將 dicesVC 添加為 UITabBarController 的一部分
tabBarController.viewControllers = [dicesVC, wheelVC, rpsVC, tttVC]
// 將 UITabBarController 設置為 window 的根視圖控制器
window?.rootViewController = tabBarController
// 顯示 window
window?.makeKeyAndVisible()
// 將創建的 window 設置為 SceneDelegate 的屬性,以保持對它的引用
//self.window = window
}
/// 以下的程式碼如同每個專案都會預設好的 SceneDelegate.swift 裡的程式碼所以省略
簡單介紹:
在 iOS 應用開發中,SceneDelegate
的 scene(_:willConnectTo:options:)
方法是非常重要且常用的。從 iOS 13 開始,Apple 引入了 SceneDelegate 來管理應用的一個或多個 UI scene。每個 scene 表現為應用中的一個獨立的顯示界面或窗口。scene(_:willConnectTo:options:)
方法是每個 scene 在被創建和設置時被調用的,這讓你有機會進行一些初始配置,比如設置根視圖控制器。
如果你的應用是基於 iOS 13 或更高版本開發的,那麼這段代碼是會被使用的。當應用啟動時,它會被調用來配置你的主窗口(UIWindow)和其根視圖控制器。在這個方法中設置 UITabBarController 作為根視圖控制器,就可以在應用啟動時直接顯示一個帶有 Tab 的界面。
如果你的應用還需要支援 iOS 12 或更低版本,你可能需要在 AppDelegate
中進行類似的配置,因為在這些舊版本中沒有 SceneDelegate。不過,對於現代的 iOS 應用開發來說,使用 SceneDelegate 是標準做法。
原本的 scene(_:willConnectTo:options:)
方法
原本的方法看起來像這樣:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
// 這裡沒有進一步的代碼
}
這是 SceneDelegate.swift
文件中的標準方法,用於設置和配置 scene(即 UI 界面或窗口)。在這個原始版本中:
- 有一個
guard
語句用來確保傳入的scene
可以被轉換成UIWindowScene
。如果轉換失敗,方法就會提前返回。 - 這個方法的主體是空的,沒有進行進一步的配置或設置。這意味著如果你的應用是基於 storyboard 設計的,那麼 storyboard 將會控制界面的初始顯示。
以下是我的疑問,順便記錄一下
為什麼不將函數的參數改成 UIWindow
型別?
scene(_:willConnectTo:options:)
方法的參數不能被更改為 UIWindow
型別,因為這個方法是 UISceneDelegate
協議的一部分,其定義是由 iOS SDK 提供的。這個方法的目的是在新的 scene(即 UI 界面)即將與應用連接時被調用,而不僅僅是與窗口相關。scene 可以是任何類型的 UI 界面,不僅限於包含窗口的那種。
在這個函數中,scene
參數是 UIScene
型別,它是一個抽象類型,可以代表任何形式的 app 界面。在多窗口支援的設備(如 iPad)上,一個應用可能有多個 UIScene
實例,每個都有自己的 UIWindow
。因此,這個方法的設計使其能夠更加通用和靈活。
為什麼使用底線(_)而不是命名變量?
在 Swift 中,底線(_)被用作一個“通配符”,它表示你可以忽略這個值或變量。在這個上下文中:
guard let _ = (scene as? UIWindowScene) else { return }
這行代碼的目的是檢查 scene
是否可以被安全地轉換為 UIWindowScene
。如果轉換成功,代碼將繼續執行;如果失敗,則會觸發 guard
語句的 else
塊並退出方法。在這裡,我們不需要轉換後的值本身(即轉換後的 UIWindowScene
),我們只關心轉換是否成功。因此,我們使用底線來表示我們不打算使用這個值,這是 Swift 語言中避免不必要變量命名的一種常用技巧。
這個方法遵循的協議是 UISceneDelegate
還是 UIWindowSceneDelegate
?
這個方法 scene(_:willConnectTo:options:)
遵循的是 UISceneDelegate
協議。在 iOS 13 及以後的版本中,UISceneDelegate
被用來管理應用的 scene(即 UI 界面或窗口)的生命周期和狀態。UIWindowSceneDelegate
是 UISceneDelegate
的一個子協議,專門用於管理基於窗口的 UI 界面。
在大多數典型的 iOS 應用中,你會使用 UIWindowSceneDelegate
,因為它提供了一個標準的窗口基礎界面。然而,scene(_:willConnectTo:options:)
這個方法本身是 UISceneDelegate
協議的一部分。如果你的應用只處理基於窗口的 UI 界面,你通常會在 SceneDelegate.swift
中實現 UIWindowSceneDelegate
相關的方法。
在網路上爬文時偶然看到有與 GPT 提供的稍微不同的寫法,主要是直接用原本 SceneDelegate 裡的屬性
var window: UIWindow?
,窗口設置也不同,最後我選擇用原本的屬性去寫,所以可以看到我把//let window = UIWindow(windowScene: windowScene)
給註解掉了,但依然向 GPT 提問如以下
以下這組程式碼會有什麼不同結果嗎?
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
在這個版本中:
guard let windowScene = (scene as? UIWindowScene) else { return }
: 這行代碼將scene
對象轉換為UIWindowScene
,並在轉換失敗時提早退出方法。window = UIWindow(frame: windowScene.coordinateSpace.bounds)
: 這裡創建了一個新的UIWindow
,並設置它的大小以匹配windowScene
的坐標空間範圍。這種方式可以確保窗口的大小正好填滿整個屏幕。window?.windowScene = windowScene
: 將窗口與windowScene
關聯起來。window?.rootViewController = ViewController()
: 創建了ViewController
的實例,並將其設置為窗口的根視圖控制器。這裡假設有一個名為ViewController
的視圖控制器。window?.makeKeyAndVisible()
: 使窗口成為主窗口並顯示它。
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let tabBarController = UITabBarController()
let dicesVC = dicesViewViewController()
dicesVC.tabBarItem = UITabBarItem(title: "Dices", image: UIImage(systemName: "dice.fill"), tag: 0)
tabBarController.viewControllers = [dicesVC]
window.rootViewController = tabBarController
window.makeKeyAndVisible()
self.window = window
而在這個版本中:
- 同樣將
scene
轉換為UIWindowScene
。 - 創建
UIWindow
時,直接使用了UIWindow(windowScene: windowScene)
初始化方法,這樣窗口會自動匹配windowScene
的大小和坐標。 - 創建了
UITabBarController
並將自定義的dicesViewViewController
添加到其中。這樣就建立了一個包含 Tab Bar 的界面。 - 將
UITabBarController
設置為窗口的根視圖控制器。 - 使窗口成為主窗口並顯示它。
- 將新創建的
window
保存到SceneDelegate
的window
屬性中,以保持對它的引用。
主要差異
- 視圖控制器的設置:你提供的代碼直接使用了一個單獨的
ViewController
,而我提供的代碼則是設置了一個UITabBarController
,其中包含了dicesViewViewController
。 - 窗口大小的設置:你的代碼中,窗口的大小是通過
windowScene.coordinateSpace.bounds
明確設置的,而我提供的代碼則是利用UIWindow(windowScene: windowScene)
初始化方法,讓窗口自動匹配windowScene
的大小。
窗口大小的設置兩者會有什麼不同的影響嗎?
兩個版本中,窗口大小的設置方式略有不同,但實際上這兩種方式在大多數情況下會產生相同的結果。讓我們來看看這兩種方法和它們可能的影響:
代碼版本一中的窗口設置:
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
在這種方法中,窗口的大小是通過設置 frame
來明確指定的,這裡使用的 frame
是 windowScene.coordinateSpace.bounds
。這樣做的結果是窗口會精確地填滿整個 windowScene
的坐標空間。這種方法的好處是它給你提供了對窗口大小和位置的完全控制。
代碼版本二中的窗口設置:
let window = UIWindow(windowScene: windowScene)
在這裡,窗口是通過指定其所屬的 windowScene
來創建的。當你使用這種方法時,UIWindow
的初始化程序會自動為窗口設置合適的 frame
,使其匹配相關 windowScene
的大小和位置。這種方法的好處是簡便性和保證窗口與 windowScene
完全匹配。
影響:
- 實際效果:對於絕大多數標準 iOS 應用來說,這兩種方法的效果是一樣的,因為它們都將窗口的大小設置為填滿整個屏幕。
- 代碼簡潔性:使用
UIWindow(windowScene: windowScene)
的方法更為簡潔,因為它自動處理了窗口的大小和位置,減少了需要手動設置的代碼量。 - 靈活性:如果你需要更精確地控制窗口的大小和位置,比如在特定情況下需要一個小於全屏的窗口,那麼使用
frame
屬性來明確設置窗口大小會更加合適。
總的來說,選擇哪種方法取決於你對窗口大小和位置控制的需求。對於絕大多數標準的全屏 iOS 應用,兩種方法都可以達到相同的效果。
UI設計與邏輯
程式碼
import UIKit
class dicesViewViewController: UIViewController {
// UI元件宣告區
var topContainerView: UIView! // 頂部容器視圖,用於容納上半部分的其他UI元件
var topStackView: UIStackView! // 一個垂直堆疊視圖,用於在頂部容器中組織元件
var topUpLabel: UILabel! // 標籤,顯示在頂部堆疊視圖的最上方,用於顯示遊戲狀態或指示
var topMiddleView: UIView! // 一個中間視圖,用於容納骰子和擲骰按鈕
var cupImageView: UIImageView! // 用於顯示骰子杯的圖片視圖
var rollButton: UIButton! // 用於觸發擲骰動作的按鈕
let randomDicesStackView = UIStackView() // 用於顯示骰子圖像的水平堆疊視圖
var randomDices = [UIImageView]() // 存儲骰子圖像的陣列
var randomDice1 = UIImageView() // 第一個骰子圖像
var randomDice2 = UIImageView() // 第二個骰子圖像
var randomDice3 = UIImageView() // 第三個骰子圖像
var randomNumerArray = [Int]() // 存儲隨機骰子數字的陣列
var thirdSameDices = false // 標記是否擲出了三個相同的骰子
var totalNumber = 0 // 骰子數字總和
var bottomContainerView: UIView! // 底部容器視圖,用於容納下半部分的其他UI元件
var bottomStackView: UIStackView! // 一個水平堆疊視圖,用於在底部容器中組織元件
var biggerNumberButton = UIButton() // “大”按鈕,用於下注骰子點數和
var smallerNumberButton = UIButton() // “小”按鈕,用於下注骰子點數和
var numberThirdDiceButtons = [UIButton]() // 存儲用豹子按鈕的陣列
var oneThreeFiveStackView: UIStackView! // 用於組織1、3、5數字按鈕的垂直堆疊視圖
var oneThirdDicesButton = UIButton() // “豹子1”數字按鈕
var threeThirdDicesButton = UIButton() // “豹子3”數字按鈕
var fiveThirdDicesButton = UIButton() // “豹子5”數字按鈕
var twoFourSixStackView: UIStackView! // 用於組織2、4、6數字按鈕的垂直堆疊視圖
var twoThirdDicesButton = UIButton() // “豹子2”數字按鈕
var fourThirdDicesButton = UIButton() // “豹子4”數字按鈕
var sixThirdDicesButton = UIButton() // “豹子6”數字按鈕
var time = Timer() // 用於控制動畫或遊戲邏輯的計時器
var played = false // 標記是否已進行過遊戲
var rolled = false // 標記是否已經擲過骰子
// MARK: - viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// 設定視圖控制器的背景顏色為黑色
view.backgroundColor = .black
// 初始化頂部容器視圖
topContainerView = UIView()
topContainerView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束,以便手動設置約束
topContainerView.layer.borderWidth = 1.5 // 為視圖設置邊框寬度
topContainerView.layer.borderColor = UIColor.white.cgColor // 設置邊框顏色為白色
view.addSubview(topContainerView) // 將頂部容器視圖添加到主視圖
// 設置頂部容器視圖的約束
NSLayoutConstraint.activate([
topContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), // 頂部與安全區頂部對齊
topContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), // 左側與主視圖左側對齊
topContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), // 右側與主視圖右側對齊
topContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), // 水平中心與主視圖中心對齊
topContainerView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.4) // 高度為安全區高度的40%
])
// 初始化頂部堆疊視圖
topStackView = UIStackView()
topStackView.axis = .vertical // 設置堆疊軸為垂直
topStackView.spacing = 5 // 設置堆疊視圖中元件的間距
topStackView.alignment = .fill // 元件填滿整個堆疊視圖
topStackView.distribution = .equalSpacing // 元件之間的間距均等
topStackView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
topContainerView.addSubview(topStackView) // 將堆疊視圖添加到頂部容器視圖
// 設置頂部堆疊視圖的約束
NSLayoutConstraint.activate([
topStackView.topAnchor.constraint(equalTo: topContainerView.topAnchor, constant: 5), // 頂部與容器頂部對齊,並設定5點的間距
topStackView.bottomAnchor.constraint(equalTo: topContainerView.bottomAnchor), // 底部與容器底部對齊
topStackView.leadingAnchor.constraint(equalTo: topContainerView.leadingAnchor), // 左側與容器左側對齊
topStackView.trailingAnchor.constraint(equalTo: topContainerView.trailingAnchor) // 右側與容器右側對齊
])
// 初始化並設定顯示在頂部堆疊視圖上方的標籤
topUpLabel = UILabel()
topUpLabel.text = "來擲骰" // 設置標籤的文字
topUpLabel.font = .boldSystemFont(ofSize: 30) // 設置字體為粗體,大小為30
topUpLabel.textAlignment = .center // 文字置中對齊
topUpLabel.numberOfLines = 0 // 允許多行顯示
topUpLabel.textColor = .white // 文字顏色設為白色
topUpLabel.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
topStackView.addArrangedSubview(topUpLabel) // 將標籤加入頂部堆疊視圖
// 初始化並設定頂部中間的視圖
topMiddleView = UIView()
topMiddleView.backgroundColor = .white // 設置背景顏色為白色
topMiddleView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
topStackView.addArrangedSubview(topMiddleView) // 將中間視圖加入頂部堆疊視圖
// 初始化並設定用於顯示骰子杯的圖片視圖
cupImageView = UIImageView(image: UIImage(named: "cup")) // 加載名為"cup"的圖片
cupImageView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
// 初始化並設定擲骰按鈕
rollButton = UIButton()
rollButton.setTitle("擲骰", for: .normal) // 設置按鈕文字
rollButton.backgroundColor = .black // 背景顏色設為黑色
rollButton.setTitleColor(.white, for: .normal) // 文字顏色為白色
rollButton.setTitleColor(.lightGray, for: .highlighted) // 高亮狀態下文字顏色為淺灰色
rollButton.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
rollButton.addTarget(self, action: #selector(rollButtonReleased(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel]) // 設定按鈕觸發的動作
// 初始化並設定用於顯示骰子的圖片視圖陣列
randomDices = [randomDice1, randomDice2, randomDice3] // 將三個骰子圖片視圖存入陣列
// 分別設定每個骰子的顏色和佈局約束
randomDice1.tintColor = .black
randomDice2.tintColor = .black
randomDice3.tintColor = .black
randomDice1.translatesAutoresizingMaskIntoConstraints = false
randomDice2.translatesAutoresizingMaskIntoConstraints = false
randomDice3.translatesAutoresizingMaskIntoConstraints = false
// 設定用於顯示骰子的堆疊視圖
randomDicesStackView.axis = .horizontal // 設定為水平軸向
randomDicesStackView.distribution = .fillEqually // 元件均等填充
randomDicesStackView.alignment = .center // 中心對齊
randomDicesStackView.spacing = 1 // 元件間的間距設為1
randomDicesStackView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
// 將骰子圖片視圖添加到堆疊視圖中
randomDicesStackView.addArrangedSubview(randomDice1)
randomDicesStackView.addArrangedSubview(randomDice2)
randomDicesStackView.addArrangedSubview(randomDice3)
// 將骰子堆疊視圖、骰子杯圖片視圖和擲骰按鈕添加到中間視圖中
topMiddleView.addSubview(randomDicesStackView)
topMiddleView.addSubview(cupImageView)
topMiddleView.addSubview(rollButton)
// 設定骰子杯圖片視圖和擲骰按鈕的佈局約束
NSLayoutConstraint.activate([
cupImageView.centerXAnchor.constraint(equalTo: topMiddleView.centerXAnchor),
cupImageView.bottomAnchor.constraint(equalTo: topMiddleView.bottomAnchor, constant: -45),
cupImageView.heightAnchor.constraint(equalTo: topMiddleView.heightAnchor, multiplier: 0.6),
cupImageView.widthAnchor.constraint(equalTo: topMiddleView.widthAnchor, multiplier: 0.5),
rollButton.centerXAnchor.constraint(equalTo: topMiddleView.centerXAnchor),
rollButton.widthAnchor.constraint(equalTo: cupImageView.widthAnchor, multiplier: 0.5),
rollButton.bottomAnchor.constraint(equalTo: topMiddleView.bottomAnchor, constant: -10),
randomDicesStackView.centerXAnchor.constraint(equalTo: topMiddleView.centerXAnchor),
randomDicesStackView.bottomAnchor.constraint(equalTo: topMiddleView.bottomAnchor, constant: -75)
])
// 初始化底部容器視圖
bottomContainerView = UIView()
bottomContainerView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
bottomContainerView.layer.borderWidth = 2.5 // 設置邊框寬度
view.addSubview(bottomContainerView) // 將底部容器視圖添加到主視圖
// 設定底部容器視圖的佈局約束
NSLayoutConstraint.activate([
bottomContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bottomContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bottomContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bottomContainerView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.6)
])
// 初始化底部堆疊視圖
bottomStackView = UIStackView()
bottomStackView.axis = .horizontal // 設定為水平軸向
bottomStackView.spacing = 1 // 元件間的間距設為1
bottomStackView.alignment = .fill // 元件填滿整個堆疊視圖
bottomStackView.distribution = .fillEqually // 元件均等分配
bottomStackView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
bottomContainerView.addSubview(bottomStackView) // 將底部堆疊視圖添加到底部容器視圖
// 設定底部堆疊視圖的佈局約束
NSLayoutConstraint.activate([
bottomStackView.topAnchor.constraint(equalTo: bottomContainerView.topAnchor),
bottomStackView.bottomAnchor.constraint(equalTo: bottomContainerView.bottomAnchor),
bottomStackView.leadingAnchor.constraint(equalTo: bottomContainerView.leadingAnchor),
bottomStackView.trailingAnchor.constraint(equalTo: bottomContainerView.trailingAnchor)
])
// 初始化並設定兩個垂直堆疊視圖,分別用於放置不同數字的按鈕
// 1、3、5號按鈕的堆疊視圖
oneThreeFiveStackView = UIStackView()
oneThreeFiveStackView.axis = .vertical // 垂直方向
oneThreeFiveStackView.spacing = 1 // 元件間距為1
oneThreeFiveStackView.alignment = .fill // 填滿整個堆疊視圖
oneThreeFiveStackView.distribution = .fillEqually // 等分分配空間
oneThreeFiveStackView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
// 2、4、6號按鈕的堆疊視圖
twoFourSixStackView = UIStackView()
twoFourSixStackView.axis = .vertical // 垂直方向
twoFourSixStackView.spacing = 1 // 元件間距為1
twoFourSixStackView.alignment = .fill // 填滿整個堆疊視圖
twoFourSixStackView.distribution = .fillEqually // 等分分配空間
twoFourSixStackView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束
// 初始化並設定“大”按鈕的屬性
biggerNumberButton.setTitle("大", for: .normal) // 設定按鈕文字
biggerNumberButton.backgroundColor = .white // 背景顏色為白色
biggerNumberButton.setTitleColor(.black, for: .normal) // 正常狀態下文字顏色為黑色
biggerNumberButton.setTitleColor(.lightGray, for: .highlighted) // 高亮狀態下文字顏色為淺灰色
biggerNumberButton.setTitleColor(.lightGray, for: .disabled) // 禁用狀態下文字顏色為淺灰色
biggerNumberButton.addTarget(self, action: #selector(dicesButtonTouched(_:)), for: .touchDown)
biggerNumberButton.addTarget(self, action: #selector(dicesButtonReleased(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
biggerNumberButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 50) // 字體大小和樣式
biggerNumberButton.tag = 7 // 設定標籤為7,用於識別
// 初始化數字按鈕陣列,並為每個按鈕賦予特定的數字
numberThirdDiceButtons = [oneThirdDicesButton, twoThirdDicesButton, threeThirdDicesButton, fourThirdDicesButton, fiveThirdDicesButton, sixThirdDicesButton]
for n in 1...6 {
numberThirdDiceButtons[n - 1] = createThirdDice(number: n)
}
// 初始化並設定“小”按鈕的屬性
smallerNumberButton.setTitle("小", for: .normal) // 設定按鈕文字
smallerNumberButton.backgroundColor = .white // 背景顏色為白色
smallerNumberButton.setTitleColor(.black, for: .normal) // 正常狀態下文字顏色為黑色
smallerNumberButton.setTitleColor(.lightGray, for: .highlighted) // 高亮狀態下文字顏色為淺灰色
smallerNumberButton.setTitleColor(.lightGray, for: .disabled) // 禁用狀態下文字顏色為淺灰色
smallerNumberButton.addTarget(self, action: #selector(dicesButtonTouched(_:)), for: .touchDown)
smallerNumberButton.addTarget(self, action: #selector(dicesButtonReleased(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
smallerNumberButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 50) // 字體大小和樣式
smallerNumberButton.tag = 0 // 設定標籤為0,用於識別
// 將“大”按鈕、1、3、5號按鈕堆疊視圖、2、4、6號按鈕堆疊視圖和“小”按鈕添加到底部堆疊視圖
bottomStackView.addArrangedSubview(biggerNumberButton)
bottomStackView.addArrangedSubview(oneThreeFiveStackView)
bottomStackView.addArrangedSubview(twoFourSixStackView)
bottomStackView.addArrangedSubview(smallerNumberButton)
// 在視圖加載時關閉所有按鈕
turnOffAllButtons()
}
// MARK: - function
// 函數 `createDiceImage`: 用於創建並返回一個包含三個骰子圖像的UIImage物件
func createDiceImage(systerName: String) -> UIImage? {
// 創建一個圖形渲染器,設定圖像大小為寬90點,高30點
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 90, height: 30))
// 使用渲染器生成圖像
let image = renderer.image { ctx in
// 根據提供的系統名稱,加載對應的骰子圖像(可能是SF符號)
let diceImage = UIImage(systemName: systerName)
// 設定單個骰子圖像的大小為寬30點,高30點
let diceSize = CGSize(width: 30, height: 30)
// 繪製三個骰子圖像,水平排列
for i in 0..<3 {
diceImage?.draw(in: CGRect(x: CGFloat(i * 30), y: 0, width: diceSize.width, height: diceSize.height))
}
}
// 將生成的圖像設定為模板渲染模式,這允許它在不同狀態下適應顏色變化
return image.withRenderingMode(.alwaysTemplate) // 設置圖像渲染模式為alwaysTemplate
/*
有時候,按鈕的圖像可能不會響應tintColor的變化,這可能是因為圖像的渲染模式設置不正確。你需要確保圖像的渲染模式設為.alwaysTemplate。
在createDiceImage函數中,在返回圖像之前,嘗試將圖像的渲染模式設置為.alwaysTemplate。
*/
}
// 函數 `createThirdDice`: 創建並返回一個帶有骰子圖像的按鈕
func createThirdDice(number: Int) -> UIButton {
let button = UIButton() // 創建一個新的按鈕
button.backgroundColor = .white // 設置按鈕背景顏色為白色
// 使用前面定義的`createDiceImage`函數來生成骰子圖像,並將其設置為按鈕的圖像
if let diceImage = createDiceImage(systerName: "die.face.\(number).fill") {
button.setImage(diceImage, for: .normal)
}
button.tag = number // 設置按鈕的標籤,用於識別
button.tintColor = .black // 設置圖像的著色為黑色
// 為按鈕添加觸摸事件的處理
button.addTarget(self, action: #selector(dicesButtonTouched(_:)), for: .touchDown)
button.addTarget(self, action: #selector(dicesButtonReleased(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
// 根據數字的奇偶性,將按鈕添加到不同的堆疊視圖中
if number % 2 == 0 {
twoFourSixStackView.addArrangedSubview(button)
} else {
oneThreeFiveStackView.addArrangedSubview(button)
}
return button // 返回創建的按鈕
}
// 函數 `turnOffAllButtons`: 禁用所有按鈕
func turnOffAllButtons() {
for button in numberThirdDiceButtons {
button.isEnabled = false // 將每個數字按鈕設為不可用
}
// 將“大”和“小”按鈕設為不可用
biggerNumberButton.isEnabled = false
smallerNumberButton.isEnabled = false
}
// 函數 `turnOnAllButtons`: 啟用所有按鈕
func turnOnAllButtons() {
for button in numberThirdDiceButtons {
button.isEnabled = true // 將每個數字按鈕設為可用
}
// 將“大”和“小”按鈕設為可用
biggerNumberButton.isEnabled = true
smallerNumberButton.isEnabled = true
}
// 函數 `scaleRandomDices`: 對骰子圖像進行縮放動畫
func scaleRandomDices (number: CGFloat) {
let scale: CGFloat = number // 縮放的大小
// 使用動畫改變骰子堆疊視圖的縮放比例
UIView.animate(withDuration: 2, animations: {
self.randomDicesStackView.transform = CGAffineTransform(scaleX: scale, y: scale)
})
}
// MARK: - @objc function
// 函數 `calculateAndPrintTotalNumber`: 計算並打印骰子數字的總和
@objc func calculateAndPrintTotalNumber() {
// 使用reduce函數計算randomNumerArray中所有數字的總和
totalNumber = randomNumerArray.reduce(0, { $0 + $1 })
print("total Number is: \(totalNumber)") // 打印總和
}
// 函數 `checkThirdDices`: 檢查三個骰子是否全部相同
@objc func checkThirdDices() {
if let firstNumber = randomNumerArray.first {
// 使用allSatisfy函數檢查數組中所有元素是否與第一個元素相同
let allSame = randomNumerArray.allSatisfy{ $0 == firstNumber }
/*
要檢查一個整數陣列中的所有元素是否相同,我們可以使用 allSatisfy 函數。
allSatisfy 會檢查集合中的所有元素是否都滿足某個條件。如果所有元素都滿足給定的條件,則該函數返回 true;否則返回 false。
*/
thirdSameDices = allSame // 根據結果更新thirdSameDices變量
print("thirdSameDices = \(thirdSameDices)")
}
}
// 函數 `shakeDiceCup`: 實現骰子杯的搖動動畫
@objc func shakeDiceCup() {
let animation = CABasicAnimation(keyPath: "transform.rotation") // 創建旋轉動畫
animation.duration = 0.1 // 持續時間
animation.repeatCount = 5 // 重複次數
animation.autoreverses = true // 動畫執行完畢後自動反向執行
animation.fromValue = NSNumber(value: Double.pi / 15) // 起始角度
animation.toValue = NSNumber(value: -Double.pi / 15) // 終止角度
cupImageView.layer.add(animation, forKey: "shakeAnimation") // 將動畫添加到骰子杯圖層
}
// 函數 `moveCupUpwards`: 向上移動骰子杯
@objc func moveCupUpwards() {
let upwardMovement: CGFloat = 60 // 向上移動的距離
// 使用動畫改變骰子杯的位置
UIView.animate(withDuration: 0.5, animations: {
self.cupImageView.center.y -= upwardMovement
})
}
// 函數 `moveCupDownwards`: 向下移動骰子杯
@objc func moveCupDownwards() {
let downwardMovement: CGFloat = 60 // 向下移動的距離
// 使用動畫改變骰子杯的位置
UIView.animate(withDuration: 0.5, animations: {
self.cupImageView.center.y += downwardMovement
})
}
// 函數 `createRandomDicePoint`: 隨機生成骰子點數
@objc func createRandomDicePoint() {
randomNumerArray.removeAll() // 清空之前的骰子點數
for n in 0...2 {
let randomNumber = Int.random(in: 1...6) // 隨機生成一個1到6的數字
randomDices[n].image = UIImage(systemName: "die.face.\(randomNumber).fill") // 更新骰子圖像
randomNumerArray.append(randomNumber) // 將隨機數字添加到陣列中
}
print("randice number is: \(randomNumerArray)")
}
// 函數 `resetButton`: 重置遊戲界面上所有按鈕的狀態
@objc func resetButton() {
var number = 1 // 設置一個計數器,用於更新骰子圖像
for button in numberThirdDiceButtons {
button.isEnabled = true // 啟用按鈕
button.backgroundColor = .darkGray // 初始背景色設為深灰色
button.alpha = 0.5 // 初始透明度設為0.5,表示半透明
// 使用動畫過渡到完全不透明和白色背景
UIView.animate(withDuration: 1) {
button.alpha = 1 // 設置為完全不透明
button.backgroundColor = .white // 背景色改為白色
}
// 使用先前定義的函數創建新的骰子圖像並設置給按鈕
if let diceImage = createDiceImage(systerName: "die.face.\(number).fill") {
button.setImage(diceImage, for: .normal)
button.setTitle(nil, for: .normal) // 移除任何現有的標題
}
number += 1 // 增加計數器
}
// 更新“大”按鈕的狀態和樣式
biggerNumberButton.isEnabled = true
biggerNumberButton.backgroundColor = .darkGray
biggerNumberButton.alpha = 0.5
UIView.animate(withDuration: 1) { [self] in
biggerNumberButton.alpha = 1
biggerNumberButton.backgroundColor = .white
}
biggerNumberButton.setTitle("大", for: .normal)
biggerNumberButton.setTitleColor(.black, for: .normal)
biggerNumberButton.setTitleColor(.white, for: .highlighted)
biggerNumberButton.setTitleColor(.gray, for: .disabled)
// 更新“小”按鈕的狀態和樣式
smallerNumberButton.isEnabled = true
smallerNumberButton.backgroundColor = .darkGray
smallerNumberButton.alpha = 0.5
UIView.animate(withDuration: 1) { [self] in
smallerNumberButton.alpha = 1
smallerNumberButton.backgroundColor = .white
}
smallerNumberButton.setTitle("小", for: .normal)
smallerNumberButton.setTitleColor(.black, for: .normal)
smallerNumberButton.setTitleColor(.white, for: .highlighted)
smallerNumberButton.setTitleColor(.gray, for: .disabled)
}
//MARK: - @objc func BUTTON TOUCH
// 函數 `dicesButtonTouched`: 當按鈕被按下時觸發
@objc func dicesButtonTouched(_ sender: UIButton) {
sender.tintColor = .white // 將按鈕圖標的顏色設為白色
sender.backgroundColor = .black // 將按鈕背景設為黑色
}
// 函數 `dicesButtonReleased`: 當按鈕釋放時觸發
@objc func dicesButtonReleased(_ sender: UIButton) {
sender.tintColor = .black // 恢復按鈕圖標顏色為黑色
sender.backgroundColor = .white // 恢復按鈕背景為白色
// 產生物理觸覺反饋
let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
feedbackGenerator.impactOccurred()
// 透過動畫使按鈕逐漸顯示
sender.alpha = 0
UIView.animate(withDuration: 0.5) {
sender.alpha = 1
}
rollButton.isEnabled = true // 啟用擲骰按鈕
// 根據遊戲狀態和按鈕標籤處理遊戲邏輯
if thirdSameDices {
if randomNumerArray.first == sender.tag {
turnOffAllButtons()
sender.updateButtonState(title: "中", color: .systemRed)
topUpLabel.text = "豹子!"
} else {
turnOffAllButtons()
sender.updateButtonState(title: "失", color: .systemGreen)
topUpLabel.text = "答錯!"
}
} else {
if totalNumber <= 9 {
if sender.tag == 0 {
turnOffAllButtons()
sender.updateButtonState(title: "中", color: .systemRed)
topUpLabel.text = "答對!"
} else {
turnOffAllButtons()
sender.updateButtonState(title: "失", color: .systemGreen)
topUpLabel.text = "答錯!"
}
} else {
if sender.tag == 7 {
turnOffAllButtons()
sender.updateButtonState(title: "中", color: .systemRed)
topUpLabel.text = "答對!"
} else {
turnOffAllButtons()
sender.updateButtonState(title: "失", color: .systemGreen)
topUpLabel.text = "答錯!"
}
}
}
sender.setTitleColor(.white, for: .disabled)
time = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(moveCupUpwards), userInfo: nil, repeats: false)
scaleRandomDices(number: 2)
played = true
}
// 函數 `rollButtonReleased`: 當擲骰按鈕釋放時觸發
@objc func rollButtonReleased(_ sender: UIButton) {
// 使用物理觸覺反饋生成器產生反饋
let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
feedbackGenerator.impactOccurred()
topUpLabel.text = "下好離手" // 更新標籤文本
totalNumber = 0 // 重置總數字
if played {
// 如果已經玩過,則執行一系列動畫和計時器
moveCupDownwards() // 移動骰子杯向下
// 設置計時器來執行不同的動作
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(shakeDiceCup), userInfo: nil, repeats: false)
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(createRandomDicePoint), userInfo: nil, repeats: false)
Timer.scheduledTimer(timeInterval: 2.1, target: self, selector: #selector(calculateAndPrintTotalNumber), userInfo: nil, repeats: false)
Timer.scheduledTimer(timeInterval: 2.2, target: self, selector: #selector(checkThirdDices), userInfo: nil, repeats: false)
Timer.scheduledTimer(timeInterval: 2.2, target: self, selector: #selector(resetButton), userInfo: nil, repeats: false)
scaleRandomDices(number: 0.5) // 縮放骰子圖像
} else {
// 如果是第一次玩,則直接執行動作
shakeDiceCup() // 搖動骰子杯
createRandomDicePoint() // 創建隨機骰子點數
calculateAndPrintTotalNumber() // 計算並打印總數字
checkThirdDices() // 檢查三個骰子是否相同
resetButton() // 重置按鈕狀態
}
rollButton.isEnabled = false // 禁用擲骰按鈕,避免重複點擊
}
}
// 輔助函數:更新按鈕狀態並改變上方標籤文字
extension UIButton {
func updateButtonState(title: String, color: UIColor) {
setImage(nil, for: .normal)
setTitle(title, for: .normal)
backgroundColor = color
setTitleColor(.white, for: .normal)
titleLabel?.font = .boldSystemFont(ofSize: 50)
}
}
佈局
佈局主要分為上下兩個區塊,各自有一個 view 套住 stackview,
按鈕、圖片、文字再放進 stackview 做排列和約束
在使用程式碼進行 Autolayout 時需要先關閉自動佈局約束,這個一開始超容易忘記,導致搞不清楚到底哪裡出了問題
topContainerView.translatesAutoresizingMaskIntoConstraints = false // 關閉自動佈局約束,以便手動設置約束
比如說這個區塊就是一個 view (大) 套住一個 stackview
stackview 裡面塞了一個 label、view (小)
view (小) 裡面又塞了 imageview、button
這樣對我來說方便的地方在於適應不同機型的 size 只要設定最大的 view (大)的約束然後讓 stackview 跟著這個 view (大) 就好,至於圖片跟按鈕就是設定與 view (小) 的約束,在使用純程式碼編程時最大的使用 UIStackview 會省力許多,Stackview 本身常用的屬性和方法可以參考以下
但我還是有遇到怎麼樣都無法達到我想要的樣子,只好將排版換成 Stackview 可以做到的樣子😂
按鈕有三個一樣的骰子圖
因為豹子的素材很難找,所以我只想要用系統給的骰子圖案,只是要如何塞入三個圖片呢
使用 UIGraphicsImageRenderer 來幫忙,記得如果有需要在按鈕狀態改變時圖片需要變色記得要 設置圖像渲染模式為alwaysTemplate
// 函數 `createDiceImage`: 用於創建並返回一個包含三個骰子圖像的UIImage物件
func createDiceImage(systerName: String) -> UIImage? {
// 創建一個圖形渲染器,設定圖像大小為寬90點,高30點
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 90, height: 30))
// 使用渲染器生成圖像
let image = renderer.image { ctx in
// 根據提供的系統名稱,加載對應的骰子圖像(可能是SF符號)
let diceImage = UIImage(systemName: systerName)
// 設定單個骰子圖像的大小為寬30點,高30點
let diceSize = CGSize(width: 30, height: 30)
// 繪製三個骰子圖像,水平排列
for i in 0..<3 {
diceImage?.draw(in: CGRect(x: CGFloat(i * 30), y: 0, width: diceSize.width, height: diceSize.height))
}
}
// 將生成的圖像設定為模板渲染模式,這允許它在不同狀態下適應顏色變化
return image.withRenderingMode(.alwaysTemplate) // 設置圖像渲染模式為alwaysTemplate
/*
有時候,按鈕的圖像可能不會響應tintColor的變化,這可能是因為圖像的渲染模式設置不正確。你需要確保圖像的渲染模式設為.alwaysTemplate。
在createDiceImage函數中,在返回圖像之前,嘗試將圖像的渲染模式設置為.alwaysTemplate。
*/
}
只有一種動作的動畫
比如說像是往上移動或是放大可以使用 UIView.animate
// 函數 `scaleRandomDices`: 對骰子圖像進行縮放動畫
func scaleRandomDices (number: CGFloat) {
let scale: CGFloat = number // 縮放的大小
// 使用動畫改變骰子堆疊視圖的縮放比例
UIView.animate(withDuration: 2, animations: {
self.randomDicesStackView.transform = CGAffineTransform(scaleX: scale, y: scale)
})
}
// 函數 `moveCupUpwards`: 向上移動骰子杯
@objc func moveCupUpwards() {
let upwardMovement: CGFloat = 60 // 向上移動的距離
// 使用動畫改變骰子杯的位置
UIView.animate(withDuration: 0.5, animations: {
self.cupImageView.center.y -= upwardMovement
})
}
// 函數 `moveCupDownwards`: 向下移動骰子杯
@objc func moveCupDownwards() {
let downwardMovement: CGFloat = 60 // 向下移動的距離
// 使用動畫改變骰子杯的位置
UIView.animate(withDuration: 0.5, animations: {
self.cupImageView.center.y += downwardMovement
})
}
二種動作的動畫
可以使用 CABasicAnimation,在參數欄內需要輸入預設的字串
(keyPath: “這裡輸入預設動作”),可以參考
// 函數 `shakeDiceCup`: 實現骰子杯的搖動動畫
@objc func shakeDiceCup() {
let animation = CABasicAnimation(keyPath: "transform.rotation") // 創建旋轉動畫
animation.duration = 0.1 // 持續時間
animation.repeatCount = 5 // 重複次數
animation.autoreverses = true // 動畫執行完畢後自動反向執行
animation.fromValue = NSNumber(value: Double.pi / 15) // 起始角度
animation.toValue = NSNumber(value: -Double.pi / 15) // 終止角度
cupImageView.layer.add(animation, forKey: "shakeAnimation") // 將動畫添加到骰子杯圖層
}
確認陣列內元素是否值相同
第一次使用這個 allSatisfy
// 函數 `checkThirdDices`: 檢查三個骰子是否全部相同
@objc func checkThirdDices() {
if let firstNumber = randomNumerArray.first {
// 使用allSatisfy函數檢查數組中所有元素是否與第一個元素相同
let allSame = randomNumerArray.allSatisfy{ $0 == firstNumber }
/*
要檢查一個整數陣列中的所有元素是否相同,我們可以使用 allSatisfy 函數。
allSatisfy 會檢查集合中的所有元素是否都滿足某個條件。如果所有元素都滿足給定的條件,則該函數返回 true;否則返回 false。
*/
thirdSameDices = allSame // 根據結果更新thirdSameDices變量
print("thirdSameDices = \(thirdSameDices)")
}
}
計算骰盅內所有骰子點數和
本來是用 for in 一個一個加的,後來有想起來 reduce
// 函數 `calculateAndPrintTotalNumber`: 計算並打印骰子數字的總和
@objc func calculateAndPrintTotalNumber() {
// 使用reduce函數計算randomNumerArray中所有數字的總和
totalNumber = randomNumerArray.reduce(0, { $0 + $1 })
print("total Number is: \(totalNumber)") // 打印總和
}