SwiftUI100天:使用SwiftUI搭建一個計時器App

語言: CN / TW / HK

theme: smartblue

在本章中,你將學會使用SwiftUI搭建一個計時器App

前言

為了更加熟悉和了解SwiftUI,本系列將從實戰角度出發完成100個SwiftUI專案,方便大家更好地學習和掌握SwiftUI

這同時也是對自己學習SwiftUI過程的知識整理。

如有錯誤,以你為準。

專案搭建

首先,建立一個新的SwiftUI專案,命名為Timer

1.png

邏輯分析

計時器的原理比較簡單,對於使用者而言主要操作就3個:開始暫停復位

使用者點選開始按鈕,計時器上的文字開始按照時間累加點選暫停時,計時器的數字停止並展示暫停時的數字,點選復位按鈕,則計時器重新歸零

但其中還是會有一些容易遺忘的邏輯,比如剛開始時,使用者只能點選開始按鈕,系統隱藏或者禁用暫停和復位操作。

而計時器開始計時後,使用者只能點選暫停操作,系統隱藏或者禁用開始和復位操作。點選暫停按鈕後,使用者才能點選復位操作。

頁面樣式

瞭解完計時器的邏輯之後,我們來完成頁面樣式的設計。

2.png

App標題

App標題,我們使用Text文字作為標題樣式,示例:

// 計時器標題 func titleView() -> some View {     HStack {         Text("計時器")             .font(.title)             .fontWeight(.bold)         Spacer()     } }

3.png

為了讓App更加美觀,我們在Assets檔案中匯入了一張圖片作為App主檢視的展示,示例:

// 圖片 func dinnerImageView() -> some View {     Image("dinner")         .resizable()         .scaledToFit() }

4.png

上述程式碼中,我們給Image圖片設定了2個修飾符,進行等比例縮放

這樣,我們就得到了標題和App示例圖片。

計時文字

計時文字部分,首先我們需要宣告一個變數儲存我們的計時數值,示例:

@State var timeText: String = "0.00"

然後,我們可以使用Text繫結並展示計時的文字,示例:

// 計時文字 func timerTextView() -> some View {     Text(timeText)         .font(.system(size: 48))         .padding(.horizontal)         .background(Color(.systemGray6))         .cornerRadius(8) }

5.png

上述程式碼中,我們使用Text文字樣式,繫結timeText引數,並使用了一些修飾符設定了文字的大小、計時文字的排布位置、背景顏色和圓角。

操作按鈕

對於操作按鈕部分,我們需要3個按鈕:開始按鈕、暫停按鈕、復位按鈕。

開始按鈕

開始按鈕部分,由於和其他按鈕樣式分離,我們可以單獨構建,示例:

// 開始按鈕 func startBtn() -> some View {     ZStack {         Circle()             .frame(width: 60, height: 60)             .foregroundColor(.green)         Image(systemName: "play.fill")             .foregroundColor(.white)             .font(.system(size: 32))     } }

6.png

上述程式碼中,我們構建了一個圓形背景,設定大小為60*60,顏色為綠色。按鈕本身使用Apple提供的系統圖標,設定尺寸為32,填充顏色為白色

暫停和復位

當我們點選開始按鈕,那麼操作按鈕就會變成2個:暫停和復位

其中,暫停按鈕有2種狀態,一種是未操作時,一種則是已經點選暫停,因此我們需要宣告一個是否暫停的變數來儲存它,示例:

@State var isPause: Bool = false

然後和開始按鈕一樣,我們構建暫停和復位按鈕的樣式,示例:

``` // 暫停和復位按鈕 func pauseAndResetBtn() -> some View {     HStack(spacing: 60) {         // 暫停按鈕         ZStack {             Circle()                 .frame(width: 60, height: 60)                 .foregroundColor(.red)             Image(systemName: isPause ? "play.fill" : "pause.fill")                 .foregroundColor(.white)                 .font(.system(size: 32))         }

// 復位按鈕         ZStack {             Circle()                 .frame(width: 60, height: 60)                 .foregroundColor(.blue)             Image(systemName: "arrow.uturn.backward.circle.fill")                 .foregroundColor(.white)                 .font(.system(size: 32))         }     } } ```

7.png

整體樣式佈局

整體樣式部分,由於操作區存在2種樣式,一種是點選開始前,一種是點選計時開始,我們還需要宣告一種是否開始的狀態儲存它,示例:

@State var isStart: Bool = true

最後是樣式的整體部分,我們在body中佈局樣式,示例:

``` var body: some View {     VStack(spacing: 20) {         titleView()         dinnerImageView()         timerTextView()         Spacer()

//操作按鈕         if isStart {             pauseAndResetBtn()         } else {             startBtn()         }     }     .padding()     .padding(.bottom, 40) } ```

8.png

這樣,樣式部分我們就設計好了。

計時方法

方法建立

計時的方法主要使用到了Timer函式,首先我們要宣告兩個變數,一個用來更新復位後的時間,一個用來計數,示例:

@State private var startTime = Date() @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

然後建立兩個方法,一個用來開始計數,一個用來停止計數,示例:

``` // 開始計時方法 func startTimer() {     timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() }

// 停止計時方法 func stopTimer() {     timer.upstream.connect().cancel() } ```

