简体中文 繁體中文 English Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français Japanese

站内搜索

搜索

活动公告

通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31

Swift中实现定时器按钮功能的完整指南从基础到进阶掌握定时按钮开发技巧解决常见问题提升用户体验打造专业iOS应用

SunJu_FaceMall

3万

主题

153

科技点

3万

积分

大区版主

碾压王

积分
32103
发表于 2025-9-17 18:40:06 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

定时器按钮是iOS应用中常见的UI组件,它允许用户启动一个倒计时或正计时功能,并在特定时间间隔执行某些操作。从验证码获取按钮到倒计时器,再到定时任务执行,定时器按钮在各种应用场景中都有广泛的应用。本文将全面介绍如何在Swift中实现定时器按钮功能,从基础概念到高级技巧,帮助开发者掌握这一重要功能,提升应用的专业性和用户体验。

Swift中的定时器基础

在Swift中,我们主要使用Timer类来创建定时器。Timer是Foundation框架中的一个类,它可以在指定的时间间隔后触发一个事件,或者重复触发事件。

Timer的基本用法
  1. // 创建一个一次性定时器
  2. let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
  3.     print("定时器触发")
  4. }
  5. // 创建一个重复性定时器
  6. let repeatingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
  7.     print("定时器重复触发")
  8. }
复制代码

Timer的主要属性和方法
  1. // 使定时器失效
  2. timer.invalidate()
  3. // 检查定时器是否有效
  4. if timer.isValid {
  5.     print("定时器有效")
  6. }
  7. // 获取定时器的触发时间间隔
  8. let timeInterval = timer.timeInterval
  9. // 获取定时器的触发日期
  10. let fireDate = timer.fireDate
复制代码

RunLoop与Timer

定时器需要添加到RunLoop中才能正常工作。当我们使用scheduledTimer方法创建定时器时,它会自动被添加到当前线程的RunLoop中。如果我们需要手动控制定时器的RunLoop,可以使用以下方法:
  1. // 创建定时器但不自动添加到RunLoop
  2. let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
  3. // 将定时器添加到RunLoop
  4. RunLoop.current.add(timer, forMode: .common)
复制代码

实现简单的定时器按钮

让我们从实现一个简单的定时器按钮开始,这个按钮在点击后开始倒计时,并在倒计时期间禁用。

基本实现

首先,创建一个基本的定时器按钮类:
  1. import UIKit
  2. class TimerButton: UIButton {
  3.    
  4.     // 定时器
  5.     private var timer: Timer?
  6.     // 剩余时间
  7.     private var remainingTime: Int = 0
  8.     // 总时间
  9.     private var totalTime: Int = 60
  10.    
  11.     // 初始化方法
  12.     init(frame: CGRect, totalTime: Int = 60) {
  13.         super.init(frame: frame)
  14.         self.totalTime = totalTime
  15.         setupButton()
  16.     }
  17.    
  18.     required init?(coder: NSCoder) {
  19.         super.init(coder: coder)
  20.         setupButton()
  21.     }
  22.    
  23.     // 设置按钮样式
  24.     private func setupButton() {
  25.         setTitle("获取验证码", for: .normal)
  26.         backgroundColor = .systemBlue
  27.         setTitleColor(.white, for: .normal)
  28.         layer.cornerRadius = 5
  29.         addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
  30.     }
  31.    
  32.     // 按钮点击事件
  33.     @objc private func buttonTapped() {
  34.         startTimer()
  35.     }
  36.    
  37.     // 开始定时器
  38.     private func startTimer() {
  39.         remainingTime = totalTime
  40.         isEnabled = false
  41.         updateButtonTitle()
  42.         
  43.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  44.             self?.remainingTime -= 1
  45.             self?.updateButtonTitle()
  46.             
  47.             if self?.remainingTime == 0 {
  48.                 self?.stopTimer()
  49.             }
  50.         }
  51.     }
  52.    
  53.     // 停止定时器
  54.     private func stopTimer() {
  55.         timer?.invalidate()
  56.         timer = nil
  57.         isEnabled = true
  58.         setTitle("重新获取", for: .normal)
  59.     }
  60.    
  61.     // 更新按钮标题
  62.     private func updateButtonTitle() {
  63.         setTitle("\(remainingTime)秒后重试", for: .disabled)
  64.     }
  65.    
  66.     // 销毁定时器
  67.     deinit {
  68.         timer?.invalidate()
  69.     }
  70. }
复制代码

使用示例

在ViewController中使用这个定时器按钮:
  1. import UIKit
  2. class ViewController: UIViewController {
  3.    
  4.     override func viewDidLoad() {
  5.         super.viewDidLoad()
  6.         
  7.         let timerButton = TimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
  8.         view.addSubview(timerButton)
  9.         
  10.         // 设置按钮居中
  11.         timerButton.center.x = view.center.x
  12.     }
  13. }
复制代码

这个简单的定时器按钮实现了基本的功能:点击后开始倒计时,倒计时期间按钮禁用,倒计时结束后按钮恢复可用状态。

进阶功能 - 自定义定时器按钮

现在,让我们扩展定时器按钮的功能,使其更加灵活和可定制。

可配置的定时器按钮
  1. import UIKit
  2. class AdvancedTimerButton: UIButton {
  3.    
  4.     // MARK: - 公开属性
  5.     var countdownDuration: Int = 60 {
  6.         didSet {
  7.             resetButton()
  8.         }
  9.     }
  10.    
  11.     var normalTitle: String = "获取验证码" {
  12.         didSet {
  13.             resetButton()
  14.         }
  15.     }
  16.    
  17.     var countingFormat: String = "%d秒后重试" {
  18.         didSet {
  19.             updateButtonTitle()
  20.         }
  21.     }
  22.    
  23.     var finishedTitle: String = "重新获取" {
  24.         didSet {
  25.             resetButton()
  26.         }
  27.     }
  28.    
  29.     var normalBackgroundColor: UIColor = .systemBlue {
  30.         didSet {
  31.             resetButton()
  32.         }
  33.     }
  34.    
  35.     var disabledBackgroundColor: UIColor = .systemGray {
  36.         didSet {
  37.             updateButtonAppearance()
  38.         }
  39.     }
  40.    
  41.     var normalTitleColor: UIColor = .white {
  42.         didSet {
  43.             resetButton()
  44.         }
  45.     }
  46.    
  47.     var disabledTitleColor: UIColor = .white {
  48.         didSet {
  49.             updateButtonAppearance()
  50.         }
  51.     }
  52.    
  53.     // MARK: - 私有属性
  54.     private var timer: Timer?
  55.     private var remainingTime: Int = 0
  56.     private var isCountingDown: Bool = false
  57.    
  58.     // MARK: - 回调闭包
  59.     var onStartCountdown: (() -> Void)?
  60.     var onTick: ((Int) -> Void)?
  61.     var onFinishCountdown: (() -> Void)?
  62.    
  63.     // MARK: - 初始化
  64.     init(frame: CGRect, countdownDuration: Int = 60) {
  65.         self.countdownDuration = countdownDuration
  66.         super.init(frame: frame)
  67.         setupButton()
  68.     }
  69.    
  70.     required init?(coder: NSCoder) {
  71.         super.init(coder: coder)
  72.         setupButton()
  73.     }
  74.    
  75.     // MARK: - 设置按钮
  76.     private func setupButton() {
  77.         resetButton()
  78.         addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
  79.     }
  80.    
  81.     // MARK: - 重置按钮
  82.     private func resetButton() {
  83.         isCountingDown = false
  84.         isEnabled = true
  85.         setTitle(normalTitle, for: .normal)
  86.         backgroundColor = normalBackgroundColor
  87.         setTitleColor(normalTitleColor, for: .normal)
  88.     }
  89.    
  90.     // MARK: - 按钮点击事件
  91.     @objc private func buttonTapped() {
  92.         startCountdown()
  93.     }
  94.    
  95.     // MARK: - 开始倒计时
  96.     public func startCountdown() {
  97.         guard !isCountingDown else { return }
  98.         
  99.         isCountingDown = true
  100.         remainingTime = countdownDuration
  101.         isEnabled = false
  102.         updateButtonAppearance()
  103.         
  104.         onStartCountdown?()
  105.         
  106.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  107.             guard let self = self else { return }
  108.             
  109.             self.remainingTime -= 1
  110.             self.updateButtonAppearance()
  111.             self.onTick?(self.remainingTime)
  112.             
  113.             if self.remainingTime == 0 {
  114.                 self.stopCountdown()
  115.             }
  116.         }
  117.     }
  118.    
  119.     // MARK: - 停止倒计时
  120.     public func stopCountdown() {
  121.         guard isCountingDown else { return }
  122.         
  123.         timer?.invalidate()
  124.         timer = nil
  125.         isCountingDown = false
  126.         isEnabled = true
  127.         setTitle(finishedTitle, for: .normal)
  128.         backgroundColor = normalBackgroundColor
  129.         setTitleColor(normalTitleColor, for: .normal)
  130.         
  131.         onFinishCountdown?()
  132.     }
  133.    
  134.     // MARK: - 更新按钮外观
  135.     private func updateButtonAppearance() {
  136.         let formattedTitle = String(format: countingFormat, remainingTime)
  137.         setTitle(formattedTitle, for: .disabled)
  138.         backgroundColor = disabledBackgroundColor
  139.         setTitleColor(disabledTitleColor, for: .disabled)
  140.     }
  141.    
  142.     // MARK: - 销毁定时器
  143.     deinit {
  144.         timer?.invalidate()
  145.     }
  146. }
复制代码

使用示例
  1. import UIKit
  2. class ViewController: UIViewController {
  3.    
  4.     override func viewDidLoad() {
  5.         super.viewDidLoad()
  6.         
  7.         let timerButton = AdvancedTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
  8.         
  9.         // 配置按钮属性
  10.         timerButton.countdownDuration = 10
  11.         timerButton.normalTitle = "发送验证码"
  12.         timerButton.countingFormat = "剩余%d秒"
  13.         timerButton.finishedTitle = "再次发送"
  14.         timerButton.normalBackgroundColor = .systemGreen
  15.         timerButton.disabledBackgroundColor = .systemGray4
  16.         
  17.         // 设置回调
  18.         timerButton.onStartCountdown = {
  19.             print("开始倒计时")
  20.         }
  21.         
  22.         timerButton.onTick = { remainingTime in
  23.             print("剩余时间: \(remainingTime)秒")
  24.         }
  25.         
  26.         timerButton.onFinishCountdown = {
  27.             print("倒计时结束")
  28.         }
  29.         
  30.         view.addSubview(timerButton)
  31.         timerButton.center.x = view.center.x
  32.     }
  33. }
复制代码

支持多种定时器模式

