番茄鐘 – 專注草原|Timer, Player

分享
番茄鐘 – 專注草原|Timer, Player

一款簡約的番茄鐘 APP,讓你在令人心曠神怡的專注草原上,每次前進一小步,並在蟲鳴鳥叫聲中小憩片刻。

作品介紹

一款簡約的番茄鐘 APP,讓你在令人心曠神怡的專注草原上,每次前進一小步,並在蟲鳴鳥叫聲中小憩片刻。

學習目標

  1. 倒數計時:Timer
  2. 進度條控制:progressBar
  3. 音檔播放:AVAudioPlayer

練習方向

  1. 設計版面 UI 元素
  2. 用 IBOutlet 設計 UI 介面
  3. 加入程式碼
  4. 測試與優化

練習步驟

Step 1: 建立 UI

首先,用 Xcode 的 Storyboard 來設計專注草原。

@IBOutlet weak var textLabel: UILabel!
@IBOutlet weak var countdownLabel: UILabel!
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var controlButton: UIButton!

Step 2: 寫 Swift Code

  1. 連接 UI 元件到 ViewController。
  2. 宣告變數。
var timer: Timer? // 計時器
var secondsRemaining = 5 // 總秒數 25 分鐘 x 60 秒 = 1500 秒,為了方便測試先用 5 秒
let totalSeconds = 5 // 計算進度條的總秒數
var player: AVAudioPlayer!

A. 控制番茄鐘的開始和暫停

番茄鐘會有 3 種狀態以及對應的按鈕文字

  1. 全新狀態|尚未開始:開始專注
  2. 進行狀態|倒數中:停止
  3. 預設狀態|倒數完成:重新開始

尚未開始

  • 這是當 timer 是 nil(番茄鐘沒有啟動)且 secondsRemaining 是0(時間還沒開始倒數)的時候。
  • 按鈕文字為:「開始專注」

倒數中

  • timer 是 nil(番茄鐘沒有啟動)但 secondsRemaining 不是0(還有一些時間剩下)時,表示番茄鐘正在倒數,可以隨時暫停。
  • 按鈕文字為:「停止」

倒數完成

倒數完成功能是在最後測試優化階段的時候加上的,發現只有開始和停止的話,番茄鐘跑完就跑完了,沒辦法歸零重啟,因此在判斷式最後加上「重新開始」功能。

  • timer 不是 nil(番茄鐘有被開啟)的時候,就出現預設文字。
  • 按鈕文字為:「重新開始」

ChatGPT 說明關於 for: .normal :

在UIKit中,for: .normal 是用於設定 UIButton 的特定狀態。
UIButton 有多種不同的狀態,例如:
  • .normal: 按鈕的正常狀態。
  • .highlighted: 按鈕被按下,但還沒放開的狀態。
  • .disabled: 按鈕被禁用的狀態。
  • .selected: 按鈕被選中的狀態。
@IBAction func controlButtonPressed(_ sender: UIButton) {
    if timer == nil { // 檢查 timer 變數如果是 nil,表示計時器目前沒有開始或已被停止。
        if secondsRemaining == 0 {
          // Reset the timer to the default value
            secondsRemaining = totalSeconds
            updateUI()
            controlButton.setTitle("開始專注", for: .normal)
        } else {
            startTimer()
            controlButton.setTitle("停止", for: .normal)
        }
    } else {
        stopTimer()
        controlButton.setTitle("重新開始", for: .normal)
    }
}

要設定下方倒數按鈕的 function 需要拖拉按鈕

@IBAction func controlButtonPressed(_ sender: UIButton) 


點擊 button,從右側Connection Inspector找到Touch Up Inside

點擊 button 後,在右側形狀像圓圈中帶有右箭頭的 icon “Connection Inspector” 找到 “Touch Up Inside”,拖拉到 veiewController 這邊


命名 controlButtonPressed 並且設定 type 為 UIbutton

B. 開始計時

startTimer() 函數啟動一個計時器,每秒減少 secondsRemaining 的值並更新UI,直到時間完全倒數完畢。

使用 Timer 類別的 scheduledTimer 方法來建立一個計時器。

這個計時器每一秒 ( withTimeInterval: 1 ) 就會觸發一次,因為 repeats 設定為 true

這裡用判斷式,檢查時間倒數完了沒?

  • 如果還有剩餘時間,那每次計時器觸發,就將 secondsRemaining 減 1,代表時間過去了一秒。
  • 倒數過程,呼叫 updateUI() 函數來更新 UI 介面,顯示新的剩餘時間並更新進度條。
  • 如果 secondsRemaining 不大於 0(時間已經倒數完畢),則標題文字為「時間到!」。

ChatGPT 說明關於 self :