開始計時

然後在點選開始按鈕時,呼叫開始計數的方法,示例:

// 開始按鈕 func startBtn() -> some View {     ZStack {         Circle()             .frame(width: 60, height: 60)             .foregroundColor(.green)         Image(systemName: "play.fill")             .foregroundColor(.white)             .font(.system(size: 32))     }.onTapGesture {         self.isStart = true         timeText = "0.00"         startTime = Date()         self.startTimer()     } }

上述程式碼中,我們使用onTapGesture修飾符給開始按鈕新增互動,當我們點選開始按鈕時,首先轉換isStart狀態,這樣我們的操作按鈕樣式就會切換到暫停和復位的操作。

然後是timeText初始化展示內容為0.00,然後startTime從當前timeText開始,再呼叫startTimer方法開始計時。

停止計時

停止計時方法也很簡單,不過這裡要注意的是,暫停按鈕承載了暫時和繼續計時的操作,示例:

// 暫停和復位按鈕 func pauseAndResetBtn() -> some View {     HStack(spacing: 60) {         // 暫停按鈕         ZStack {             Circle()                 .frame(width: 60, height: 60)                 .foregroundColor(.red)             Image(systemName: isPause ? "play.fill" : "pause.fill")                 .foregroundColor(.white)                 .font(.system(size: 32))         }         .onTapGesture {             if !isPause {                 self.isPause = true                 self.stopTimer()             } else {                 self.isPause = false                 self.startTimer()             }         } }

上述程式碼中,我們也給暫停按鈕添加了互動,當我們isPause沒有停止時,我們點選暫停按鈕,則isPause狀態切換為停止,這樣我們對應的暫停按鈕的樣式也會切換,然後呼叫stopTimer停止計時的方法。

而當我們暫停的時候點選暫停按鈕時,我們切換isPause狀態更新樣式,同時又呼叫startTimer開始計時的方法繼續計時。

計時復位

對於復位操作,我們要簡單很多,我們只需要在點選時將isStartisPause更新為false,最後把計時展示文字timeText更新為0.00就可以了。程式碼如下:

// 復位按鈕 ZStack {     Circle()         .frame(width: 60, height: 60)         .foregroundColor(.blue)     Image(systemName: "arrow.uturn.backward.circle.fill")         .foregroundColor(.white)         .font(.system(size: 32)) } .onTapGesture {     self.isStart = false     self.isPause = false     timeText = "0.00" }

完成後,我們預覽下專案成果。

專案預覽

9.gif

本章完整程式碼

``` import SwiftUI

struct ContentView: View {     @State var timeText: String = "0.00"     @State var isPause: Bool = false     @State var isStart: Bool = false     @State private var startTime = Date()     @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var body: some View {         VStack(spacing: 20) {             titleView()             dinnerImageView()             timerTextView()             Spacer()             // 操作按鈕             if isStart {                 pauseAndResetBtn()             } else {                 startBtn()             }         }         .padding()         .padding(.bottom, 40)     }

// 計時器標題     func titleView() -> some View {         HStack {             Text("計時器")                 .font(.title)                 .fontWeight(.bold)             Spacer()         }     }

// 圖片     func dinnerImageView() -> some View {         Image("dinner")             .resizable()             .scaledToFit()     }

// 計時文字     func timerTextView() -> some View {         Text(timeText)             .font(.system(size: 48))             .padding(.horizontal)             .background(Color(.systemGray6))             .cornerRadius(8)             .onReceive(timer) { _ in                 if self.isStart {                     timeText = String(format: "%.2f", Date().timeIntervalSince(self.startTime))                 }            }     }

// 開始按鈕     func startBtn() -> some View {         ZStack {             Circle()                 .frame(width: 60, height: 60)                 .foregroundColor(.green)             Image(systemName: "play.fill")                 .foregroundColor(.white)                 .font(.system(size: 32))         }.onTapGesture {             self.isStart = true             timeText = "0.00"             startTime = Date()             self.startTimer()         }     }

// 暫停和復位按鈕     func pauseAndResetBtn() -> some View {         HStack(spacing: 60) {             // 暫停按鈕             ZStack {                 Circle()                     .frame(width: 60, height: 60)                     .foregroundColor(.red)                 Image(systemName: isPause ? "play.fill" : "pause.fill")                     .foregroundColor(.white)                     .font(.system(size: 32))             }             .onTapGesture {                 if !isPause {                     self.isPause = true                     self.stopTimer()                 } else {                     self.isPause = false                     self.startTimer()                 }             }

// 復位按鈕             ZStack {                 Circle()                     .frame(width: 60, height: 60)                     .foregroundColor(.blue)                 Image(systemName: "arrow.uturn.backward.circle.fill")                     .foregroundColor(.white)                     .font(.system(size: 32))             }             .onTapGesture {                 self.isStart = false                 self.isPause = false                 timeText = "0.00"             }         }     }

// 開始計時方法     func startTimer() {         timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()     }     // 停止計時方法     func stopTimer() {         timer.upstream.connect().cancel()     } } ```

不錯不錯!

如果本專欄對你有幫助,不妨點贊、評論、關注~

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿

「其他文章」