让我们进一步扩展定时器按钮,使其支持多种定时器模式,如倒计时、正计时和一次性执行等。
  1. import UIKit
  2. enum TimerMode {
  3.     case countdown(totalTime: Int)  // 倒计时模式
  4.     case countup                   // 正计时模式
  5.     case singleShot(timeInterval: TimeInterval) // 一次性执行模式
  6. }
  7. class MultiModeTimerButton: UIButton {
  8.    
  9.     // MARK: - 公开属性
  10.     var timerMode: TimerMode = .countdown(totalTime: 60) {
  11.         didSet {
  12.             resetButton()
  13.         }
  14.     }
  15.    
  16.     var normalTitle: String = "开始计时" {
  17.         didSet {
  18.             resetButton()
  19.         }
  20.     }
  21.    
  22.     var countingFormat: String = "%d" {
  23.         didSet {
  24.             updateButtonTitle()
  25.         }
  26.     }
  27.    
  28.     var finishedTitle: String = "完成" {
  29.         didSet {
  30.             resetButton()
  31.         }
  32.     }
  33.    
  34.     var normalBackgroundColor: UIColor = .systemBlue {
  35.         didSet {
  36.             resetButton()
  37.         }
  38.     }
  39.    
  40.     var activeBackgroundColor: UIColor = .systemGreen {
  41.         didSet {
  42.             updateButtonAppearance()
  43.         }
  44.     }
  45.    
  46.     var normalTitleColor: UIColor = .white {
  47.         didSet {
  48.             resetButton()
  49.         }
  50.     }
  51.    
  52.     var activeTitleColor: UIColor = .white {
  53.         didSet {
  54.             updateButtonAppearance()
  55.         }
  56.     }
  57.    
  58.     // MARK: - 私有属性
  59.     private var timer: Timer?
  60.     private var elapsedTime: TimeInterval = 0
  61.     private var remainingTime: Int = 0
  62.     private var isActive: Bool = false
  63.    
  64.     // MARK: - 回调闭包
  65.     var onStart: (() -> Void)?
  66.     var onTick: ((TimeInterval) -> Void)?
  67.     var onFinish: (() -> Void)?
  68.     var onSingleShot: (() -> Void)?
  69.    
  70.     // MARK: - 初始化
  71.     init(frame: CGRect, timerMode: TimerMode = .countdown(totalTime: 60)) {
  72.         self.timerMode = timerMode
  73.         super.init(frame: frame)
  74.         setupButton()
  75.     }
  76.    
  77.     required init?(coder: NSCoder) {
  78.         super.init(coder: coder)
  79.         setupButton()
  80.     }
  81.    
  82.     // MARK: - 设置按钮
  83.     private func setupButton() {
  84.         resetButton()
  85.         addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
  86.     }
  87.    
  88.     // MARK: - 重置按钮
  89.     private func resetButton() {
  90.         isActive = false
  91.         isEnabled = true
  92.         elapsedTime = 0
  93.         
  94.         switch timerMode {
  95.         case .countdown(let totalTime):
  96.             remainingTime = totalTime
  97.             setTitle(normalTitle, for: .normal)
  98.         case .countup:
  99.             setTitle(normalTitle, for: .normal)
  100.         case .singleShot:
  101.             setTitle(normalTitle, for: .normal)
  102.         }
  103.         
  104.         backgroundColor = normalBackgroundColor
  105.         setTitleColor(normalTitleColor, for: .normal)
  106.     }
  107.    
  108.     // MARK: - 按钮点击事件
  109.     @objc private func buttonTapped() {
  110.         if isActive {
  111.             stopTimer()
  112.         } else {
  113.             startTimer()
  114.         }
  115.     }
  116.    
  117.     // MARK: - 开始定时器
  118.     public func startTimer() {
  119.         guard !isActive else { return }
  120.         
  121.         isActive = true
  122.         updateButtonAppearance()
  123.         onStart?()
  124.         
  125.         switch timerMode {
  126.         case .countdown:
  127.             timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  128.                 self?.handleCountdownTick()
  129.             }
  130.         case .countup:
  131.             timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
  132.                 self?.handleCountupTick()
  133.             }
  134.         case .singleShot(let timeInterval):
  135.             timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in
  136.                 self?.handleSingleShot()
  137.             }
  138.         }
  139.     }
  140.    
  141.     // MARK: - 停止定时器
  142.     public func stopTimer() {
  143.         guard isActive else { return }
  144.         
  145.         timer?.invalidate()
  146.         timer = nil
  147.         isActive = false
  148.         isEnabled = true
  149.         setTitle(finishedTitle, for: .normal)
  150.         backgroundColor = normalBackgroundColor
  151.         setTitleColor(normalTitleColor, for: .normal)
  152.         
  153.         onFinish?()
  154.     }
  155.    
  156.     // MARK: - 处理倒计时
  157.     private func handleCountdownTick() {
  158.         remainingTime -= 1
  159.         updateButtonAppearance()
  160.         onTick?(TimeInterval(remainingTime))
  161.         
  162.         if remainingTime == 0 {
  163.             stopTimer()
  164.         }
  165.     }
  166.    
  167.     // MARK: - 处理正计时
  168.     private func handleCountupTick() {
  169.         elapsedTime += 0.1
  170.         updateButtonAppearance()
  171.         onTick?(elapsedTime)
  172.     }
  173.    
  174.     // MARK: - 处理一次性执行
  175.     private func handleSingleShot() {
  176.         timer?.invalidate()
  177.         timer = nil
  178.         isActive = false
  179.         isEnabled = true
  180.         setTitle(finishedTitle, for: .normal)
  181.         backgroundColor = normalBackgroundColor
  182.         setTitleColor(normalTitleColor, for: .normal)
  183.         
  184.         onSingleShot?()
  185.         onFinish?()
  186.     }
  187.    
  188.     // MARK: - 更新按钮外观
  189.     private func updateButtonAppearance() {
  190.         switch timerMode {
  191.         case .countdown:
  192.             let formattedTitle = String(format: countingFormat, remainingTime)
  193.             setTitle(formattedTitle, for: .disabled)
  194.         case .countup:
  195.             let formattedTitle = String(format: "%.1f", elapsedTime)
  196.             setTitle(formattedTitle, for: .disabled)
  197.         case .singleShot:
  198.             setTitle("执行中...", for: .disabled)
  199.         }
  200.         
  201.         backgroundColor = activeBackgroundColor
  202.         setTitleColor(activeTitleColor, for: .disabled)
  203.     }
  204.    
  205.     // MARK: - 销毁定时器
  206.     deinit {
  207.         timer?.invalidate()
  208.     }
  209. }
复制代码

使用示例
  1. import UIKit
  2. class ViewController: UIViewController {
  3.    
  4.     override func viewDidLoad() {
  5.         super.viewDidLoad()
  6.         
  7.         setupCountdownButton()
  8.         setupCountupButton()
  9.         setupSingleShotButton()
  10.     }
  11.    
  12.     private func setupCountdownButton() {
  13.         let countdownButton = MultiModeTimerButton(
  14.             frame: CGRect(x: 50, y: 100, width: 200, height: 50),
  15.             timerMode: .countdown(totalTime: 10)
  16.         )
  17.         
  18.         countdownButton.normalTitle = "开始倒计时"
  19.         countdownButton.countingFormat = "剩余%d秒"
  20.         countdownButton.finishedTitle = "倒计时结束"
  21.         
  22.         countdownButton.onStart = {
  23.             print("倒计时开始")
  24.         }
  25.         
  26.         countdownButton.onTick = { time in
  27.             print("倒计时剩余: \(Int(time))秒")
  28.         }
  29.         
  30.         countdownButton.onFinish = {
  31.             print("倒计时结束")
  32.         }
  33.         
  34.         view.addSubview(countdownButton)
  35.         countdownButton.center.x = view.center.x
  36.     }
  37.    
  38.     private func setupCountupButton() {
  39.         let countupButton = MultiModeTimerButton(
  40.             frame: CGRect(x: 50, y: 170, width: 200, height: 50),
  41.             timerMode: .countup
  42.         )
  43.         
  44.         countupButton.normalTitle = "开始计时"
  45.         countupButton.countingFormat = "%.1f秒"
  46.         countupButton.finishedTitle = "停止计时"
  47.         
  48.         countupButton.onStart = {
  49.             print("正计时开始")
  50.         }
  51.         
  52.         countupButton.onTick = { time in
  53.             print("已计时: \(time)秒")
  54.         }
  55.         
  56.         countupButton.onFinish = {
  57.             print("正计时结束")
  58.         }
  59.         
  60.         view.addSubview(countupButton)
  61.         countupButton.center.x = view.center.x
  62.     }
  63.    
  64.     private func setupSingleShotButton() {
  65.         let singleShotButton = MultiModeTimerButton(
  66.             frame: CGRect(x: 50, y: 240, width: 200, height: 50),
  67.             timerMode: .singleShot(timeInterval: 3.0)
  68.         )
  69.         
  70.         singleShotButton.normalTitle = "3秒后执行"
  71.         singleShotButton.finishedTitle = "执行完成"
  72.         
  73.         singleShotButton.onStart = {
  74.             print("一次性定时器启动")
  75.         }
  76.         
  77.         singleShotButton.onSingleShot = {
  78.             print("一次性任务执行")
  79.         }
  80.         
  81.         singleShotButton.onFinish = {
  82.             print("一次性定时器结束")
  83.         }
  84.         
  85.         view.addSubview(singleShotButton)
  86.         singleShotButton.center.x = view.center.x
  87.     }
  88. }
复制代码

常见问题及解决方案

在实现定时器按钮功能时,开发者可能会遇到一些常见问题。下面我们将讨论这些问题及其解决方案。

1. 定时器与RunLoop的问题

问题描述:定时器在某些情况下可能不会按预期工作,例如在滚动视图时定时器暂停。

原因分析:这是因为定时器默认运行在RunLoop的.default模式下,当用户滚动视图时,RunLoop会切换到.tracking模式,导致定时器暂停。

解决方案:将定时器添加到RunLoop的.common模式中,这样定时器会在所有常见的RunLoop模式下运行。
  1. // 创建定时器
  2. let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
  3. // 将定时器添加到RunLoop的.common模式
  4. RunLoop.current.add(timer, forMode: .common)
