A native Swift port of Lexical for iOS and macOS, with UIKit, AppKit, and SwiftUI editor views. It conforms to the upstream JS packages: the same documents, the same JSON (byte for byte), and the same editing behavior.
Pre-release. The API is unstable until 1.0, and most of Lexical isn't ported yet. See DESIGN.md for the plan.
0.1.0 conforms to upstream Lexical main just after 0.53.0 (2b388b92d;
see UPSTREAM_VERSION and DESIGN.md). Each release's notes in
CHANGELOG.md name the upstream code it conforms to.
.package(url: "https://github.com/wevm/lexical-swift", from: "0.1.0")Requires iOS 26 or macOS 26 and Swift 6.3 (Xcode 26.5) or later. The non-UI modules also build on Linux.
Add the products you use to your target, for example:
.target(name: "MyApp", dependencies: [
.product(name: "Lexical", package: "lexical-swift"),
.product(name: "LexicalExtension", package: "lexical-swift"),
.product(name: "LexicalRichText", package: "lexical-swift"),
.product(name: "LexicalHistory", package: "lexical-swift"),
.product(name: "LexicalSwiftUI", package: "lexical-swift"),
])Build an editor from extensions, as in JS, and show it with LexicalEditor
(SwiftUI), LexicalUITextView (UIKit) or LexicalNSTextView (AppKit):
import Lexical
import LexicalExtension
import LexicalHistory
import LexicalRichText
import LexicalSwiftUI
import SwiftUI
@MainActor
func makeEditor() throws -> Editor {
try buildEditorFromExtensions(
defineExtension(name: "my-app", dependencies: [RichTextExtension, HistoryExtension]))
}
struct ContentView: View {
@State private var editor = try! makeEditor()
var body: some View {
LexicalEditor(editor)
}
}The model API is Lexical's, without the $ prefix (Swift identifiers can't
start with $), and the $ functions throw:
try editor.update(discrete: true) {
let paragraph = try createParagraphNode()
try paragraph.append(createTextNode("Hello, Lexical"))
try getRoot().clear().append(paragraph)
}
let text = try editor.read { try getRoot().getTextContent() }
// The same JSON as JS, byte for byte; parse it back with parseEditorState.
let json = try editor.getEditorState().toJSON().stringify()
try editor.setEditorState(editor.parseEditorState(json))Offsets in selections and points are UTF-16 code units, as in JS and
NSString. The differences from JS are listed in the documentation's
JSParity article; Examples/LexicalDemo and apps/lexical-playground-swift
are complete apps.
An Editor is bound to the thread that creates it, and only the view
modules are @MainActor, so a headless editor can run on any thread. Its
listeners run on that thread and aren't @MainActor themselves. In an app,
build the editor on the main actor and give the app's module default main
actor isolation (the default for new Xcode 26 app targets; in SwiftPM,
swiftSettings: [.defaultIsolation(MainActor.self)]). Listeners written
anywhere in it, including in an extension defined as a global let, can then
use main-actor state without MainActor.assumeIsolated.
LexicalUITextView is its own UITextViewDelegate, and both views turn
native input into Lexical commands instead of editing their own text. Observe
the editor instead, with the commands and listeners a Lexical web app uses:
| Instead of | Use |
|---|---|
textViewDidBeginEditing |
FOCUS_COMMAND |
textViewDidEndEditing |
BLUR_COMMAND |
textViewDidChangeSelection |
SELECTION_CHANGE_COMMAND |
textViewDidChange |
registerUpdateListener, registerTextContentListener |
shouldChangeTextIn, Return |
CONTROLLED_TEXT_INSERTION_COMMAND, KEY_ENTER_COMMAND, BEFORE_INPUT_COMMAND |
LexicalCodeCore's CodeHighlighterExtension highlights code blocks with
the CodeTokenizer in its config, as upstream swaps Prism for Shiki.
LexicalCodeTreeSitter is a tokenizer built on
tree-sitter (its C core, vendored), whose
captures are mapped to Prism's token names, so a theme's codeHighlight
colors apply. Each grammar is its own product, so an app links only the
languages it uses: LexicalCodeTreeSitterJavaScript, …TypeScript,
…Swift, …Python, …JSON, …HTML, …CSS, …Rust, …Go, …Bash,
…C, …CPP, …Java, …Markdown, …ObjC, …PowerShell, …SQL and
…XML. Markdown highlights its block structure (headings, lists, quotes,
fences) only, without the inline grammar.
configExtension(CodeHighlighterExtension) {
$0.tokenizer = TreeSitterTokenizer([.javaScript, .swift])
}scripts/vendor-tree-sitter.sh pins and refreshes the vendored sources.
This package is developed in wevm/lexical,
a fork of meta/lexical, under swift/.
wevm/lexical-swift is a read-only
mirror for SwiftPM; open issues and changes against wevm/lexical.
cd swift
swift build --build-tests && swift test
# Conformance vectors and the parity ledger, from the repository root:
pnpm --filter lexical-conformance run generate # rewrite vectors/
pnpm --filter lexical-conformance run ledger # rewrite parity/upstream-tests.json
pnpm --filter lexical-conformance run check-ledger # ported/skipped counts per packageMIT. Lexical is copyright Meta Platforms, Inc. and affiliates. The vendored
tree-sitter core and grammars under Sources/CTreeSitter and
Sources/TreeSitter* keep their own licenses, in each directory.