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

简介: 在本章中,你将学会使用SwiftUI搭建一个计时器App。

image.png

前言

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

这同时也是对自己学习SwiftUI过程的知识整理。

如有错误,以你为准。

项目搭建

首先,创建一个新的SwiftUI项目,命名为Timer

image.png

逻辑分析

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

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

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

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

页面样式

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

image.png

App标题

App标题,我们使用Text文本作为标题样式,示例:

// 计时器标题
func titleView() -> some View {
    HStack {
        Text("计时器")
            .font(.title)
            .fontWeight(.bold)
        Spacer()
    }
}
复制代码

image.png

为了让App更加美观,我们在Assets文件中导入了一张图片作为App主视图的展示,示例:

// 图片
func dinnerImageView() -> some View {
    Image("dinner")
        .resizable()
        .scaledToFit()
}
复制代码

image.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)
}
复制代码

image.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))
    }
}
复制代码

image.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))
        }
    }
}
复制代码

image.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)
}
复制代码

image.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"
}
复制代码

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

项目预览

image.png

本章完整代码

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()
    }
}
复制代码

不错不错!

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


相关文章
|
iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(3)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
193 0
|
前端开发 数据处理 iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
108 0
|
iOS开发 Kotlin 容器
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
150 0
|
存储 缓存 前端开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)
153 0
|
前端开发 Swift iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)
250 0
|
存储 PHP Swift
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)
195 0
|
前端开发 iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)
189 0
|
iOS开发 开发者 容器
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)
163 0
|
iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)
161 0
|
Swift iOS开发 容器
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(一)(3)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(一)
195 0

热门文章

最新文章

  • 1
    MNN-LLM App:在手机上离线运行大模型,阿里巴巴开源基于 MNN-LLM 框架开发的手机 AI 助手应用
  • 2
    原生鸿蒙版小艺APP接入DeepSeek-R1,为HarmonyOS应用开发注入新活力
  • 3
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 5
    【Azure App Service】基于Linux创建的App Service是否可以主动升级内置的Nginx版本呢?
  • 6
    【Azure Function】Function App出现System.IO.FileNotFoundException异常
  • 7
    1688APP 原数据 API 接口的开发、应用与收益
  • 8
    PiliPala:开源项目真香,B站用户狂喜!这个开源APP竟能自定义主题+去广告?PiliPala隐藏功能大揭秘
  • 9
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 10
    语音app系统软件源码开发搭建新手启蒙篇
  • 1
    iOS|记一名 iOS 开发新手的前两次 App 审核经历
    10
  • 2
    2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
    17
  • 3
    flutter3-wetrip跨平台自研仿携程app预约酒店系统模板
    25
  • 4
    通过外部链接启动 Flutter App(详细介绍及示例)
    21
  • 5
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    55
  • 6
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    46
  • 7
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    40
  • 8
    【Azure Function】Function App门户上的Test/Run返回错误:Failed to fetch
    36
  • 9
    小游戏源码开发之可跨app软件对接是如何设计和开发的
    42
  • 10
    原生鸿蒙版小艺APP接入DeepSeek-R1,为HarmonyOS应用开发注入新活力
    178