在 Swift 中,self 是一個特殊的關鍵字,用於指代當前的實例或物件。使用 self 可以區分屬性或方法與局部變數之間的名稱衝突,並明確指示我們正在存取或修改的是物件的屬性還是方法。
在您提供的 startTimer() 函數中,我們在閉包 (closure) 或區塊 (block) 內部存取或修改物件的屬性或方法,這時通常需要明確使用 self 來指代當前的實例。
func startTimer(){
    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
        if self.secondsRemaining > 0 { //檢查 secondsRemaining 是否大於 0。如果是,代表還有時間尚未倒數完畢。
            self.secondsRemaining -= 1 
            self.updateUI()
        } else {
            self.textLabel.text = "時間到!"
            self.playSound()
            self.stopTimer()
        }
    })
}

更新 UI 設定更新 UI 的 function,更新用戶界面,讓它反映目前的倒數時間和進度。 根據目前剩餘的秒數(secondsRemaining)來更新倒數標籤的文本和進度條的進度。

C. 更新UI

以下分為兩個部分:countdownLabelprogressBar

countdownLabel 的文本設定為 MM:SS 的格式,例如 “02:05″。

countdownLabel.text = "\(secondsRemaining / 60):\(String(format: "%02d", secondsRemaining % 60))"
  1. countdownLabel.text:這裡設定 countdownLabel(一個 UILabel)的文本屬性,以顯示倒數時間。
  2. "\(secondsRemaining / 60)":這部分計算剩餘的分鐘數。例如,如果 secondsRemaining 是 125,那麼 125 除以 60 等於 2,表示還剩下 2 分鐘。
  3. "\(String(format: "%02d", secondsRemaining % 60))":這部分計算剩餘的秒數,並格式化為兩位數。
    %02d 是一個格式化字符串,表示如果秒數小於 10(例如 9),它會顯示為 09。
    使用 % 取得 secondsRemaining 的餘數是為了得到秒數,例如 125 秒 % 60 = 5 秒。

接著是設定 progressBar 的 progress 屬性,用 progressBar.progress 呈現進度條的完成程度。

progressBar.progress = 1.0 - Float(secondsRemaining) / Float(totalSeconds)
  1. Float(secondsRemaining) / Float(totalSeconds):這部分計算的是已經過去的時間所佔的比例。如果總時間(totalSeconds)是 1500 秒,並且還剩下 750 秒(secondsRemaining),那這個比例就是 0.5。
  2. 1.0 - ...:由於我們想要了解倒數的進度,所以我們將 1 減去上述的比例,這樣當時間開始時,進度條會是 0,而當時間結束時,進度條會是 1(或 100%)。
func updateUI() {
    countdownLabel.text = "\(secondsRemaining / 60):\(String(format: "%02d", secondsRemaining % 60))"
    progressBar.progress = 1.0 - Float(secondsRemaining) / Float(totalSeconds)
}

D. 停止計時

func stopTimer() {
    timer?.invalidate()
    timer = nil
}

Step 3: 測試與優化

  1. 倒數計時歸零後,應該要可以重新開始計時 可以參考 Step 2
  2. 關於增加聲音,原本一開始是用 ! 強制展開,結果 APP 只要遇到播音檔就閃退
func playSound() {
    let url = Bundle.main.url(forResource: "nature-soundstropicaljunglebirds", withExtension: "mp3")
    player = try! AVAudioPlayer(contentsOf: url!)
    player.play()
}

根據 ChatGPT 的建議,改成以下判斷式之後,才避免閃退,並發現問題在「未找到音檔」!

func playSound() {
    if let url = Bundle.main.url(forResource: "nature-soundstropicaljunglebirds", withExtension: "mp3") {
        do {
            player = try AVAudioPlayer(contentsOf: url)
            player.play()
        } catch {
            print("音檔播放錯誤: \(error.localizedDescription)")
        }
    } else {
        print("未找到音檔")
    }
}

檢查 “Build Phases” ,展開 “Copy Bundle Resources“,發現沒有 mp3 音檔

才發現原來是我沒有把音檔正確拖曳到專案導覽器(Project Navigator)。

音檔有放進 assets 資料夾,但不是放進跟 viewcontroller 同一層資料夾,因此抓不到。我直接將 mp3 檔拖曳到 viewcontroller 同一層資料夾,問題就解決了!

一拖曳過去,果然就可以正常播放音檔了~

影片 00:12 處開始播放聲音

這次經驗,讓我學習到:

  1. 如果之後遇到 APP 會閃退,那可以將 code 改為可以印出「問題狀況」的形式,觀察問題點在哪裡,再對症下藥。
  2. Timer 和 progressBar 需要考慮不同的倒數狀態、總秒數和剩餘秒數的轉換。
  3. 音檔需要放在跟 ViewController 同一層資料夾。
  4. 對 self 、 UIbutton .normal 的實際應用還需要再了解。

參考資料

Read more

