Files
ios-gitea-prs/GiteaPR/Detail/DiffView.swift
T
Hermes a3b8eefdb2
Build iOS App / Build .ipa and publish source (push) Successful in 21s
Fix diff text not rendering: LazyVStack in dual-axis ScrollView collapses row width; bump to 1.6
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.
2026-08-12 21:46:14 +00:00

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
}