swiftPractice[19]_聖誕倒數月曆

CAEmitterLayer, 亂數 & Gesture Recognizer 練習

Tania
彼得潘的 Swift iOS / Flutter App 開發教室
16 min readDec 7, 2023

--

HOHO! 聖誕節快到了,想到 Line 或是 Google 都會有一些應景的畫面或是小遊戲, 這次的練習參考了 Peter 最新聖誕倒數月曆作業+下雪動畫, 做了一個每日隨機抽卡的 APP, 內容來自正向能量滿滿的彩虹卡, 做的時候還會不小心唱起聖誕歌~🎄🎄🎄

成品

✦ 假設今天日期是 12/17 (17號之後的按鈕不能按)

APP 功能 & 練習項目:

  1. 功能:點選數字顯示卡片,卡片內容隨機,還沒到的日期不能點選
  2. 功能:點選卡片回到數字的樣子,選過的數字變灰色
  3. 使用 CAEmitterLayer 製作下雪動畫

拆解!

目錄:
1.拉 IBOutlet & IBAction
2.設定初始畫面
2-1.使用 CAEmitterLayer 製作下雪動畫
補充:使用#Preview macro即時預覽畫面
3.功能:點選數字顯示卡片(內容隨機),還沒到的日期不能點選
4.功能:點選卡片回到數字的樣子

1. 拉 IBOutlet & IBAction

2. 設定初始畫面

✦ 主要是設定彈出卡片的內容與樣式,然後在使用 .isHidden 將卡片隱藏,方便之後按下按鈕可以顯示

override func viewDidLoad() {
super.viewDidLoad()
//設定彈出畫面樣式
popUpImageView.layer.cornerRadius = 10

//文字內容設定
textLabel.font = .systemFont(ofSize: 27)
textLabel.numberOfLines = 0
textLabel.backgroundColor = .white.withAlphaComponent(CGFloat(0.85))
textLabel.textAlignment = .center
textLabel.text = contents[0]

//日期標籤設定
dayLabel.font = .systemFont(ofSize: 90, weight: UIFont.Weight.semibold)
dayLabel.textAlignment = .center
dayLabel.textColor = UIColor(red: 73/255 , green: 88/255, blue: 0, alpha: 1)

popUpImageView.addSubview(textLabel)
popUpImageView.addSubview(dayLabel)

//隱藏卡片
blurView.isHidden = true
popUpImageView.isHidden = true
}

2-1. 使用 CAEmitterLayer 製作下雪動畫

✦ 這是最好玩又療癒的部分!需要先設定 CAEmitterCell 的各種條件,再藉由 CAEmitterLayer 呈現出來,搭配最新的 #Preview macro 功能可以邊預覽邊微調!

        //下雪動畫

let snowEmitterCell = CAEmitterCell()
//設定雪花照片
snowEmitterCell.contents = UIImage(named: "snowflakes")?.cgImage
//設定每秒生成的雪花數量
snowEmitterCell.birthRate = 0.5
//設定雪花可以維持的時間(秒)
snowEmitterCell.lifetime = 50
//速度
snowEmitterCell.velocity = 0.5
//雪花大小
snowEmitterCell.scale = 0.06
//大小範圍+-0.1
snowEmitterCell.scaleRange = 0.1
//y軸加速度,>0向下;<0向上
snowEmitterCell.yAcceleration = 0.8
//旋轉速度
snowEmitterCell.spin = 0.6
//旋轉速度範圍+-0.4
snowEmitterCell.spinRange = 0.4

let snowEmitterLayer = CAEmitterLayer()
//傳入設計好的EmitterCell,可傳多種
snowEmitterLayer.emitterCells = [snowEmitterCell]
//發射位置
snowEmitterLayer.emitterPosition = CGPoint(x: view.bounds.width/2, y: -50)
//發射範圍
snowEmitterLayer.emitterSize = CGSize(width: view.bounds.width, height: 0)
//發射形狀
snowEmitterLayer.emitterShape = .line