复制代码

2. 循环引用问题

问题描述:在使用闭包创建定时器时,可能会出现循环引用,导致内存泄漏。

原因分析:当定时器的闭包中强引用了self,而self又强引用了定时器时,就会形成循环引用。

解决方案:在闭包中使用[weak self]来避免循环引用。
  1. timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  2.     guard let self = self else { return }
  3.     // 使用self
  4.     self.updateButtonTitle()
  5. }
复制代码

3. 定时器精度问题

问题描述:定时器的实际触发时间可能与指定的时间间隔有偏差,特别是在需要高精度计时的场景中。

原因分析:Timer不是实时系统,它的精度受到系统负载、RunLoop模式等多种因素的影响。

解决方案:对于需要高精度的场景,可以考虑使用DispatchSourceTimer(GCD定时器)代替Timer。
  1. import Dispatch
  2. class GCDTimerButton: UIButton {
  3.    
  4.     private var timer: DispatchSourceTimer?
  5.     private var remainingTime: Int = 0
  6.     private var totalTime: Int = 60
  7.    
  8.     // ... 其他代码 ...
  9.    
  10.     private func startGCDTimer() {
  11.         remainingTime = totalTime
  12.         isEnabled = false
  13.         updateButtonTitle()
  14.         
  15.         // 创建GCD定时器
  16.         timer = DispatchSource.makeTimerSource()
  17.         
  18.         // 设置定时器
  19.         timer?.schedule(deadline: .now(), repeating: .seconds(1))
  20.         
  21.         // 设置事件处理器
  22.         timer?.setEventHandler { [weak self] in
  23.             DispatchQueue.main.async {
  24.                 self?.remainingTime -= 1
  25.                 self?.updateButtonTitle()
  26.                
  27.                 if self?.remainingTime == 0 {
  28.                     self?.stopGCDTimer()
  29.                 }
  30.             }
  31.         }
  32.         
  33.         // 启动定时器
  34.         timer?.resume()
  35.     }
  36.    
  37.     private func stopGCDTimer() {
  38.         timer?.cancel()
  39.         timer = nil
  40.         isEnabled = true
  41.         setTitle("重新获取", for: .normal)
  42.     }
  43.    
  44.     // ... 其他代码 ...
  45. }
复制代码

4. 后台运行问题

问题描述:当应用进入后台时,定时器可能会暂停工作。

原因分析:iOS系统为了节省电量,会在应用进入后台时限制其活动,包括暂停定时器。

解决方案:如果需要在应用进入后台时继续运行定时器,可以使用beginBackgroundTask(withName:expirationHandler:)方法申请后台执行时间。
  1. var backgroundTask: UIBackgroundTaskIdentifier = .invalid
  2. func startTimer() {
  3.     // 注册后台任务
  4.     backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "TimerBackgroundTask") {
  5.         // 后台任务即将过期时调用
  6.         self.endBackgroundTask()
  7.     }
  8.    
  9.     // 启动定时器
  10.     timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  11.         guard let self = self else { return }
  12.         
  13.         // 更新UI
  14.         self.remainingTime -= 1
  15.         self.updateButtonTitle()
  16.         
  17.         if self.remainingTime == 0 {
  18.             self.stopTimer()
  19.         }
  20.         
  21.         // 检查后台时间是否即将用完
  22.         if UIApplication.shared.backgroundTimeRemaining < 10 {
  23.             self.endBackgroundTask()
  24.         }
  25.     }
  26. }
  27. func endBackgroundTask() {
  28.     if backgroundTask != .invalid {
  29.         UIApplication.shared.endBackgroundTask(backgroundTask)
  30.         backgroundTask = .invalid
  31.     }
  32. }
  33. func stopTimer() {
  34.     timer?.invalidate()
  35.     timer = nil
  36.     isEnabled = true
  37.     setTitle("重新获取", for: .normal)
  38.     endBackgroundTask()
  39. }
复制代码

5. 多定时器管理问题

问题描述:在一个界面中有多个定时器按钮时,如何有效管理它们,避免冲突和资源浪费。

原因分析:每个定时器按钮都有自己的定时器实例,如果没有统一管理,可能会导致多个定时器同时运行,浪费系统资源。

解决方案:创建一个定时器管理器,统一管理所有的定时器。
  1. import UIKit
  2. class TimerManager {
  3.    
  4.     static let shared = TimerManager()
  5.    
  6.     private var timers: [String: Timer] = [:]
  7.    
  8.     private init() {}
  9.    
  10.     func addTimer(id: String, timer: Timer) {
  11.         // 如果已存在相同ID的定时器,先移除
  12.         removeTimer(id: id)
  13.         
  14.         // 添加新定时器
  15.         timers[id] = timer
  16.     }
  17.    
  18.     func removeTimer(id: String) {
  19.         if let timer = timers[id] {
  20.             timer.invalidate()
  21.             timers.removeValue(forKey: id)
  22.         }
  23.     }
  24.    
  25.     func removeAllTimers() {
  26.         for (_, timer) in timers {
  27.             timer.invalidate()
  28.         }
  29.         timers.removeAll()
  30.     }
  31.    
  32.     func isTimerActive(id: String) -> Bool {
  33.         return timers[id] != nil
  34.     }
  35. }
  36. class ManagedTimerButton: UIButton {
  37.    
  38.     private var timerId: String
  39.     private var remainingTime: Int = 0
  40.     private var totalTime: Int = 60
  41.    
  42.     init(frame: CGRect, timerId: String, totalTime: Int = 60) {
  43.         self.timerId = timerId
  44.         self.totalTime = totalTime
  45.         super.init(frame: frame)
  46.         setupButton()
  47.     }
  48.    
  49.     required init?(coder: NSCoder) {
  50.         fatalError("init(coder:) has not been implemented")
  51.     }
  52.    
  53.     private func setupButton() {
  54.         setTitle("获取验证码", for: .normal)
  55.         backgroundColor = .systemBlue
  56.         setTitleColor(.white, for: .normal)
  57.         layer.cornerRadius = 5
  58.         addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
  59.     }
  60.    
  61.     @objc private func buttonTapped() {
  62.         // 检查定时器是否已在运行
  63.         if TimerManager.shared.isTimerActive(id: timerId) {
  64.             return
  65.         }
  66.         
  67.         startTimer()
  68.     }
  69.    
  70.     private func startTimer() {
  71.         remainingTime = totalTime
  72.         isEnabled = false
  73.         updateButtonTitle()
  74.         
  75.         let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  76.             guard let self = self else { return }
  77.             
  78.             self.remainingTime -= 1
  79.             self.updateButtonTitle()
  80.             
  81.             if self.remainingTime == 0 {
  82.                 self.stopTimer()
  83.             }
  84.         }
  85.         
  86.         // 将定时器添加到管理器
  87.         TimerManager.shared.addTimer(id: timerId, timer: timer)
  88.     }
  89.    
  90.     private func stopTimer() {
  91.         TimerManager.shared.removeTimer(id: timerId)
  92.         isEnabled = true
  93.         setTitle("重新获取", for: .normal)
  94.     }
  95.    
  96.     private func updateButtonTitle() {
  97.         setTitle("\(remainingTime)秒后重试", for: .disabled)
  98.     }
  99.    
  100.     deinit {
  101.         TimerManager.shared.removeTimer(id: timerId)
  102.     }
  103. }
