Moodify - Student Engineering Journal
Moodify began as an iOS prototype while I was taking a Special Topics computer science class in early 2025. Later, I rebuilt it as a larger personal iPhone app. The early version asked a simple question: could a mood choice lead to a useful song from the user's own music library? The newer version kept playback inside the app and became more focused on queue behavior, personalization, and explaining why a recommendation changed.
The question I kept coming back to was: how much can an app learn from messy listening behavior without pretending it knows exactly how someone feels?
About the early dates
I looked back through an older Xcode archive, source-code headers, file dates, and dated screenshots to organize the 2025 entries. I cross-checked them with each other and only included files and screenshots that clearly matched the work from that time, so the timeline is as accurate as possible.
The 2026 entries come from the newer project's git history, and I grouped related commits when they were part of the same problem.
| Date | Area | Engineering journal |
|---|---|---|
| 2025-01-24 | Initial structure and data model | I created the first model, controller, and view-model files, plus a Core Data MoodEntry and a history view model. I wanted Spotify, mood history, and interface code to stay separate, and I decided that each mood entry should persist after the app closed. |
| 2025-02-01 | Navigation and history fetching | I added the first tab-navigation files and made mood history load newest-first from Core Data. I also created the default Xcode test targets. The main problem was getting saved data onto the correct managed-object context and then exposing it so SwiftUI could update. |
| 2025-02-10 to 2025-02-12 | Mood selection, HealthKit experiment, and Spotify state | I started the home screen, built a HealthKit experiment for mindful-session data, and created a Spotify player view model that watched playback state, song information, and album artwork. Both services depended on permissions and asynchronous callbacks, so I had to be careful about when the UI changed. |
| 2025-02-23 to 2025-02-25 | Spotify login and permissions | I built Spotify response models and worked through the login flow. I requested the scopes needed for playback and library data, and I added logout behavior that removed saved tokens so an old session would not hide a login bug. |
| 2025-02-26 to 2025-02-28 | SwiftUI session-manager ownership | I hit compiler errors while passing the Spotify session manager into the root SwiftUI view. The SDK object was not itself a SwiftUI view model, so I changed the design so one observable wrapper owned the Spotify objects and was passed through the SwiftUI environment. |
| 2025-03-03 | App capabilities and HealthKit setup | I added the HealthKit entitlement and privacy-purpose strings. Writing the integration code was not enough; the target capabilities and permission descriptions also had to be correct. |
| 2025-03-11 to 2025-03-18 | Persistence and Spotify services | I expanded MoodEntry to save the emoji, mood level, genre, track, artist, Spotify URI, cover art, playlist, timestamp, and HealthKit-sync state. I also built Spotify profile, playlist, track, token, pagination, and playback-control code. |
| 2025-05-27 to 2025-05-30 | Custom mood mappings | I replaced permanently hard-coded mood mappings with a MoodMapping model and editing UI. I stored the nine small settings as encoded JSON in AppStorage, while keeping the growing mood history in Core Data. |
| 2025-06-01 | Shared app composition | I created one shared Spotify session wrapper, one shared data manager, and one Core Data context at the app level, then injected them into the Home, Entries, and Settings tabs. This kept login state and network work consistent across screens. |
| 2025-06-04 | Spotify redirect and Now Playing | I registered the moodify:// URL scheme, handled the return from Spotify login, and built a Now Playing screen with artwork, song details, duration, and a Play on Spotify action. I also added a fallback so the screen would not fail when a matching track was unavailable during development. |
| 2025-06-05 | Entry management and history charts | I refined the emoji grid, Settings, saved-entry list, and history dashboard. I grouped entries by calendar day and by emoji, then converted those groups into small models for Swift Charts. |
| 2025-06-06 | Dynamic mood mappings | I finished a screen where each of nine moods could be mapped to a genre from the user's Spotify data. Because the screen could load before the network request finished, I showed fallback genres first and replaced them when the live list became available. |
| 2025-06-08 | Concurrent genre collection | I collected genres for every artist on a track using a DispatchGroup, combined them in a Set, and protected the shared set with a serial queue because several callbacks could finish at once. |
| 2025-06-11 | End-to-end V1 workflow | I connected mood selection to genre lookup, track filtering, fallback selection, Core Data saving, and the Now Playing sheet. The school-project version could take a mood, choose a Spotify-library recommendation, save the mood/music pairing, and show history and statistics. |
| 2025-06 to 2026-03 | Gap before the newer version | The saved development history has a gap here. The next major direction change appears in March 2026, when MusicKit made it possible to keep Apple Music playback inside Moodify. |
| 2026-03-15 | V2 rebuild with MusicKit | I started a newer version so mood choice, recommendation, playback, and feedback could stay inside one app instead of handing the user off to Spotify. |
| 2026-03-21 | Apple Music only | I removed Spotify from the newer version and made Apple Music the only provider. Supporting two providers no longer made sense when only one supported the integrated playback loop I was building. |
| 2026-04-21 | Beta analytics and listening signals | I added a small Mixpanel analytics service and began tracking session starts, skips with listening position, song completions, and explicit likes or dislikes. I wanted a way to study patterns across real listening sessions instead of depending only on what testers remembered to tell me. These actions still did not have one automatic meaning. A quick skip could suggest something different from a skip near the end, so I treated the analytics as evidence to investigate rather than a simple good-or-bad score. |
| 2026-04-22 to 2026-05-02 | Expanding the TestFlight beta | I uploaded the first internal TestFlight build on April 22 for family and engineering testers. Build 115, uploaded on May 2, was also made available to an external Friends group. Moving beyond testing on my own device gave me real listening sessions and behavioral signals to inspect. Watching my mom use the beta, I noticed that even though she wanted music to fit her mood, she still browsed a lot before finding what felt right. That made me question whether the app should always make someone name a mood before listening. I also learned to separate group access from actual use, so I looked at installs, sessions, feedback, and Mixpanel events instead of treating the number of invited testers as engagement. |
| 2026-05-05 to 2026-05-11 | Session navigation and Now Playing state | I fixed previous/next oscillation, swipe cards disappearing, artwork identity mistakes, snap-back flashes, and layout problems. I first treated many of them as animation bugs, but they kept pointing back to unclear boundaries between browsing, confirmed playback, and cleanup timing. |
| 2026-05-16 to 2026-05-19 | Queue backfill and artwork identity | I started Apple queue backfill earlier, showed progress, prevented some navigation reactions to temporary pauses, and froze outgoing artwork while the real player settled. Queue timing changed what the user saw, so it could not stay hidden as an internal detail. |
| 2026-05-24 | Mood mappings and recommendation tests | I added multiple genres per mood and tests for repeated songs and artists. One genre per mood felt too narrow, and a queue could feel broken even when it technically returned music. |
| 2026-05-25 | Media ownership and lifecycle | I kept media ownership until the session explicitly ended and tightened background, lock-screen, and remote-control behavior. I learned that iOS media behavior depends on which part of the app owns the session, not only whether the play button works. |
| 2026-05-26 | Safer refactoring | I added boundary checks, behavior tests, one clearer path for playback commands, and smaller testable pieces for queue and recommendation logic. I wanted risky changes to be guided by tests instead of memory. |
| 2026-05-27 | Playback truth and learning signals | I made the session model follow what Apple Music was actually playing, cleaned up skip/completion/listen-depth processing, and made logs easier to search. |
| 2026-05-31 to 2026-06-07 | Data cleanup and remote playback | I guarded duplicate mood ordering, separated starting mood definitions from learned profiles, and added AirPods and Siri playback support that still needs real-device testing. |
| 2026-06-28 to 2026-07-05 | Mood Vector and inspectable personalization | I added an experimental song mood vector, personal adjustments, inspection views, recommendation explanations, and a newer CloudKit record structure. I treated the vector as one scoring signal, not proof that the app understood emotion. |
| 2026-07-06 to 2026-07-07 | Queue controls and recommendation diagnostics | I blocked controls during unstable queue boundaries, handled pending advances after backfill, treated equivalent playable IDs as the same track when needed, and added recommendation outcome exports. I wanted to inspect what the recommender did instead of judging it only by whether one song felt right. |
| 2026-07-19 | Product measurement | I rebuilt the Mixpanel dashboard around three questions: does Moodify find a fitting song quickly, learn from repeated mood sessions, and bring listeners back? I also added an engagement funnel for app use, session depth, and return frequency. The new analytics are live, but more data and a few tracking improvements are still needed before drawing conclusions. |
| 2026-07-24 to 2026-07-28 | Comparing repeated mood sessions | I added a same-mood session counter so I could compare a first, second, and later session instead of mixing them together. I also improved playback and personalization reliability and saved more context behind the experimental emotional-index chart so older points could be rebuilt and grouped more clearly. The goal was to make changes easier to inspect, not to claim the chart directly measures emotion. |
| 2026-08-02 | Starting music without forcing a mood label | I came back to what I had noticed while my mom was beta testing: sometimes listening helps someone figure out what fits instead of the other way around. I added a “Just Play” option so the app can start from a private learned guess without forcing a mood label first. It keeps that guess separate from explicit mood history so it does not silently turn an inference into a fact. I also added a dedicated mood-grid editor and one clearer status flow for preparing, playing, refilling the queue, or ending a mix. This made the app more flexible without weakening the meaning of user-chosen moods. |
| 2026-08-02 to 2026-08-03 | Mood cloud home and in-session tuning | I replaced the old stacked home dashboard with a rotatable mood cloud so choosing music feels less like filling out a form. Recent listening patterns can change which mood is emphasized, but the user still decides what to play. I also moved skip-based refinement into a small “Tune this mix” sheet and tightened session and playback state so an active mix can adjust more reliably. I added reduced-motion and accessibility behavior, but I still need more real-device and tester feedback on the new interaction. |
| 2026-08-04 | Safer personalization updates | I tightened what happens after someone tunes a mix so one refinement is handled as a single transaction. Moodify now keeps the previous state and can roll back if a write fails instead of leaving the session partly updated. I also reduced duplicate analytics and CloudKit writes, batched record updates, and split some large preference and view-model code so the interface does less work during playback. The new tests cover successful updates and rollback paths, but I still need more real-device sessions to confirm the performance and sync changes under normal use. |
| 2026-08-05 | Keeping automatic guesses from teaching themselves too strongly | Magic Wand can begin with a mood that Moodify inferred, but I did not want the app's own guess to become as strong as a mood someone chose directly. I separated explicit, inferred, and unattributed learning, capped inferred evidence at half the strength of explicit evidence and then scaled it by confidence, and made the explanation screen say when a mix began from a hypothesis. I also added tests comparing explicit and inferred learning. This lets automatic sessions learn cautiously without turning Moodify's guess into a fact. |
| 2026-08-07 to 2026-08-08 | Making listening evidence easier to trust and inspect | I fixed two measurement problems before relying on the new charts. A skip or completion can arrive after Apple Music has already moved to the next track, so I kept the outgoing recommendation impression long enough to attach the outcome to the right song. I also started separating same-day, 1–7 day, and later returns instead of treating every repeat session the same. Then I moved mood-match, emotional-index, and recommendation-outcome views into a dedicated Mood Insights screen instead of leaving several analytics cards on Home. The tests cover attribution and saved return windows, but I still need real beta data before saying the recommender or retention improved. |
| 2026-08-08 | Smoother artwork transitions and bounded image caches | I found that Now Playing could replace artwork while a swipe was already leaving the screen, which made the transition hitch even when playback was correct. I changed the outgoing card to use only captured or same-track artwork, lined up the committed artwork reveal with the swipe timing, and added count and memory limits to decoded artwork caches. I added tests for the transition timing and cache-cost calculation, but I still need longer real-device sessions to see how much this helps under normal use. |
| 2026-08-09 | Optional Apple Health State of Mind logging | I brought an Apple Health idea from the early Moodify prototype into the current app using State of Mind. Moodify can save a reflection only when I explicitly choose or confirm a mood; inferred or Just Play moods are not written to Health. I kept the mood-to-Health mapping conservative and inspectable, added permission, opt-in, failure, and duplicate-save handling, and validated the write flow on a real device. The goal is to make reflection easier without turning Moodify's own guesses into health facts. |
| 2026-08-10 | Keeping a reset from coming back after cloud sync | I found that clearing a learned mood profile on one device could be undone when an older cloud copy merged back in. I added timestamped reset markers so an intentional reset wins over older learning, while genuinely newer learning can retire the marker. I updated the newer cloud-sync format and added tests for reset-only snapshots and local/cloud merge cases. This is a reliability fix, not a claim that the personalization itself got better. |
Looking back
The 2025 version taught me how much work sits behind a simple interaction. Choosing an emoji and receiving music required OAuth redirects, permissions, asynchronous networking, pagination, shared SwiftUI state, persistence, and fallbacks.
The newer version changed the problem. Once playback stayed inside Moodify, most of the difficult work became state, timing, and interpretation. A lot of progress came from trying a direct fix, finding another edge case, adding logs or tests, and then making the structure clearer. Family and friends are now testing the app on their own devices, but I still need more coverage for edge cases around Apple Music, AirPods, Siri, and CloudKit.