Build iOS App / Build .ipa and publish source (push) Successful in 21s
The diff data was fine, but the render collapsed: a LazyVStack inside a dual-axis ScrollView([.horizontal, .vertical]) gets an unbounded proposal and rows collapse to near-zero width — the green addition background rendered but the monospaced text was laid out at ~0 width and disappeared. Use a plain VStack inside nested vertical→horizontal ScrollViews (the stable pattern for diff text) and give each line fixedSize(horizontal:) so the text isn't clipped. Bump to 1.6.
138 lines
5.1 KiB
Swift
138 lines
5.1 KiB
Swift
import SwiftUI
|
|
|
|
/// Read-only unified diff viewer for a single changed file.
|
|
///
|
|
/// This Gitea instance does NOT return a per-file `patch` from
|
|
/// `/pulls/{n}/files` (the field is absent), so the diff is fetched from the
|
|
/// PR's `.diff` endpoint and the hunks belonging to this file are extracted.
|
|
/// Added/deleted lines are colour-coded.
|
|
///
|
|
/// Rendering: a plain VStack inside nested vertical→horizontal ScrollViews.
|
|
/// A LazyVStack inside a dual-axis `ScrollView([.horizontal, .vertical])` is a
|
|
/// known-bad combination (rows collapse to ~zero width, so the background tint
|
|
/// renders but the text disappears); VStack + nested scroll is the stable
|
|
/// pattern for monospaced diff text.
|
|
struct DiffView: View {
|
|
let repo: Repository
|
|
let prNumber: Int
|
|
let file: ChangedFile
|
|
@State private var lines: [DiffLine] = []
|
|
@State private var isLoading = true
|
|
@State private var errorMessage: String?
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView("Loading diff…")
|
|
} else if let errorMessage {
|
|
ContentUnavailableView(
|
|
"Couldn't load diff",
|
|
systemImage: "doc.text",
|
|
description: Text(errorMessage))
|
|
} else {
|
|
ScrollView(.vertical) {
|
|
ScrollView(.horizontal) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
ForEach(lines) { line in
|
|
diffLine(line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(file.filename)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task { await load() }
|
|
}
|
|
|
|
private func load() async {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
guard let client = SettingsStore.shared.client() else {
|
|
errorMessage = "Not configured."
|
|
return
|
|
}
|
|
do {
|
|
let full = try await client.diff(owner: repo.owner.login,
|
|
repo: repo.name,
|
|
number: prNumber)
|
|
let patch = extractSection(named: file.filename, from: full)
|
|
lines = parse(patch.isEmpty ? full : patch)
|
|
if lines.isEmpty {
|
|
errorMessage = "No diff content for this file."
|
|
}
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
/// Extract the unified-diff block for `filename` from a multi-file diff.
|
|
private func extractSection(named filename: String, from diff: String) -> String {
|
|
let marker = "+++ b/\(filename)"
|
|
guard let idx = diff.range(of: marker)?.upperBound else {
|
|
return ""
|
|
}
|
|
let tail = String(diff[idx...])
|
|
if let next = tail.range(of: "\ndiff --git ") {
|
|
return String(tail[..<next.lowerBound])
|
|
}
|
|
return tail
|
|
}
|
|
|
|
private func diffLine(_ line: DiffLine) -> some View {
|
|
HStack(spacing: 0) {
|
|
Text(line.text)
|
|
.font(.caption.monospaced())
|
|
.foregroundColor(foreground(for: line.type))
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 1)
|
|
.background(background(for: line.type))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private func background(for type: DiffLineType) -> Color {
|
|
switch type {
|
|
case .addition: return Color.green.opacity(0.15)
|
|
case .deletion: return Color.red.opacity(0.15)
|
|
case .hunk, .meta: return Color(.secondarySystemBackground)
|
|
case .context: return .clear
|
|
}
|
|
}
|
|
private func foreground(for type: DiffLineType) -> Color {
|
|
switch type {
|
|
case .addition: return .green
|
|
case .deletion: return .red
|
|
case .hunk: return .secondary
|
|
case .meta, .context: return .primary
|
|
}
|
|
}
|
|
|
|
private func parse(_ patch: String) -> [DiffLine] {
|
|
patch.components(separatedBy: "\n").map { line in
|
|
if line.hasPrefix("@@") { return DiffLine(text: line, type: .hunk) }
|
|
if line.hasPrefix("diff ") || line.hasPrefix("index ")
|
|
|| line.hasPrefix("--- ") || line.hasPrefix("+++ ") {
|
|
return DiffLine(text: line, type: .meta)
|
|
}
|
|
if line.hasPrefix("+") { return DiffLine(text: line, type: .addition) }
|
|
if line.hasPrefix("-") { return DiffLine(text: line, type: .deletion) }
|
|
return DiffLine(text: line, type: .context)
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum DiffLineType { case context, addition, deletion, hunk, meta }
|
|
|
|
private struct DiffLine: Identifiable {
|
|
// Stable id: generate once, not on every access (a fresh UUID each read
|
|
// churns SwiftUI's ForEach on the main thread and can crash).
|
|
let id: UUID = UUID()
|
|
let text: String
|
|
let type: DiffLineType
|
|
}
|