复制代码

提升用户体验的技巧

定时器按钮不仅仅是功能的实现,还需要考虑用户体验。下面是一些提升用户体验的技巧。

1. 视觉反馈

为定时器按钮添加视觉反馈,可以帮助用户更好地理解当前状态。
  1. class VisualFeedbackTimerButton: UIButton {
  2.    
  3.     // ... 其他代码 ...
  4.    
  5.     private func updateButtonAppearance() {
  6.         // 更新标题
  7.         let formattedTitle = String(format: countingFormat, remainingTime)
  8.         setTitle(formattedTitle, for: .disabled)
  9.         
  10.         // 根据剩余时间改变颜色
  11.         let progress = Double(remainingTime) / Double(totalTime)
  12.         
  13.         if progress > 0.5 {
  14.             backgroundColor = normalBackgroundColor
  15.         } else if progress > 0.2 {
  16.             backgroundColor = .systemOrange
  17.         } else {
  18.             backgroundColor = .systemRed
  19.         }
  20.         
  21.         // 添加脉冲动画
  22.         pulseAnimation()
  23.     }
  24.    
  25.     private func pulseAnimation() {
  26.         let pulse = CABasicAnimation(keyPath: "transform.scale")
  27.         pulse.duration = 0.5
  28.         pulse.fromValue = 1.0
  29.         pulse.toValue = 1.05
  30.         pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  31.         pulse.autoreverses = true
  32.         pulse.repeatCount = 1
  33.         
  34.         layer.add(pulse, forKey: "pulse")
  35.     }
  36.    
  37.     // ... 其他代码 ...
  38. }
复制代码

2. 声音反馈

在定时器开始和结束时添加声音反馈,可以增强用户体验。
  1. import AVFoundation
  2. class SoundFeedbackTimerButton: UIButton {
  3.    
  4.     private var audioPlayer: AVAudioPlayer?
  5.    
  6.     // ... 其他代码 ...
  7.    
  8.     private func setupAudioPlayer() {
  9.         guard let soundURL = Bundle.main.url(forResource: "tick", withExtension: "mp3") else {
  10.             print("声音文件未找到")
  11.             return
  12.         }
  13.         
  14.         do {
  15.             audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  16.             audioPlayer?.prepareToPlay()
  17.         } catch {
  18.             print("音频播放器初始化失败: \(error)")
  19.         }
  20.     }
  21.    
  22.     private func startTimer() {
  23.         // ... 其他代码 ...
  24.         
  25.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  26.             guard let self = self else { return }
  27.             
  28.             self.remainingTime -= 1
  29.             self.updateButtonTitle()
  30.             
  31.             // 播放滴答声
  32.             self.playTickSound()
  33.             
  34.             if self.remainingTime == 0 {
  35.                 self.stopTimer()
  36.                 // 播放完成声音
  37.                 self.playFinishSound()
  38.             }
  39.         }
  40.     }
  41.    
  42.     private func playTickSound() {
  43.         // 如果剩余时间小于等于5秒,播放滴答声
  44.         if remainingTime <= 5 {
  45.             audioPlayer?.play()
  46.         }
  47.     }
  48.    
  49.     private func playFinishSound() {
  50.         // 这里可以播放不同的完成声音
  51.         // 为了简单起见,我们使用相同的音频文件
  52.         audioPlayer?.play()
  53.     }
  54.    
  55.     // ... 其他代码 ...
  56. }
复制代码

3. 震动反馈

在定时器开始和结束时添加震动反馈,可以提供触觉反馈。
  1. import UIKit
  2. class HapticFeedbackTimerButton: UIButton {
  3.    
  4.     // ... 其他代码 ...
  5.    
  6.     private func startTimer() {
  7.         // ... 其他代码 ...
  8.         
  9.         // 开始震动反馈
  10.         triggerHapticFeedback(style: .medium)
  11.         
  12.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  13.             guard let self = self else { return }
  14.             
  15.             self.remainingTime -= 1
  16.             self.updateButtonTitle()
  17.             
  18.             if self.remainingTime == 0 {
  19.                 self.stopTimer()
  20.                 // 结束震动反馈
  21.                 self.triggerHapticFeedback(style: .heavy)
  22.             }
  23.         }
  24.     }
  25.    
  26.     private func triggerHapticFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle) {
  27.         let generator = UIImpactFeedbackGenerator(style: style)
  28.         generator.impactOccurred()
  29.     }
  30.    
  31.     // ... 其他代码 ...
  32. }
复制代码

4. 进度指示

为定时器按钮添加进度指示,可以直观地展示剩余时间。
  1. import UIKit
  2. class ProgressTimerButton: UIButton {
  3.    
  4.     private var progressLayer = CAShapeLayer()
  5.     private var progressBackgroundLayer = CAShapeLayer()
  6.    
  7.     // ... 其他代码 ...
  8.    
  9.     private func setupProgressLayers() {
  10.         // 创建背景层
  11.         progressBackgroundLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
  12.         progressBackgroundLayer.fillColor = UIColor.clear.cgColor
  13.         progressBackgroundLayer.strokeColor = UIColor.systemGray5.cgColor
  14.         progressBackgroundLayer.lineWidth = 3.0
  15.         layer.addSublayer(progressBackgroundLayer)
  16.         
  17.         // 创建进度层
  18.         progressLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
  19.         progressLayer.fillColor = UIColor.clear.cgColor
  20.         progressLayer.strokeColor = tintColor.cgColor
  21.         progressLayer.lineWidth = 3.0
  22.         progressLayer.strokeEnd = 1.0
  23.         progressLayer.lineCap = .round
  24.         layer.addSublayer(progressLayer)
  25.     }
  26.    
  27.     private func updateProgress() {
  28.         let progress = Double(remainingTime) / Double(totalTime)
  29.         
  30.         CATransaction.begin()
  31.         CATransaction.setDisableActions(true)
  32.         progressLayer.strokeEnd = CGFloat(progress)
  33.         CATransaction.commit()
  34.         
  35.         // 根据进度改变颜色
  36.         if progress > 0.5 {
  37.             progressLayer.strokeColor = normalBackgroundColor.cgColor
  38.         } else if progress > 0.2 {
  39.             progressLayer.strokeColor = UIColor.systemOrange.cgColor
  40.         } else {
  41.             progressLayer.strokeColor = UIColor.systemRed.cgColor
  42.         }
  43.     }
  44.    
  45.     private func startTimer() {
  46.         // ... 其他代码 ...
  47.         
  48.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  49.             guard let self = self else { return }
  50.             
  51.             self.remainingTime -= 1
  52.             self.updateButtonTitle()
  53.             self.updateProgress()
  54.             
  55.             if self.remainingTime == 0 {
  56.                 self.stopTimer()
  57.             }
  58.         }
  59.     }
  60.    
  61.     private func stopTimer() {
  62.         // ... 其他代码 ...
  63.         
  64.         // 重置进度
  65.         CATransaction.begin()
  66.         CATransaction.setDisableActions(true)
  67.         progressLayer.strokeEnd = 1.0
  68.         CATransaction.commit()
  69.     }
  70.    
  71.     override func layoutSubviews() {
  72.         super.layoutSubviews()
  73.         
  74.         // 更新进度层路径
  75.         progressBackgroundLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
  76.         progressLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
  77.     }
  78.    
  79.     // ... 其他代码 ...
  80. }
