← Home

Moji Architecture

Moji ships as separate iOS and macOS apps from a single codebase. The two platforms share their entire domain layer and service layer through local Swift packages, diverging only at the view layer — iOS uses a tab-based navigation stack, macOS uses a three-column split view. Both apps share the same bundle ID, which enables Universal Purchase on the App Store and a single CloudKit container for cross-device sync.

Multiplatform Strategy

Rather than using a single multiplatform target, Moji keeps iOS and macOS as separate targets within one Xcode project. This gives each platform its own view layer and entitlements while sharing everything underneath. The shared code lives in two local Swift packages: one for domain logic (models, use cases, repository protocols, feature gating) and one for the design system (color tokens, spacing, typography).

The only external runtime dependencies are an HTML parser and an in-memory cache. Everything else — networking, persistence, content extraction — is built on platform frameworks. This keeps the dependency surface small and avoids version conflicts across platforms.

Content Extraction

The hardest problem in a read-it-later app is turning arbitrary web pages into clean, readable content. Moji bundles two JavaScript extraction engines and routes between them based on the source domain. Well-known sites like Twitter, Reddit, YouTube, and GitHub get site-specific extractors that understand their DOM structure. Everything else falls through to a generic extractor.

Both engines run inside an off-screen web view. After extraction, images are downloaded and rewritten to local paths so articles render fully offline. If extraction fails — the site blocks scraping, the page requires authentication, the network drops — the article is saved as a stub and a background service retries it later with exponential backoff.

Reader

The reader doesn't use a web view. Instead, extracted HTML is parsed into a structured array of content blocks — headings, paragraphs, images, code blocks, tables, quotes — and each block is rendered as native SwiftUI. This gives the reader full control over typography, scroll behavior, and text selection without fighting a web view's rendering quirks.

Scroll position is persisted per article, so reopening an article resumes where you left off. Parsed content blocks are cached in memory to avoid re-parsing when navigating back and forth.

Data and Sync

Persistence uses SwiftData with an adapter layer that bridges between persistence models and immutable domain types. This keeps the domain layer testable without any persistence framework dependency.

CloudKit sync is opt-in. When enabled, the SwiftData container connects to a private CloudKit database shared across both platforms. Because iOS and macOS use the same bundle ID and CloudKit container, articles saved on one device appear on the other automatically.

Extensions

The Share Extension and Safari Extensions let users save articles without opening the app. The Share Extension runs its own lightweight extraction at share time and writes the result to an app group container. The main app picks it up on next launch. Safari Extensions follow a similar pattern — a content script captures the page, and the result flows through the shared app group. Each platform needs its own Safari Extension target because macOS sandboxing requires different entitlements.