YiTong provides DiffView and DiffViewController for rendering diffs on Apple platforms.
It is an Apple-platform wrapper around diffs.com, rendered through WKWebView.
It supports both unified patch input and file-based old/new contents input.
YiTong = 异同
析其所异,合其所同。
YiTong supports:
- iOS 16+
- macOS 13+
For released builds, prefer a tagged Swift Package dependency:
dependencies: [
.package(url: "https://github.com/onevcat/YiTong.git", from: "0.1.0")
]Then add the product to your target:
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "YiTong", package: "YiTong")
]
)
]Replace 0.1.0 with the latest tagged release.
If you need unreleased changes during development, you can temporarily track branch: "master" instead.
import YiTong
let document = DiffDocument(
files: [
DiffFile(
oldPath: "Sources/Counter.swift",
newPath: "Sources/Counter.swift",
oldContents: "struct Counter { var value: Int }\n",
newContents: "struct Counter { private(set) var value: Int }\n"
),
],
title: "Counter.swift"
)If you also have the original patch text, pass it as fallbackPatch.
YiTong automatically falls back to patch-based rendering when file-based input exceeds supported limits.
import YiTong
let document = DiffDocument(
patch: """
diff --git a/Example.swift b/Example.swift
--- a/Example.swift
+++ b/Example.swift
@@ -1,3 +1,3 @@
-let value = 1
+let value = 2
"""
)Use files when your app already has old/new contents in memory.
Use patch when you already have unified diff output, or when patch text is the more natural transport format.
import SwiftUI
import YiTong
struct ContentView: View {
let document: DiffDocument
var body: some View {
DiffView(
document: document,
configuration: DiffConfiguration(
appearance: .automatic,
style: .split,
indicators: .bars
)
)
}
}When your app already provides file navigation, render one file at a time and hide YiTong's built-in file header.
This is the integration pattern used in supacode.
import SwiftUI
import YiTong
struct DiffDetailView: View {
let selectedFile: DiffFile
let selectedTitle: String
let selectedPatch: String?
var body: some View {
DiffView(
document: DiffDocument(
files: [selectedFile],
title: selectedTitle,
fallbackPatch: selectedPatch
),
configuration: DiffConfiguration(
style: .split,
showsFileHeaders: false
)
)
}
}import YiTong
let controller = DiffViewController(
document: document,
configuration: DiffConfiguration(style: .unified)
)DiffView(
document: document,
onEvent: { event in
switch event {
case .didFinishInitialLoad:
break
case .didRender(let summary):
print(summary.fileCount)
case .didClickLine(let line):
print(line.fileIndex, line.number)
case .didChangeSelection(let selection):
print(selection as Any)
case .didActivateAnnotation(let action):
print(action.annotationID, action.action)
case .didFail(let error):
print(error.code, error.message)
}
}
)Annotations attach host-rendered content beneath a diff line: a review thread, a comment composer, a lint warning. YiTong stays agnostic about what the content means; the host renders it and reacts to clicks.
import YiTong
let thread = DiffAnnotation(
id: "thread-42",
fileIndex: 0,
side: .new,
lineNumber: 17,
kind: "thread",
content: .html(
"""
<p><strong>Amadeus</strong> Should this be optional?</p>
<button data-action="reply">Reply</button>
"""
)
)
DiffView(
document: document,
annotations: [thread],
onEvent: { event in
if case .didActivateAnnotation(let action) = event {
// action.annotationID == "thread-42", action.action == "reply"
}
}
)sideandlineNumberaddress the line the same wayDiffLineReferencedoes. Multi-line annotations anchor to one line (typically the end of the range); draw range information into the content yourself.contentis either.html(String)or.text(String). HTML is inserted verbatim, apart from script execution vectors (<script>,on*handlers,javascript:URLs), which the renderer strips. Everything else, including inline styles and<style>blocks, is the host's responsibility.- Any element inside the HTML carrying a
data-actionattribute reports clicks throughDiffEvent.didActivateAnnotation, echoing the annotation'sid,kindand line coordinates alongside the action string. - Changing only
annotationsonDiffView, or callingDiffViewController.update(annotations:), updates the annotations in place without re-rendering the document. annotationsdefaults to an empty list, so existing call sites need no changes.
DiffConfiguration currently supports:
appearance:.automatic,.light,.darkstyle:.split,.unifiedindicators:.bars,.classic,.noneshowsLineNumbersshowsChangeBackgroundswrapsLinesshowsFileHeadersinlineChangeStyle:.wordAlt,.word,.char,.noneallowsSelection- On touch devices the line-number gutter opts out of scrolling so a finger drag selects lines; start scroll gestures from the code area instead.
YiTong exposes a smaller Swift-facing API than the full diffs vanilla JS surface.
diffs vanilla option |
YiTong public API | Status |
|---|---|---|
diffStyle |
DiffConfiguration.style |
Supported |
lineDiffType |
DiffConfiguration.inlineChangeStyle |
Supported |
diffIndicators |
DiffConfiguration.indicators |
Supported |
disableBackground |
DiffConfiguration.showsChangeBackgrounds |
Supported |
disableLineNumbers |
DiffConfiguration.showsLineNumbers |
Supported |
overflow |
DiffConfiguration.wrapsLines |
Supported |
disableFileHeader |
DiffConfiguration.showsFileHeaders |
Supported |
enableLineSelection |
DiffConfiguration.allowsSelection |
Supported |
themeType |
DiffConfiguration.appearance |
Supported |
theme |
Not public | Hidden |
renderHeaderMetadata |
Not public | Not supported |
lineAnnotations / renderAnnotation |
DiffAnnotation, DiffEvent.didActivateAnnotation |
Supported |
| worker pool options | Not public | Not supported |
| custom DOM / unsafe CSS hooks | Not public | Not supported |
Most common commands:
make verify
make run-example
make update-web-assetsmake run-example launches the bundled macOS example app for manual verification.
YiTong is licensed under the Apache License 2.0.
This repository also bundles generated web assets derived from
@pierre/diffs (Apache-2.0) for local WKWebView rendering.
See:
