import UIKit private final class CommitHeaderView: UIView { private let stack = UIStackView() private let titleLabel = UILabel() private var descriptionView: UIView? private let metadataStack = UIStackView() private let bottomSeparator = separator() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .systemBackground directionalLayoutMargins = .init(top: 20, leading: 20, bottom: 20, trailing: 20) titleLabel.font = .preferredFont(forTextStyle: .title2) titleLabel.adjustsFontForContentSizeCategory = true titleLabel.numberOfLines = 0 titleLabel.accessibilityTraits = .header metadataStack.axis = .vertical metadataStack.spacing = 10 stack.axis = .vertical stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false stack.addArrangedSubview(titleLabel) stack.addArrangedSubview(metadataStack) addSubview(stack) bottomSeparator.translatesAutoresizingMaskIntoConstraints = false addSubview(bottomSeparator) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), stack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), stack.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), stack.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), bottomSeparator.leadingAnchor.constraint(equalTo: leadingAnchor), bottomSeparator.trailingAnchor.constraint(equalTo: trailingAnchor), bottomSeparator.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } func configure(_ page: CommitDetailsPage) { titleLabel.text = page.title 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 label = UILabel() label.text = metadata.label.uppercased() label.font = .preferredFont(forTextStyle: .caption2) label.adjustsFontForContentSizeCategory = true label.textColor = .secondaryLabel let value = UILabel() value.text = metadata.value value.font = metadata.monospaced ? UIFontMetrics(forTextStyle: .subheadline).scaledFont( for: .monospacedSystemFont(ofSize: 15, weight: .regular) ) : .preferredFont(forTextStyle: .subheadline) value.adjustsFontForContentSizeCategory = true value.numberOfLines = 0 value.lineBreakMode = metadata.monospaced ? .byCharWrapping : .byWordWrapping let row = UIStackView(arrangedSubviews: [label, value]) row.axis = .vertical row.spacing = 2 row.isAccessibilityElement = true row.accessibilityLabel = "\(metadata.label): \(metadata.value)" metadataStack.addArrangedSubview(row) } } } @MainActor final class FilesViewController: RefreshingTableViewController { private let context: AppContext private let owner: String private let repository: String private let sha: String private let branch: String? private let commitHeader = CommitHeaderView() private var page: CommitDetailsPage? init(context: AppContext, owner: String, repository: String, sha: String, branch: String? = nil) { self.context = context self.owner = owner self.repository = repository self.sha = sha self.branch = branch super.init() title = "Changed Files" } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func viewDidLoad() { super.viewDidLoad() loadContent(refreshing: false) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard let header = tableView.tableHeaderView else { return } let size = header.systemLayoutSizeFitting( CGSize(width: tableView.bounds.width, height: 0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) guard header.frame.height != size.height else { return } header.frame.size.height = size.height tableView.tableHeaderView = header } override func loadContent(refreshing: Bool) { beginLoading(refreshing: refreshing) loadingTask?.cancel() loadingTask = Task { do { let page = try await context.core.commitDetails( owner: owner, repository: repository, sha: sha, branch: branch ) self.page = page commitHeader.configure(page) tableView.tableHeaderView = commitHeader tableView.reloadData() tableView.backgroundView = page.files.isEmpty ? EmptyBackgroundView(title: "No changed files", detail: "This commit does not contain file changes.") : nil } catch { if !Task.isCancelled { show(error: error) } } endLoading() } } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { page?.files.count ?? 0 } override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "file") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "file") let row = page!.files[indexPath.row] configureTextCell(cell, title: row.path, detail: row.status) cell.accessoryType = .disclosureIndicator return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let row = page?.files[indexPath.row] else { return } navigationController?.pushViewController( DiffViewController( context: context, source: .commit( owner: owner, repository: repository, sha: sha, path: row.path ) ), animated: true ) } }