How does NSTextView invoke grammar checking internally

I'm building a macOS app that uses WKWebView for text editing (not NSTextView). I need to provide grammar checking by calling NSSpellChecker programmatically and sending results back to the web editor.

The problem: TextEdit (which uses NSTextView) catches grammar errors like "Can I has pie?" and "These are have" — but when I call NSSpellChecker's APIs directly, those same errors are never flagged.

I've tried both APIs:

1. The unified check() API:

let results = checker.check(
    text, range: range,
    types: NSTextCheckingAllTypes,
    options: [:],
    inSpellDocumentWithTag: tag,
    orthography: &orthography,
    wordCount: &wordCount)

This returns only .orthography results (language detection). No .spelling, no .grammar — just orthography.

2. The dedicated checkGrammar(of:startingAt:...) API:

let sentenceRange = checker.checkGrammar(
    of: text,
    startingAt: offset,
    language: nil,
    wrap: false,
    inSpellDocumentWithTag: tag,
    details: &details)

This catches sentence fragments ("The.", "No.") and some agreement errors ("The is anyone.") but misses "Can I has pie?", "These are have", "This will be happened", and other subject-verb agreement errors that TextEdit highlights.

What I've confirmed through systematic testing:

  • "Check Grammar With Spelling" is enabled in System Settings
  • TextEdit reliably catches all these errors with green underlines
  • Both APIs are called with a valid spellDocumentTag from uniqueSpellDocumentTag()
  • The text is passed as plain strings (no attributed string context)

Additional experiments I've run (all failed to produce grammar results matching TextEdit):

3. Windowed NSTextView with checkText(in:range:types:options:):

I created a real NSTextView inside an NSWindow, subclassed it to override handleTextCheckingResults(_:forRange:types:options:orthography:wordCount:), and called checkText(in:range:types:options:):

class DiagnosticTextView: NSTextView {
    var capturedResults: [NSTextCheckingResult] = []

    override func handleTextCheckingResults(
        _ results: [NSTextCheckingResult],
        forRange range: NSRange,
        types checkingTypes: NSTextCheckingTypes,
        options: [NSSpellChecker.OptionKey: Any],
        orthography: NSOrthography,
        wordCount: Int
    ) {
        capturedResults.append(contentsOf: results)
        super.handleTextCheckingResults(results, forRange: range, types: checkingTypes,
                                        options: options, orthography: orthography, wordCount: wordCount)
    }
}

let window = NSWindow(
    contentRect: NSRect(x: -10000, y: -10000, width: 400, height: 200),
    styleMask: [.borderless], backing: .buffered, defer: false)
let textView = DiagnosticTextView(frame: NSRect(x: 0, y: 0, width: 400, height: 200))
window.contentView = textView
textView.string = "Can I has pie? The is anyone."
textView.isGrammarCheckingEnabled = true
textView.isContinuousSpellCheckingEnabled = true

let range = NSRange(location: 0, length: (textView.string as NSString).length)
textView.checkText(in: range, types: NSTextCheckingAllTypes, options: [:])

I tried this in three configurations:

  • Window behind (orderBack): Only .orthography results delivered to handleTextCheckingResults. Zero grammar.
  • Key window + first responder (makeKeyAndOrderFront + makeFirstResponder): Still only .orthography results. Even after waiting 5 seconds for the continuous checking timer to fire.
  • With isGrammarCheckingEnabled = true and isContinuousSpellCheckingEnabled = true: No change — still orthography only.

In all cases, the handleTextCheckingResults callback delivered results with resultType.rawValue == 1 (orthography) — never .grammar (rawValue 4) or .spelling (rawValue 2).

Summary of what each API returns for "Can I has pie? The is anyone. This will be happened.":

API "The is anyone." "Can I has pie?" "This will be happened."
checker.check() (unified) orthography only orthography only orthography only
checker.checkGrammar(of:) "may not agree" missed missed
textView.checkText(in:) orthography only orthography only orthography only
TextEdit (reference) flagged flagged flagged

My question: How does NSTextView's grammar checking work internally in apps like TextEdit? It clearly produces results that none of these public APIs can reproduce programmatically. I'm considering:

  1. Does NSTextView use the NSTextCheckingClient protocol / requestChecking(of:range:types:options:) with NSTextCheckingController in a way that differs from checkText(in:)? My override of handleTextCheckingResults should have captured those results, but maybe the controller delivers them through a different path.
  2. Does NSTextCheckingController use a private grammar engine that's distinct from NSSpellChecker.checkGrammar(of:)? The fact that checkGrammar(of:) catches some errors but misses others (while TextEdit catches all of them) suggests there may be a second grammar pipeline.
  3. Is there a private/undocumented API or framework (e.g., the grammar analysis in Apple's language frameworks) that NSTextView invokes for deeper analysis?
  4. Does TextEdit somehow configure NSTextView differently — e.g., through NSDocument integration, rich text mode, or specific user defaults — that activates a more thorough grammar checking path?

Any insight from anyone who has dug into NSTextView's internal grammar checking mechanism would be appreciated.

NOTE: This post was composed with the help of Claude Code, which I am using to help write a word-processing application, but I am frustrated because Claude Code wants to give up and switch to a 3rd party grammar checker, like LanguageTool, and it seems to me that it should be possible to use native Apple tools to achieve this goal without requiring the user to send their data elsewhere for checking. I've spent a lot of time searching the web for answers and have found surprisingly little on this. Any pointers people might have would be very much appreciated! Thanks.

Hi, we would like to refer you to the Apple Developer Forums for apple framework questions. You can use this forum for Swift (Compiler) specific questions.

3 Likes