复制代码

5. 动画效果

为定时器按钮添加动画效果,可以增强用户体验。
  1. import UIKit
  2. class AnimatedTimerButton: UIButton {
  3.    
  4.     // ... 其他代码 ...
  5.    
  6.     private func startTimer() {
  7.         // ... 其他代码 ...
  8.         
  9.         // 添加开始动画
  10.         animateStart()
  11.         
  12.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  13.             guard let self = self else { return }
  14.             
  15.             self.remainingTime -= 1
  16.             self.updateButtonTitle()
  17.             
  18.             // 添加滴答动画
  19.             self.animateTick()
  20.             
  21.             if self.remainingTime == 0 {
  22.                 self.stopTimer()
  23.                 // 添加完成动画
  24.                 self.animateFinish()
  25.             }
  26.         }
  27.     }
  28.    
  29.     private func animateStart() {
  30.         let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
  31.         scaleAnimation.duration = 0.2
  32.         scaleAnimation.fromValue = 1.0
  33.         scaleAnimation.toValue = 0.95
  34.         scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  35.         scaleAnimation.autoreverses = true
  36.         scaleAnimation.repeatCount = 1
  37.         
  38.         layer.add(scaleAnimation, forKey: "startScale")
  39.     }
  40.    
  41.     private func animateTick() {
  42.         // 如果剩余时间小于等于5秒,添加更明显的动画
  43.         if remainingTime <= 5 {
  44.             let shakeAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
  45.             shakeAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
  46.             shakeAnimation.duration = 0.5
  47.             shakeAnimation.values = [-5.0, 5.0, -5.0, 5.0, 0.0]
  48.             
  49.             layer.add(shakeAnimation, forKey: "shake")
  50.         }
  51.     }
  52.    
  53.     private func animateFinish() {
  54.         let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
  55.         bounceAnimation.duration = 0.5
  56.         bounceAnimation.values = [1.0, 1.2, 0.9, 1.1, 1.0]
  57.         bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  58.         
  59.         layer.add(bounceAnimation, forKey: "finishBounce")
  60.     }
  61.    
  62.     // ... 其他代码 ...
  63. }
复制代码

打造专业iOS应用的最佳实践

在开发专业的iOS应用时,定时器按钮的实现需要遵循一些最佳实践,以确保代码的可维护性、可扩展性和性能。

1. 使用协议和委托模式

使用协议和委托模式可以使定时器按钮更加灵活,便于与其他组件交互。
  1. import UIKit
  2. // 定义定时器按钮的协议
  3. protocol TimerButtonDelegate: AnyObject {
  4.     func timerButtonDidStartCountdown(_ button: TimerButton)
  5.     func timerButton(_ button: TimerButton, didUpdateRemainingTime time: Int)
  6.     func timerButtonDidFinishCountdown(_ button: TimerButton)
  7. }
  8. // 实现协议的定时器按钮
  9. class ProtocolTimerButton: UIButton {
  10.    
  11.     weak var delegate: TimerButtonDelegate?
  12.    
  13.     private var timer: Timer?
  14.     private var remainingTime: Int = 0
  15.     private var totalTime: Int = 60
  16.    
  17.     // ... 其他代码 ...
  18.    
  19.     private func startTimer() {
  20.         remainingTime = totalTime
  21.         isEnabled = false
  22.         updateButtonTitle()
  23.         
  24.         // 通知委托
  25.         delegate?.timerButtonDidStartCountdown(self)
  26.         
  27.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  28.             guard let self = self else { return }
  29.             
  30.             self.remainingTime -= 1
  31.             self.updateButtonTitle()
  32.             
  33.             // 通知委托
  34.             self.delegate?.timerButton(self, didUpdateRemainingTime: self.remainingTime)
  35.             
  36.             if self.remainingTime == 0 {
  37.                 self.stopTimer()
  38.             }
  39.         }
  40.     }
  41.    
  42.     private func stopTimer() {
  43.         timer?.invalidate()
  44.         timer = nil
  45.         isEnabled = true
  46.         setTitle("重新获取", for: .normal)
  47.         
  48.         // 通知委托
  49.         delegate?.timerButtonDidFinishCountdown(self)
  50.     }
  51.    
  52.     // ... 其他代码 ...
  53. }
  54. // 使用示例
  55. class ViewController: UIViewController, TimerButtonDelegate {
  56.    
  57.     override func viewDidLoad() {
  58.         super.viewDidLoad()
  59.         
  60.         let timerButton = ProtocolTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
  61.         timerButton.delegate = self
  62.         view.addSubview(timerButton)
  63.         timerButton.center.x = view.center.x
  64.     }
  65.    
  66.     // MARK: - TimerButtonDelegate
  67.    
  68.     func timerButtonDidStartCountdown(_ button: TimerButton) {
  69.         print("定时器开始倒计时")
  70.     }
  71.    
  72.     func timerButton(_ button: TimerButton, didUpdateRemainingTime time: Int) {
  73.         print("剩余时间: \(time)秒")
  74.     }
  75.    
  76.     func timerButtonDidFinishCountdown(_ button: TimerButton) {
  77.         print("定时器倒计时结束")
  78.     }
  79. }
复制代码

2. 使用响应式编程

