Render descriptions with shared Markdown preview

Use one Markdown presentation for free-form content.

- issue and pull-request descriptions and comments
- milestone and commit descriptions
This commit is contained in:
Georg Bauer
2026-08-01 15:24:01 +02:00
parent 047f0ab0dc
commit d0ec533da6
4 changed files with 54 additions and 44 deletions

View File

@@ -733,7 +733,8 @@ final class MilestoneViewController: RefreshingTableViewController {
final class MilestoneCell: UITableViewCell {
private let stateIcon = UIImageView()
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let stack = UIStackView()
private var descriptionView: UIView?
private let metaLabel = UILabel()
private let progress = UIProgressView(progressViewStyle: .bar)
@@ -744,16 +745,13 @@ final class MilestoneCell: UITableViewCell {
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
titleStack.alignment = .firstBaseline
titleStack.spacing = 8
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textColor = .secondaryLabel
descriptionLabel.numberOfLines = 2
metaLabel.font = .preferredFont(forTextStyle: .caption1)
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
progress.progressTintColor = .systemGreen
let stack = UIStackView(
arrangedSubviews: [titleStack, descriptionLabel, progress, metaLabel]
)
stack.addArrangedSubview(titleStack)
stack.addArrangedSubview(progress)
stack.addArrangedSubview(metaLabel)
stack.axis = .vertical
stack.spacing = 7
stack.translatesAutoresizingMaskIntoConstraints = false
@@ -778,7 +776,12 @@ final class MilestoneCell: UITableViewCell {
textStyle: .headline
)
titleLabel.text = row.title
descriptionLabel.text = row.description
descriptionView?.removeFromSuperview()
if !row.description.isEmpty {
let view = markdownView(row.description)
stack.insertArrangedSubview(view, at: 1)
descriptionView = view
}
metaLabel.text = row.meta
progress.progress = Float(row.progress)
progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5

View File

@@ -1,5 +1,4 @@
import Highlighter
import MarkdownUI
import QuickLook
import SwiftUI
import UIKit
@@ -402,7 +401,7 @@ private final class FileListView: UITableView, UITableViewDataSource, UITableVie
private final class CommitHeaderView: UIView {
private let stack = UIStackView()
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private var descriptionView: UIView?
private let metadataStack = UIStackView()
override init(frame: CGRect) {
@@ -414,9 +413,6 @@ private final class CommitHeaderView: UIView {
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.numberOfLines = 0
titleLabel.accessibilityTraits = .header
descriptionLabel.font = .preferredFont(forTextStyle: .body)
descriptionLabel.adjustsFontForContentSizeCategory = true
descriptionLabel.numberOfLines = 0
metadataStack.axis = .vertical
metadataStack.spacing = 10
@@ -424,7 +420,6 @@ private final class CommitHeaderView: UIView {
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(titleLabel)
stack.addArrangedSubview(descriptionLabel)
stack.addArrangedSubview(metadataStack)
addSubview(stack)
NSLayoutConstraint.activate([
@@ -440,8 +435,12 @@ private final class CommitHeaderView: UIView {
func configure(_ page: CommitDetailsPage) {
titleLabel.text = page.title
descriptionLabel.text = page.description
descriptionLabel.isHidden = page.description.isEmpty
descriptionView?.removeFromSuperview()
if !page.description.isEmpty {
let view = markdownView(page.description)
stack.insertArrangedSubview(view, at: 1)
descriptionView = view
}
metadataStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
for metadata in page.metadata {
let isCommit = metadata.label == "Commit"
@@ -1132,9 +1131,7 @@ struct RepositoryMarkdownPreview: View {
var body: some View {
ScrollView {
Markdown(source)
.markdownTheme(.gitHub)
.frame(maxWidth: .infinity, alignment: .leading)
MarkdownContent(source: source)
.padding()
}
.background(Color(uiColor: .systemBackground))
@@ -1235,6 +1232,7 @@ final class DiffViewController: UIViewController {
}
}
@MainActor
private func detailViews(
title: String,
state: String? = nil,
@@ -1285,6 +1283,7 @@ private func stateTitle(_ titleLabel: UILabel, state: String, subject: String) -
return stack
}
@MainActor
private func commentViews(
_ comments: [CommentRow],
editComment: ((CommentRow) -> Void)? = nil
@@ -1323,19 +1322,6 @@ private func commentViews(
return views
}
private func markdownView(_ source: String) -> UITextView {
let view = UITextView()
view.attributedText = markdown(source)
view.isEditable = false
view.isSelectable = true
view.isScrollEnabled = false
view.backgroundColor = .clear
view.textContainerInset = .zero
view.textContainer.lineFragmentPadding = 0
view.adjustsFontForContentSizeCategory = true
return view
}
private func sectionLabel(_ text: String) -> UILabel {
let label = UILabel()
label.text = text

View File

@@ -1,5 +1,28 @@
import MarkdownUI
import SwiftUI
import UIKit
struct MarkdownContent: View {
let source: String
var body: some View {
Markdown(source)
.markdownTheme(.gitHub)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@MainActor
func markdownView(_ source: String) -> UIView {
UIHostingConfiguration {
MarkdownContent(source: source)
.fixedSize(horizontal: false, vertical: true)
}
.margins(.all, 0)
.makeContentView()
}
func localDate(forUTCTimestamp timestamp: Int64) -> Date {
var utc = Calendar(identifier: .gregorian)
utc.timeZone = TimeZone(secondsFromGMT: 0)!
@@ -179,17 +202,6 @@ extension UIColor {
}
}
func markdown(_ source: String, textStyle: UIFont.TextStyle = .body) -> NSAttributedString {
let attributed = (try? AttributedString(
markdown: source,
options: .init(interpretedSyntax: .full)
)) ?? AttributedString(source)
var mutable = attributed
mutable.font = .preferredFont(forTextStyle: textStyle)
mutable.foregroundColor = UIColor.label
return NSAttributedString(mutable)
}
func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) {
var content = cell.defaultContentConfiguration()
content.text = title