Skip to content

Instantly share code, notes, and snippets.

@Aayush9029
Last active October 25, 2025 01:42
Show Gist options
  • Select an option

  • Save Aayush9029/40afffc39c358c346f248b37704764b6 to your computer and use it in GitHub Desktop.

Select an option

Save Aayush9029/40afffc39c358c346f248b37704764b6 to your computer and use it in GitHub Desktop.
Local Interval Client
import Dependencies
import Foundation
public struct IntervalKey {
let key: String
let config: IntervalConfig
public init(_ key: String, config: IntervalConfig) {
self.key = "interval." + key
self.config = config
}
}
public struct IntervalConfig: Equatable {
let component: Calendar.Component
let value: Int
public static let hourly = Self(component: .hour, value: 1)
public static let daily = Self(component: .day, value: 1)
public static let weekly = Self(component: .weekOfYear, value: 1)
public static let monthly = Self(component: .month, value: 1)
public static let yearly = Self(component: .year, value: 1)
public static func custom(_ component: Calendar.Component, _ value: Int) -> Self {
Self(component: component, value: value)
}
}
public extension IntervalKey {
static func key(_ name: String, _ config: IntervalConfig) -> IntervalKey {
IntervalKey(name, config: config)
}
}
public struct IntervalClient {
public var shouldRun: @Sendable (IntervalKey) -> Bool
public var markRun: @Sendable (IntervalKey) async -> Void
public var reset: @Sendable (IntervalKey) async -> Void
}
extension IntervalClient: DependencyKey {
public static let liveValue = Self(
shouldRun: { key in
let defaults = UserDefaults.standard
// Never run before
guard let lastRunTimestamp = defaults.object(forKey: key.key) as? TimeInterval else {
return true
}
let lastRunDate = Date(timeIntervalSince1970: lastRunTimestamp)
guard let nextRunDate = Calendar.current.date(
byAdding: key.config.component,
value: key.config.value,
to: lastRunDate
) else {
return true // Fallback if date calculation fails
}
// Handles wrap-around via proper date math
return Date() >= nextRunDate
},
markRun: { key in
let timestamp = Date().timeIntervalSince1970
UserDefaults.standard.set(timestamp, forKey: key.key)
},
reset: { key in
UserDefaults.standard.removeObject(forKey: key.key)
}
)
}
public extension DependencyValues {
var intervalClient: IntervalClient {
get { self[IntervalClient.self] }
set { self[IntervalClient.self] = newValue }
}
}
@Aayush9029
Copy link
Author

Aayush9029 commented Oct 25, 2025

IntervalClient

What Does It Do?

IntervalClient prevents actions from happening too frequently.

Think of it like a cooldown timer in a game - once you use an ability, you can't use it again until the cooldown is over.

Real-World Problem

Your app wants to ask users for a review, but you don't want to annoy them:

  • ❌ Without IntervalClient: Ask every time they open the app (annoying!)
  • ✅ With IntervalClient: Ask once per month maximum (polite!)

How To Use It

Step 1: Define Your Intervals

import Foundation

public extension IntervalKey {
    // Ask for review once per month
    static let reviewPrompt = key("reviewPrompt", .monthly)
    
    // Show paywall once per week
    static let paywallPrompt = key("paywallPrompt", .weekly)
    
    // Show daily tip once per day
    static let dailyTip = key("dailyTip", .daily)
}

Step 2: Use It In Your App

import Dependencies
import Observation

@Observable
final class AppViewModel {
    @ObservationIgnored
    @Dependency(\.intervalClient) var intervalClient
    
    func showReviewPromptIfNeeded() {
        // Check if enough time has passed
        guard intervalClient.shouldRun(.reviewPrompt) else {
            print("⏰ Too soon, not showing review prompt")
            return
        }
        
        print("✅ Showing review prompt!")
        
        Task {
            // Mark that we showed it now
            await intervalClient.markRun(.reviewPrompt)
            
            // Show the actual review prompt
            await showReviewDialog()
        }
    }
}

Timeline Example

Let's say today is January 1st, 2025 at 9:00 AM:

// January 1st, 9:00 AM - First launch ever
intervalClient.shouldRun(.reviewPrompt)  // ✅ true (never run before)
await intervalClient.markRun(.reviewPrompt)
// Stores: "Last run = Jan 1, 9:00 AM"

// January 5th, 3:00 PM - 4 days later
intervalClient.shouldRun(.reviewPrompt)  // ❌ false (only 4 days passed)

// January 25th, 10:00 AM - 24 days later
intervalClient.shouldRun(.reviewPrompt)  // ❌ false (not quite a month)

// February 1st, 9:00 AM - Exactly 1 month later
intervalClient.shouldRun(.reviewPrompt)  // ✅ true (1 month passed!)
await intervalClient.markRun(.reviewPrompt)
// Updates: "Last run = Feb 1, 9:00 AM"

// February 2nd - Next day
intervalClient.shouldRun(.reviewPrompt)  // ❌ false (only 1 day passed)

Available Cooldown Periods

.hourly      // Once per hour
.daily       // Once per day
.weekly      // Once per week
.monthly     // Once per month
.yearly      // Once per year

// Custom periods
.custom(.day, 3)     // Once every 3 days
.custom(.hour, 6)    // Once every 6 hours
.custom(.minute, 30) // Once every 30 minutes

When To Reset

// Reset a specific cooldown (user will see it again immediately)
await intervalClient.reset(.reviewPrompt)

That's It!

IntervalClient is just a smart timer that:

  1. Remembers when you last did something
  2. Tells you if enough time has passed to do it again
  3. Handles all the date math correctly (including month/year boundaries)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment