Application Design: Shoe Choo (Consolidated)
Architecture Overview
Pattern: MVVM with SwiftUI shell + AppKit editor surface UI Framework: SwiftUI (window, toolbar, sidebar) + NSViewRepresentable (NSTextView/TextKit 2) Rendering Pipeline: 3-stage — Parser → EditorNodeModel → Renderer State Management: EditorViewModel (per-document) + EditorSettings (shared)
Components (13)
| ID | Component | Type | Responsibility |
|---|---|---|---|
| C-01 | ShoechooApp | SwiftUI App | App lifecycle, document scene, menus |
| C-02 | MarkdownDocument | NSDocument | File I/O, auto-save, versioning |
| C-03 | EditorViewModel | @Observable | Editor logic, rendering orchestration |
| C-04 | EditorSettings | @Observable (singleton) | App-wide preferences (font, theme) |
| C-05 | MarkdownParser | Struct | Markdown → swift-markdown AST |
| C-06 | EditorNodeModel | Class | Intermediate block model with stable IDs |
| C-07 | MarkdownRenderer | Struct | EditorNode → NSAttributedString |
| C-08 | WYSIWYGTextView | NSViewRepresentable | TextKit 2 editing surface |
| C-09 | EditorView | SwiftUI View | Editor + toolbar composition |
| C-10 | SidebarView | SwiftUI View | Recent files list |
| C-11 | ExportService | Actor | HTML/PDF export |
| C-12 | ImageService | Actor | Image asset management |
| C-13 | FileService | Actor | Low-level file I/O utilities |
Rendering Pipeline
Source Text (String)
|
v
MarkdownParser (swift-markdown)
| produces Markup AST
v
EditorNodeModel
| block-level EditorNodes with stable IDs
| tracks active block (cursor location)
v
MarkdownRenderer
| active block → raw syntax with highlighting
| inactive blocks → styled rendering
v
NSAttributedString → WYSIWYGTextView
Key Design Decision: Paragraph-level delayed rendering for MVP. When the cursor leaves a paragraph, it renders as styled output. When the cursor enters, raw Markdown syntax is shown. This achieves 90% of Typora’s UX at 30% of the implementation complexity. Full inline-element-level toggle rendering planned for post-MVP.
State Management
EditorSettings (shared, persisted)
|
+--- font, fontSize, lineSpacing
+--- appearanceOverride
+--- defaultFocusMode, defaultTypewriterScroll
|
EditorViewModel (per-document, transient)
|
+--- sourceText (synced with MarkdownDocument)
+--- nodeModel (EditorNodeModel)
+--- cursorPosition, activeBlockID
+--- isFocusModeEnabled, isTypewriterScrollEnabled
Primary Data Flows
- Editing: User types → NSTextView → EditorViewModel.textDidChange() → Parser → NodeModel update → Renderer → Display
- Cursor Move: Cursor moves → EditorViewModel.cursorDidMove() → NodeModel.setActiveBlock() → Re-render affected blocks
- Image Drop: Drop image → ImageService copies to assets/ → EditorViewModel inserts reference → Normal editing flow
- Export: User triggers → ExportService.generateHTML/PDF() → Save dialog → Write file
Key Architectural Decisions
| Decision | Choice | Rationale |
|---|---|---|
| SwiftUI/AppKit boundary | SwiftUI shell + NSViewRepresentable | TextKit 2 requires NSTextView; SwiftUI handles everything else |
| Rendering pipeline | 3-stage with intermediate model | Enables paragraph-level delayed rendering and incremental re-rendering |
| State split | Per-document ViewModel + Shared Settings | Avoids accidental state sharing across windows |
| Services as Actors | ExportService, ImageService, FileService | Thread-safe async I/O without manual locking |
| Parser as Struct | MarkdownParser | Stateless, synchronous, fast enough for main thread |
External Dependencies
| Dependency | Version | Purpose |
|---|---|---|
| swift-markdown (Apple) | Latest stable | Markdown parsing to typed AST |
| Highlightr | Latest stable | Code block syntax highlighting |
Detailed method signatures: see component-methods.md
Service orchestration: see services.md
Dependency matrix and data flows: see component-dependency.md