view.layer.addSublayer(snowEmitterLayer)

更多細節跟詳細解說可以參考下面文章👇

3. 功能:點選數字顯示卡片(內容隨機),還沒到的日期不能點選

✦ 在 function 外宣告需要的變數,方便在各處使用;並用 array 儲存卡片內容

//內容標籤
var textLabel = UILabel(frame: CGRect(x: 21, y: 130, width: 320, height: 150))
//日期標籤
var dayLabel = UILabel(frame: CGRect(x: 130, y: 20, width: 120, height: 100))
//卡片內容array
var contents = [
"I approach other people confidently and lovingly.",
"When I need help, I ask my heart or the universe.",
"I am not here to satisfy others and live my life according to their ideas.",
"I am still worthy of love when I ask others for help.",
"Everything I need to know I am told at the right time and in the right place.",
"Peace start with myself.",
"I know the difference between true needs and neediness.",
"I am thankful that everything that I need is provided.",
"I greet every new day with love in my heart.",
"I rejoice in the company of friends."]

✦ 使用 DateFormatter 得到當天日期字串,因為是 12 月專屬,我只需要確認日期跟按鈕上的日期是否對等(或大於),確保還沒到的日子不能抽卡!

✦ 使用 sender.titleLabel!.text! 讀出使用者按下的按鍵日期,再轉為 Int 比較

✦ 我準備了 4 種卡片背景、9個卡片內容,皆用亂數方式 Int.random(in: ) 取得,再分別存入對應的位置

✦ 用 .isHidden = false 將卡片顯示出來

✦ 將 sender 讀取到的按鈕用 .configuration?.baseBackgroundColor = .systemGray3 讓被選過的按鈕變成灰色

 @IBAction func tapOnDay(_ sender: UIButton) {

let today = Date()
let formatter = DateFormatter()
//設定日期呈現格式
formatter.dateFormat = "dd"
//生成當日日期字串
let dateString = formatter.string(from: today)

//當天日期>=選擇日期時
if Int(dateString)! >= Int(sender.titleLabel!.text!)!{
//亂數取得背景圖
popUpImageView.image = UIImage(named: "background\(Int.random(in: 1...4))")
//亂數取得卡片內容
textLabel.text = contents[Int.random(in: 0...9)]
//將按鈕對應日期更新到卡片上
dayLabel.text = "\(sender.titleLabel!.text!)"

//顯示卡片
blurView.isHidden = false
popUpImageView.isHidden = false
//選擇過後的按鈕變灰色
sender.configuration?.baseBackgroundColor = .systemGray3
//當天日期<選擇日期時不能點選
}else{
sender.isUserInteractionEnabled = false
}
}

4. 功能:點選卡片回到數字的樣子

✦ 在 popUpImageView 加入 TapGestureRecognizer,拉一條 IBAction,當使用者點擊卡片時將卡片與模糊背景隱藏,回到最開始的數字畫面

✦ 這裡的 BlurView 使用的是 Object Library 裡的 Visual Effect View with Blur

@IBAction func tapGoBack(_ sender: UITapGestureRecognizer) {
blurView.isHidden = true
popUpImageView.isHidden = true
}

完整程式碼

import UIKit

