Skip to content

Keshavkumar96/Jarvis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jarvis

An offline-first multi-chat iOS application built entirely with SwiftUI and SwiftData. Jarvis manages multiple concurrent conversations between a user and a simulated AI agent, persisting everything locally with zero network dependency.

Table of Contents

Requirements

Dependency Version
Xcode 16.2+
iOS Deployment Target 17.0+
Swift 5.9+
External Libraries None

The project uses only Apple system frameworks: SwiftUI, SwiftData, PhotosUI, and UIKit (for camera).

Setup

  1. Open Jarvis.xcodeproj in Xcode.
  2. Select a simulator or physical device (iOS 17.0+).
  3. Build and run (Cmd + R).

No dependency installation (SPM, CocoaPods, Carthage) is needed.

Features

Feature Description
Multi-Chat Create and manage multiple concurrent conversations
Text Messaging Send and receive text messages with styled bubbles
Image Messaging Attach images from Photo Library or Camera
AI Agent Simulation Simulated agent replies with debounced, randomized cadence
Swipe-to-Delete Swipe chats to delete with a confirmation dialog
Editable Titles Auto-generated from the first message; tap to rename
Full-Screen Images Tap images to view full-screen with pinch-to-zoom and pan
Smart Timestamps "Just now", "2m ago", "Yesterday", "Dec 20", "Dec 20, 2024"
Seed Data 3 pre-loaded chats with 10 messages each for demo purposes
Offline-First All data persisted locally via SwiftData; no network required

Architecture

The project follows MVVM (Model-View-ViewModel) with a Service Layer:

┌─────────────────────────────────────────────────┐
│                     View                        │
│  (SwiftUI Views — screens & subviews)           │
│  Observes ViewModel via @Observable / @Bindable │
└──────────────────────┬──────────────────────────┘
                       │ user actions / state
                       ▼
┌─────────────────────────────────────────────────┐
│                  ViewModel                      │
│  (@Observable, @MainActor)                      │
│  Business logic, state management               │
│  Delegates persistence to ModelContext           │
│  Delegates AI replies to AgentService            │
└──────────┬──────────────────┬───────────────────┘
           │                  │
           ▼                  ▼
┌────────────────┐  ┌─────────────────────────────┐
│  SwiftData     │  │       Services              │
│  ModelContext   │  │  AgentService               │
│  (persistence) │  │  ImageCacheService           │
└────────────────┘  └─────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────┐
│                   Models                        │
│  Chat (@Model) ──< Message (@Model)             │
│  SwiftData persistent models                    │
└─────────────────────────────────────────────────┘

Why MVVM?

  • Separation of concerns: Views are purely declarative UI; ViewModels own all business logic and state.
  • Testability: ViewModels can be unit-tested without instantiating views.
  • SwiftUI alignment: @Observable provides fine-grained reactivity, and @Bindable enables two-way bindings between Views and ViewModels.

Key Architectural Decisions

Decision Rationale
@Observable over ObservableObject Fine-grained reactivity; only properties that change trigger view updates
@MainActor on ViewModels & AgentService Guarantees all UI-related state mutations happen on the main thread
SwiftData over Core Data Modern, declarative persistence with native SwiftUI integration
Actor for ImageCacheService Thread-safe image operations without manual locking
No DI container Project scope is small; SwiftUI's @Environment handles ModelContext injection
No external dependencies Reduces maintenance burden; Apple frameworks cover all requirements

Project Structure

