I built an alarm app. Not because the world needed another alarm app, but because every alarm app I used did the same thing: ring, snooze, repeat. Snooze If You Can is different. It makes you earn your snooze.
The Idea
The concept is simple: when your alarm goes off, you cannot just tap a button to snooze. You have to complete a challenge first. Solve a math problem. Shake your phone 30 times. Scan a barcode in your kitchen. By the time you have completed the challenge, you are awake.
I had been using Android apps with similar concepts for years. When I switched to iOS, nothing matched the experience I wanted. So I built it.
Why Swift and SwiftUI
This was my first iOS application, and I chose to go fully native with Swift and SwiftUI. No React Native, no Flutter, no cross-platform compromise.
The reasoning:
-
Alarm functionality requires deep OS integration. Push notifications, background audio, scheduling wake alarms: these APIs are iOS-native. Cross-platform frameworks add a layer of abstraction that makes debugging harder.
-
SwiftUI's learning curve is worth it. Declarative UI in Swift feels natural coming from React. The mental model transfers: state drives UI, components compose into screens.
-
Performance matters for alarms. An alarm that lags or crashes does not wake you up. Native performance means the app launches instantly and the alarm sounds immediately.
struct ChallengeView: View {
@State private var answer = ""
let challenge: MathChallenge
var body: some View {
VStack(spacing: 24) {
Text(challenge.question)
.font(.system(size: 48, weight: .bold, design: .rounded))
TextField("Answer", text: $answer)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.font(.title)
Button("Submit") {
if answer == challenge.correctAnswer {
dismissAlarm()
}
}
.buttonStyle(.borderedProminent)
}
}
}SwiftUI's declarative approach made the challenge screens straightforward to build. Each challenge type (math, shake, barcode) is a separate view with its own state management.
The AlarmKit Architecture
Under the hood, the app is structured around what I call AlarmKit, the core scheduling and playback engine:
AlarmKit
├── AlarmScheduler (manages UNNotification scheduling)
├── AudioEngine (plays alarm tones, handles interruptions)
├── ChallengeEngine (selects and validates challenges)
└── SettingsStore (persists user preferences)
AlarmScheduler wraps UNUserNotificationCenter. Each alarm is a scheduled local notification with a custom sound. When the notification fires, the app launches into the challenge screen.
AudioEngine handles alarm audio playback, including edge cases: What happens when the phone is in silent mode? What about Do Not Disturb? What if another app is playing audio? Each case requires specific AVAudioSession configuration.
class AudioEngine {
private var audioSession: AVAudioSession
func configureForAlarm() throws {
try audioSession.setCategory(
.playback,
mode: .default,
options: [.mixWithOthers]
)
try audioSession.setActive(true)
}
}The Challenges System
Each challenge type implements a protocol:
protocol AlarmChallenge {
var type: ChallengeType { get }
var difficulty: ChallengeDifficulty { get }
func generateChallenge() -> ChallengeContent
func validateAnswer(_ answer: String) -> Bool
}Current challenge types:
- Math Problems: Addition, subtraction, multiplication at configurable difficulty. Easy: single digits. Hard: three-digit multiplication.
- Shake Challenge: Accelerometer-based. Shake the phone N times (configurable from 10 to 50).
- Barcode Scan: Scan a specific barcode you have registered. Forces you to physically move to the location.
- Memory Pattern: Repeat a sequence of colours/numbers that flashes on screen.
The protocol-based design means adding new challenge types is straightforward: implement the protocol, register the type, done. Community contributors have already proposed step-counter and photo challenges.
Going Open Source
Snooze If You Can is fully open source. The decision was not difficult. It is a personal utility app, not a revenue-generating product. Open-sourcing it means:
- Community contributions: better challenges, more features, bug fixes from real users
- Learning resource: the codebase demonstrates iOS patterns (SwiftUI architecture, local notifications, audio handling, sensor integration)
- Trust: users can verify exactly what the app does with their permissions
The licensing decisions I have made with other projects (see Open Source Licensing: What I Learned the Hard Way) informed this choice. For a utility app, MIT licensing makes sense: maximum adoption, no restrictions.
Lessons from iOS Development
Coming from web development (TypeScript, React, Node.js), iOS development was surprisingly different:
- Xcode is... Xcode. The IDE is powerful but opinionated. Coming from VS Code, the adjustment was significant.
- Apple's review process adds friction. Web deployments are instant. iOS updates take 24-48 hours for review. This changes how you think about release cadence.
- The iOS ecosystem is smaller but deeper. Fewer packages than npm, but the quality bar is generally higher. You write more code yourself, but you understand it better.
- SwiftUI is not React. Despite surface similarities (declarative, component-based), the state management model is different.
@State,@Binding,@ObservedObject: each has specific semantics that do not map directly to React hooks.
What Is Next
The next feature is charitable donations: every snooze can optionally trigger a small donation to Darüşşafaka, a Turkish educational foundation. I am writing about the StoreKit integration and the philosophy behind it in Building Apps With a Purpose.