Folio is a native iOS client developed by NUS Technology for private research, grounded Q&A, and knowledge management. Sources, notes, and citations are organized in one unified, private archive.
This repository is the SwiftUI iOS client: users can authenticate, manage workspaces, import and read research sources, compose notes with rich-text formatting and citations, export research notebooks, and stream grounded AI answers using real-time SSE (Server-Sent Events).
- Company: NUS Technology
- Labs: Built by Our Engineers
- Tech stack
- Getting started
- Project structure
- iOS architecture
- Sequence diagrams
- Deployment / runtime topology
- Conceptual data model
- Main screens & key features
- Rich text & citation engine
- Localization
- Testing
- Conventions
- Detailed documentation
- License
| Layer | Technology |
|---|---|
| Language | Swift 5.0 (Swift Concurrency async/await, @MainActor) |
| UI | SwiftUI + Combine (@Published, ObservableObject) |
| Navigation | Tabbed MainView + NavigationStack + Sheets & Covers |
| Architecture | Clean Architecture + MVVM (Presentation → Domain ← Data) |
| Dependency Injection | Manual AppDIContainer (Composition Root) |
| Networking | NetworkService (URLSession) + APIEndpoint + Resilience retries |
| Streaming | AskAnswerSSEClient for Server-Sent Events (SSE) streaming answers |
| Auth | Bearer token auth via REST (/api/v1/auth); tokens in Keychain |
| Persistence | UserDefaultsStorage + KeychainStorage |
| Dependencies | Zero external third-party dependencies (Apple frameworks only; a GoogleService-Info.plist config is present for Firebase but no SDK is linked) |
| i18n | String Catalog (Localizable.xcstrings — English en, Vietnamese vi) |
| Testing | XCTest / Swift Testing (FolioTests — ViewModels, SSE, RichText, Network) |
| Code quality | SwiftLint (.swiftlint.yml — line length 120, 4-space indent, sorted imports) |
- Xcode 15.0+ (target iOS 17.0+)
- iOS Simulator or physical iOS device running iOS 17.0+
- Reachable REST API server at
https://folio.nustechnology.com
open Folio.xcodeproj- Select the Folio scheme and an iOS Simulator or device.
- Build and run (
⌘R).
You can build and test the project via xcodebuild:
# Build Debug configuration
xcodebuild -project Folio.xcodeproj -scheme Folio -configuration Debug build
# Run unit tests suite
xcodebuild -project Folio.xcodeproj -scheme Folio -configuration Debug testThe API base URL is configured via environment .xcconfig files:
| Config | API Base URL | Display Name | Bundle ID |
|---|---|---|---|
| Debug | https://folio.nustechnology.com |
Folio Dev | com.nus.folio |
| Release | https://folio.nustechnology.com |
Folio | com.nus.folio |
Configuration/Shared.xcconfig— deployment targetConfiguration/Debug.xcconfig&Release.xcconfig—INFOPLIST_KEY_*values read byAppConfiguration- Fail-fast behavior:
AppConfigurationvalidates thatAPI_BASE_URLis an explicit HTTPS URL on startup; it fails fast if missing or invalid without falling back to embedded development endpoints.
Folio/
├── FolioApp.swift # @main entry point, creates AppDIContainer
├── App/
│ └── AppDIContainer.swift # Composition root, lazy dependency wiring
├── Configuration/
│ └── AppConfiguration.swift # Reads API_BASE_URL from Info.plist
├── Domain/
│ ├── Entities/ # Business models (structs)
│ ├── RepositoryProtocols/ # Repository abstractions (protocols)
│ └── UseCases/ # Business logic (protocol + final class)
├── Data/
│ ├── DataSources/
│ │ ├── Local/ # LocalStorageProtocol, UserDefaultsStorage, KeychainStorage
│ │ └── Remote/ # NetworkServiceProtocol, NetworkService, APIEndpoint, AskAnswerSSEClient
│ ├── DTOs/ # Codable data transfer objects with toDomain()
│ └── Repositories/ # Repository implementations (conform to domain protocols)
├── Presentation/
│ ├── Common/
│ │ ├── Components/ # ErrorView, LoadingView, etc.
│ │ ├── Extensions/ # View+Extensions
│ │ ├── Protocols/ # ViewModelProtocol
│ │ └── ViewState.swift # Generic ViewState<T> enum
│ └── Scenes/ # Feature modules (Views + ViewModels per scene)
│ ├── Main/ # Auth, Tab shell, Q&A / Ask, Export, Account
│ ├── Sources/ # Spaces, source list, upload, status SSE
│ ├── Notes/ # Rich text notes, citations, convert to source
│ ├── Reader/ # Source HTML reader, web view, citations
│ └── Workspace/ # Workspace management & switching
├── Resources/ # Localizable.xcstrings, Assets.xcassets
└── Utils/ # Logger
Feature layer dependency flow: Presentation → Domain ← Data (Domain remains isolated at the center).
Code-layer view of the Folio iOS client based on Clean Architecture + MVVM:
flowchart TB
subgraph Presentation["Presentation Layer (SwiftUI + Combine)"]
direction TB
Views["Views<br/>MainView · FolioAskView · SourcesView · NoteListView · ReaderView"]
ViewModels["ViewModels<br/>MainViewModel · FolioAskViewModel · NoteListViewModel · WorkspaceViewModel"]
ViewState["ViewState<T><br/>.idle · .loading · .loaded(T) · .error(String)"]
Views --> ViewModels
ViewModels --> ViewState
end
subgraph Domain["Domain Layer (Pure Swift)"]
direction TB
UseCases["Use Cases<br/>SignInUseCase · FetchSourcesUseCase · NoteUseCases · AskUseCases"]
RepoProtocols["Repository Protocols<br/>AuthRepositoryProtocol · SourceRepositoryProtocol · NoteRepositoryProtocol"]
Entities["Entities<br/>User · Workspace · Source · Note · AskConversation"]
UseCases --> RepoProtocols
UseCases --> Entities
end
subgraph Data["Data Layer (Networking & Storage)"]
direction TB
Repositories["Repositories<br/>AuthRepository · SourceRepository · NoteRepository · AskRepository"]
RemoteDS["Remote DataSources<br/>NetworkService · AskAnswerSSEClient · APIEndpoints"]
LocalDS["Local DataSources<br/>KeychainStorage · UserDefaultsStorage"]
DTOs["DTOs & Mappers<br/>Codable DTOs + toDomain()"]
Repositories --> RemoteDS
Repositories --> LocalDS
Repositories --> DTOs
end
subgraph DI["Composition Root"]
AppDIContainer["AppDIContainer<br/>Lazy dependency wiring & injection"]
end
ViewModels --> UseCases
Repositories -.->|"Conforms to"| RepoProtocols
AppDIContainer --> Presentation
AppDIContainer --> Domain
AppDIContainer --> Data
| Layer | Dependency Rules | Description |
|---|---|---|
| Presentation | Depends on Domain |
UI components and ViewModels conforming to ViewModelProtocol. Never imports Data. |
| Domain | Independent | Business logic, protocols, and domain entities. Has zero dependencies on outer layers. |
| Data | Depends on Domain |
DTOs, network requests, local caching, and repository implementations. |
| AppDIContainer | Assembles all layers | Lazy dependency wiring instantiated once at FolioApp entry point. |
sequenceDiagram
autonumber
actor User
participant View as FolioLoginView
participant VM as MainViewModel
participant UC as SignInUseCase
participant Repo as AuthRepository
participant API as REST API (/api/v1/auth)
participant Keychain as KeychainStorage
User->>View: Enter credentials & Sign In
View->>VM: handle(.signIn(credentials))
VM->>UC: execute(credentials)
UC->>Repo: signIn(email, password)
Repo->>API: POST /api/v1/auth/login
API-->>Repo: AuthTokenDTO (accessToken, refreshToken)
Repo->>Keychain: Save access & refresh tokens
Repo-->>UC: AuthToken domain object
UC-->>VM: Success
VM->>VM: Update state (isAuthenticated = true)
VM-->>View: Render MainView tabs
sequenceDiagram
autonumber
actor User
participant View as FolioAskView
participant VM as FolioAskViewModel
participant UC as AskUseCases
participant SSE as AskAnswerSSEClient
participant API as REST API (SSE stream)
User->>View: Submit question / prompt
View->>VM: handle(.submitQuestion(query))
VM->>VM: state = .loading / streaming
VM->>UC: streamAnswer(query, scope)
UC->>SSE: connectStream(endpoint)
SSE->>API: POST /api/v1/spaces/{spaceId}/ask (URLSession.bytes)
loop SSE Stream Events
API-->>SSE: data chunk / token event
SSE-->>UC: AskAnswerStream payload
UC-->>VM: Append token / update citations
VM-->>View: Update answer UI in real time
end
API-->>SSE: stream complete event
SSE-->>VM: close connection
flowchart TB
subgraph Client["iOS Client"]
App["Folio iOS App<br/>(com.nus.folio)"]
Keychain["Keychain Storage<br/>(Tokens)"]
App --> Keychain
end
subgraph Cloud["Backend Infrastructure"]
REST["REST API<br/>folio.nustechnology.com"]
SSE["SSE Streaming Server<br/>/api/v1/spaces/{spaceId}/ask"]
DB[("Database & Knowledge Index")]
REST --> DB
SSE --> DB
end
App -->|"HTTPS REST API"| REST
App -->|"HTTPS SSE Stream"| SSE
| Communication Channel | Protocol | Usage |
|---|---|---|
| Control Plane | HTTPS JSON REST API | Auth, workspace management, sources, notes |
| Grounded Q&A | HTTPS Server-Sent Events (SSE) | Real-time streaming answers with live citations |
| Secure Token Storage | On-Device iOS Keychain | Access token & Refresh token persistence |
erDiagram
USER ||--o{ WORKSPACE : owns_or_belongs_to
WORKSPACE ||--o{ SOURCE : contains
WORKSPACE ||--o{ NOTE : contains
WORKSPACE ||--o{ ASK_CONVERSATION : stores
SOURCE ||--o{ NOTE : cited_by
ASK_CONVERSATION ||--o{ NOTE : exported_to
USER {
string id PK
string email
string display_name
}
WORKSPACE {
string id PK
string name
string owner_id FK
}
SOURCE {
string id PK
string workspace_id FK
string title
string url
string status
}
NOTE {
string id PK
string workspace_id FK
string title
string content
string source_id FK
}
ASK_CONVERSATION {
string id PK
string workspace_id FK
string query
string answer
}
| Entity | Description |
|---|---|
| USER | Authenticated user entity |
| WORKSPACE | Isolated research archive owning sources, notes, and Q&A conversations |
| SOURCE | Research document or web source ingested for grounded Q&A |
| NOTE | Rich-text research note containing source citations |
| ASK_CONVERSATION | Grounded Q&A conversation thread with citation references |
| Screen / Feature | Description |
|---|---|
| Login / Sign Up | Sign in with email or register an account (password reset UI present; backend pending) |
| Sources (Spaces) | Ingest web links and documents, track background parsing status via SSE |
| Source Reader | Built-in HTML reader and web view with citation preview sheets |
| Notes & Citations | Create and organize rich-text notes with source citations and back-links |
| Convert Note to Source | Convert user draft notes directly into ingested research sources |
| Folio Ask (Q&A) | Real-time SSE streaming Q&A grounded on workspace sources |
| Notebook Export | Export research notes and Q&A conversations to Markdown / text files |
| Workspace Selector | Multi-workspace management and seamless switching |
| Account Settings | Manage user profile and application settings |
Folio features a custom rich-text and citation engine built using pure Swift and SwiftUI:
- Markdown Round-Trip Support: Parsers and formatters handle headings, blockquotes, bullet/numbered lists, and inline styles (bold, italic, code).
- Citation Badges & Preview Sheets: Tapping a citation in an answer or note opens
CitationDetailSheet, displaying exact quoted passages and source metadata. - Save Q&A as Note: Convert grounded Q&A streams directly into saved rich-text notes (
SaveAskNoteSheet).
Supported languages in String Catalog (Folio/Resources/Localizable.xcstrings):
- English (
en) — primary - Vietnamese (
vi)
All user-facing strings are stored in Localizable.xcstrings. When adding new strings, append them to the catalog to avoid merge conflicts.
The test suite in FolioTests/ covers ViewModels, networking resilience, SSE stream parsing, and rich-text round-trip formatting:
# Execute unit test suite
xcodebuild -project Folio.xcodeproj -scheme Folio -configuration Debug test- Follow rules defined in
CLAUDE.mdandAGENTS.md. - Strict Clean Architecture dependency flow:
Presentation → Domain ← Data. - All ViewModels conform to
ViewModelProtocol(State,Action,handle(_:)). - UI state updates take place on
@MainActor. - Zero external third-party dependencies (Apple frameworks only).
| Document | Purpose |
|---|---|
| CLAUDE.md | Architecture, coding guidelines, conventions, and project commands |
| AGENTS.md | Compatibility entry point for AI development assistants |
This project is licensed under the NUS Technology Non-Commercial License 1.0. Commercial use and redistribution are not permitted. The bundled Cormorant Garamond font files are licensed separately under the SIL Open Font License 1.1. For commercial licensing, contact NUS Technology.