Jarvis/
├── JarvisApp.swift                          # App entry point, ModelContainer setup, seed data
│
├── Models/
│   ├── Chat.swift                           # SwiftData model — chat entity
│   └── Message.swift                        # SwiftData model — message entity + enums
│
├── Modules/
│   ├── ChatList/                            # Chat list feature module
│   │   ├── View/
│   │   │   ├── Screen/
│   │   │   │   └── ChatListView.swift       # Main chat list screen
│   │   │   └── SubViews/
│   │   │       ├── ChatRowView.swift         # Individual chat row
│   │   │       └── MessageInputView.swift    # Text input + image attachment bar
│   │   └── ViewModel/
│   │       └── ChatListViewModel.swift       # Create & delete chat logic
│   │
│   └── ChatDetails/                         # Chat detail feature module
│       ├── View/
│       │   ├── Screen/
│       │   │   └── ChatDetailView.swift      # Detail screen entry point
│       │   └── SubViews/
│       │       ├── ChatDetailContent.swift    # Message list + input composition
│       │       ├── EmptyStateView.swift       # Empty conversation placeholder
│       │       ├── FullScreenImageView.swift  # Pinch-to-zoom image viewer
│       │       ├── ImageMessageView.swift     # Image bubble (bundle + async)
│       │       └── MessageBubbleView.swift    # Text/image message bubble
│       └── ViewModel/
│           └── ChatDetailViewModel.swift      # Send messages, agent replies, title editing
│
├── Services/
│   ├── AgentService.swift                    # AI agent reply simulation with debouncing
│   └── ImageCacheService.swift               # Image saving & thumbnail generation
│
├── Utilities/
│   ├── CameraView.swift                      # UIImagePickerController wrapper
│   ├── Constants.swift                       # Agent response templates & config
│   ├── DesignTokens.swift                    # Spacing, radius, size constants
│   └── TimestampFormatter.swift              # Smart relative date formatting
│
├── Data/
│   └── SeedData.swift                        # Initial demo data (3 chats, 30 messages)
│
└── Assets.xcassets/                          # App icon + placeholder images
    ├── AppIcon.appiconset/
    └── placeholder_*.imageset/               # 5 placeholder images for agent replies

Module Organization

Each feature module follows a consistent internal structure:

Module/
├── View/
│   ├── Screen/        # Top-level screen view (one per module)
│   └── SubViews/      # Reusable child views scoped to this module
└── ViewModel/         # @Observable ViewModel for the module

Code Flow

App Startup

JarvisApp.swift
  │
  ├── Creates ModelContainer with Chat & Message schema
  ├── Injects container via .modelContainer() modifier
  └── On appear:
        └── SeedData.seedIfNeeded(context:)
              └── If DB is empty → inserts 3 chats + 30 messages

Chat List Flow

ChatListView
  │
  ├── @Query fetches all chats sorted by lastMessageTimestamp (descending)
  ├── Displays ChatRowView for each chat
  │     └── Shows avatar initial, title, last message, smart timestamp
  │
  ├── Tap "+" button
  │     └── ChatListViewModel.createChat()
  │           ├── Creates new Chat with default title "New Chat"
  │           ├── Inserts into ModelContext and saves
  │           └── Navigates to ChatDetailView
  │
  ├── Tap a chat row
  │     └── NavigationDestination → ChatDetailView(chat:)
  │
  └── Swipe to delete
        └── Confirmation alert → ChatListViewModel.deleteChat()
              └── ModelContext.delete() with cascade (removes all messages)

Chat Detail Flow

ChatDetailView
  │
  └── On appear → creates ChatDetailViewModel(chat:, context:)
        │
        └── ChatDetailContent (main UI)
              │
              ├── ScrollView with LazyVStack of MessageBubbleView
              │     ├── Text messages → styled bubble (blue for user, gray for agent)
              │     └── Image messages → ImageMessageView (bundle or async)
              │           └── Tap → FullScreenImageView (pinch-to-zoom, pan, double-tap)
              │
              ├── MessageInputView (bottom bar)
              │     ├── Text field with send button
              │     ├── Attachment button → Photo Library or Camera
              │     └── On send:
              │           └── ViewModel.sendTextMessage() or sendImageMessage(data:)
              │
              └── Edit title (toolbar pencil icon)
                    └── Alert with TextField → ViewModel.saveTitle()

Message Sending Flow

User types message and taps Send
  │
  └── ChatDetailViewModel.sendTextMessage()
        │
        ├── Creates Message(sender: .user, type: .text)
        ├── Inserts into ModelContext
        ├── Updates chat title (if first message), lastMessage, timestamps
        ├── Saves context
        ├── Clears input field
        ├── Refreshes message list
        │
        └── AgentService.onUserMessageSent(chat:, context:)
              │
              ├── Cancels any previously pending reply task
              ├── Increments user message counter for this chat
              ├── Checks against random threshold (2–4 messages)
              │
              ├── If threshold NOT reached → returns (waits for more messages)
              │
              └── If threshold reached:
                    ├── Resets counter, picks new random threshold
                    └── Schedules delayed Task (1–2 second random delay)
                          │
                          └── generateReply()
                                ├── 70% chance → text reply (random from templates)
                                ├── 30% chance → image reply (placeholder asset)
                                ├── Inserts Message(sender: .agent) into context
                                ├── Updates chat's lastMessage and timestamps
                                └── Saves context

