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. |
| 2026-08-12 | Keeping Home responsive and fallbacks playable | I found that Home was re-reading more history and recommendation state than it needed, so I bounded the Core Data history fetches, cached the learning snapshot, and moved the automatic listening fallback away from the main actor. Then I fixed a refresh case that could briefly erase a still-valid Magic Play suggestion and made the Siri fallback reject placeholder tracks that could not actually play. I added tests around the date window, preserved suggestion, and fallback selection. These changes are about responsiveness and failure handling; they do not show that the recommendations themselves got better. |
| 2026-08-14 to 2026-08-15 | Making Siri start Magic Play more reliably | Siri playback had grown through several paths, and I found that a voice command could still hit an empty library or trigger large MusicKit queue updates that briefly interrupted audio. I changed the main voice shortcut to start Magic Play directly, kept older Siri media requests routed into the same playback handler, waited for a playable library before using a fallback, and limited the live Apple Music mirror to a small protected window that only refills near the end. I also made failures visible instead of treating every command like success and added tests around the queue window and refill rule. This is a playback and reliability change; I still need more real-device Siri and AirPods use before calling the voice flow reliable. |
| 2026-08-15 | Connecting mood-specific Siri requests to synced personalization | After getting Magic Play to start more reliably, I wanted Siri to carry a little more structure than just “play something.” I added one request model for current mood, desired mood, energy, or activity and routed both App Intents and older Siri media requests through it. I also made the interaction profile part of Moodify's persisted learning snapshot, bounded its history, merged it across local and cloud copies, and rebuilt derived counts from the durable history. Tests cover mood routing, successful and failed interactions, stale snapshots, and partial-device merges. This connects voice requests and personalization more cleanly, but it does not prove Siri recognition, cross-device sync, or the recommendations are better yet; I still need real-device and multi-device validation. |
| 2026-08-16 | Choosing the name Onda | When I restarted Moodify in early 2026, the name was not yet an issue. Recently, as I prepared the app for the App Store, I needed a new customer-facing name. I chose Onda because “onda” means “wave” in Spanish and “온다” means “is coming” in Korean. Together, it felt like “a wave is coming” or “my music vibe is coming,” which fit the project better than a random replacement name. |
| 2026-08-16 to 2026-08-17 | Giving Onda its own Library | At the same time, I separated “saved in my app” from “saved in Apple Music.” I added an Onda Library with its own tab, Core Data-backed saved songs, a first-run import path, search and sorting, and an optional setting to mirror Onda saves into Apple Music. Then I made startup prefer the local saved-song pool, kept catalog and artwork identity more durable, filtered songs that could not actually play when the Apple Music app was unavailable, and allowed careful recovery when the protected MusicKit queue ran out. I also reverted one queue-backfill change when it was not stable enough. This makes ownership and playback boundaries clearer, but I still need longer real-device testing, and it does not show that recommendations are better. |
| 2026-08-18 | Making Onda Library the real playback boundary | After giving Onda its own Library, I found that the app could still act ready before Apple Music access and the saved library were actually usable. I added explicit states for authorization, subscription, library loading, setup, and ready, and I stopped mood playback until Onda Library has at least one saved song. I also added a Recently Listened view backed by persisted playback records and included those records in local fallback recovery so recent history is not silently lost during store reconciliation. This makes the Library feel more like part of the app's playback state instead of just a saved-song screen, but I still need longer real-device and multi-device use before calling the new flow reliable. |
| 2026-08-19 | Making first-run Library failures recoverable | After making Onda Library part of playback readiness, I found that first run still had two bad failure modes: the app could hide a failed library load behind a generic setup state, and fallback recovery could restore listening history without restoring the saved library itself. I made library hydration distinguish retryable and unavailable failures, added clear retry and Settings paths, kept first-run setup from appearing until Apple Music and the library are actually ready, and extended local fallback recovery to include Onda Library items and metadata without re-importing duplicates. This is recovery and first-run reliability work; I still need longer real-device and multi-device testing before calling the Library recovery path reliable. |
| 2026-08-20 | Making startup and playback state tell the truth | I rebuilt first run so Onda checks whether an iCloud backup exists before choosing between restore, local-only, or fresh setup, and I kept Apple Music setup from appearing until the saved library has actually resolved. I also made recovery status messages lead to the right place instead of only describing a problem. On playback, I changed queue moves so Onda does not commit a Next or Previous transition until Apple Music confirms that the physical player really moved, and I moved song-pool composition off the main actor so refreshes do less work in the UI path. Tests cover the new first-run choices and queue-transition rules. This is state and recovery work, not proof that startup or playback is fully reliable yet; I still need longer real-device and multi-device use. |
| 2026-08-22 | Keeping the recommender from getting stuck on one artist | I found two ways the ranking could give a misleading result even when the scores looked reasonable. A queue could still bunch too many songs from one artist together, and decaying old listening evidence was also updating the song's last-interaction time, which could make old behavior look recent. I added an artist-balancing pass that uses reserve candidates when one artist dominates, separated the decay checkpoint from real interaction time, and added tests including older saved behavior that does not have the new checkpoint yet. I also tightened the weekly insight rule so a clear overall direction stays the headline even when the week has a few reversals. These changes make the recommendation state more internally consistent, but they do not prove the music feels more diverse or better yet; I still need real listening sessions to judge that. |
| 2026-08-27 | Keeping listening behavior separate from direct feedback | While checking the recommendation analytics, I realized I was mixing two different kinds of evidence. Finishing or skipping a song says something about listening, but tapping Like or Dislike is a direct answer, and one should not erase the other. I split listening outcomes from explicit feedback, kept a song that had already been mostly listened to from being reclassified just because playback stopped later, and made Insights wait for enough samples before saying a mood is working well or needs tuning. I also fixed Previous so reaching the right song while paused no longer gets reported as if it were already playing. Tests cover the new outcome separation and small-sample rules. This makes the app's evidence and playback state more honest, but it still does not prove the recommendations are better; I need more real listening data. |
| 2026-08-28 | Making important choices reachable with accessibility settings | While testing first run, I realized some of the screens where a person makes the most important choices—restore a backup, share analytics, connect Apple Health, or tune a mix—could become hard to use at very large text sizes. I made those flows scroll instead of clipping, let the tuning choices switch to a one-column layout for accessibility text sizes, and made guide animations respect Reduce Motion. I added UI and unit tests at accessibility text sizes and for the motion policy. This makes those choices more reachable in the tested cases, but it is not a full accessibility audit; I still need VoiceOver and real-device testing across the rest of the app. |
| 2026-09-05 to 2026-09-06 | Making data reset work even when iCloud is off | I found that deleting Onda data could get stuck behind iCloud even when sync was turned off, and a stale or failed history load could leave the Library showing the wrong thing. I changed reset so a local deletion can finish immediately while saving a pending marker to carry the deletion into iCloud before sync is used again, added merge and reconciliation tests so an older cloud copy does not silently restore deleted data, gave failed Recent Listening loads an explicit retry, and made loaded history refresh after reset purges remove stale records. Tests cover local-only resets, cloud reconciliation, failed reads, cached rows, large-text and dark-mode retry, and post-reset refresh. This makes deletion and recovery more consistent in code, but I still need longer multi-device and offline-to-online real-device testing before calling the reset flow reliable. |
| 2026-09-16 | Reducing short-window repeats with confirmed listening history | I found a concrete failure in the recommender: with a stable pool, it could keep choosing the same tracks on consecutive days even when similarly qualified alternatives were available. I added a 72-hour view of confirmed listening starts and use it to prefer less-recent alternatives without changing the learned preference scores themselves. I also rotate oversized saved-song pools so the same prefix does not permanently crowd out the rest of the library. In controlled authored tests, the baseline repeated the same eight tracks across seven days, while the new selection spread choices across more tracks; follow-up tests cover confirmed starts without inventing completions, simultaneous moods, exclusions, source balance, and reset generation. This shows that the repeat-protection mechanism works in the tested fixtures, not that listeners prefer the recommendations. Deliberate replays and real-listener quality still need separate validation. |
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.