|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
定时器按钮是iOS应用中常见的UI组件,它允许用户启动一个倒计时或正计时功能,并在特定时间间隔执行某些操作。从验证码获取按钮到倒计时器,再到定时任务执行,定时器按钮在各种应用场景中都有广泛的应用。本文将全面介绍如何在Swift中实现定时器按钮功能,从基础概念到高级技巧,帮助开发者掌握这一重要功能,提升应用的专业性和用户体验。
Swift中的定时器基础
在Swift中,我们主要使用Timer类来创建定时器。Timer是Foundation框架中的一个类,它可以在指定的时间间隔后触发一个事件,或者重复触发事件。
Timer的基本用法
- // 创建一个一次性定时器
- let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
- print("定时器触发")
- }
- // 创建一个重复性定时器
- let repeatingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
- print("定时器重复触发")
- }
复制代码
Timer的主要属性和方法
- // 使定时器失效
- timer.invalidate()
- // 检查定时器是否有效
- if timer.isValid {
- print("定时器有效")
- }
- // 获取定时器的触发时间间隔
- let timeInterval = timer.timeInterval
- // 获取定时器的触发日期
- let fireDate = timer.fireDate
复制代码
RunLoop与Timer
定时器需要添加到RunLoop中才能正常工作。当我们使用scheduledTimer方法创建定时器时,它会自动被添加到当前线程的RunLoop中。如果我们需要手动控制定时器的RunLoop,可以使用以下方法:
- // 创建定时器但不自动添加到RunLoop
- let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
- // 将定时器添加到RunLoop
- RunLoop.current.add(timer, forMode: .common)
复制代码
实现简单的定时器按钮
让我们从实现一个简单的定时器按钮开始,这个按钮在点击后开始倒计时,并在倒计时期间禁用。
基本实现
首先,创建一个基本的定时器按钮类:
- import UIKit
- class TimerButton: UIButton {
-
- // 定时器
- private var timer: Timer?
- // 剩余时间
- private var remainingTime: Int = 0
- // 总时间
- private var totalTime: Int = 60
-
- // 初始化方法
- init(frame: CGRect, totalTime: Int = 60) {
- super.init(frame: frame)
- self.totalTime = totalTime
- setupButton()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupButton()
- }
-
- // 设置按钮样式
- private func setupButton() {
- setTitle("获取验证码", for: .normal)
- backgroundColor = .systemBlue
- setTitleColor(.white, for: .normal)
- layer.cornerRadius = 5
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- }
-
- // 按钮点击事件
- @objc private func buttonTapped() {
- startTimer()
- }
-
- // 开始定时器
- private func startTimer() {
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- self?.remainingTime -= 1
- self?.updateButtonTitle()
-
- if self?.remainingTime == 0 {
- self?.stopTimer()
- }
- }
- }
-
- // 停止定时器
- private func stopTimer() {
- timer?.invalidate()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
- }
-
- // 更新按钮标题
- private func updateButtonTitle() {
- setTitle("\(remainingTime)秒后重试", for: .disabled)
- }
-
- // 销毁定时器
- deinit {
- timer?.invalidate()
- }
- }
复制代码
使用示例
在ViewController中使用这个定时器按钮:
- import UIKit
- class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let timerButton = TimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
- view.addSubview(timerButton)
-
- // 设置按钮居中
- timerButton.center.x = view.center.x
- }
- }
复制代码
这个简单的定时器按钮实现了基本的功能:点击后开始倒计时,倒计时期间按钮禁用,倒计时结束后按钮恢复可用状态。
进阶功能 - 自定义定时器按钮
现在,让我们扩展定时器按钮的功能,使其更加灵活和可定制。
可配置的定时器按钮
- import UIKit
- class AdvancedTimerButton: UIButton {
-
- // MARK: - 公开属性
- var countdownDuration: Int = 60 {
- didSet {
- resetButton()
- }
- }
-
- var normalTitle: String = "获取验证码" {
- didSet {
- resetButton()
- }
- }
-
- var countingFormat: String = "%d秒后重试" {
- didSet {
- updateButtonTitle()
- }
- }
-
- var finishedTitle: String = "重新获取" {
- didSet {
- resetButton()
- }
- }
-
- var normalBackgroundColor: UIColor = .systemBlue {
- didSet {
- resetButton()
- }
- }
-
- var disabledBackgroundColor: UIColor = .systemGray {
- didSet {
- updateButtonAppearance()
- }
- }
-
- var normalTitleColor: UIColor = .white {
- didSet {
- resetButton()
- }
- }
-
- var disabledTitleColor: UIColor = .white {
- didSet {
- updateButtonAppearance()
- }
- }
-
- // MARK: - 私有属性
- private var timer: Timer?
- private var remainingTime: Int = 0
- private var isCountingDown: Bool = false
-
- // MARK: - 回调闭包
- var onStartCountdown: (() -> Void)?
- var onTick: ((Int) -> Void)?
- var onFinishCountdown: (() -> Void)?
-
- // MARK: - 初始化
- init(frame: CGRect, countdownDuration: Int = 60) {
- self.countdownDuration = countdownDuration
- super.init(frame: frame)
- setupButton()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupButton()
- }
-
- // MARK: - 设置按钮
- private func setupButton() {
- resetButton()
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- }
-
- // MARK: - 重置按钮
- private func resetButton() {
- isCountingDown = false
- isEnabled = true
- setTitle(normalTitle, for: .normal)
- backgroundColor = normalBackgroundColor
- setTitleColor(normalTitleColor, for: .normal)
- }
-
- // MARK: - 按钮点击事件
- @objc private func buttonTapped() {
- startCountdown()
- }
-
- // MARK: - 开始倒计时
- public func startCountdown() {
- guard !isCountingDown else { return }
-
- isCountingDown = true
- remainingTime = countdownDuration
- isEnabled = false
- updateButtonAppearance()
-
- onStartCountdown?()
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonAppearance()
- self.onTick?(self.remainingTime)
-
- if self.remainingTime == 0 {
- self.stopCountdown()
- }
- }
- }
-
- // MARK: - 停止倒计时
- public func stopCountdown() {
- guard isCountingDown else { return }
-
- timer?.invalidate()
- timer = nil
- isCountingDown = false
- isEnabled = true
- setTitle(finishedTitle, for: .normal)
- backgroundColor = normalBackgroundColor
- setTitleColor(normalTitleColor, for: .normal)
-
- onFinishCountdown?()
- }
-
- // MARK: - 更新按钮外观
- private func updateButtonAppearance() {
- let formattedTitle = String(format: countingFormat, remainingTime)
- setTitle(formattedTitle, for: .disabled)
- backgroundColor = disabledBackgroundColor
- setTitleColor(disabledTitleColor, for: .disabled)
- }
-
- // MARK: - 销毁定时器
- deinit {
- timer?.invalidate()
- }
- }
复制代码
使用示例
- import UIKit
- class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let timerButton = AdvancedTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
-
- // 配置按钮属性
- timerButton.countdownDuration = 10
- timerButton.normalTitle = "发送验证码"
- timerButton.countingFormat = "剩余%d秒"
- timerButton.finishedTitle = "再次发送"
- timerButton.normalBackgroundColor = .systemGreen
- timerButton.disabledBackgroundColor = .systemGray4
-
- // 设置回调
- timerButton.onStartCountdown = {
- print("开始倒计时")
- }
-
- timerButton.onTick = { remainingTime in
- print("剩余时间: \(remainingTime)秒")
- }
-
- timerButton.onFinishCountdown = {
- print("倒计时结束")
- }
-
- view.addSubview(timerButton)
- timerButton.center.x = view.center.x
- }
- }
复制代码
支持多种定时器模式
让我们进一步扩展定时器按钮,使其支持多种定时器模式,如倒计时、正计时和一次性执行等。
- import UIKit
- enum TimerMode {
- case countdown(totalTime: Int) // 倒计时模式
- case countup // 正计时模式
- case singleShot(timeInterval: TimeInterval) // 一次性执行模式
- }
- class MultiModeTimerButton: UIButton {
-
- // MARK: - 公开属性
- var timerMode: TimerMode = .countdown(totalTime: 60) {
- didSet {
- resetButton()
- }
- }
-
- var normalTitle: String = "开始计时" {
- didSet {
- resetButton()
- }
- }
-
- var countingFormat: String = "%d" {
- didSet {
- updateButtonTitle()
- }
- }
-
- var finishedTitle: String = "完成" {
- didSet {
- resetButton()
- }
- }
-
- var normalBackgroundColor: UIColor = .systemBlue {
- didSet {
- resetButton()
- }
- }
-
- var activeBackgroundColor: UIColor = .systemGreen {
- didSet {
- updateButtonAppearance()
- }
- }
-
- var normalTitleColor: UIColor = .white {
- didSet {
- resetButton()
- }
- }
-
- var activeTitleColor: UIColor = .white {
- didSet {
- updateButtonAppearance()
- }
- }
-
- // MARK: - 私有属性
- private var timer: Timer?
- private var elapsedTime: TimeInterval = 0
- private var remainingTime: Int = 0
- private var isActive: Bool = false
-
- // MARK: - 回调闭包
- var onStart: (() -> Void)?
- var onTick: ((TimeInterval) -> Void)?
- var onFinish: (() -> Void)?
- var onSingleShot: (() -> Void)?
-
- // MARK: - 初始化
- init(frame: CGRect, timerMode: TimerMode = .countdown(totalTime: 60)) {
- self.timerMode = timerMode
- super.init(frame: frame)
- setupButton()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupButton()
- }
-
- // MARK: - 设置按钮
- private func setupButton() {
- resetButton()
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- }
-
- // MARK: - 重置按钮
- private func resetButton() {
- isActive = false
- isEnabled = true
- elapsedTime = 0
-
- switch timerMode {
- case .countdown(let totalTime):
- remainingTime = totalTime
- setTitle(normalTitle, for: .normal)
- case .countup:
- setTitle(normalTitle, for: .normal)
- case .singleShot:
- setTitle(normalTitle, for: .normal)
- }
-
- backgroundColor = normalBackgroundColor
- setTitleColor(normalTitleColor, for: .normal)
- }
-
- // MARK: - 按钮点击事件
- @objc private func buttonTapped() {
- if isActive {
- stopTimer()
- } else {
- startTimer()
- }
- }
-
- // MARK: - 开始定时器
- public func startTimer() {
- guard !isActive else { return }
-
- isActive = true
- updateButtonAppearance()
- onStart?()
-
- switch timerMode {
- case .countdown:
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- self?.handleCountdownTick()
- }
- case .countup:
- timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
- self?.handleCountupTick()
- }
- case .singleShot(let timeInterval):
- timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in
- self?.handleSingleShot()
- }
- }
- }
-
- // MARK: - 停止定时器
- public func stopTimer() {
- guard isActive else { return }
-
- timer?.invalidate()
- timer = nil
- isActive = false
- isEnabled = true
- setTitle(finishedTitle, for: .normal)
- backgroundColor = normalBackgroundColor
- setTitleColor(normalTitleColor, for: .normal)
-
- onFinish?()
- }
-
- // MARK: - 处理倒计时
- private func handleCountdownTick() {
- remainingTime -= 1
- updateButtonAppearance()
- onTick?(TimeInterval(remainingTime))
-
- if remainingTime == 0 {
- stopTimer()
- }
- }
-
- // MARK: - 处理正计时
- private func handleCountupTick() {
- elapsedTime += 0.1
- updateButtonAppearance()
- onTick?(elapsedTime)
- }
-
- // MARK: - 处理一次性执行
- private func handleSingleShot() {
- timer?.invalidate()
- timer = nil
- isActive = false
- isEnabled = true
- setTitle(finishedTitle, for: .normal)
- backgroundColor = normalBackgroundColor
- setTitleColor(normalTitleColor, for: .normal)
-
- onSingleShot?()
- onFinish?()
- }
-
- // MARK: - 更新按钮外观
- private func updateButtonAppearance() {
- switch timerMode {
- case .countdown:
- let formattedTitle = String(format: countingFormat, remainingTime)
- setTitle(formattedTitle, for: .disabled)
- case .countup:
- let formattedTitle = String(format: "%.1f", elapsedTime)
- setTitle(formattedTitle, for: .disabled)
- case .singleShot:
- setTitle("执行中...", for: .disabled)
- }
-
- backgroundColor = activeBackgroundColor
- setTitleColor(activeTitleColor, for: .disabled)
- }
-
- // MARK: - 销毁定时器
- deinit {
- timer?.invalidate()
- }
- }
复制代码
使用示例
- import UIKit
- class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setupCountdownButton()
- setupCountupButton()
- setupSingleShotButton()
- }
-
- private func setupCountdownButton() {
- let countdownButton = MultiModeTimerButton(
- frame: CGRect(x: 50, y: 100, width: 200, height: 50),
- timerMode: .countdown(totalTime: 10)
- )
-
- countdownButton.normalTitle = "开始倒计时"
- countdownButton.countingFormat = "剩余%d秒"
- countdownButton.finishedTitle = "倒计时结束"
-
- countdownButton.onStart = {
- print("倒计时开始")
- }
-
- countdownButton.onTick = { time in
- print("倒计时剩余: \(Int(time))秒")
- }
-
- countdownButton.onFinish = {
- print("倒计时结束")
- }
-
- view.addSubview(countdownButton)
- countdownButton.center.x = view.center.x
- }
-
- private func setupCountupButton() {
- let countupButton = MultiModeTimerButton(
- frame: CGRect(x: 50, y: 170, width: 200, height: 50),
- timerMode: .countup
- )
-
- countupButton.normalTitle = "开始计时"
- countupButton.countingFormat = "%.1f秒"
- countupButton.finishedTitle = "停止计时"
-
- countupButton.onStart = {
- print("正计时开始")
- }
-
- countupButton.onTick = { time in
- print("已计时: \(time)秒")
- }
-
- countupButton.onFinish = {
- print("正计时结束")
- }
-
- view.addSubview(countupButton)
- countupButton.center.x = view.center.x
- }
-
- private func setupSingleShotButton() {
- let singleShotButton = MultiModeTimerButton(
- frame: CGRect(x: 50, y: 240, width: 200, height: 50),
- timerMode: .singleShot(timeInterval: 3.0)
- )
-
- singleShotButton.normalTitle = "3秒后执行"
- singleShotButton.finishedTitle = "执行完成"
-
- singleShotButton.onStart = {
- print("一次性定时器启动")
- }
-
- singleShotButton.onSingleShot = {
- print("一次性任务执行")
- }
-
- singleShotButton.onFinish = {
- print("一次性定时器结束")
- }
-
- view.addSubview(singleShotButton)
- singleShotButton.center.x = view.center.x
- }
- }
复制代码
常见问题及解决方案
在实现定时器按钮功能时,开发者可能会遇到一些常见问题。下面我们将讨论这些问题及其解决方案。
1. 定时器与RunLoop的问题
问题描述:定时器在某些情况下可能不会按预期工作,例如在滚动视图时定时器暂停。
原因分析:这是因为定时器默认运行在RunLoop的.default模式下,当用户滚动视图时,RunLoop会切换到.tracking模式,导致定时器暂停。
解决方案:将定时器添加到RunLoop的.common模式中,这样定时器会在所有常见的RunLoop模式下运行。
- // 创建定时器
- let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
- // 将定时器添加到RunLoop的.common模式
- RunLoop.current.add(timer, forMode: .common)
复制代码
2. 循环引用问题
问题描述:在使用闭包创建定时器时,可能会出现循环引用,导致内存泄漏。
原因分析:当定时器的闭包中强引用了self,而self又强引用了定时器时,就会形成循环引用。
解决方案:在闭包中使用[weak self]来避免循环引用。
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
- // 使用self
- self.updateButtonTitle()
- }
复制代码
3. 定时器精度问题
问题描述:定时器的实际触发时间可能与指定的时间间隔有偏差,特别是在需要高精度计时的场景中。
原因分析:Timer不是实时系统,它的精度受到系统负载、RunLoop模式等多种因素的影响。
解决方案:对于需要高精度的场景,可以考虑使用DispatchSourceTimer(GCD定时器)代替Timer。
- import Dispatch
- class GCDTimerButton: UIButton {
-
- private var timer: DispatchSourceTimer?
- private var remainingTime: Int = 0
- private var totalTime: Int = 60
-
- // ... 其他代码 ...
-
- private func startGCDTimer() {
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- // 创建GCD定时器
- timer = DispatchSource.makeTimerSource()
-
- // 设置定时器
- timer?.schedule(deadline: .now(), repeating: .seconds(1))
-
- // 设置事件处理器
- timer?.setEventHandler { [weak self] in
- DispatchQueue.main.async {
- self?.remainingTime -= 1
- self?.updateButtonTitle()
-
- if self?.remainingTime == 0 {
- self?.stopGCDTimer()
- }
- }
- }
-
- // 启动定时器
- timer?.resume()
- }
-
- private func stopGCDTimer() {
- timer?.cancel()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
- }
-
- // ... 其他代码 ...
- }
复制代码
4. 后台运行问题
问题描述:当应用进入后台时,定时器可能会暂停工作。
原因分析:iOS系统为了节省电量,会在应用进入后台时限制其活动,包括暂停定时器。
解决方案:如果需要在应用进入后台时继续运行定时器,可以使用beginBackgroundTask(withName:expirationHandler:)方法申请后台执行时间。
- var backgroundTask: UIBackgroundTaskIdentifier = .invalid
- func startTimer() {
- // 注册后台任务
- backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "TimerBackgroundTask") {
- // 后台任务即将过期时调用
- self.endBackgroundTask()
- }
-
- // 启动定时器
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- // 更新UI
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
-
- // 检查后台时间是否即将用完
- if UIApplication.shared.backgroundTimeRemaining < 10 {
- self.endBackgroundTask()
- }
- }
- }
- func endBackgroundTask() {
- if backgroundTask != .invalid {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backgroundTask = .invalid
- }
- }
- func stopTimer() {
- timer?.invalidate()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
- endBackgroundTask()
- }
复制代码
5. 多定时器管理问题
问题描述:在一个界面中有多个定时器按钮时,如何有效管理它们,避免冲突和资源浪费。
原因分析:每个定时器按钮都有自己的定时器实例,如果没有统一管理,可能会导致多个定时器同时运行,浪费系统资源。
解决方案:创建一个定时器管理器,统一管理所有的定时器。
- import UIKit
- class TimerManager {
-
- static let shared = TimerManager()
-
- private var timers: [String: Timer] = [:]
-
- private init() {}
-
- func addTimer(id: String, timer: Timer) {
- // 如果已存在相同ID的定时器,先移除
- removeTimer(id: id)
-
- // 添加新定时器
- timers[id] = timer
- }
-
- func removeTimer(id: String) {
- if let timer = timers[id] {
- timer.invalidate()
- timers.removeValue(forKey: id)
- }
- }
-
- func removeAllTimers() {
- for (_, timer) in timers {
- timer.invalidate()
- }
- timers.removeAll()
- }
-
- func isTimerActive(id: String) -> Bool {
- return timers[id] != nil
- }
- }
- class ManagedTimerButton: UIButton {
-
- private var timerId: String
- private var remainingTime: Int = 0
- private var totalTime: Int = 60
-
- init(frame: CGRect, timerId: String, totalTime: Int = 60) {
- self.timerId = timerId
- self.totalTime = totalTime
- super.init(frame: frame)
- setupButton()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func setupButton() {
- setTitle("获取验证码", for: .normal)
- backgroundColor = .systemBlue
- setTitleColor(.white, for: .normal)
- layer.cornerRadius = 5
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- }
-
- @objc private func buttonTapped() {
- // 检查定时器是否已在运行
- if TimerManager.shared.isTimerActive(id: timerId) {
- return
- }
-
- startTimer()
- }
-
- private func startTimer() {
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
- }
-
- // 将定时器添加到管理器
- TimerManager.shared.addTimer(id: timerId, timer: timer)
- }
-
- private func stopTimer() {
- TimerManager.shared.removeTimer(id: timerId)
- isEnabled = true
- setTitle("重新获取", for: .normal)
- }
-
- private func updateButtonTitle() {
- setTitle("\(remainingTime)秒后重试", for: .disabled)
- }
-
- deinit {
- TimerManager.shared.removeTimer(id: timerId)
- }
- }
复制代码
提升用户体验的技巧
定时器按钮不仅仅是功能的实现,还需要考虑用户体验。下面是一些提升用户体验的技巧。
1. 视觉反馈
为定时器按钮添加视觉反馈,可以帮助用户更好地理解当前状态。
- class VisualFeedbackTimerButton: UIButton {
-
- // ... 其他代码 ...
-
- private func updateButtonAppearance() {
- // 更新标题
- let formattedTitle = String(format: countingFormat, remainingTime)
- setTitle(formattedTitle, for: .disabled)
-
- // 根据剩余时间改变颜色
- let progress = Double(remainingTime) / Double(totalTime)
-
- if progress > 0.5 {
- backgroundColor = normalBackgroundColor
- } else if progress > 0.2 {
- backgroundColor = .systemOrange
- } else {
- backgroundColor = .systemRed
- }
-
- // 添加脉冲动画
- pulseAnimation()
- }
-
- private func pulseAnimation() {
- let pulse = CABasicAnimation(keyPath: "transform.scale")
- pulse.duration = 0.5
- pulse.fromValue = 1.0
- pulse.toValue = 1.05
- pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
- pulse.autoreverses = true
- pulse.repeatCount = 1
-
- layer.add(pulse, forKey: "pulse")
- }
-
- // ... 其他代码 ...
- }
复制代码
2. 声音反馈
在定时器开始和结束时添加声音反馈,可以增强用户体验。
- import AVFoundation
- class SoundFeedbackTimerButton: UIButton {
-
- private var audioPlayer: AVAudioPlayer?
-
- // ... 其他代码 ...
-
- private func setupAudioPlayer() {
- guard let soundURL = Bundle.main.url(forResource: "tick", withExtension: "mp3") else {
- print("声音文件未找到")
- return
- }
-
- do {
- audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
- audioPlayer?.prepareToPlay()
- } catch {
- print("音频播放器初始化失败: \(error)")
- }
- }
-
- private func startTimer() {
- // ... 其他代码 ...
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- // 播放滴答声
- self.playTickSound()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- // 播放完成声音
- self.playFinishSound()
- }
- }
- }
-
- private func playTickSound() {
- // 如果剩余时间小于等于5秒,播放滴答声
- if remainingTime <= 5 {
- audioPlayer?.play()
- }
- }
-
- private func playFinishSound() {
- // 这里可以播放不同的完成声音
- // 为了简单起见,我们使用相同的音频文件
- audioPlayer?.play()
- }
-
- // ... 其他代码 ...
- }
复制代码
3. 震动反馈
在定时器开始和结束时添加震动反馈,可以提供触觉反馈。
- import UIKit
- class HapticFeedbackTimerButton: UIButton {
-
- // ... 其他代码 ...
-
- private func startTimer() {
- // ... 其他代码 ...
-
- // 开始震动反馈
- triggerHapticFeedback(style: .medium)
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- // 结束震动反馈
- self.triggerHapticFeedback(style: .heavy)
- }
- }
- }
-
- private func triggerHapticFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle) {
- let generator = UIImpactFeedbackGenerator(style: style)
- generator.impactOccurred()
- }
-
- // ... 其他代码 ...
- }
复制代码
4. 进度指示
为定时器按钮添加进度指示,可以直观地展示剩余时间。
- import UIKit
- class ProgressTimerButton: UIButton {
-
- private var progressLayer = CAShapeLayer()
- private var progressBackgroundLayer = CAShapeLayer()
-
- // ... 其他代码 ...
-
- private func setupProgressLayers() {
- // 创建背景层
- progressBackgroundLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
- progressBackgroundLayer.fillColor = UIColor.clear.cgColor
- progressBackgroundLayer.strokeColor = UIColor.systemGray5.cgColor
- progressBackgroundLayer.lineWidth = 3.0
- layer.addSublayer(progressBackgroundLayer)
-
- // 创建进度层
- progressLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
- progressLayer.fillColor = UIColor.clear.cgColor
- progressLayer.strokeColor = tintColor.cgColor
- progressLayer.lineWidth = 3.0
- progressLayer.strokeEnd = 1.0
- progressLayer.lineCap = .round
- layer.addSublayer(progressLayer)
- }
-
- private func updateProgress() {
- let progress = Double(remainingTime) / Double(totalTime)
-
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- progressLayer.strokeEnd = CGFloat(progress)
- CATransaction.commit()
-
- // 根据进度改变颜色
- if progress > 0.5 {
- progressLayer.strokeColor = normalBackgroundColor.cgColor
- } else if progress > 0.2 {
- progressLayer.strokeColor = UIColor.systemOrange.cgColor
- } else {
- progressLayer.strokeColor = UIColor.systemRed.cgColor
- }
- }
-
- private func startTimer() {
- // ... 其他代码 ...
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
- self.updateProgress()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
- }
- }
-
- private func stopTimer() {
- // ... 其他代码 ...
-
- // 重置进度
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- progressLayer.strokeEnd = 1.0
- CATransaction.commit()
- }
-
- override func layoutSubviews() {
- super.layoutSubviews()
-
- // 更新进度层路径
- progressBackgroundLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
- progressLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
- }
-
- // ... 其他代码 ...
- }
复制代码
5. 动画效果
为定时器按钮添加动画效果,可以增强用户体验。
- import UIKit
- class AnimatedTimerButton: UIButton {
-
- // ... 其他代码 ...
-
- private func startTimer() {
- // ... 其他代码 ...
-
- // 添加开始动画
- animateStart()
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- // 添加滴答动画
- self.animateTick()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- // 添加完成动画
- self.animateFinish()
- }
- }
- }
-
- private func animateStart() {
- let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
- scaleAnimation.duration = 0.2
- scaleAnimation.fromValue = 1.0
- scaleAnimation.toValue = 0.95
- scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
- scaleAnimation.autoreverses = true
- scaleAnimation.repeatCount = 1
-
- layer.add(scaleAnimation, forKey: "startScale")
- }
-
- private func animateTick() {
- // 如果剩余时间小于等于5秒,添加更明显的动画
- if remainingTime <= 5 {
- let shakeAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
- shakeAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
- shakeAnimation.duration = 0.5
- shakeAnimation.values = [-5.0, 5.0, -5.0, 5.0, 0.0]
-
- layer.add(shakeAnimation, forKey: "shake")
- }
- }
-
- private func animateFinish() {
- let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
- bounceAnimation.duration = 0.5
- bounceAnimation.values = [1.0, 1.2, 0.9, 1.1, 1.0]
- bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
-
- layer.add(bounceAnimation, forKey: "finishBounce")
- }
-
- // ... 其他代码 ...
- }
复制代码
打造专业iOS应用的最佳实践
在开发专业的iOS应用时,定时器按钮的实现需要遵循一些最佳实践,以确保代码的可维护性、可扩展性和性能。
1. 使用协议和委托模式
使用协议和委托模式可以使定时器按钮更加灵活,便于与其他组件交互。
- import UIKit
- // 定义定时器按钮的协议
- protocol TimerButtonDelegate: AnyObject {
- func timerButtonDidStartCountdown(_ button: TimerButton)
- func timerButton(_ button: TimerButton, didUpdateRemainingTime time: Int)
- func timerButtonDidFinishCountdown(_ button: TimerButton)
- }
- // 实现协议的定时器按钮
- class ProtocolTimerButton: UIButton {
-
- weak var delegate: TimerButtonDelegate?
-
- private var timer: Timer?
- private var remainingTime: Int = 0
- private var totalTime: Int = 60
-
- // ... 其他代码 ...
-
- private func startTimer() {
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- // 通知委托
- delegate?.timerButtonDidStartCountdown(self)
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- // 通知委托
- self.delegate?.timerButton(self, didUpdateRemainingTime: self.remainingTime)
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
- }
- }
-
- private func stopTimer() {
- timer?.invalidate()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
-
- // 通知委托
- delegate?.timerButtonDidFinishCountdown(self)
- }
-
- // ... 其他代码 ...
- }
- // 使用示例
- class ViewController: UIViewController, TimerButtonDelegate {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let timerButton = ProtocolTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
- timerButton.delegate = self
- view.addSubview(timerButton)
- timerButton.center.x = view.center.x
- }
-
- // MARK: - TimerButtonDelegate
-
- func timerButtonDidStartCountdown(_ button: TimerButton) {
- print("定时器开始倒计时")
- }
-
- func timerButton(_ button: TimerButton, didUpdateRemainingTime time: Int) {
- print("剩余时间: \(time)秒")
- }
-
- func timerButtonDidFinishCountdown(_ button: TimerButton) {
- print("定时器倒计时结束")
- }
- }
复制代码
2. 使用响应式编程
使用响应式编程(如Combine或RxSwift)可以简化定时器按钮的事件处理。
- import UIKit
- import Combine
- class ReactiveTimerButton: UIButton {
-
- private var timer: Timer?
- private var remainingTime: Int = 0
- private var totalTime: Int = 60
-
- // 使用Combine发布者
- private var cancellables = Set<AnyCancellable>()
-
- // 定义事件发布者
- let didStartCountdown = PassthroughSubject<Void, Never>()
- let didUpdateRemainingTime = PassthroughSubject<Int, Never>()
- let didFinishCountdown = PassthroughSubject<Void, Never>()
-
- // ... 其他代码 ...
-
- private func startTimer() {
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- // 发送开始事件
- didStartCountdown.send()
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- // 发送更新事件
- self.didUpdateRemainingTime.send(self.remainingTime)
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
- }
- }
-
- private func stopTimer() {
- timer?.invalidate()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
-
- // 发送完成事件
- didFinishCountdown.send()
- }
-
- // ... 其他代码 ...
- }
- // 使用示例
- class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let timerButton = ReactiveTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
- view.addSubview(timerButton)
- timerButton.center.x = view.center.x
-
- // 订阅事件
- timerButton.didStartCountdown
- .sink { _ in
- print("定时器开始倒计时")
- }
- .store(in: &timerButton.cancellables)
-
- timerButton.didUpdateRemainingTime
- .sink { time in
- print("剩余时间: \(time)秒")
- }
- .store(in: &timerButton.cancellables)
-
- timerButton.didFinishCountdown
- .sink { _ in
- print("定时器倒计时结束")
- }
- .store(in: &timerButton.cancellables)
- }
- }
复制代码
3. 使用SwiftUI实现定时器按钮
对于使用SwiftUI开发的应用,可以使用SwiftUI的方式实现定时器按钮。
- import SwiftUI
- struct SwiftUIButton: View {
-
- @State private var remainingTime: Int = 0
- @State private var isCountingDown: Bool = false
- @State private var timer: Timer?
-
- let totalTime: Int
- let normalTitle: String
- let countingFormat: String
- let finishedTitle: String
- let action: () -> Void
-
- init(
- totalTime: Int = 60,
- normalTitle: String = "获取验证码",
- countingFormat: String = "%d秒后重试",
- finishedTitle: String = "重新获取",
- action: @escaping () -> Void = {}
- ) {
- self.totalTime = totalTime
- self.normalTitle = normalTitle
- self.countingFormat = countingFormat
- self.finishedTitle = finishedTitle
- self.action = action
- }
-
- var body: some View {
- Button(action: {
- if !isCountingDown {
- startCountdown()
- action()
- }
- }) {
- Text(buttonTitle)
- .font(.headline)
- .foregroundColor(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(buttonColor)
- .cornerRadius(10)
- }
- .disabled(isCountingDown)
- .onDisappear {
- stopCountdown()
- }
- }
-
- private var buttonTitle: String {
- if isCountingDown {
- return String(format: countingFormat, remainingTime)
- } else if remainingTime == 0 {
- return finishedTitle
- } else {
- return normalTitle
- }
- }
-
- private var buttonColor: Color {
- if isCountingDown {
- let progress = Double(remainingTime) / Double(totalTime)
- if progress > 0.5 {
- return .blue
- } else if progress > 0.2 {
- return .orange
- } else {
- return .red
- }
- } else {
- return .blue
- }
- }
-
- private func startCountdown() {
- isCountingDown = true
- remainingTime = totalTime
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
- if self.remainingTime > 0 {
- self.remainingTime -= 1
- } else {
- self.stopCountdown()
- }
- }
- }
-
- private func stopCountdown() {
- timer?.invalidate()
- timer = nil
- isCountingDown = false
- }
- }
- // 使用示例
- struct ContentView: View {
- var body: some View {
- VStack(spacing: 20) {
- SwiftUIButton(
- totalTime: 10,
- normalTitle: "发送验证码",
- countingFormat: "剩余%d秒",
- finishedTitle: "再次发送"
- ) {
- print("验证码已发送")
- }
- .padding(.horizontal)
-
- SwiftUIButton(
- totalTime: 5,
- normalTitle: "开始计时",
- countingFormat: "计时中: %d",
- finishedTitle: "计时完成"
- ) {
- print("计时开始")
- }
- .padding(.horizontal)
- }
- }
- }
复制代码
4. 使用MVVM架构
使用MVVM(Model-View-ViewModel)架构可以使定时器按钮的代码更加结构化和可测试。
- import UIKit
- // Model
- struct TimerModel {
- var totalTime: Int
- var remainingTime: Int
- var isActive: Bool
- }
- // ViewModel
- class TimerButtonViewModel {
-
- // 使用@Published属性,以便视图可以订阅它们的变化
- @Published var model: TimerModel
-
- // 定义事件
- let didStart = PassthroughSubject<Void, Never>()
- let didTick = PassthroughSubject<Int, Never>()
- let didFinish = PassthroughSubject<Void, Never>()
-
- private var timer: Timer?
- private var cancellables = Set<AnyCancellable>()
-
- init(totalTime: Int = 60) {
- model = TimerModel(totalTime: totalTime, remainingTime: totalTime, isActive: false)
- }
-
- func startTimer() {
- guard !model.isActive else { return }
-
- model.isActive = true
- model.remainingTime = model.totalTime
-
- didStart.send()
-
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
-
- self.model.remainingTime -= 1
- self.didTick.send(self.model.remainingTime)
-
- if self.model.remainingTime == 0 {
- self.stopTimer()
- }
- }
- }
-
- func stopTimer() {
- guard model.isActive else { return }
-
- timer?.invalidate()
- timer = nil
- model.isActive = false
-
- didFinish.send()
- }
-
- func resetTimer() {
- stopTimer()
- model.remainingTime = model.totalTime
- }
- }
- // View
- class MVVMTimerButton: UIButton {
-
- private var viewModel: TimerButtonViewModel
- private var cancellables = Set<AnyCancellable>()
-
- init(frame: CGRect, viewModel: TimerButtonViewModel) {
- self.viewModel = viewModel
- super.init(frame: frame)
- setupButton()
- setupBindings()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func setupButton() {
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- updateButtonAppearance()
- }
-
- private func setupBindings() {
- // 订阅模型变化
- viewModel.$model
- .receive(on: RunLoop.main)
- .sink { [weak self] model in
- self?.updateButtonAppearance()
- }
- .store(in: &cancellables)
-
- // 订阅事件
- viewModel.didStart
- .receive(on: RunLoop.main)
- .sink { [weak self] _ in
- print("定时器开始")
- }
- .store(in: &cancellables)
-
- viewModel.didTick
- .receive(on: RunLoop.main)
- .sink { time in
- print("剩余时间: \(time)秒")
- }
- .store(in: &cancellables)
-
- viewModel.didFinish
- .receive(on: RunLoop.main)
- .sink { [weak self] _ in
- print("定时器结束")
- }
- .store(in: &cancellables)
- }
-
- @objc private func buttonTapped() {
- if viewModel.model.isActive {
- viewModel.stopTimer()
- } else {
- viewModel.startTimer()
- }
- }
-
- private func updateButtonAppearance() {
- isEnabled = !viewModel.model.isActive
-
- if viewModel.model.isActive {
- let formattedTitle = String(format: "%d秒后重试", viewModel.model.remainingTime)
- setTitle(formattedTitle, for: .disabled)
- backgroundColor = .systemGray
- } else if viewModel.model.remainingTime == 0 {
- setTitle("重新获取", for: .normal)
- backgroundColor = .systemBlue
- } else {
- setTitle("获取验证码", for: .normal)
- backgroundColor = .systemBlue
- }
- }
-
- deinit {
- viewModel.stopTimer()
- }
- }
- // 使用示例
- class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let viewModel = TimerButtonViewModel(totalTime: 10)
- let timerButton = MVVMTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50), viewModel: viewModel)
-
- view.addSubview(timerButton)
- timerButton.center.x = view.center.x
- }
- }
复制代码
5. 性能优化
在实现定时器按钮时,需要注意性能优化,特别是在有多个定时器按钮的场景中。
- import UIKit
- class OptimizedTimerButton: UIButton {
-
- // 使用轻量级的GCD定时器代替Timer
- private var timer: DispatchSourceTimer?
- private var remainingTime: Int = 0
- private var totalTime: Int = 60
-
- // 使用静态的定时器队列,避免创建多个队列
- private static let timerQueue = DispatchQueue(label: "com.example.timerbutton.queue", attributes: .concurrent)
-
- // 使用弱引用避免循环引用
- private weak var weakSelf: OptimizedTimerButton?
-
- // ... 其他代码 ...
-
- private func startTimer() {
- // 停止现有定时器
- stopTimer()
-
- remainingTime = totalTime
- isEnabled = false
- updateButtonTitle()
-
- // 保存弱引用
- weakSelf = self
-
- // 创建GCD定时器
- timer = DispatchSource.makeTimerSource(queue: OptimizedTimerButton.timerQueue)
-
- // 设置定时器
- timer?.schedule(deadline: .now(), repeating: .seconds(1))
-
- // 设置事件处理器
- timer?.setEventHandler { [weak self] in
- guard let self = self else { return }
-
- // 在主线程更新UI
- DispatchQueue.main.async {
- self.remainingTime -= 1
- self.updateButtonTitle()
-
- if self.remainingTime == 0 {
- self.stopTimer()
- }
- }
- }
-
- // 启动定时器
- timer?.resume()
- }
-
- private func stopTimer() {
- timer?.cancel()
- timer = nil
- isEnabled = true
- setTitle("重新获取", for: .normal)
- }
-
- // ... 其他代码 ...
-
- deinit {
- stopTimer()
- }
- }
复制代码
总结
本文全面介绍了在Swift中实现定时器按钮功能的方法,从基础概念到高级技巧,帮助开发者掌握这一重要功能,提升应用的专业性和用户体验。
我们首先介绍了Swift中Timer的基础知识,包括Timer的基本用法、主要属性和方法,以及RunLoop与Timer的关系。然后,我们逐步实现了从简单的定时器按钮到支持多种定时器模式的高级定时器按钮。
接着,我们讨论了在实现定时器按钮时可能遇到的常见问题及其解决方案,包括定时器与RunLoop的问题、循环引用问题、定时器精度问题、后台运行问题和多定时器管理问题。
为了提升用户体验,我们介绍了一些技巧,包括视觉反馈、声音反馈、震动反馈、进度指示和动画效果。这些技巧可以使定时器按钮更加直观、生动和用户友好。
最后,我们探讨了打造专业iOS应用的最佳实践,包括使用协议和委托模式、使用响应式编程、使用SwiftUI实现定时器按钮、使用MVVM架构和性能优化。这些实践可以帮助开发者编写更加结构化、可维护和高性能的代码。
通过本文的学习,开发者应该能够掌握在Swift中实现定时器按钮的全面技能,并能够根据具体需求选择合适的实现方式,打造出专业、高效且用户友好的iOS应用。
版权声明
1、转载或引用本网站内容(Swift中实现定时器按钮功能的完整指南从基础到进阶掌握定时按钮开发技巧解决常见问题提升用户体验打造专业iOS应用)须注明原网址及作者(威震华夏关云长),并标明本网站网址(https://pixtech.org/)。
2、对于不当转载或引用本网站内容而引起的民事纷争、行政处理或其他损失,本网站不承担责任。
3、对不遵守本声明或其他违法、恶意使用本网站内容者,本网站保留追究其法律责任的权利。
本文地址: https://pixtech.org/thread-36729-1-1.html
|
|