Skip to content
arthurrollPublic

About

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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Suit UP

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: generated look with the You + Clothing → Look composer   Wardrobe: looks with clothing badges   Settings: AI service, API key and prompt

Studio · Wardrobe · Settings


Requirements

Xcode 27.1 (tested)
iOS deployment target 18.5
Language Swift 5 mode, SwiftUI
Dependencies None (Apple frameworks only)

Getting Started

  1. Open SuitUp.xcodeproj in Xcode.
  2. Select the SuitUp scheme and your device or a simulator, then run.
  3. 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.

How It Works

  1. 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.
  2. Clothing: tap the Clothing slot, or just press SuitUp. Without clothing it asks for it, then generates automatically.
  3. 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.
  4. 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.

Displaying Images

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.
  • Sharing:
    • Share the image alone, or a before → after collage (ShareImageRenderer).
    • Optional "Made with Suit UP" watermark (Settings → Results).
  • 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.

Models

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.

Project Structure

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/)

Architecture

  • Data model:
    • HistoryItem is a stored image: source (person), garment or generated.
    • Look links a person file + garment file → result file, plus prompt, service, timing and a variation batchID.
    • 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. HomeViewModel is owned by MainView, so the SuitUp button works from any tab and state survives tab switches.
  • Main-actor UI state, background heavy work:
    • StorageManager, ConfigurationManager, ImageGenerationManager and NetworkManager are @MainActor ObservableObjects.
    • Image encoding, disk I/O and JSON decoding run off the main thread (Task.detached and nonisolated networking).
  • Providers conform to ImageGenerationProvider and receive the API key through their initializer, so they hold no global state.
  • Errors are typed (AppError). isRetryable decides whether the error dialog offers Try Again.

Design System

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.
  • FontManagerTests fail if a font silently falls back to the system font.

Performance Notes

  • 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 an NSCache.
  • 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 .blur while animating.
  • The viewer shows the cached card thumbnail instantly, then swaps in the full-resolution image.

Testing

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-image response
  • 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' test

Installing on a Device

Signing 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

Privacy

  • 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).

About

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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages