Design Philosophy • April 23, 2026
The toggle is the tax
Why Obsidian's split-pane is a cognitive drag and how we built an inline styler that runs in <10ms.
In the early days of Markdown editors, there were two camps. There was the "Raw" camp (think early TextEdit or nvALT) where you saw all the asterisks and hashes, and the "Preview" camp where you had to toggle a switch to see what your document actually looked like.
The History Lesson: Bear and Ulysses
Apps like Bear and Ulysses changed the game by introducing "hybrid" editors. They showed you the markdown syntax but styled it in real-time. This was a massive leap forward, but it still felt like you were looking at "code" for your text.
Obsidian took a different route with its dual-pane view. On the left, the raw text. On the right, the rendered HTML. While powerful, this creates a constant cognitive tax. Your eyes are constantly jumping between two representations of the same thought.
Our Principle: Inline or Nothing
At SwiftyNotes, we believe the "toggle" is a tax on your creativity. When you're in the flow, you shouldn't have to think about "modes." That's why we built our `InlineMarkdownStyler`.
The goal was simple: when you type `**bold**`, it should immediately look bold, but the asterisks should remain accessible for editing. When you move your cursor away, the syntax should gracefully fade or transform.
The Engineering Challenge
Achieving this in a native macOS/iOS text view is notoriously difficult. `NSTextView` and `UITextView` weren't designed for real-time regex-based styling on every keystroke.
Our solution? A highly optimized styler that runs in <10ms on paragraph edits. We use a combination of character-range tracking and a custom parser that only re-evaluates the modified block.
// Simplified look at our InlineMarkdownStyler
func applyStyles(to textStorage: NSTextStorage, in range: NSRange) {
let text = textStorage.string as NSString
let paragraphRange = text.paragraphRange(for: range)
// Clear existing styles in the paragraph
textStorage.removeAttribute(.font, range: paragraphRange)
// Find and apply markdown patterns
let matches = regex.matches(in: text as String, range: paragraphRange)
for match in matches {
let style = styleForMatch(match)
textStorage.addAttributes(style, range: match.range)
}
}By keeping the styling inline and the performance "instant," we remove the friction between thought and expression. You aren't "writing markdown"; you're just writing.