An iOS app that prevents the most painful loss in a memorizer's journey: the decay of verses you've already memorized.
An active Memorize → Memorized loop where you learn ayat with blind recall, then keep them alive with spaced repetition, and you can see the algorithm working.
Submitted to the Quran Foundation Hackathon (Ramadan 2026). iOS 17+, SwiftUI, SwiftData, on-device speech recognition.
Millions reconnect with the Quran in Ramadan and emerge with new verses internalised. Within months, most of those verses fade. There's no daily structure to keep them alive, and traditional revision (open the mushaf, read aloud, hope) doesn't tell you which ayat are slipping or when to revisit them.
Murajah turns Quran retention into a measured, scheduled, observable practice. The same way Anki turned vocabulary retention into one. The user recites, the app listens, and the algorithm decides what comes back tomorrow.
Two interconnected tracks, plus a continuous mushaf reader and bookmarks tying them together.
For verses the user is learning. MemorizingSessionView walks one un-mastered ayah at a time:
- Read. Full text shown with translation. Hear cue plays the verse from the user's chosen reciter, +1 read increments the exposure counter. No gate, the user decides when to test.
- Test. Text hidden behind word-shaped placeholders that preserve the ayah's length and rhythm without revealing letters. The user recites from memory, on-device speech recognition transcribes,
WordAlignmentscores them. Tap a placeholder to peek one word, toggle Show all to reveal. - Feedback. Verse fully revealed with missed words tinted rose. A score of 0.85 or better graduates the ayah to the SM-2 review loop, seeded with a 1-day initial interval.
When every ayah in the surah passes, the whole surah graduates: the MemorizedSet flips from .memorizing to .maintenance and a celebration screen offers a first review session right there.
Once mastered, ayat enter the spaced-repetition loop. Murajah picks what's due each day, the user recites aloud. Standard SuperMemo SM-2 runs against a continuous 0...1 accuracy score from word alignment, with one adaptation: under 0.7 resets reps to 0 (the verse comes back tomorrow), above 0.85 grows the interval by ease factor.
Crucially, the algorithm is visible, not hidden. After every ayah a Schedule transition card shows old vs new interval (1d → 3d), ease delta (2.50 ▲ +0.13), the relative due date, and a 6-rung stage ladder (New, Learning, Practiced, Familiar, Strong, Mature). A ? button opens a full How Murajah works explainer with a worked 5-day example. Users don't have to trust the schedule blindly. They understand it.
Continuous flowing layout with quran.com-style 8-pointed rosette verse-end markers (rendered as inline UIImage, not OpenType substitutions, so they look identical on every device) and a calligraphic surah-name banner using the Tarteel surah-names v4 font.
Two rasms, switchable inline:
- Uthmani. Madani mushaf with alef wasla
ٱand classical letterforms. Amiri when bundled, SF Arabic otherwise. - IndoPak. Real Imlaei rasm pulled from
api.quran.com/api/v4/quran/verses/imlaei. Regular alefا, modern letterforms.
Tajweed coloring is an orthogonal toggle. Qalqalah red, ghunnah orange, madd blue/green, idgham teal, ikhfa magenta, silent/wasl gray.
Tap any verse for an action sheet (Hear, Bookmark, Memorize this verse, Copy text). Single-verse memorize launches the active learning loop for just that ayah, so a struggling reader can pick anything they hit and start memorising immediately.
@Model Bookmark keyed by verseKey. Saved verses get a saffron flag overlaid in the reader. A dedicated Bookmarks list sorts newest-first with verse text, translation, relative-time labels, swipe-to-delete, and tap-to-jump-back-to-reader. Bookmarks sync to the user's Quran.com collection via the QF Bookmarks API (see below).
The hackathon criterion is at least one Content API plus at least one User API. Murajah ships two Content APIs (Quran, Audio) and two User APIs (Activity & Goals, Bookmarks).
| Category | Endpoint | What it powers in Murajah |
|---|---|---|
| Content · Quran | GET /content/api/v4/chapters |
The 114-surah picker |
| Content · Quran | GET /content/api/v4/verses/by_chapter/:id |
Uthmani text plus Sahih International translation for any memorisation range, in one call |
| Content · Audio | GET /content/api/v4/recitations/:id/by_ayah/:key |
Per-ayah audio for the user's reciter (5-reciter catalog). Feeds both AudioPlaybackService (mono) and SpatialAudioService (HRTF). |
| User · Activity & Goals | POST /personal/api/v4/activities |
Every completed review session posts {type: "QURAN", duration_seconds, verse_keys, completed_at} so Murajah work rolls into the user's QF streak and daily goal |
| User · Bookmarks | POST/DELETE /personal/api/v4/bookmarks |
A bookmark added in Murajah appears in the same user's Quran.com collection, and vice versa |
User-scoped calls require an OAuth2 PKCE bearer token. For hackathon judging, pass a pre-issued token via MURAJAH_QF_USER_TOKEN. Without it the calls log no user token, local-only and the app falls back to SwiftData. The full PKCE flow needs a small backend proxy, explicitly out of scope for the iOS hackathon build.
LocalQuranProvider (bundled 6,236-ayah JSON) serves the same QuranProvider protocol so the app stays usable offline and when the API is rate-limited. Demos never break.
Three pedagogically-motivated features that distinguish Murajah from a generic Quran reader or a generic flashcard app.
In Memorize → Test, as speech.partialTranscript updates, MemorizingSessionView runs WordAlignment.align(canonical:recited:) on every transcript change and feeds matched indices into revealedIndices. Words emerge from their dashed placeholders one by one, in real time, in zaytun green, as the user recites them. formUnion(...) ensures the reveal only ever grows: a transient partial that drops a word doesn't yank the verse back behind its placeholders.
SpatialAudioService wraps AVAudioEngine plus AVAudioEnvironmentNode with renderingAlgorithm = .HRTFHQ. The reciter is anchored to AVAudio3DPoint(x: 1.5, y: 0, z: -0.6), about 1.5 m to the right and slightly behind the listener. The user can't passively mouth along because the sound source is outside their head; they have to actively retrieve the rest of the ayah. Mirrors how a hifz teacher prompts from across the room. Toggle in Settings → Audio. Best in headphones.
Spaced repetition apps usually feel like a black box. Murajah surfaces the schedule decision every single time: the old interval, the new interval, the ease factor delta, the next due date in human language ("in 2 days"), and a six-rung stage ladder. The HowItWorksView sheet walks through a worked 5-day example so the user understands why a verse is coming back when it is. Trust through transparency.
Murajah/
├── App/ MurajahApp · AppRouter · RootView (splash → onboarding → auth → shell) · AppState
├── Theme/ Colors (adaptive light/dark) · Typography (Amiri / SF Arabic / Tarteel) · Theme (Liquid Glass)
├── Models/ Surah · Verse · ScriptStyle · MemorizationPhase · Reciter · PersistedModels (SwiftData)
├── Engine/ SM2Engine · ArabicNormalizer · WordAlignment · ScheduleEngine · TajweedRenderer
├── Services/ QuranProvider · LocalQuranProvider · QFAPIClient · QFUserSync · ScriptProvider
│ SpeechRecognitionService · AudioPlaybackService · SpatialAudioService
│ NotificationService · StreakTracker · AuthService · MurajahEnvironment
├── Views/
│ ├── Splash/ SplashView (1.6 s halo-bloom cold-launch animation)
│ ├── Auth/ AuthView (Sign in with Apple plus guest mode)
│ ├── Onboarding/ OnboardingView (3-page intro)
│ ├── Shell/ MainShellView (4-tab Liquid Glass tab bar)
│ ├── Selection/ ContentSelectionView (rasm picker, "memorize" vs "already memorized")
│ ├── Dashboard/ DashboardView · WeakAyatView
│ ├── Library/ LibraryView (Memorizing plus Memorized) · BookmarksView
│ ├── Reader/ MushafReaderView · VerseActionSheet
│ ├── Memorize/ MemorizingSessionView (Read → Test → Feedback → Graduation)
│ ├── Session/ ReviewSessionView (the SM-2 review loop)
│ ├── Results/ SessionResultsView (per-ayah breakdown)
│ ├── Stats/ StatsView (window picker, summary tiles, upcoming, accuracy chart)
│ ├── Profile/ ProfileView · SettingsView · HowItWorksView
│ └── Components/ PrimaryButton · ScoreRing · StageLadder · MemoryBar · ScheduleTransitionCard
│ ArabicVerseView · BlindVerseView · Chips · FlowLayout
└── Resources/
├── Quran/ surahs.json (114) · verses.json (6,236 Uthmani plus EN) · scripts.json (6,236 Imlaei plus tajweed)
├── SurahNameV4.ttf
└── Assets.xcassets (AppIcon, MurajahLogo, AccentColor, LaunchBackground)
SM2Engine.swift. Standard SuperMemo SM-2. Continuous 0...1 accuracy mapped to 0...5 quality bucket. Under 0.7 resets reps, above 0.85 grows interval by ease.ArabicNormalizer.swift. On-device transcription returns text without diacritics, often with hamza/alif variant differences. Collapses ZWNJ/ZWJ, tatweel, all alef variants, ta marbuta → ha, alef maksura → ya so a recitation matches even when tashkeel is omitted.WordAlignment.swift. Forward greedy alignment with bounded 3-word look-ahead. Best Levenshtein match in a small window for each canonical token. Threshold 0.55 marks a word as missed. Returns per-token match info and a weighted accuracy.ScheduleEngine.swift. SwiftData glue.recordBlindRecall(...)returns aBlindResultwith graduation flag,recordReview(...)returns aReviewTransitionwith old vs new interval and ease for the UI.TajweedRenderer.swift. Parses bracket-tagged tajweed ([<class>[<text>]) into a SwiftUIAttributedString. Walks the source by Unicode scalar (not Character): Swift fuses[with a following combining damma into a single grapheme, silently breaking naive parsers.QuranProvider.swift. Single protocol over local and remote content.QFAPIClientfalls through toLocalQuranProvideron any network or auth failure so demos never break.SpeechRecognitionService.swift.SFSpeechRecognizerin on-device mode withar-SAlocale and.dictationtask hint. Streams partial transcripts for the live UI.
Everything that touches audio or transcription is on-device. No recitation leaves the phone.
brew install xcodegen
xcodegen generate
open Murajah.xcodeprojRun on a real device for live recitation. The simulator can't capture mic input, but every screen and the full SR loop are reachable via deep-link env vars (see below).
Bundle id: com.murajah.app. Set DEVELOPMENT_TEAM in project.yml (or in Xcode signing) before installing on device.
- All 114 surahs in
Resources/Quran/surahs.json - All 6,236 ayat. Uthmani plus Sahih International English in
verses.json - All 6,236 ayat in Imlaei rasm plus tajweed-tagged Uthmani in
scripts.json - Tarteel surah-names v4 font (ligature-based:
surah001→ ornate Al-Fatiha calligraphy)
Info.plist's UIAppFonts also registers Amiri-Regular.ttf for Uthmani rendering. Drop the file into Resources/ to get the classical Quranic mushaf face; without it Typeface.arabic(...) falls through to SF Arabic.
// Services/MurajahEnvironment.swift
enum MurajahEnvironment {
static let useRemoteProvider = true
static let qfClientId = "<your client id>"
static let qfClientSecret = "<your client secret>"
}For per-user Activity plus Bookmarks calls, set the user token at run time:
SIMCTL_CHILD_MURAJAH_QF_USER_TOKEN=<token> \
SIMCTL_CHILD_MURAJAH_QF_BOOKMARK_COLLECTION=<uuid> \
xcrun simctl launch booted com.murajah.app| Var | Effect |
|---|---|
MURAJAH_INITIAL=onboarding|selection|library|stats|profile|settings|read|read_ikhlas|read_baqarah|memorize|session|weak|help|bookmarks |
Deep-link to a specific screen on launch |
MURAJAH_SEED=1|memorizing|graduated|bookmarks |
Pre-populate SwiftData with a sample collection |
MURAJAH_SCRIPT=uthmani|indopak |
Override the rasm in the reader |
MURAJAH_TAJWEED=1 |
Force the tajweed toggle on |
MURAJAH_AUTO_SCORE=<0.0...1.0> |
Synthesize a recitation result on the first ayah |
MURAJAH_AUTO_FINISH=<0.0...1.0> |
Drive an entire session through to .completed |
MURAJAH_MEMORIZE_STAGE=test|feedback|done |
Fast-forward the memorize flow |
MURAJAH_VERSE_ACTION=1 |
Auto-open the verse action sheet on the first verse |
Pass via SIMCTL_CHILD_<VAR> to xcrun simctl launch. Every screenshot in this README was captured with these flags.
- Microphone (
NSMicrophoneUsageDescription). Captures recitation. - Speech recognition (
NSSpeechRecognitionUsageDescription). On-device transcription. Asked on first Recite tap, not at launch. - Notifications. Optional. Requested only when the user enables daily reminders in Settings.
All audio and transcription stays on-device.
The Murajah mark, a human profile with a tree that resembles a brain, lives as a vector PDF in Resources/Assets.xcassets/MurajahLogo.imageset and is imported as a template image so it can be tinted from SwiftUI:
Image("MurajahLogo")
.renderingMode(.template)
.resizable()
.scaledToFit()
.foregroundStyle(Palette.zaytunDeep)It appears on the Auth screen, the first onboarding page, the dashboard greeting, and the splash. The 1024×1024 app icon (cream mark on a zaytun green field) is composed from the same source SVG via rsvg-convert plus Pillow and lives at Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png.
- Tafsir API integration on the breakdown screen for missed words.
- Apple Watch complication for streak.
- Background audio session refinements for car / commute review.
- Optional iCloud sync of
ReviewItemandBookmark(currently device-local). - Word-by-word color overlay so tajweed coloring works on IndoPak rasm too.
MIT. See LICENSE.








