如何创建可在月份变更时发送事件的Publisher?
如何创建可在月份变更时发送事件的Publisher?
嘿,我刚好碰到过几乎一模一样的需求——做一个每月自动重置额度的功能,既要前台能精准触发,又要覆盖后台切回、APP重启的场景,给你分享几个靠谱的实现思路:
一、组合系统通知+前台激活事件(简单易上手)
你提到的NotificationCenter.default.publisher(for: UIApplication.significantTimeChangeNotification)其实是个不错的基础,但确实它在后台不一定会触发。那我们可以把它和UIApplication.didBecomeActiveNotification(前台激活通知)结合起来,刚好覆盖所有场景:
- 当APP在前台时,
significantTimeChangeNotification会在时间跳变(包括跨月零点)时自动触发,我们可以直接在这个信号里重置额度 - 当APP从后台切回前台时,主动检查当前日期和上次重置日期是否跨月,如果是就触发重置
这个方案的好处是不用自己算时间,完全依赖系统通知,代码量小,而且完全符合你说的“后台时切回前台再重置”的需求。
二、精准Timer+循环递推(前台实时触发)
如果想要在前台时精准到每月1号0点触发,我们可以自己计算到下一个月1号的时间差,用Timer.publish创建一次性Timer,触发后再自动计算下一个月的时间,循环往复。不过要注意,APP后台被挂起时Timer会暂停,所以同样要结合前台激活时的检查来补漏:
- 先写个工具函数,计算当前时区下下一个月1号0点的时间:
func nextMonthStartDate() -> Date { let calendar = Calendar.current let now = Date() // 先拿到下个月的当前日期,再调整到当天0点 guard let nextMonth = calendar.date(byAdding: .month, value: 1, to: now) else { // 极端情况拿当前日期的0点兜底 return calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now)! } return calendar.date(bySettingHour: 0, minute: 0, second: 0, of: nextMonth)! }
- 基于这个函数,创建循环触发的月度Publisher:
func monthlyTriggerPublisher() -> AnyPublisher<Date, Never> { // 启动时先发送一次当前日期,用于初始化检查 let initialCheck = Just(Date()) // 递归创建每个月的Timer Publisher,触发后自动生成下一个月的Timer let timerSequence = initialCheck.flatMap { _ in let interval = nextMonthStartDate().timeIntervalSinceNow return Timer.publish(every: interval, on: .main, in: .common) .autoconnect() .flatMap { triggerDate in Just(triggerDate) .merge(with: monthlyTriggerPublisher()) } } // 前台激活事件,用于后台切回时的补查 let foregroundActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) .map { _ in Date() } // 合并两个信号,并且去重:如果两次事件在同一个月,只保留一次 return timerSequence.merge(with: foregroundActive) .removeDuplicates { date1, date2 in Calendar.current.isDate(date1, equalTo: date2, toGranularity: .month) } .eraseToAnyPublisher() }
三、关键补充:APP启动/重启时的检查
不管用哪个方案,都必须处理APP重启的情况——用户可能在跨月后第一次打开APP,这时候我们要主动检查是否需要重置额度。可以用UserDefaults存储上次重置的日期,启动时对比:
let monthlyAllowance = CurrentValueSubject<Int, Never>(100) var cancellables = Set<AnyCancellable>() // 启动时执行检查 func checkAndResetOnLaunch() { let calendar = Calendar.current let lastResetDate = UserDefaults.standard.object(forKey: "LastAllowanceReset") as? Date ?? Date.distantPast let now = Date() if !calendar.isDate(lastResetDate, equalTo: now, toGranularity: .month) { monthlyAllowance.send(100) UserDefaults.standard.set(now, forKey: "LastAllowanceReset") } }
最终使用方式
把上面的Publisher和你的CurrentValueSubject绑定,就能实现自动重置了:
// 启动先检查一次 checkAndResetOnLaunch() // 绑定月度触发信号 monthlyTriggerPublisher() .sink { [weak self] _ in guard let self = self else { return } self.monthlyAllowance.send(100) UserDefaults.standard.set(Date(), forKey: "LastAllowanceReset") } .store(in: &cancellables)
几个要注意的点
- 时区问题:一定要用
Calendar.current(本地时区)计算日期,别用UTC,不然会出现跨月时间不对的情况 - 后台限制:iOS后台不会让APP一直运行Timer,所以永远不能依赖后台Timer持续触发,必须结合前台激活和启动检查来覆盖所有场景
- 去重处理:避免同一个月内多次触发重置,比如前台激活时如果没跨月,就不执行操作
内容来源于stack exchange
相关产品推荐
相关产品推荐

