I added spell-checking to the Notes Editor1 today. It took three lines of code.
Not three files. Not three hundred lines. Not a third-party dependency, a language server, a WebAssembly dictionary, or an npm package with forty transitive dependencies. Three lines of Swift, setting three boolean properties on an NSTextView:
textView.isContinuousSpellCheckingEnabled = true
textView.isGrammarCheckingEnabled = true
textView.isAutomaticSpellingCorrectionEnabled = false
That last one is deliberate — I want the red squiggles under misspelled words, but I don’t want the system silently “fixing” things as I type. I’m writing markdown with HTML entities and code fragments scattered through it. Autocorrect in that context is not helpful. Squiggles, on the other hand, are perfect: they tell you something looks wrong without presuming to know what you meant.
With those three properties set, the Notes Editor gained full macOS spell-checking. Every dictionary the user has installed. Every language they’ve configured. Right-click suggestions. “Learn Spelling” and “Ignore Spelling” in the context menu. Grammar checking. All of it. For free. Because NSTextView has had this built in since — and I am not exaggerating — Mac OS X 10.0.
There was one complication. The Notes Editor writes markdown with HTML entities: … for ellipses, — for em dashes, – for en dashes, “ and ” for curly quotes. The spell checker doesn’t know what these are. It sees … and thinks you’ve misspelled something. Every entity in every article gets a red squiggle. It looks like a crime scene.
The fix was one delegate method. NSTextViewDelegate has a callback called shouldSetSpellingState that fires before the spell checker draws a squiggle. It hands you the range it’s about to flag, and you return the spelling state you’d like applied. Return the value unchanged and the squiggle appears. Return 0 and it doesn’t.
The implementation walks through every …-style entity on the flagged line. If the squiggle would overlap one, suppress it. Otherwise, let it through:
func textView(_ textView: NSTextView,
shouldSetSpellingState value: Int,
range: NSRange) -> Int {
let text = textView.string as NSString
let lineRange = text.lineRange(for: range)
let line = text.substring(with: lineRange)
let offsetInLine = range.location - lineRange.location
var i = line.startIndex
while let ampIdx = line[i...].firstIndex(of: "&") {
guard let semiIdx = line[ampIdx...].firstIndex(of: ";")
else { break }
let entityStart = line.distance(from: line.startIndex,
to: ampIdx)
let entityEnd = line.distance(from: line.startIndex,
to: line.index(after: semiIdx))
let flagStart = offsetInLine
let flagEnd = offsetInLine + range.length
if flagStart < entityEnd && flagEnd > entityStart {
return 0 // suppress — this is an HTML entity, not a typo
}
i = line.index(after: semiIdx)
}
return value
}
Twenty-eight lines including comments and whitespace. The spell checker now understands that ’ is punctuation, not a misspelling.
I keep coming back to this: building native gives you things for free that other approaches make you build from scratch — and my appreciation for that keeps getting refreshed.
Spell-checking is a solved problem. Apple solved it decades ago and built it into the text system. Every NSTextView in every macOS application has access to it — the same dictionaries, the same grammar engine, the same learned words, shared across the entire OS. When you add a word to your dictionary in one app, every other app learns it too. It’s not a feature you implement. It’s infrastructure you opt into.
The same is true for so much of what NSTextView provides. Undo and redo? Built in. Find and replace? One property: textView.usesFindBar = true. Text completion? Built in. Accessibility? Built in. Right-to-left language support? Built in. You don’t write code for these things. You just don’t disable them.
Compare this to the alternative. If you’re building a text editor in Electron, spell-checking means bundling Hunspell or calling out to the Chromium spell-check API, managing dictionaries, rendering your own squiggles, building your own suggestion UI, handling your own learned-words persistence. If you’re building in a web view, you get the browser’s built-in spellcheck attribute — which is reasonable — but the moment you need to customise it (say, to ignore HTML entities), you’re on your own. There’s no shouldSetSpellingState callback in the DOM.
That delegate method is the key insight. Apple didn’t just build spell-checking into NSTextView — they built hooks into spell-checking. They anticipated that developers would need to say “yes, check spelling, but not there.” The API isn’t just capable, it’s considerate. It respects that your application has domain-specific knowledge that the system spell checker doesn’t.
Let me tally this up:
The entire Notes Editor gained a professional-grade spell checker — with custom entity awareness — for less code than most apps spend on a loading spinner.
This is the tax you don’t pay when you build native. Every one of these features would be a project in a cross-platform framework. In AppKit, they’re a line of configuration. The cumulative effect is significant: you spend your time on the things that make your app yours, not on reimplementing what the operating system already provides.
Three lines. One delegate method. That’s the whole story.
The Notes Editor is a tool I built for my own use for maintaining these pages. ↩