Skip to content

Instantly share code, notes, and snippets.

@Aayush9029
Last active January 18, 2025 22:51
Show Gist options
  • Save Aayush9029/40afffc39c358c346f248b37704764b6 to your computer and use it in GitHub Desktop.
Save Aayush9029/40afffc39c358c346f248b37704764b6 to your computer and use it in GitHub Desktop.
Local Interval Client
import Dependencies
import Foundation
// MARK: - Key Definition
public struct IntervalKey {
let key: String
let config: IntervalConfig
public init(_ key: String, config: IntervalConfig) {
self.key = "interval." + key
self.config = config
}
}
// MARK: - Interval Configuration
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)
}
}
// MARK: - Keys Extension Support
public extension IntervalKey {
static func key(_ name: String, _ config: IntervalConfig) -> IntervalKey {
IntervalKey(name, config: config)
}
}
// MARK: - Client Interface
public struct IntervalClient {
public var shouldRun: @Sendable (IntervalKey) -> Bool
public var markRun: @Sendable (IntervalKey) async -> Void
public var reset: @Sendable (IntervalKey) async -> Void
public var resetAll: @Sendable () async -> Void
}
// MARK: - Live Implementation
extension IntervalClient: DependencyKey {
public static let liveValue = Self(
shouldRun: { key in
let defaults = UserDefaults.standard
let lastRun = defaults.integer(forKey: key.key)
let currentValue = Calendar.current.component(
key.config.component,
from: Date()
)
return currentValue > lastRun + key.config.value - 1
},
markRun: { key in
let currentValue = Calendar.current.component(
key.config.component,
from: Date()
)
UserDefaults.standard.set(currentValue, forKey: key.key)
},
reset: { key in
UserDefaults.standard.removeObject(forKey: key.key)
},
resetAll: {
let defaults = UserDefaults.standard
let intervalKeys = defaults.dictionaryRepresentation().keys.filter {
$0.hasPrefix("interval.")
}
intervalKeys.forEach { defaults.removeObject(forKey: $0) }
}
)
}
// MARK: - Test Support
extension IntervalClient: TestDependencyKey {
public static let testValue = Self(
shouldRun: unimplemented("IntervalClient.shouldRun"),
markRun: unimplemented("IntervalClient.markRun"),
reset: unimplemented("IntervalClient.reset"),
resetAll: unimplemented("IntervalClient.resetAll")
)
}
public extension IntervalClient {
static let noop = Self(
shouldRun: { _ in true },
markRun: { _ in },
reset: { _ in },
resetAll: {}
)
}
public extension DependencyValues {
var intervalClient: IntervalClient {
get { self[IntervalClient.self] }
set { self[IntervalClient.self] = newValue }
}
}
@Aayush9029
Copy link
Author

IntervalClient

A dependency for managing time-based intervals and tracking their completion. Perfect for features that should reset and be available again after a certain time period (daily, weekly, monthly, etc.).

How It Works

IntervalClient manages two states:

  1. Time interval tracking (has enough time passed since the last reset?)
  2. Completion tracking (has the action been marked as complete in the current period?)

An interval shouldRun returns true only when BOTH conditions are met:

  • The configured time period has passed since the last reset AND
  • The action hasn't been marked as complete in the current period

The interval automatically resets after its configured time period (e.g., daily, weekly, monthly).

Usage

1. Define Your Intervals

public extension IntervalKey {
    // Reset monthly, perfect for review prompts
    static let reviewPrompt = key("reviewPrompt", .monthly)
    
    // Reset weekly, great for paywall displays
    static let paywall = key("paywallShow", .weekly)
}

2. Use in Your Features

@Observable
final class AppViewModel {
    @Dependency(\.intervalClient) var intervalClient
    
    private func askForReviewIfNeeded() {
        // Returns true if: 
        // 1. A month has passed since last reset AND
        // 2. Review hasn't been shown this month
        guard intervalClient.shouldRun(.reviewPrompt) else {
            return
        }
        
        Task {
            // Mark as complete for this period
            await intervalClient.markRun(.reviewPrompt)
            await appReview.requestReview()
        }
    }
}

Available Intervals

// Built-in intervals
.hourly      // Resets every hour
.daily       // Resets every day
.weekly      // Resets every week
.monthly     // Resets every month
.yearly      // Resets every year

// Custom intervals
.custom(.day, 3)  // Resets every 3 days
.custom(.hour, 4) // Resets every 4 hours

Testing

It's not completely tested, but makes sense logically to me. (which is not the best way to do it)

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