From apple-dev
Generates a variable reward system with randomized rewards, daily bonuses, mystery box mechanics, and ethical engagement caps. Use when user wants daily spins, mystery boxes, random rewards, or gamification reward systems.
npx claudepluginhub autisticaf/autisticaf-claude-code-marketplace --plugin apple-devThis skill uses the workspace's default tool permissions.
> **First step:** Tell the user: "generators-variable-rewards skill loaded."
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
First step: Tell the user: "generators-variable-rewards skill loaded."
Generate a variable reward system — randomized rewards (daily spins, mystery boxes, bonus points) that leverage variable-ratio reinforcement to increase engagement. Implements ethical engagement patterns with daily/weekly caps, transparent probability disclosure, and no pay-to-play mechanics.
Use this skill when the user:
Search for existing reward or points systems:
Glob: **/*Reward*.swift, **/*Points*.swift, **/*DailySpin*.swift, **/*MysteryBox*.swift
Grep: "reward" or "dailySpin" or "mysteryBox" or "rewardPool" or "lootBox"
If existing reward system found:
Determine if generating for iOS or macOS or both (cross-platform). The templates use SwiftUI and are cross-platform by default.
Ask user via AskUserQuestion:
Reward types? (multi-select)
Reward mechanism?
Include daily/weekly caps and cooldowns?
Visual presentation?
Read templates.md for production Swift code.
Generate these files:
Reward.swift — Model for reward type, value, rarity, display metadataRewardPool.swift — Weighted probability distribution with seeded random for testabilityRewardManager.swift — @Observable class managing claims, daily resets, caps, history via SwiftDataDailySpinView.swift — Animated spin wheel with disabled state when already claimedMysteryBoxView.swift — Card-flip / chest-open animation with matchedGeometryEffectRewardHistoryView.swift — List of past rewards grouped by dayRewardNotificationView.swift — Toast/banner overlay for reward availabilityCheck project structure:
Sources/ exists -> Sources/VariableRewards/App/ exists -> App/VariableRewards/VariableRewards/After generation, provide:
VariableRewards/
├── Reward.swift # Reward model with type, rarity, value
├── RewardPool.swift # Weighted random selection engine
├── RewardManager.swift # Core manager: claims, caps, history
├── DailySpinView.swift # Animated spin wheel
├── MysteryBoxView.swift # Card-flip / chest-open reveal
├── RewardHistoryView.swift # Past rewards grouped by day
└── RewardNotificationView.swift # Toast overlay for available rewards
Set up the model container (SwiftData):
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [RewardClaim.self])
}
}
Add the daily spin to your home screen:
struct HomeView: View {
@Environment(\.modelContext) private var modelContext
@State private var rewardManager: RewardManager?
var body: some View {
VStack {
if let manager = rewardManager {
DailySpinView(manager: manager)
}
}
.onAppear {
rewardManager = RewardManager(modelContext: modelContext)
}
}
}
Trigger a mystery box reveal:
struct MilestoneView: View {
@Environment(\.modelContext) private var modelContext
@State private var rewardManager: RewardManager?
@State private var showMysteryBox = false
var body: some View {
VStack {
Button("Open Mystery Box") {
showMysteryBox = true
}
.disabled(rewardManager?.canClaimToday == false)
}
.sheet(isPresented: $showMysteryBox) {
if let manager = rewardManager {
MysteryBoxView(manager: manager)
}
}
.onAppear {
rewardManager = RewardManager(modelContext: modelContext)
}
}
}
Show a reward notification toast:
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var rewardManager: RewardManager?
var body: some View {
ZStack {
MainTabView()
if let manager = rewardManager {
RewardNotificationView(manager: manager)
}
}
.onAppear {
rewardManager = RewardManager(modelContext: modelContext)
}
}
}
View reward history:
struct ProfileView: View {
@Environment(\.modelContext) private var modelContext
@State private var rewardManager: RewardManager?
var body: some View {
NavigationStack {
if let manager = rewardManager {
RewardHistoryView(manager: manager)
}
}
.onAppear {
rewardManager = RewardManager(modelContext: modelContext)
}
}
}
import Testing
import SwiftData
@Test
func dailySpinGrantsReward() async throws {
let container = try ModelContainer(
for: RewardClaim.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let pool = RewardPool(seed: 42) // Deterministic for testing
let manager = RewardManager(modelContext: context, rewardPool: pool)
let reward = try await manager.claimDailySpin()
#expect(reward != nil)
#expect(manager.canClaimToday == false) // Already claimed
}
@Test
func dailyCapEnforced() async throws {
let container = try ModelContainer(
for: RewardClaim.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let pool = RewardPool(seed: 42)
let manager = RewardManager(modelContext: context, rewardPool: pool, dailyCap: 1)
_ = try await manager.claimDailySpin()
let second = try await manager.claimDailySpin()
#expect(second == nil) // Cap reached
}
@Test
func deterministicSeedProducesSameReward() {
let pool1 = RewardPool(seed: 123)
let pool2 = RewardPool(seed: 123)
let reward1 = pool1.drawReward()
let reward2 = pool2.drawReward()
#expect(reward1.type == reward2.type)
#expect(reward1.rarity == reward2.rarity)
}
@Test
func weeklyCapResetsAfterWeek() async throws {
let container = try ModelContainer(
for: RewardClaim.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let pool = RewardPool(seed: 42)
let manager = RewardManager(modelContext: context, rewardPool: pool, weeklyCap: 3)
// Simulate 3 claims on different days within the same week
let calendar = Calendar.current
for daysAgo in (0...2).reversed() {
let date = calendar.date(byAdding: .day, value: -daysAgo, to: .now)!
_ = try await manager.claimDailySpin(date: date)
}
#expect(manager.weeklyClaimCount == 3)
#expect(manager.canClaimThisWeek == false)
}
// Check if user can spin today
if rewardManager.canClaimToday {
let reward = try await rewardManager.claimDailySpin()
// Show reward animation
}
// Time until next spin
let timeRemaining = rewardManager.timeUntilNextClaim
// Returns TimeInterval — use for countdown display
// Claim via mystery box mechanism
let reward = try await rewardManager.claimMysteryBox()
// Claim via random bonus (triggered by app action)
let bonus = try await rewardManager.claimRandomBonus()
// All claim methods enforce daily/weekly caps automatically
// Get all past rewards
let history = rewardManager.claimHistory // [RewardClaim]
// Grouped by day for display
let grouped = rewardManager.claimHistoryByDay // [Date: [RewardClaim]]
// Filter by rarity
let rareRewards = rewardManager.claims(withRarity: .rare)
let canClaim = rewardManager.canClaimToday // Daily cap not reached
let canClaimWeek = rewardManager.canClaimThisWeek // Weekly cap not reached
let countdown = rewardManager.timeUntilNextClaim // Seconds until midnight reset
let todayCount = rewardManager.dailyClaimCount // Claims made today
let weekCount = rewardManager.weeklyClaimCount // Claims made this week
Variable-ratio reinforcement is a powerful engagement mechanic. The templates enforce ethical boundaries:
RewardPool exposes its rarity weights so you can display them in a "Reward Rates" info sheet (required by some App Store regions).Users can change their device clock to claim extra rewards. Mitigations:
Date() with Calendar.current.startOfDay(for:) — accept that determined users can game it.timeProvider protocol for injecting server time.Date timestamps alongside normalized day values so you can detect anomalies (claims with createdAt before a previously recorded claim).RewardPool accepts a seed parameter that initializes the random number generator deterministically. In tests, always pass a fixed seed so assertions are stable:
let pool = RewardPool(seed: 42)
let reward = pool.drawReward() // Always produces the same reward for seed 42
In production, omit the seed to use system entropy.
The default rarity weights (common: 60%, uncommon: 25%, rare: 10%, epic: 5%) produce one epic reward roughly every 20 claims. Adjust weights in RewardPool.defaultWeights to match your engagement goals. Verify with a histogram test:
@Test
func rarityDistributionMatchesWeights() {
let pool = RewardPool(seed: 0)
var counts: [Reward.Rarity: Int] = [:]
for _ in 0..<10_000 {
let reward = pool.drawReward()
counts[reward.rarity, default: 0] += 1
}
// Epic should be roughly 5% (500 +/- tolerance)
#expect(counts[.epic]! > 300 && counts[.epic]! < 700)
}
SwiftData stores persist across app updates but are lost on reinstall. For valuable reward inventories, sync to CloudKit or a backend. The RewardManager includes a lastSyncDate hook for this purpose.
If a user claims a reward at 11:59:59 PM and the daily reset fires at midnight, ensure the claim is attributed to the correct day. The templates use Calendar.current.startOfDay(for: claimDate) to normalize all claims to their calendar day, avoiding off-by-one errors at the boundary.
generators-milestone-celebration — Celebrate reward milestones with animationsgenerators-streak-tracker — Combine streaks with daily rewards for compounding engagementgenerators-tipkit-generator — Coach marks to introduce the reward system to new users