天天看点

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

在本章中,你将学会使用​

​SwiftUI​

​​搭建一个计时器​

​App​

​。

前言

为了更加熟悉和了解​

​SwiftUI​

​,本系列将从实战角度出发完成100个SwiftUI项目,方便大家更好地学习和掌握​

​SwiftUI​

​。

这同时也是对自己学习​

​SwiftUI​

​过程的知识整理。

如有错误,以你为准。

项目搭建

首先,创建一个新的​

​SwiftUI​

​​项目,命名为​

​Timer​

​。

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

逻辑分析

计时器的原理比较简单,对于用户而言主要操作就3个:开始、暂停、复位。

用户点击开始按钮,计时器上的文字开始按照时间累加,点击暂停时,计时器的数字停止并展示暂停时的数字,点击复位按钮,则计时器重新归零。

但其中还是会有一些容易遗忘的逻辑,比如刚开始时,用户只能点击开始按钮,系统隐藏或者禁用暂停和复位操作。

而计时器开始计时后,用户只能点击暂停操作,系统隐藏或者禁用开始和复位操作。点击暂停按钮后,用户才能点击复位操作。

页面样式

了解完计时器的逻辑之后,我们来完成页面样式的设计。

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

App标题

​App​

​​标题,我们使用​

​Text​

​文本作为标题样式,示例:

// 计时器标题
func titleView() -> some View {
    HStack {
        Text("计时器")
            .font(.title)
            .fontWeight(.bold)
        Spacer()
    }
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

为了让​

​App​

​​更加美观,我们在​

​Assets​

​​文件中导入了一张图片作为​

​App​

​主视图的展示,示例:

// 图片
func dinnerImageView() -> some View {
    Image("dinner")
        .resizable()
        .scaledToFit()
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

上述代码中,我们给​

​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)
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

上述代码中,我们使用​

​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))
    }
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

上述代码中,我们构建了一个圆形背景,设置大小为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))
        }
    }
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

整体样式布局

整体样式部分,由于操作区存在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)
}      
SwiftUI100天:使用SwiftUI搭建一个计时器App

这样,样式部分我们就设计好了。

计时方法

方法创建

计时的方法主要使用到了​

​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​

​开始计时的方法继续计时。

计时复位

对于复位操作,我们要简单很多,我们只需要在点击时将​

​isStart​

​​、​

​isPause​

​​更新为​

​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"      

完成后,我们预览下项目成果。

项目预览

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

本章完整代码

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()
    }
}