Your AI fitting room for iPhone. Add a photo of yourself, pick any clothing item (a product photo or a screenshot works), and SuitUp generates you wearing it.
- Looks, not loose images: every try-on is saved as one look (you + clothing → result), with before/after comparison.
- Studio composer: two slots (You, Clothing) and a result, so you can see exactly what you're sending, swap one input and regenerate.
- Chain outfits: "Dress This Look" makes a result the person photo for the next item.
- Variations: ask for 1, 2 or 4 results per SuitUp and swipe to pick your favorite.
- Choice of AI service: OpenRouter (default, Meta Muse Image), Google Gemini, or Fal.ai (Seedream v4).
- Private by design: API keys live in the iOS Keychain, and photos are stored only on the device.
- Light and dark mode: both follow the brand palette (blue → purple → pink).
Studio · Wardrobe · Settings
| Xcode | 27.1 (tested) |
| iOS deployment target | 18.5 |
| Language | Swift 5 mode, SwiftUI |
| Dependencies | None (Apple frameworks only) |
- Open
SuitUp.xcodeprojin Xcode. - Select the SuitUp scheme and your device or a simulator, then run.
- In the app, open Settings (gear icon) → API Key, paste your key and tap Verify Key.
| Service | Key format | Where to get it |
|---|---|---|
| OpenRouter (default) | sk-or-v1-… |
https://openrouter.ai/keys |
| Google Gemini | AIza… |
https://aistudio.google.com/apikey |
| Fal.ai | Key ID + Key Secret | https://fal.ai/dashboard/keys |
Keys are entered once in Settings and stored in the Keychain (
kSecAttrAccessibleWhenUnlockedThisDeviceOnly). They are never compiled into the binary.
- You: tap the You slot (or the header button) to choose a photo: Camera, Library, or a recent photo or look. Photos are decoded at 2048 px or less, off the main thread, and saved to the Wardrobe.
- Clothing: tap the Clothing slot, or just press SuitUp. Without clothing it asks for it, then generates automatically.
- Generation happens in place: the hero card shows your photo blurred with a shimmer and progress, and the composer stays visible. You can cancel at any time. Both images are downscaled to 1024 px or less before upload.
- Result: the look unblurs into the hero (a plain fade when Reduce Motion is on). Hold to compare shows the original. Swipe the hero to browse looks; each one loads its recipe into the composer, ready to tweak and regenerate.
The prompt is editable in Settings → Prompt (templates: Default, Professional, Casual, Fashion, Accessories). If the prompt is blank, the default one is used.
| Content | Framing | Why |
|---|---|---|
| Person photos and results | Aspect-fill 2:3, cropped around the face (Vision), falling back to a top bias | Center crops cut off heads; 2:3 matches the generated output |
| Clothing | Aspect-fit on a neutral tile, never cropped | Sleeves, hems and logos matter |
| Full screen | Aspect-fit with pinch zoom and double-tap to zoom on the tapped point | Judging fit needs detail |
- Recipe strip: You + Clothing → Look appears under the hero and in the viewer. Wardrobe cells show the clothing as a round badge.
- Before/after comparison:
- Hold to compare is a crossfade inside the same frame. It's forgiving when the AI shifted the framing.
- Slider is a drag divider for when the images line up.
- Viewer:
- Zoom transition from the tapped card (
matchedTransitionSource+.navigationTransition(.zoom)), with swipe-down to dismiss. - Pages through the list and preloads neighboring thumbnails.
- Zoom transition from the tapped card (
- Sharing:
- Share the image alone, or a before → after collage (
ShareImageRenderer). - Optional "Made with Suit UP" watermark (Settings → Results).
- Share the image alone, or a before → after collage (
- Labels and accessibility:
- Results carry an AI look badge.
- Overlays sit on gradient scrims for contrast.
- VoiceOver labels describe looks, and the compare controls work with VoiceOver (toggle / adjustable).
- Original quality: results are stored in their original format (e.g. WebP/PNG) without re-encoding.
Model IDs live in SuitUp/Core/Constants/AppConstants.swift (AppConstants.Models).
| Service | Model | Endpoint |
|---|---|---|
| OpenRouter | meta/muse-image |
POST /api/v1/images (input_references, aspect_ratio: 9:16) |
| Google Gemini | gemini-2.5-flash-image |
POST /v1beta/models/{model}:generateContent |
| Fal.ai | fal-ai/bytedance/seedream/v4/edit |
POST https://fal.run/… (sync_mode: true) |
Image-only OpenRouter models (such as meta/muse-image) must use the /images endpoint. Multimodal chat
models (such as google/gemini-2.5-flash-image) use /chat/completions. To switch, set
AppConstants.Models.openRouterImageAPI to .chatCompletions and change openRouterImageModel.
SuitUp/
├── App/ # @main entry, root ContentView, splash timing
├── Core/
│ ├── Constants/ # AppConstants: endpoints, models, limits, keys
│ ├── Design/ # Theme (colors, radii, spacing), FontManager (Josefin Sans)
│ ├── Models/ # Look, HistoryItem, GenerationRequest/Response, AppError, API DTOs
│ ├── Services/
│ │ ├── Providers/ # OpenRouterService, GoogleGeminiService, FalAIService
│ │ ├── ImageGenerationManager.swift # picks provider, tracks loading state
│ │ ├── NetworkManager.swift # reachability, retries, decoding off the main thread
│ │ ├── StorageManager.swift # history.json + Images/, thumbnails, cache
│ │ ├── ConfigurationManager.swift # selected service, key access
│ │ ├── KeychainManager.swift
│ │ ├── PromptManager.swift
│ │ ├── PhotoLibraryService.swift # add-only save to Photos
│ │ └── HapticManager.swift
│ └── Utilities/ # ImageProcessor (resize, encode, validate, downsample)
├── Features/
│ ├── Main/ # MainView: tabs, sheets, dialogs, loading overlay
│ ├── Home/ # HomeView (hero + composer) + HomeViewModel
│ ├── Account/ # Wardrobe: looks / photos / clothing grid
│ ├── Viewer/ # MediaViewer: paging, zoom, compare, recipe, share
│ └── Settings/ # service, API keys, prompt, storage
└── Shared/Components/ # Brand UI kit and UIKit wrappers (see below)
SuitUpTests/ # XCTest unit tests (+ Fixtures/)
- Data model:
HistoryItemis a stored image:source(person),garmentorgenerated.Looklinks a person file + garment file → result file, plus prompt, service, timing and a variationbatchID.- Looks are persisted in
looks.json. Deleting an image keeps looks consistent: a missing result removes the look, and a missing input only clears that reference. - On first launch after updating, earlier generated images are converted into looks automatically.
- MVVM with shared services.
HomeViewModelis owned byMainView, so the SuitUp button works from any tab and state survives tab switches. - Main-actor UI state, background heavy work:
StorageManager,ConfigurationManager,ImageGenerationManagerandNetworkManagerare@MainActorObservableObjects.- Image encoding, disk I/O and JSON decoding run off the main thread (
Task.detachedandnonisolatednetworking).
- Providers conform to
ImageGenerationProviderand receive the API key through their initializer, so they hold no global state. - Errors are typed (
AppError).isRetryabledecides whether the error dialog offers Try Again.
All screens use the tokens and components below instead of default system styling:
| File | Contents |
|---|---|
Core/Design/Theme.swift |
Brand colors and gradient, adaptive surfaces/text colors, radii, spacing |
Shared/Components/BrandComponents.swift |
brandCard(), BrandButtonStyle (.brandPrimary/.brandSecondary/.brandDestructive), BrandIconButton, BrandSegmentedPicker, BrandSecureField, BrandChip, StatusPill, ToastView, BrandWordmark |
Shared/Components/BrandDialog.swift |
Styled replacement for alerts: .brandDialog($dialog), BrandDialog.error(_:onRetry:) |
Shared/Components/PhotoPickerSheet.swift |
Camera / Library / recent photos sheet for a composer slot |
Shared/Components/StoredImageView.swift |
Async, cached history image with .person (face-aware fill) or .garment (fit) framing |
Shared/Components/ComposerStrip.swift |
You + Clothing → Look slots |
Shared/Components/GeneratingCard.swift, CompareControls.swift, RevealTransition.swift |
In-place progress, hold/slider comparison, unblur reveal |
Core/Design/FontManager.swift |
Typography: Text.appFont(_:weight:) (scales with Dynamic Type) and FontManager.brandFont for the wordmark |
The accent color (Assets.xcassets/AccentColor) is brand purple, so system controls such as cursors and links match.
Fonts (bundled in SuitUp/Resources/Fonts, registered via UIAppFonts):
- Plus Jakarta Sans (SIL OFL 1.1, license file bundled alongside) is the UI text font: modern and legible at caption sizes.
Each weight is resolved by its exact PostScript name (e.g.
PlusJakartaSans-Regular_SemiBold) rather than.weight()on the variable family. - Josefin Sans is used only for the "Suit UP" wordmark.
FontManagerTestsfail if a font silently falls back to the system font.
- Picked photos are decoded straight from the file with ImageIO at 2048 px or less, so 12–48 MP bitmaps are never fully decoded.
- Cards and the grid load downsampled thumbnails (
ThumbnailSize.card/.grid) off the main thread, cached in anNSCache. - Saving to history (orientation fix, JPEG encoding, disk write) runs in the background via
saveHistoryItem. - Card shadows are drawn from plain rounded rectangles rather than image pixels. The tab bar has no live blur.
- Card tilt angles come from the item's UUID, so cards don't jump when lazy views recycle them.
- Haptic generators are reused, and font availability is cached.
- Face detection runs once per image on the 600 px thumbnail, off the main thread, and is cached.
- The generating backdrop is blurred once with Core Image instead of using a live
.blurwhile animating. - The viewer shows the cached card thumbnail instantly, then swaps in the full-resolution image.
Unit tests live in SuitUpTests/ and cover:
- storage, using an isolated temporary directory (never your real history)
- image processing
- provider response parsing, including a fixture captured from a real
meta/muse-imageresponse - error mapping
- looks: saving and reloading, conversion of old history, reference cleanup, lossless saves
- the view model's composer flows
- face-aware crop math and the share renderer
# Compile app + tests without launching a simulator
xcodebuild -scheme SuitUp -destination 'generic/platform=iOS Simulator' build-for-testing
# Run the tests (needs a simulator or a connected device)
xcodebuild -scheme SuitUp -destination 'platform=iOS Simulator,name=iPhone 18 Pro' testSigning uses automatic provisioning. Sign in to Xcode → Settings → Accounts, then select your own team under Signing & Capabilities (no team is committed to the project).
xcrun devicectl list devices # find the device identifier
xcodebuild -scheme SuitUp -configuration Debug \
-destination 'id=<DEVICE_UDID>' -derivedDataPath build -allowProvisioningUpdates build
xcrun devicectl device install app --device <DEVICE_UDID> build/Build/Products/Debug-iphoneos/SuitUp.app
xcrun devicectl device process launch --device <DEVICE_UDID> LlamaBoo.SuitUp- Photos are stored in the app's Documents folder with complete file protection, and are sent only to the AI service you select.
- Saving to Photos asks for add-only access (
NSPhotoLibraryAddUsageDescription). - Camera access is requested only when you take a photo (
NSCameraUsageDescription).