使用响应式编程(如Combine或RxSwift)可以简化定时器按钮的事件处理。
  1. import UIKit
  2. import Combine
  3. class ReactiveTimerButton: UIButton {
  4.    
  5.     private var timer: Timer?
  6.     private var remainingTime: Int = 0
  7.     private var totalTime: Int = 60
  8.    
  9.     // 使用Combine发布者
  10.     private var cancellables = Set<AnyCancellable>()
  11.    
  12.     // 定义事件发布者
  13.     let didStartCountdown = PassthroughSubject<Void, Never>()
  14.     let didUpdateRemainingTime = PassthroughSubject<Int, Never>()
  15.     let didFinishCountdown = PassthroughSubject<Void, Never>()
  16.    
  17.     // ... 其他代码 ...
  18.    
  19.     private func startTimer() {
  20.         remainingTime = totalTime
  21.         isEnabled = false
  22.         updateButtonTitle()
  23.         
  24.         // 发送开始事件
  25.         didStartCountdown.send()
  26.         
  27.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  28.             guard let self = self else { return }
  29.             
  30.             self.remainingTime -= 1
  31.             self.updateButtonTitle()
  32.             
  33.             // 发送更新事件
  34.             self.didUpdateRemainingTime.send(self.remainingTime)
  35.             
  36.             if self.remainingTime == 0 {
  37.                 self.stopTimer()
  38.             }
  39.         }
  40.     }
  41.    
  42.     private func stopTimer() {
  43.         timer?.invalidate()
  44.         timer = nil
  45.         isEnabled = true
  46.         setTitle("重新获取", for: .normal)
  47.         
  48.         // 发送完成事件
  49.         didFinishCountdown.send()
  50.     }
  51.    
  52.     // ... 其他代码 ...
  53. }
  54. // 使用示例
  55. class ViewController: UIViewController {
  56.    
  57.     override func viewDidLoad() {
  58.         super.viewDidLoad()
  59.         
  60.         let timerButton = ReactiveTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
  61.         view.addSubview(timerButton)
  62.         timerButton.center.x = view.center.x
  63.         
  64.         // 订阅事件
  65.         timerButton.didStartCountdown
  66.             .sink { _ in
  67.                 print("定时器开始倒计时")
  68.             }
  69.             .store(in: &timerButton.cancellables)
  70.         
  71.         timerButton.didUpdateRemainingTime
  72.             .sink { time in
  73.                 print("剩余时间: \(time)秒")
  74.             }
  75.             .store(in: &timerButton.cancellables)
  76.         
  77.         timerButton.didFinishCountdown
  78.             .sink { _ in
  79.                 print("定时器倒计时结束")
  80.             }
  81.             .store(in: &timerButton.cancellables)
  82.     }
  83. }
复制代码

3. 使用SwiftUI实现定时器按钮

对于使用SwiftUI开发的应用,可以使用SwiftUI的方式实现定时器按钮。
  1. import SwiftUI
  2. struct SwiftUIButton: View {
  3.    
  4.     @State private var remainingTime: Int = 0
  5.     @State private var isCountingDown: Bool = false
  6.     @State private var timer: Timer?
  7.    
  8.     let totalTime: Int
  9.     let normalTitle: String
  10.     let countingFormat: String
  11.     let finishedTitle: String
  12.     let action: () -> Void
  13.    
  14.     init(
  15.         totalTime: Int = 60,
  16.         normalTitle: String = "获取验证码",
  17.         countingFormat: String = "%d秒后重试",
  18.         finishedTitle: String = "重新获取",
  19.         action: @escaping () -> Void = {}
  20.     ) {
  21.         self.totalTime = totalTime
  22.         self.normalTitle = normalTitle
  23.         self.countingFormat = countingFormat
  24.         self.finishedTitle = finishedTitle
  25.         self.action = action
  26.     }
  27.    
  28.     var body: some View {
  29.         Button(action: {
  30.             if !isCountingDown {
  31.                 startCountdown()
  32.                 action()
  33.             }
  34.         }) {
  35.             Text(buttonTitle)
  36.                 .font(.headline)
  37.                 .foregroundColor(.white)
  38.                 .frame(maxWidth: .infinity)
  39.                 .padding()
  40.                 .background(buttonColor)
  41.                 .cornerRadius(10)
  42.         }
  43.         .disabled(isCountingDown)
  44.         .onDisappear {
  45.             stopCountdown()
  46.         }
  47.     }
  48.    
  49.     private var buttonTitle: String {
  50.         if isCountingDown {
  51.             return String(format: countingFormat, remainingTime)
  52.         } else if remainingTime == 0 {
  53.             return finishedTitle
  54.         } else {
  55.             return normalTitle
  56.         }
  57.     }
  58.    
  59.     private var buttonColor: Color {
  60.         if isCountingDown {
  61.             let progress = Double(remainingTime) / Double(totalTime)
  62.             if progress > 0.5 {
  63.                 return .blue
  64.             } else if progress > 0.2 {
  65.                 return .orange
  66.             } else {
  67.                 return .red
  68.             }
  69.         } else {
  70.             return .blue
  71.         }
  72.     }
  73.    
  74.     private func startCountdown() {
  75.         isCountingDown = true
  76.         remainingTime = totalTime
  77.         
  78.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
  79.             if self.remainingTime > 0 {
  80.                 self.remainingTime -= 1
  81.             } else {
  82.                 self.stopCountdown()
  83.             }
  84.         }
  85.     }
  86.    
  87.     private func stopCountdown() {
  88.         timer?.invalidate()
  89.         timer = nil
  90.         isCountingDown = false
  91.     }
  92. }
  93. // 使用示例
  94. struct ContentView: View {
  95.     var body: some View {
  96.         VStack(spacing: 20) {
  97.             SwiftUIButton(
  98.                 totalTime: 10,
  99.                 normalTitle: "发送验证码",
  100.                 countingFormat: "剩余%d秒",
  101.                 finishedTitle: "再次发送"
  102.             ) {
  103.                 print("验证码已发送")
  104.             }
  105.             .padding(.horizontal)
  106.             
  107.             SwiftUIButton(
  108.                 totalTime: 5,
  109.                 normalTitle: "开始计时",
  110.                 countingFormat: "计时中: %d",
  111.                 finishedTitle: "计时完成"
  112.             ) {
  113.                 print("计时开始")
  114.             }
  115.             .padding(.horizontal)
  116.         }
  117.     }
  118. }
复制代码

4. 使用MVVM架构

