A Swift wrapper around jsdiff using JavaScriptCore. Provides text diffing, patch creation, and patch application capabilities.
Requirements
JSDiff (Core Library)
- macOS 11.0+ / iOS 12.0+ / tvOS 12.0+ / visionOS 1.0+
- Swift 5.5+ (for actor-based concurrency)
JSDiffUI (SwiftUI Views)
- macOS 12.0+ / iOS 15.0+ / tvOS 15.0+ / watchOS 8.0+ / visionOS 1.0+
- Swift 5.5+
Installation
Swift Package Manager
Add the following to your Package.swift dependencies:
dependencies: [ .package(url: "https://github.com/your-repo/JSDiff.git", from: "1.0.0") ]
Or add it directly in Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Select the version and add to your target
Importing
// Core diffing functionality import JSDiff // SwiftUI visualization views (optional) import JSDiffUI
Usage
Initialization
import JSDiff // JSDiff is an actor, so initialize it in an async context let diff = JSDiff()
Character-Level Diff
let changes = await diff?.diffChars("abc", "adc") for change in changes ?? [] { if change.isAdded { print("Added: \(change.value)") } else if change.isRemoved { print("Removed: \(change.value)") } else { print("Unchanged: \(change.value)") } }
Word-Level Diff
let changes = await diff?.diffWords("hello world", "hello there")
Line-Level Diff
let oldText = """ line1 line2 line3 """ let newText = """ line1 line2 modified line3 """ let changes = await diff?.diffLines(oldText, newText)
Options
Each diff method accepts an options object:
// Character diff with case insensitivity let options = DiffCharsOptions(ignoreCase: true) let changes = await diff?.diffChars("ABC", "abc", options: options) // Line diff with whitespace ignoring let lineOptions = DiffLinesOptions(ignoreWhitespace: true, newlineIsToken: true) let changes = await diff?.diffLines(oldText, newText, options: lineOptions)
JSON Diff
Diff any Encodable types:
struct User: Codable { let name: String let age: Int } let oldUser = User(name: "Alice", age: 30) let newUser = User(name: "Alice", age: 31) let changes = try await diff?.diffJson(oldUser, newUser)
Or diff JSON strings directly:
let changes = await diff?.diffJson(#"{"name": "Alice"}"#, #"{"name": "Bob"}"#)
Array Diff
let oldArray = ["a", "b", "c"] let newArray = ["a", "x", "c"] let changes = await diff?.diffArrays(oldArray, newArray) for change in changes ?? [] { print("Value: \(change.value)") // Array of strings print("Count: \(change.count)") }
Creating Patches
let patch = await diff?.createPatch( fileName: "example.txt", old: "old content\nline2", new: "new content\nline2" ) print(patch) // Output: // --- example.txt // +++ example.txt // @@ -1,2 +1,2 @@ // -old content // +new content // line2
Create patches for two different files:
let patch = await diff?.createTwoFilesPatch( oldFileName: "original.txt", newFileName: "modified.txt", old: oldContent, new: newContent )
Applying Patches
let original = "old content\nline2" let patch = await diff?.createPatch(fileName: "file.txt", old: original, new: "new content\nline2") let result = await diff?.applyPatch(original, patch: patch!) print(result) // "new content\nline2"
Structured Patches
For programmatic access to patch data:
let structured = await diff?.structuredPatch( oldFileName: "old.txt", newFileName: "new.txt", old: oldContent, new: newContent ) print(structured?.hunks.first?.oldStart) // Starting line in old file print(structured?.hunks.first?.lines) // Changed lines
Parsing Patches
let patchString = """ --- old.txt +++ new.txt @@ -1,3 +1,3 @@ line1 -line2 +line2 modified line3 """ let patches = await diff?.parsePatch(patchString)
Reversing Patches
let reversed = await diff?.reversePatch(patchString)
Converting Change Output
Convert changes to XML format:
let changes = await diff?.diffChars("abc", "adc") let xml = await diff?.convertChangesToXML(changes ?? []) // Output: <del>a</del><ins>a</ins><span>b</span>...
Convert to diff-match-patch format:
let dmp = await diff?.convertChangesToDMP(changes ?? []) for change in dmp ?? [] { print("Operation: \(change.operation), Value: \(change.value)") // operation: 0 = unchanged, 1 = added, -1 = removed }
SwiftUI Visualization (JSDiffUI)
The JSDiffUI module provides ready-to-use SwiftUI views for visualizing diff output. It supports three display styles: inline, side-by-side, and unified.
Basic Usage
import JSDiff import JSDiffUI // Compute the diff let diff = JSDiff() let changes = await diff?.diffLines(oldText, newText) // Display inline style DiffView(changes: changes ?? [], displayStyle: .inline) // Or side-by-side comparison DiffView(changes: changes ?? [], displayStyle: .sideBySide) // Or unified diff format DiffView(changes: changes ?? [], displayStyle: .unified)
Display Styles
| Style | Description |
|---|---|
.inline |
Single text view with changes highlighted in-place |
.sideBySide |
Two-column view showing original and new versions |
.unified |
Traditional patch format with +/- prefixes |
Styling Configuration
The DiffStyle struct provides extensive customization:
// Use a preset style DiffView(changes: changes, style: .dark) DiffView(changes: changes, style: .minimal) DiffView(changes: changes, style: .colorful) // Custom configuration let customStyle = DiffStyle( addedBackground: .green.opacity(0.25), addedText: .primary, removedBackground: .red.opacity(0.25), removedText: .primary, font: .system(.body, design: .monospaced), showLineNumbers: true, addedPrefix: "+", removedPrefix: "-" ) DiffView(changes: changes, style: customStyle)
Modifier Methods
Chain modifiers for convenient configuration:
DiffView(changes: changes, displayStyle: .sideBySide) .diffStyle(.colorful) .showLineNumbers(false) .font(.system(.caption, design: .monospaced))
Available Style Presets
| Preset | Description |
|---|---|
.default |
Standard colors with line numbers |
.dark |
Optimized for dark mode |
.minimal |
Subtle highlighting, no line numbers |
.colorful |
Vibrant green/red highlights |
DiffStyle Configuration Options
| Property | Type | Default | Description |
|---|---|---|---|
addedBackground |
Color |
Green 20% | Background for added inline text |
addedText |
Color |
.primary |
Text color for added content |
removedBackground |
Color |
Red 20% | Background for removed inline text |
removedText |
Color |
.primary |
Text color for removed content |
addedLineBackground |
Color |
Green 15% | Background for added lines |
removedLineBackground |
Color |
Red 15% | Background for removed lines |
font |
Font |
Monospaced body | Font for diff content |
showLineNumbers |
Bool |
true |
Show line numbers column |
lineSpacing |
CGFloat |
2 |
Spacing between lines |
addedPrefix |
String |
"+" |
Prefix for added lines (unified style) |
removedPrefix |
String |
"-" |
Prefix for removed lines (unified style) |
minLineHeight |
CGFloat |
22 |
Minimum height per line |
DiffStyle Builder Methods
let style = DiffStyle.default .withShowLineNumbers(false) .withFont(.system(.caption, design: .monospaced)) .withLineSpacing(4) .withAddedColors(background: .green.opacity(0.3), text: .green) .withRemovedColors(background: .red.opacity(0.3), text: .red) .withPrefixes(added: "→", removed: "←")
Individual Views
For more control, use individual view components:
// Inline view with character-level highlighting InlineDiffView(changes: changes, style: .default) // Side-by-side comparison SideBySideDiffView(changes: changes, style: .default) // Unified diff view UnifiedDiffView(changes: changes, style: .default)
Patch Views
Display structured patches with file headers:
// Single patch let patch = await diff?.structuredPatch( oldFileName: "original.txt", newFileName: "modified.txt", old: oldContent, new: newContent ) if let patch { PatchView(patch: patch) } // Multiple patches let patches = await diff?.parsePatch(patchString) if let patches { PatchesView(patches: patches) }
Style Picker Component
Include a style picker for user customization:
struct DiffPreviewView: View { @State private var displayStyle: DiffDisplayStyle = .inline let changes: [Change] var body: some View { VStack { DiffStylePicker(displayStyle: $displayStyle) DiffView(changes: changes, displayStyle: displayStyle) } } }
Thread Safety
JSDiff is implemented as a Swift actor, providing compile-time thread safety. All methods are isolated to the actor's context.
// Safe to use from multiple concurrent tasks Task { let changes1 = await diff?.diffChars("a", "b") } Task { let changes2 = await diff?.diffWords("hello", "world") }
Change Object
public struct Change: Sendable, Codable, Equatable { public let value: String // The text content public let added: Bool // true if inserted public let removed: Bool // true if deleted public let count: Int // Number of tokens (chars/words/lines) public var isUnchanged: Bool // true if neither added nor removed public var isAdded: Bool // convenience property public var isRemoved: Bool // convenience property }
Available Diff Methods
| Method | Token Type | Use Case |
|---|---|---|
diffChars |
Characters | Fine-grained text comparison |
diffWords |
Words (ignoring whitespace) | Document editing |
diffWordsWithSpace |
Words + whitespace | Precise whitespace tracking |
diffLines |
Lines | File version comparison |
diffSentences |
Sentences | Paragraph-level changes |
diffCss |
CSS tokens | Stylesheet comparison |
diffJson |
JSON lines | API responses, config files |
diffArrays |
Custom arrays | Tokenized comparison |
License
This package wraps jsdiff which is licensed under BSD-3-Clause. See Sources/JSDiff/Resources/LICENSE for details.
The Swift wrapper code is available under the MIT license.