This document describes the current implementation. Target membership in
MiaoYan.xcodeproj/project.pbxprojis authoritative when the code changes.
.
├── Business/ # Models and core domain logic (Storage, Note, Project, WikilinkIndex, ...)
├── Controllers/ # AppKit view controllers and window controllers
├── Views/ # AppKit UI components (NSView / NSOutlineView / NSTableView subclasses)
├── Helpers/ # Utilities and services (highlighting, formatting, theming, diagnostics)
├── Extensions/ # Swift extensions on Foundation / AppKit types
├── Resources/ # Bundled assets, including DownView.bundle (HTML/CSS/JS for preview)
├── MiaoYanMobile/ # iOS SwiftUI target (App/Services/Views/Resources)
├── MiaoYanTests/ # Unit tests for pure-logic surfaces
└── scripts/ # Local build, App Store, release helpers, target wiring (Ruby + bash)
A single macOS application process owns:
- One
NSApplication(subclass-free;AppDelegateis the delegate). - One
MainWindowController(Controllers/MainWindowController.swift), which loadsResources/Localization/Base.lproj/Main.storyboard. - One
ViewController(Controllers/ViewController.swift+ its+extensions), the host for the sidebar / notes list / editor / preview. - One
WKWebViewinstance per editor pane that loadsResources/DownView.bundle/index.htmlfor live preview.
The iOS target (MiaoYanMobile/) is a separate executable with its own models,
storage services, renderer, and SwiftUI entry point (MiaoYanMobileApp.swift).
The apps share filesystem conventions, not the macOS Business/ compile pool.
Process-wide singletons (each "global" surface that holds state):
| Singleton | Where | Role |
|---|---|---|
Storage.sharedInstance() |
Business/Storage.swift |
Filesystem-backed note + project model |
WikilinkIndex.shared |
Business/WikilinkIndex.swift |
[[note]] outgoing/incoming index |
CloudSyncManager.shared |
Business/CloudSyncManager.swift |
iCloud Drive coordination |
NoteVersionManager.shared |
Business/NoteVersionManager.swift |
Per-note version history |
UserDataService.instance |
Helpers/UserDataService.swift |
Cached user-level appearance state |
AppContext.shared |
Business/Types.swift |
Holds storage + sessionState + view ref |
EditorStateManager.shared |
Helpers/UserDefaultsManagement.swift |
Editor mode toggles |
ShortcutTemplateManager.shared |
Helpers/ShortcutTemplateManager.swift |
Markdown insert templates |
ToastManager.shared |
Views/Toast.swift |
Non-modal status messages |
These are NOT going to be replaced wholesale; Storage is genuinely a single filesystem mapping and that semantic is correct. New code should access them through the facade:
// Preferred (new code)
let storage = AppEnvironment.current.storage
// Tolerated (existing code) but a SwiftLint warning under
// `no_direct_singleton_in_new_code`
let storage = Storage.sharedInstance()Business/AppEnvironment.swift is a read-only facade over the existing
macOS services. It does not provide test overrides.
The following bindings live in Main.storyboard and depend on selectors /
identifiers existing on the named class. Renaming or moving any of these
breaks the UI at runtime without a compile-time error:
ViewController.swiftis the storyboard'sviewControllerscene; all@IBOutletand@IBActiondeclarations must stay on this class.SidebarProjectViewis loaded as the storyboard's outline view subclass; itsawakeFromNibis the construction entry point, notinit.NotesTableViewcell views haveNSUserInterfaceItemIdentifier("NoteCellView")registered in the storyboard and dequeued atViews/NotesTableView.swift:526.SidebarProjectViewcell views use identifier"DataCell"similarly (Views/SidebarProjectView.swift:639).- The First Responder action chain is selector-based.
@IBActionmethods onViewController+Action.swiftmust keep their exact ObjC selectors.
If you need to split ViewController, leave outlets and actions on the host
class and forward to coordinator objects from the action body.
keystroke
→ EditTextView (NSTextView subclass; CustomTextStorage)
→ NSTextStorage.processEditing
→ NotesTextProcessor.checkPerformanceLevel
├─ short text: full Markdown highlight via MarkdownRuleHighlighter
└─ long/large: simplified highlight (skips code block regex)
→ ViewController.textDidChange (URL drift tripwire fires here)
├─ debounced disk save: Note.save(content:)
└─ debounced preview: MPreviewView.updateContent (adaptive 0.3/0.6/1.0s)
Performance ceilings live in Helpers/NotesTextProcessor.swift:
-
1 MB total length OR > 64 KB in a single paragraph: simplified highlight, no code highlight.
-
5000 lines: simplified, no code highlight.
-
2000 lines: simplified, code highlight still on.
Note.content (NSMutableAttributedString)
→ swift-cmark-gfm → HTML string
↳ post-render transforms in renderMarkdownHTML (math <br> cleanup,
GitHub Alerts blockquote → callout rewrite)
→ MPreviewView (WKWebView)
↳ loads Resources/DownView.bundle/index.html one-shot
↳ postReadyCallbacks fire when WKWebView didFinish navigation lands
↳ subsequent edits use incremental DOM mutation, not full reload
Bundled JS used by the preview is vendored under
Resources/DownView.bundle/js/ and indexed in
Resources/DownView.bundle/js/vendor/MANIFEST.json (versions + SHA-256).
- Note files: user-selected storage path (security-scoped bookmark in
UserDefaultsManagement.storageBookmark). - Note attachments:
i/(inline images) andfiles/(other attachments) subdirectories at the note's level. - Trash: a
.Trashdirectory inside the storage root, plus a fallback to the OS-level trash viaFileManager.default.trashItem(...). - Symlinked directories: supported but indexed in a way that avoids recursion
loops (
Business/Storage.swift::checkSub). - Version history:
Library/Application Support/MiaoYan/Versions/<note-id>/managed byNoteVersionManager. - Diagnostics log:
~/Library/Logs/MiaoYan/diagnostics.log(ring buffer, 50 lines, JSON per line). SeeHelpers/Diagnostics.swift.
MiaoYanMobile/ builds as a separate target in MiaoYan.xcodeproj. Its
models and services live inside MiaoYanMobile/. SwiftUI lives there; AppKit
lives only outside. There is no shared UI layer. The iOS target reads notes
through MiaoYanMobile/Services/FileReader.swift, which is a parallel
implementation to the macOS storage flow. NoteSearchReader reads complete
local bodies in cancellable chunks and is also compiled into the macOS test
bundle for platform-independent regression tests.
- Mac App Store builds: signed and uploaded by the maintainer;
Sparkleis excluded via#if !APPSTORE. - Direct downloads:
bash scripts/build.shproduces a zipped.app. The Sparkleappcast.xmlis updated byscripts/release-ci/update_appcast.sh(needs the Sparkle EdDSA private key). - Version triplet (must stay aligned): git tag
Vx.y.z,MARKETING_VERSION,CURRENT_PROJECT_VERSION. CI rejects mismatches when a tag is pushed.
Helpers/Diagnostics.swiftis in the app target, andAppDelegate.trackErrorrecords release diagnostics through it.Business/AppEnvironment.swiftis in the app target and is the preferred facade for new singleton access.Helpers/UIDelay.swiftis in the app target for semantic async delay names.MiaoYanTests/is wired intoMiaoYan.xcodeproj, and CI runs the macOS unit test step together with the Debug app build.
AGENTS.md- agent-facing repo guide (commands, hot files, current risk areas)CLAUDE.md- project-level overrides for Claude Code.claude/rules/swift.md- project-level Swift conventions~/.claude/CLAUDE.md- global rules (writing style, commit policy, git safety)