class XmasViewController: UIViewController {

@IBOutlet weak var popUpImageView: UIImageView!
@IBOutlet weak var blurView: UIVisualEffectView!

var textLabel = UILabel(frame: CGRect(x: 21, y: 130, width: 320, height: 150))
var dayLabel = UILabel(frame: CGRect(x: 130, y: 20, width: 120, height: 100))

var contents = [
"I approach other people confidently and lovingly.",
"When I need help, I ask my heart or the universe.",
"I am not here to satisfy others and live my life according to their ideas.",
"I am still worthy of love when I ask others for help.",
"Everything I need to know I am told at the right time and in the right place.",
"Peace start with myself.",
"I know the difference between true needs and neediness.",
"I am thankful that everything that I need is provided.",
"I greet every new day with love in my heart.",
"I rejoice in the company of friends."]

override func viewDidLoad() {
super.viewDidLoad()

let snowEmitterCell = CAEmitterCell()
snowEmitterCell.contents = UIImage(named: "snowflakes")?.cgImage
snowEmitterCell.birthRate = 0.5
snowEmitterCell.lifetime = 50
snowEmitterCell.velocity = 0.5
snowEmitterCell.scale = 0.06
snowEmitterCell.scaleRange = 0.1
snowEmitterCell.yAcceleration = 0.8
snowEmitterCell.spin = 0.6
snowEmitterCell.spinRange = 0.4

let snowEmitterLayer = CAEmitterLayer()
snowEmitterLayer.emitterCells = [snowEmitterCell]
snowEmitterLayer.emitterPosition = CGPoint(x: view.bounds.width/2, y: -50)
snowEmitterLayer.emitterSize = CGSize(width: view.bounds.width, height: 0)
snowEmitterLayer.emitterShape = .line

view.layer.addSublayer(snowEmitterLayer)

//設定彈出畫面樣式
popUpImageView.layer.cornerRadius = 10

//文字內容設定
textLabel.font = .systemFont(ofSize: 27)
textLabel.numberOfLines = 0
textLabel.backgroundColor = .white.withAlphaComponent(CGFloat(0.85))
textLabel.textAlignment = .center
textLabel.text = contents[0]

//日期標籤設定
dayLabel.font = .systemFont(ofSize: 90, weight: UIFont.Weight.semibold)
dayLabel.textAlignment = .center
dayLabel.textColor = UIColor(red: 73/255 , green: 88/255, blue: 0, alpha: 1)

popUpImageView.addSubview(textLabel)
popUpImageView.addSubview(dayLabel)

blurView.isHidden = true
popUpImageView.isHidden = true
}

//點選數字顯示卡片(內容隨機),還沒到的日期不能點選
@IBAction func tapOnDay(_ sender: UIButton) {

let today = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd"
let dateString = formatter.string(from: today)

if Int(dateString)! >= Int(sender.titleLabel!.text!)!{
popUpImageView.image = UIImage(named: "background\(Int.random(in: 1...4))")
textLabel.text = contents[Int.random(in:0...9)]
dayLabel.text = "\(sender.titleLabel!.text!)"

blurView.isHidden = false
popUpImageView.isHidden = false
sender.configuration?.baseBackgroundColor = .systemGray3
}else{
sender.isUserInteractionEnabled = false
}
}
//點選卡片回到數字的樣子
@IBAction func tapGoBack(_ sender: UITapGestureRecognizer) {
blurView.isHidden = true
popUpImageView.isHidden = true

}

問題!!!!!

問題 1: 有改變按鈕顏色卻不需要拉 IBOutlet ?

我一開始拉了所有按鈕的 IBOutlet,但是完成後越看越覺得好像不需要,於是實驗性的刪了發現程式還是可以跑!於是我問了ChatGPT

我的理解是:因為 sender 徵測到的是 UIButton,我要更改的剛好就是每一個選到的 Button 自己的屬性,所以可以直接讀取更改;但如果我要更改其他不是使用者當前按下的 Button 的屬性,就會需要用到 IBOutlet

問題 2: 要怎麼儲存亂數取得的內容到對應的 Button 裡?(待解決)

當前的程式會遇到一個問題,當我重按過去選到的 Button 時會得到一組新的內容,表示前幾天抽到的卡再也不會看到,每次點只會得到一張新卡。

目前找到的方向是要存在 dictionary 裡面,但是有點超出範圍也花太多時間了,決定先留著問題,等想到再回來升級它!

作業出處:

GitHub

--

--