Supporting desktop-class features in your iPad app
At a glance
| Item | Summary |
|---|---|
| Purpose | Build a document-based Markdown editor with customizable toolbars, menus, pointer interactions, keyboard commands, and multi-column presentation. |
| App architecture | DocumentBrowserViewController opens a MarkdownDocument and presents a SplitViewController, which owns editor and outline children; local delegates synchronize parsed outline state and document edits. |
| Main patterns | Document-controller composition, Local delegate coordination, Background parse and preview pipeline |
| Project style | Seventeen Swift files grouped into document management, Markdown parsing, and specialized view/controller layers. |
Project structure
Source bundle/
└── DesktopClassMarkdownEditor/
└── DesktopClassMarkdownEditor/
├── AppDelegate.swift
├── Views/
│ ├── OutlineViewController.swift
│ ├── EditorViewController.swift
│ ├── SplitView.swift
│ ├── SplitViewController.swift
│ ├── DocumentBrowserViewController.swift
│ ├── EditorView.swift
│ └── PreviewView.swift
├── SceneDelegate.swift
└── Markdown/
├── MarkdownTag.swift
├── Outline.swift
└── Parser.swift
Structure observations
- Document lifecycle is isolated in
MarkdownDocumentandDocumentBrowserViewController. SplitViewControlleris the composition and coordination root for editor/outline presentation.- Parser/model files are independent of UIKit presentation; the editor schedules parsing and preview updates.
Overall architecture
flowchart LR
BROWSER["DocumentBrowserViewController"] --> DOC["MarkdownDocument"]
BROWSER --> SPLIT["SplitViewController"]
SPLIT --> EDITOR["EditorViewController"]
SPLIT --> OUTLINE["OutlineViewController"]
EDITOR --> PARSER["Parser and ParsedDocument"]
PARSER --> PREVIEW["PreviewView and outline updates"]
Reference code
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/DocumentBrowserViewController.swift:82 — document presentation composition
func presentDocument(_ documentURL: URL) {
let document = MarkdownDocument(fileURL: documentURL)
let splitVC = SplitViewController(document: document)
splitVC.transitioningDelegate = self
splitVC.modalPresentationStyle = .fullScreen
splitVC.editorViewController.documentBrowser = self
present(splitVC, animated: true)
}Interpretation
The browser creates the document session and composition root. The split controller connects child controllers through narrow local delegate protocols; the editor owns parsing/preview mechanics, while the outline sends user commands back through the split controller to the document.
Ownership and state
classDiagram
DocumentBrowserViewController *-- MarkdownDocument : creates per URL
DocumentBrowserViewController *-- SplitViewController : presents
SplitViewController *-- EditorViewController : let
SplitViewController *-- OutlineViewController : let
EditorViewController *-- Parser : let
EditorViewController o-- EditorViewControllerDelegate : weak
Ownership evidence
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/SplitViewController.swift:13 — split-controller object graph
let document: MarkdownDocument
let editorViewController: EditorViewController
let outlineViewController: OutlineViewController
init(document: MarkdownDocument) {
self.document = document
editorViewController = EditorViewController(document: document)
outlineViewController = OutlineViewController()
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
DocumentBrowserViewController |
MarkdownDocument and SplitViewController |
Creates both for a selected URL and presents the split controller | The browser begins/ends the document presentation session. |
SplitViewController |
Document, editor, and outline controllers | Strong immutable references created in init |
It wires delegates and routes outline/editor actions. |
EditorViewController |
EditorView, PreviewView, Parser, OperationQueue |
Creates and retains feature subsystems | It schedules parsing, refreshes preview, and updates the document. |
EditorViewController |
Delegate and document browser | Weak references | Collaborators receive callbacks without ownership cycles. |
Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.
Class and protocol design
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/EditorViewController.swift:11 — editor collaboration contract
protocol EditorViewControllerDelegate: AnyObject {
func editor(_ editorViewController: EditorViewController, didParse document: ParsedDocument)
}| Type | Responsibility | Depends on or conforms to |
|---|---|---|
DocumentBrowserViewController |
Creates/imports document sessions | UIDocumentBrowserViewControllerDelegate |
SplitViewController |
Composes children and mediates editor/outline events | EditorViewControllerDelegate, OutlineViewControllerDelegate |
EditorViewController |
Edits text, parses Markdown, and updates preview | UIDocumentViewController plus UIKit delegates |
OutlineViewController |
Displays parsed outline and emits structural edit commands | UICollectionViewDelegate and its local delegate |
MarkdownDocument |
Owns document text and structural mutations | UIDocument |
Parser / ParsedDocument |
Transform Markdown text into preview and outline representations | Markdown model types |
The local editor and outline delegate protocols are real capability seams. They decouple child controllers from their concrete parent, but do not make every model or view protocol-based.
Access control
| Symbol | Access | Verified effect | Design reason |
|---|---|---|---|
MarkdownTag.htmlTransformer |
private |
Only MarkdownTag can invoke/store the transformation closure |
Protects tag rendering invariants. |
OutlineViewController.dataSource and update helpers |
private |
Only the outline implementation mutates snapshots | Prevents other controllers from bypassing outline update methods. |
EditorViewController.updatePreview |
private |
Callable only inside the editor file/type | Parsing and queue scheduling remain an implementation detail. |
| Feature types and local protocols | implicit internal |
Visible across the app target | The document, split, and editor files collaborate without exporting a framework API. |
Reference code
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Markdown/MarkdownTag.swift:39 — tag data and private transformer
let openMarkdown: String
let midMarkdown: String
let closeMarkdown: String
let htmlTag: String
let options: Options
let outlineRepresentation: OutlineRepresentation?
private let htmlTransformer: ((MarkdownTag, String) -> String)?The source has no Swift open declaration here: init(open:) is an initializer parameter, not an access modifier, and the local prompt variable is likewise not open. Core feature types remain module-internal.
Logic ownership and placement
| Logic | Owning type or file | Why it lives there |
|---|---|---|
| Document open/import | DocumentBrowserViewController |
It owns UIDocumentBrowserViewController callbacks and presentation. |
| Text editing, parse scheduling, preview | EditorViewController |
These operations share editor state and cancellation/queue concerns. |
| Outline UI and structural commands | OutlineViewController |
Collection snapshots and outline menus stay together. |
| Cross-child synchronization | SplitViewController |
It conforms to both local delegate protocols and knows both children. |
| Markdown transformation | Parser, MarkdownTag, ParsedDocument |
Pure transformation remains outside view controllers. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Document-controller composition | DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/DocumentBrowserViewController.swift:82 |
Creates one explicit object graph per open document. |
| Local delegate coordination | DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/SplitViewController.swift:11 |
Child controllers report intent/results without retaining their coordinator. |
| Background parse and preview pipeline | DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/EditorViewController.swift:39 and DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/EditorViewController.swift:326 |
Parser and operation queue isolate expensive transformation from UI composition. |
Naming conventions
- Types use domain/role names (
MarkdownDocument,Parser,OutlineElement,EditorViewController). - Local protocols name the collaborator role with a
Delegatesuffix. - Commands use concrete verbs (
presentDocument,updatePreview,swapTags,duplicate,delete). - Folders separate Document Management, Markdown transformation, Utilities, and Views.
Architecture takeaways
- Make the per-document split controller the composition root, not the application delegate.
- Use narrow weak delegates for sibling-view synchronization and keep document mutation on the document model.
- Do not infer access modifiers from parameter names such as
open; verify declaration syntax. - Keep parser/preview work behind a dedicated editor-owned pipeline.
Source map
| Source file | Architectural role |
|---|---|
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/DocumentBrowserViewController.swift |
Document discovery/import and presentation |
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/SplitViewController.swift |
Editor/outline composition and coordination |
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/EditorViewController.swift |
Editing, parsing, preview, commands |
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Views/OutlineViewController.swift |
Outline snapshots and structural actions |
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Document Management/MarkDownDocument.swift |
Document state and mutations |
DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Markdown/Parser.swift / DesktopClassMarkdownEditor/DesktopClassMarkdownEditor/Markdown/MarkdownTag.swift |
Markdown transformation model |