CASE STUDY 04 / 05
CuongThai iOS
iPhone & iPad app
A native SwiftUI app for iPhone and iPad on the same REST API as the web: study by major, Apple Pencil notebooks, a book library, realtime messaging and calls.
No public link yet: test builds are distributed through TestFlight by invitation.
- Status
- In TestFlight testing — not yet on the App Store
- Timeline
- iOS repository since 19 Aug 2026 (first commit)
- Platforms
- iOS 17+ · iPhone and iPad
- Role
- One lead engineer: feature design, testing on simulator and real devices, TestFlight releases. Much of the Swift is written with an AI coding assistant (Claude Code), as the project README states.
Context & problem
Studying on a phone or iPad needs things a browser does poorly: pen handwriting, notifications, home-screen widgets, sharing from other apps, offline use.
The app has to pass App Store review: a private sign-in option, in-app account deletion, moderation of user content, no downloading of third-party media, correctly declared permissions and data use.
Who it serves
Students on their phones
Study by major, follow courses, message, get notifications.
Learners on iPad
Handwritten notebooks with Apple Pencil, a two-column layout.
Solution & key features
Study by major
Choose a major and specialisation, filter subjects, view the subject map, download lessons for offline study on iPad.
Handwritten notebooks
PencilKit notebooks; the AI can draw shapes and text onto the page as real pen strokes.
Realtime
Messaging over Socket.IO, calls over WebRTC, push notifications.
Beyond the main app
A home-screen widget (WidgetKit) and a share extension to send videos in from other apps.
Architecture
Targets in the project
- CuongThaiAppiPhone + iPad
- TienWidgetWidgetKit
- ChiaSeVideoShare extension
- CuongThaiAdminiPad admin app
Share the Shared/ folderShared layer
- SwiftUI viewsMVVM
- APIClientasync/await, token refresh
- KeychainCredentials
- SwiftDataLocal data
HTTPS · Socket.IO · WebRTCServer
- cuongthai.com APIREST /api/v1, JWT
- StoreKit verificationApple JWS signatures
Hard technical decisions & trade-offs
01The server is the source of truth for access and paid plans
- Problem
- Trusting a client-reported Pro status invites users to patch the app to unlock it.
- Decision
- The app always asks the server; every Pro gate is enforced on the backend and the app only displays what the server returns. A plan bought on the web therefore works in the app immediately.
- Trade-off
- Some screens need the network to know access rights.
02Verifying App Store transactions up to Apple’s root certificate
- Problem
- StoreKit transactions are JWS carrying their own certificate chain. Verifying with the certificate inside the payload is a trap: an attacker self-signs a “bought 12 months” transaction and the code still says valid.
- Decision
- The backend cryptographically verifies leaf ← intermediate ← root, requires the root to match the pinned SHA-256 fingerprint of Apple Root CA – G3 (never taken from the payload), checks validity periods, and only then the signature.
- Trade-off
- When Apple rotates its root, the pinned fingerprint must be updated.
03App Store compliance built in before submission
- Problem
- The first version had placeholder screens, uploads that posted a fake URL, tokens in UserDefaults, a music module pulling audio from YouTube and a shop surface — all grounds for rejection.
- Decision
- Added Sign in with Apple, in-app account deletion, report/hide/block and a rules-consent screen; removed the music module and shop; built out the placeholder screens and real image upload; moved tokens to Keychain with a one-time migration for older installs.
- Trade-off
- The app has fewer features than the web where the rules do not allow them.
04An Xcode project generated from configuration
- Problem
- Xcode project files are hard to read and conflict easily under concurrent changes.
- Decision
- Targets, packages and settings are described in project.yml and the project is generated with XcodeGen.
- Trade-off
- New Swift files require re-running generation; forgetting shows up as “symbol not found”.
Real incidents & lessons
The app is not widely released, so these are bugs caught during development and testing, not incidents with end users.
Swallowed decoding errors: an empty profile instead of an error
- What happened
- Some screens showed empty data although the API returned everything.
- Root cause
- The JSON decoder used the default date strategy, so declaring a Date failed the whole decode; and `let` properties with a default value are silently skipped by Codable.
- Fix & safeguard
- API dates are declared as strings and parsed explicitly; API model fields are `var` and optional. Written down as project rules.
AI-drawn strokes present in the drawing but invisible on a real iPad
28/09/2026
- What happened
- Shapes the AI drew into a notebook did not appear on a real iPad.
- Root cause
- Stroke points of 1.2pt with pen ink — PencilKit on a real iPad renders nothing; point size is not pen width.
- Fix & safeguard
- Point size now follows the user’s sample stroke and never drops below 2.4pt. Lesson: test on real hardware.
Two sheets on one screen: one button silently did nothing
- What happened
- A button meant to open a sheet did nothing, with no error.
- Root cause
- SwiftUI only runs the last `.sheet(isPresented:)` when two are attached to the same view.
- Fix & safeguard
- Each screen uses a single `.sheet(item:)` driven by an enum.
Measured numbers
No user, revenue or client numbers here — only what can be counted from source code and the database. Each row says how it was counted so you can check it. Anything that cannot be counted is not shown.
iOS codebase (separate repository)
Counted by command on the codebase on 04/10/2026
- Committed Swift filesgit ls-files "*.swift" | wc -l
- 343
- Committed lines of Swiftgit ls-files -z "*.swift" | xargs -0 cat | wc -l
- 132,833
- Commitsgit rev-list --count HEAD
- 263
- iOS app targetsiOS target blocks in project.yml
- 4
- Backend Apple-verification test filessrc/services/appleIAP/*.test.ts
- 3
Technology
- App
- Swift · SwiftUI · SwiftData · Keychain · XcodeGen
- Apple frameworks
- PencilKit · WidgetKit · StoreKit · Sign in with Apple · AVFoundation · Vision · PDFKit
- Packages
- Kingfisher · Socket.IO-Client-Swift · GoogleSignIn · WebRTC