You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建可在月份变更时发送事件的Publisher?

如何创建可在月份变更时发送事件的Publisher?

嘿,我刚好碰到过几乎一模一样的需求——做一个每月自动重置额度的功能,既要前台能精准触发,又要覆盖后台切回、APP重启的场景,给你分享几个靠谱的实现思路:

一、组合系统通知+前台激活事件(简单易上手)

你提到的NotificationCenter.default.publisher(for: UIApplication.significantTimeChangeNotification)其实是个不错的基础,但确实它在后台不一定会触发。那我们可以把它和UIApplication.didBecomeActiveNotification(前台激活通知)结合起来,刚好覆盖所有场景:

  • 当APP在前台时,significantTimeChangeNotification会在时间跳变(包括跨月零点)时自动触发,我们可以直接在这个信号里重置额度
  • 当APP从后台切回前台时,主动检查当前日期和上次重置日期是否跨月,如果是就触发重置

这个方案的好处是不用自己算时间,完全依赖系统通知,代码量小,而且完全符合你说的“后台时切回前台再重置”的需求。

二、精准Timer+循环递推(前台实时触发)

如果想要在前台时精准到每月1号0点触发,我们可以自己计算到下一个月1号的时间差,用Timer.publish创建一次性Timer,触发后再自动计算下一个月的时间,循环往复。不过要注意,APP后台被挂起时Timer会暂停,所以同样要结合前台激活时的检查来补漏:

  1. 先写个工具函数,计算当前时区下下一个月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)!
}
  1. 基于这个函数,创建循环触发的月度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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 12:20:29