A Swift wrapper around jsdiff using JavaScriptCore. Provides text diffing, patch creation, and patch application capabilities.
- macOS 11.0+ / iOS 12.0+ / tvOS 12.0+ / visionOS 1.0+
- Swift 5.5+ (for actor-based concurrency)
- macOS 12.0+ / iOS 15.0+ / tvOS 15.0+ / watchOS 8.0+ / visionOS 1.0+
- Swift 5.5+
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
// Core diffing functionality
import JSDiff
// SwiftUI visualization views (optional)
import JSDiffUIimport JSDiff
// JSDiff is an actor, so initialize it in an async context
let diff = JSDiff()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)")
}
}let changes = await diff?.diffWords("hello world", "hello there")let oldText = """
line1
line2
line3
"""
let newText = """
line1
line2 modified
line3
"""
let changes = await diff?.diffLines(oldText, newText)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)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"}"#)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)")
}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
// line2Create patches for two different files:
let patch = await diff?.createTwoFilesPatch(
oldFileName: "original.txt",
newFileName: "modified.txt",
old: oldContent,
new: newContent
)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"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 lineslet patchString = """
--- old.txt
+++ new.txt
@@ -1,3 +1,3 @@
line1
-line2
+line2 modified
line3
"""
let patches = await diff?.parsePatch(patchString)let reversed = await diff?.reversePatch(patchString)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
}The JSDiffUI module provides ready-to-use SwiftUI views for visualizing diff output. It supports three display styles: inline, side-by-side, and unified.
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)| 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 |
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)Chain modifiers for convenient configuration:
DiffView(changes: changes, displayStyle: .sideBySide)
.diffStyle(.colorful)
.showLineNumbers(false)
.font(.system(.caption, design: .monospaced))| Preset | Description |
|---|---|
.default |
Standard colors with line numbers |
.dark |
Optimized for dark mode |
.minimal |
Subtle highlighting, no line numbers |
.colorful |
Vibrant green/red highlights |
| 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 |
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: "←")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)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)
}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)
}
}
}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")
}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
}| 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 |
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.