Skip to content
Platforms & CRMPrototype2026

Evolve University iOS

A fully native SwiftUI member app for Evolve University's real estate training platform, with offline video, community, AI-scored practice calls and generated tools.

55
tests in UniversityKit, green against recorded server fixtures
1320 by 2868
screenshot resolution captured for the App Store's 6.9 inch size
17.0
minimum iOS deployment target
ArchitecturePrivate internal tool
  1. 01
    Interface
    • Swift 5
    • SwiftUI with Observation
    • Swift Package Manager (Universit…
  2. 02
    Operations
    • XCTest
  3. 03
    Supporting
    • AVKit, PDFKit, SafariServices, U…
    • xcodegen

Evolve University iOS holds live data, so this shows the verified technology stack by layer rather than a screenshot. Layers, not connections — which service calls which is not something a dependency list can prove. Hosts, ports and topology are deliberately absent.

Problem

Evolve University already runs as a web platform members can use in a mobile browser, but a training system built around daily habits, a lesson watched on a commute, a practice call run between showings, a commitment logged before bed, loses most of that behavior to a browser tab that gets closed and forgotten. The platform side had already shipped native-shaped API routes and a bearer-token contract for exactly this reason. What was missing was the client: something that could hold a session in the Keychain, play video with picture-in-picture and background audio, cache a lesson for a flight, and receive a push when a teammate replies, none of which a web wrapper does honestly. The repo is explicit that this could not be a WebView shell: every member screen had to be native, and the app had to consume a membership rather than sell one, since pricing and upgrades stay strictly on the website by design.

What was built

Four sub-projects, each with its own plan record and matching server and client code, add up to a complete native member surface: Learn (courses, lessons with downloadable video, quizzes, capstones), Community (spaces, a live feed, compose with reactions and one-level replies, report and block, a leaderboard), Practice and Tools (scripted role-play calls scored by a coach, generated tool forms with run history), and Account (Accountability, Certifications, Challenges, a Team view for team leads, My files, notification settings, delete account). The codebase splits cleanly into UniversityKit, a Swift package with the Codable models for every route, the error mapping, the deep-link router, the download-manifest rules and the membership sentence, with no UIKit and no networking so it is fully unit-testable, and App/, the SwiftUI shell holding the only network code (UniversityAPI), the Keychain-backed session store, the offline download store, and the screens themselves. project.yml is the xcodegen source of truth for the generated, git-ignored Xcode project. The App Store submission pack, icon, launch screen, privacy manifest, listing copy, privacy-label answers and reviewer notes, is written and sitting in the repo, along with a rendered 1024px icon and draft screenshots shot in the simulator against fixture data.

Technical approach

The load-bearing decision is the split between UniversityKit and App. Every model the native API can return, and the rules that act on those models (deep-link routing, the download-manifest, a progress-reporting throttle, the membership-status sentence), live in a package with zero UIKit or SwiftUI imports and zero network calls, so the 55 tests in that package run under plain swift test with no simulator and cannot silently drift from what the server actually sends. Tests/UniversityKitTests/Fixtures/native/ holds JSON copied directly from the platform repo's own recorded fixtures, and the stated rule is that any server route change requires re-recording there and rerunning the suite before the app can be trusted again; scripts/contract-check.sh closes the loop by diffing live production answers against those same fixtures.

Session handling is deliberately Keychain-backed rather than stored in UserDefaults, which produced a specific and instructive failure during the first simulator pass: an unsigned build compiled with CODE_SIGNING_ALLOWED=NO has no entitlements, so every SecItemAdd call fails silently after a successful sign-in, surfacing to a tester as a generic error screen with no indication the problem was code signing rather than the API. The fix, documented in the simulator-pass record, is to build simulator runs with CODE_SIGN_IDENTITY="-" instead of leaving code signing off, so the entitlements needed for Keychain writes exist even without a real provisioning profile.

The app never has its own opinion about pricing: Config.swift and the account screen surface a membership status sentence computed once on the platform side and handed to the client as a string, so the invariant that the app shows no price, no upgrade link and no tier name on a lock is enforced by the server owning that decision entirely, not by client-side conditionals that could drift. Info.plist is generated from project.yml rather than edited directly, because a direct edit is silently discarded on the next xcodegen generate, which is called out as a repeat gotcha in both the README and the handoff.

Creative approach

Craft

The design brief was consumption, not merchandising: the app was ruled from the start to show no price, no upgrade CTA and no tier name anywhere a member could hit a paywall, so every locked-content state had to communicate scarcity of access without ever becoming a sales surface. That constraint shows up structurally rather than as a style choice, in the same section headers (Learn, Community, Practice, Tools, Account) as the web platform, so a member moving between the site and the app never has to relearn where anything lives. The App Store listing copy mirrors that discipline: promotional and description text are dash-free by the same house voice rule that governs every Evolve surface, and the description leads with what a member does in the app (play a lesson, log a commitment, run a practice call) rather than with what the platform sells.

Reframe

The product insight came from driving the running app through every screen rather than trusting a green build: several defects only a real interaction surfaced were fixed in the same pass rather than being logged for later, because a submission-bound app doesn't get a second pass before Apple sees it. Tapping the selected tab did nothing, when the iOS convention is that it pops the stack to root, so Navigator.popToRoot was wired into a custom tab-selection binding to restore the expected behavior. Practice section headers were rendering as raw lowercase slugs like listing instead of a readable label, fixed by capitalizing the string in the view, and separately a tool's course line showed a slug like brand authority os instead of a proper title, both symptoms of the client trusting server strings to already be display-ready when they were actually identifiers; the course-line fix was a dedicated courseTitle(fromSlug:) formatter added to the Kit, covered by its own test, rather than a one-off string tweak in the view.

Process and what failed

The most instructive failure was two buttons inside one List row firing together: tapping a specific card in the Accountability screen both logged a +1 and opened the +N entry sheet in the same tap, because SwiftUI's default button styling inside a List row makes every button in that row respond to a row-level tap gesture, not just its own. The fix, .buttonStyle(.borderless) on the pair, is a narrow one-line change, but the repo records it as a defect a code review would not have caught, since the built views compiled cleanly and looked correct in a static screenshot; it only appeared under an actual tap in the running app. The same pass turned up that the app's local-network origin was silently blocked by App Transport Security, which was routed through project.yml's NSAllowsLocalNetworking rather than patching the generated Info.plist directly, because a direct plist edit had already been established as something xcodegen generate throws away.

Outcome

The app is not shipped. Every planned screen across all four sub-projects is built, UniversityKit's 55 tests are green, and the app was run once in the iOS Simulator (an iPhone 17 Pro Max runtime) against a local copy of the platform, where every screen rendered and the defects that run found were fixed in the same session. But the repo's own handoff is explicit about what has not happened since: the platform side that the app depends on, the native API routes, the bearer-token configuration, push notification wiring, and the universal-link route, is committed to the platform repo's local main branch but has never been pushed to production, so the app has never been run end to end against the live platform. The App Store submission pack (icon, privacy manifest, listing copy, reviewer notes) is written and complete, but no App Store Connect record has been created, no build has been archived or uploaded, and no TestFlight test has run. The screenshots in the repo are explicitly marked as drafts shot against local fixture data (four fixture courses, one fixture post, a Dev Fixture account) and are called out in the repo's own submission checklist as needing to be re-shot against production before submission. The handoff's ordered next steps start with pushing the platform, then a first pass on a physical iPhone.