第一張 AI 證照 AIF-C01 考試準備心得|AWS Certified AI Practitioner

第一張 AI 證照 AIF-C01 考試準備心得|AWS Certified AI Practitioner

2025 年的此刻,AI 工具早就像魔法一樣融入我的日常,搜尋用 Perplexity、行銷文案靠 ChatGPT、Vibe coding 則用 Cursor 搭配 Claude。 雖然用得很順手,但有時總覺得自己只會無腦放魔法——知道怎麼用,卻不太懂原理。 這讓我想到《葬送的芙莉蓮》裡的費倫。她雖然一開始只會反覆練習基礎魔法,但正因為打好了紮實的基本功,到了關鍵時刻,反而能發揮出比其他魔法使更強大的威力。我也相信,學習 AI 不能只停留在「會用」的層次,基礎觀念才是未來進階的關鍵。 所以,當我看到 2025 AWS Educate 證照陪跑計畫的宣傳,覺得這正是補足基礎、理解 AI 原理的好機會。線上直播課程教觀念、工作坊學實作,還有半價考證照和精美禮物等超多誘因,當然要把握,決定衝一波人生第一張 AWS 國際證照🚀 受惠於前輩們的分享,

By Kyle Lu
AI 如何重塑創作與職涯?

【AI EXPO Taiwan 2025 博覽會】AI 如何重塑創作與職涯?

參觀了 AI EXPO Taiwan 博覽會之後,我對 AI 如何改變內容創作、個人品牌打造和職涯發展有些想法。無論你是創作者、行銷人還是對 AI 有興趣,這次分享也許能帶給你一點啟發。 我觀察到創作者為了隨時跟粉絲互動,製作 AI 分身;想打造爆紅影片,用 AI 分析架構,模仿並超越。我們讓 AI 輔助加速創作過程,善用 AI 增加網路影響力。但越來越多人都會用 AI 的時候,我們的核心競爭力是什麼? AI 內容創作與個人影響力 當多數人都在討論 AI 如何取代工作,也許可以把握「如何善用 AI 擴大個人影響力」的機會。 FansNetwork 創辦人 李婷婷分享了有趣的 AI 分身應用。作為擁有超過 10

By Kyle Lu
關於文組點技能在 iOS 開發者這檔事

關於文組點技能在 iOS 開發者這檔事

「欸?我現在的工作好像有 1/3 都能用 AI 做了!」 2023 年中,當我發現自己思考完行銷策略後,就看著 AI 輸出行銷大綱和社群文案,內心莫名慌了一下。我開始認真思考:這樣下去,行銷人的未來在哪裡? 當 AI 成為最強戰友,是危機還是轉機? 說真的,當我第一次用 AI 幫忙分析 Persona 時,我驚呆了。它不只能根據目標市場和產品特點描繪出詳細的用戶形象,還能提供這個目標客群可能感興趣的行銷主題,根據這些主題產出一整個月的貼文草稿,甚至貼心的加上 emoji 和熱門 hashtag! 不只是內容創作,連 OKR 目標拆解和數據分析,AI 都能幫上大忙。以前要花一整天完成的工作,現在大概只需要 2-3 小時,我只需要專注在最後的微調和關鍵決策上。 未來的行銷部門,會不會變成一個人帶著一群 AI 助理就能搞定所有事?

By Kyle Lu
《蛤蟆先生去看心理師》閱讀心得

《蛤蟆先生去看心理師》閱讀心得

最近總是提不起勁,覺得什麼都做不好?你可以把《蛤蟆先生去看心理師》當書籍版的 Netflix 看。 這不只是一個關於心理諮商的故事,還巧妙地將心理學理論融入其中。作者的文筆生動、富有童趣,看諮商過程就像在追劇一樣停不下來。 透過蛤蟆先生和心理師蒼鷺的十次諮商,你會見證療癒與改變的發生。書中探討了憂鬱、自卑、潛意識、自我批判等心理問題,並運用了溝通分析理論來協助蛤蟆面對內在的小孩和成人狀態。 誰是當事人?心理諮商的第一步 在《蛤蟆先生去看心理師》中,蒼鷺心理師提出一個重要問題:「誰才是我的當事人?」這是對諮商對象一個很好的提醒。 不管別人再怎麼苦口婆心,如果自己沒有意識到需要找人聊聊/訴苦/諮商,那就跟對牛彈琴沒兩樣,牛只會覺得你很吵而已。 增加心理諮商效果的祕訣:信任加上合作 要讓諮商有效果,首要之務就是建立「信任」。這就像是相信我家巷口的滷肉飯一定好吃一樣,有了信任,才能開始「合作」關係。我很喜歡蒼鷺不斷邀請蛤蟆「合作」找出原因,這種平等關係讓人感覺不是在「花錢聽課」,而是在共同探索。 這提醒了我,

By Kyle Lu