使用MVVM(Model-View-ViewModel)架构可以使定时器按钮的代码更加结构化和可测试。
  1. import UIKit
  2. // Model
  3. struct TimerModel {
  4.     var totalTime: Int
  5.     var remainingTime: Int
  6.     var isActive: Bool
  7. }
  8. // ViewModel
  9. class TimerButtonViewModel {
  10.    
  11.     // 使用@Published属性,以便视图可以订阅它们的变化
  12.     @Published var model: TimerModel
  13.    
  14.     // 定义事件
  15.     let didStart = PassthroughSubject<Void, Never>()
  16.     let didTick = PassthroughSubject<Int, Never>()
  17.     let didFinish = PassthroughSubject<Void, Never>()
  18.    
  19.     private var timer: Timer?
  20.     private var cancellables = Set<AnyCancellable>()
  21.    
  22.     init(totalTime: Int = 60) {
  23.         model = TimerModel(totalTime: totalTime, remainingTime: totalTime, isActive: false)
  24.     }
  25.    
  26.     func startTimer() {
  27.         guard !model.isActive else { return }
  28.         
  29.         model.isActive = true
  30.         model.remainingTime = model.totalTime
  31.         
  32.         didStart.send()
  33.         
  34.         timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
  35.             guard let self = self else { return }
  36.             
  37.             self.model.remainingTime -= 1
  38.             self.didTick.send(self.model.remainingTime)
  39.             
  40.             if self.model.remainingTime == 0 {
  41.                 self.stopTimer()
  42.             }
  43.         }
  44.     }
  45.    
  46.     func stopTimer() {
  47.         guard model.isActive else { return }
  48.         
  49.         timer?.invalidate()
  50.         timer = nil
  51.         model.isActive = false
  52.         
  53.         didFinish.send()
  54.     }
  55.    
  56.     func resetTimer() {
  57.         stopTimer()
  58.         model.remainingTime = model.totalTime
  59.     }
  60. }
  61. // View
  62. class MVVMTimerButton: UIButton {
  63.    
  64.     private var viewModel: TimerButtonViewModel
  65.     private var cancellables = Set<AnyCancellable>()
  66.    
  67.     init(frame: CGRect, viewModel: TimerButtonViewModel) {
  68.         self.viewModel = viewModel
  69.         super.init(frame: frame)
  70.         setupButton()
  71.         setupBindings()
  72.     }
  73.    
  74.     required init?(coder: NSCoder) {
  75.         fatalError("init(coder:) has not been implemented")
  76.     }
  77.    
  78.     private func setupButton() {
  79.         addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
  80.         updateButtonAppearance()
  81.     }
  82.    
  83.     private func setupBindings() {
  84.         // 订阅模型变化
  85.         viewModel.$model
  86.             .receive(on: RunLoop.main)
  87.             .sink { [weak self] model in
  88.                 self?.updateButtonAppearance()
  89.             }
  90.             .store(in: &cancellables)
  91.         
  92.         // 订阅事件
  93.         viewModel.didStart
  94.             .receive(on: RunLoop.main)
  95.             .sink { [weak self] _ in
  96.                 print("定时器开始")
  97.             }
  98.             .store(in: &cancellables)
  99.         
  100.         viewModel.didTick
  101.             .receive(on: RunLoop.main)
  102.             .sink { time in
  103.                 print("剩余时间: \(time)秒")
  104.             }
  105.             .store(in: &cancellables)
  106.         
  107.         viewModel.didFinish
  108.             .receive(on: RunLoop.main)
  109.             .sink { [weak self] _ in
  110.                 print("定时器结束")
  111.             }
  112.             .store(in: &cancellables)
  113.     }
  114.    
  115.     @objc private func buttonTapped() {
  116.         if viewModel.model.isActive {
  117.             viewModel.stopTimer()
  118.         } else {
  119.             viewModel.startTimer()
  120.         }
  121.     }
  122.    
  123.     private func updateButtonAppearance() {
  124.         isEnabled = !viewModel.model.isActive
  125.         
  126.         if viewModel.model.isActive {
  127.             let formattedTitle = String(format: "%d秒后重试", viewModel.model.remainingTime)
  128.             setTitle(formattedTitle, for: .disabled)
  129.             backgroundColor = .systemGray
  130.         } else if viewModel.model.remainingTime == 0 {
  131.             setTitle("重新获取", for: .normal)
  132.             backgroundColor = .systemBlue
  133.         } else {
  134.             setTitle("获取验证码", for: .normal)
  135.             backgroundColor = .systemBlue
  136.         }
  137.     }
  138.    
  139.     deinit {
  140.         viewModel.stopTimer()
  141.     }
  142. }
  143. // 使用示例
  144. class ViewController: UIViewController {
  145.    
  146.     override func viewDidLoad() {
  147.         super.viewDidLoad()
  148.         
  149.         let viewModel = TimerButtonViewModel(totalTime: 10)
  150.         let timerButton = MVVMTimerButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50), viewModel: viewModel)
  151.         
  152.         view.addSubview(timerButton)
  153.         timerButton.center.x = view.center.x
  154.     }
  155. }
复制代码

5. 性能优化

在实现定时器按钮时,需要注意性能优化,特别是在有多个定时器按钮的场景中。
  1. import UIKit
  2. class OptimizedTimerButton: UIButton {
  3.    
  4.     // 使用轻量级的GCD定时器代替Timer
  5.     private var timer: DispatchSourceTimer?
  6.     private var remainingTime: Int = 0
  7.     private var totalTime: Int = 60
  8.    
  9.     // 使用静态的定时器队列,避免创建多个队列
  10.     private static let timerQueue = DispatchQueue(label: "com.example.timerbutton.queue", attributes: .concurrent)
  11.    
  12.     // 使用弱引用避免循环引用
  13.     private weak var weakSelf: OptimizedTimerButton?
  14.    
  15.     // ... 其他代码 ...
  16.    
  17.     private func startTimer() {
  18.         // 停止现有定时器
  19.         stopTimer()
  20.         
  21.         remainingTime = totalTime
  22.         isEnabled = false
  23.         updateButtonTitle()
  24.         
  25.         // 保存弱引用
  26.         weakSelf = self
  27.         
  28.         // 创建GCD定时器
  29.         timer = DispatchSource.makeTimerSource(queue: OptimizedTimerButton.timerQueue)
  30.         
  31.         // 设置定时器
  32.         timer?.schedule(deadline: .now(), repeating: .seconds(1))
  33.         
  34.         // 设置事件处理器
  35.         timer?.setEventHandler { [weak self] in
  36.             guard let self = self else { return }
  37.             
  38.             // 在主线程更新UI
  39.             DispatchQueue.main.async {
  40.                 self.remainingTime -= 1
  41.                 self.updateButtonTitle()
  42.                
  43.                 if self.remainingTime == 0 {
  44.                     self.stopTimer()
  45.                 }
  46.             }
  47.         }
  48.         
  49.         // 启动定时器
  50.         timer?.resume()
  51.     }
  52.    
  53.     private func stopTimer() {
  54.         timer?.cancel()
  55.         timer = nil
  56.         isEnabled = true
  57.         setTitle("重新获取", for: .normal)
  58.     }
  59.    
  60.     // ... 其他代码 ...
  61.    
  62.     deinit {
  63.         stopTimer()
  64.     }
  65. }
复制代码

总结

本文全面介绍了在Swift中实现定时器按钮功能的方法,从基础概念到高级技巧,帮助开发者掌握这一重要功能,提升应用的专业性和用户体验。

我们首先介绍了Swift中Timer的基础知识,包括Timer的基本用法、主要属性和方法,以及RunLoop与Timer的关系。然后,我们逐步实现了从简单的定时器按钮到支持多种定时器模式的高级定时器按钮。

接着,我们讨论了在实现定时器按钮时可能遇到的常见问题及其解决方案,包括定时器与RunLoop的问题、循环引用问题、定时器精度问题、后台运行问题和多定时器管理问题。

为了提升用户体验,我们介绍了一些技巧,包括视觉反馈、声音反馈、震动反馈、进度指示和动画效果。这些技巧可以使定时器按钮更加直观、生动和用户友好。

最后,我们探讨了打造专业iOS应用的最佳实践,包括使用协议和委托模式、使用响应式编程、使用SwiftUI实现定时器按钮、使用MVVM架构和性能优化。这些实践可以帮助开发者编写更加结构化、可维护和高性能的代码。

通过本文的学习,开发者应该能够掌握在Swift中实现定时器按钮的全面技能,并能够根据具体需求选择合适的实现方式,打造出专业、高效且用户友好的iOS应用。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

加入Discord频道

加入Discord频道

加入QQ社群

加入QQ社群

联系我们|小黑屋|TG频道|RSS |网站地图

Powered by Pixtech

© 2025-2026 Pixtech Team.