SwiftUI100天:使用SwiftUI搭建一个计时器App

1,664 阅读6分钟

在本章中,你将学会使用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()
    }
}

不错不错!

如果本专栏对你有帮助,不妨点赞、评论、关注~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