Image Sending Flow

User taps attachment button
  │
  ├── Photo Library → PhotosPicker selection
  │     └── Loads Data from PhotosPickerItem
  │
  └── Camera → CameraView (UIImagePickerController)
        └── Captures JPEG data (0.8 compression)

Both paths → ChatDetailViewModel.sendImageMessage(data:)
  │
  ├── ImageCacheService.saveImageLocally(data:, filename:)
  │     └── Writes to Documents/ChatImages/<uuid>.jpg
  │
  ├── ImageCacheService.generateThumbnail(from:)
  │     └── Resizes to max 100pt dimension, JPEG at 0.6 quality
  │     └── Saves as thumb_<uuid>.jpg
  │
  └── Creates Message(type: .file, filePath:, fileSize:, thumbnailPath:)
        └── Inserts and saves via ModelContext

Data Layer

Models

Chat (@Model)

Field Type Description
id String (unique) UUID string identifier
title String Chat title (auto-generated or user-edited)
lastMessage String Preview text for the chat list
lastMessageTimestamp Int64 Millisecond Unix timestamp of last message
createdAt Int64 Millisecond Unix timestamp of creation
updatedAt Int64 Millisecond Unix timestamp of last update
messages [Message] Inverse relationship with cascade delete

Message (@Model)

Field Type Description
id String (unique) UUID string identifier
chat Chat? Parent chat reference
message String Text content (empty for image-only messages)
type MessageType .text or .file
filePath String? Local file path or bundle asset name
fileSize Int? File size in bytes
thumbnailPath String? Thumbnail file path or bundle asset name
sender SenderType .user or .agent
timestamp Int64 Millisecond Unix timestamp

Enums: SenderType (.user, .agent) and MessageType (.text, .file) — both String-backed and Codable.

Relationships

Chat ──< Message    (one-to-many, cascade delete)

Deleting a Chat automatically deletes all associated Message records.

Timestamps

All timestamps are stored as Int64 millisecond Unix timestamps. Computed Date properties on both models handle the conversion:

var date: Date {
    Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
}

Service Layer

AgentService

Simulates AI agent responses with a natural conversational cadence.

  • Debouncing: Cancels any pending reply if the user sends another message quickly.
  • Random threshold: Agent waits for 2–4 user messages before replying (randomized per cycle).
  • Typing delay: 1–2 second random delay before the reply appears.
  • Response distribution: 70% text responses (from 10 templates), 30% image responses (from 5 placeholder assets).
  • Per-chat state: Tracks message counts and thresholds independently per chat.

ImageCacheService

An actor-based service (singleton) for thread-safe image operations.

  • Save locally: Writes image data to Documents/ChatImages/ directory.
  • Thumbnail generation: Resizes images to a max 100pt dimension with 0.6 JPEG compression.

Design System

The project uses a centralized design token system in DesignTokens.swift:

Token Type Examples Usage
Spacing _4, _8, _12, _16 Padding, gaps, margins
Radius _18, _20 Corner radii for bubbles and inputs
IconSize _56 SF Symbol sizing
ComponentSize _48, _100, _180, _240 Fixed component dimensions

Timestamp formatting is handled by TimestampFormatter.smartFormat(milliseconds:):

Condition Output
< 60 seconds ago "Just now"
< 60 minutes ago "2m ago"
Today "3:45 PM"
Yesterday "Yesterday"
Same year "Dec 20"
Different year "Dec 20, 2024"

Assumptions

  • All timestamps use millisecond Unix format to match the PRD JSON schema.
  • Agent responses use bundled placeholder images (not network images) to stay fully offline.
  • Camera permission is requested at first use via the standard iOS permission dialog.
  • Chat title defaults to "New Chat" and is auto-updated to the first 30 characters of the first user message.
  • The file nested object from the PRD schema is flattened into the Message model as filePath, fileSize, and thumbnailPath because SwiftData does not support nested value types directly.
  • The app seeds 3 demo chats with 30 total messages on first launch only (skips if data already exists).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages