import UIKit func errorAlert(_ message: String) -> UIAlertController { let alert = UIAlertController(title: "Something went wrong", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) return alert } extension UIViewController { func show(error: Error) { present(errorAlert(error.localizedDescription), animated: true) } func beginNavigationLoading(_ spinner: UIActivityIndicatorView) { guard navigationItem.rightBarButtonItem == nil else { return } spinner.startAnimating() navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner) } func endNavigationLoading(_ spinner: UIActivityIndicatorView) { spinner.stopAnimating() if navigationItem.rightBarButtonItem?.customView === spinner { navigationItem.rightBarButtonItem = nil } } } @MainActor class RefreshingTableViewController: UITableViewController { private let spinner = UIActivityIndicatorView(style: .medium) var loadingTask: Task? init() { super.init(style: .plain) tableView.backgroundColor = .systemGroupedBackground tableView.separatorInset = .zero tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged) navigationItem.largeTitleDisplayMode = .never } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } deinit { loadingTask?.cancel() } func loadContent(refreshing: Bool) {} func beginLoading(refreshing: Bool) { if !refreshing { beginNavigationLoading(spinner) } } func endLoading() { endNavigationLoading(spinner) refreshControl?.endRefreshing() } @objc private func refreshRequested() { loadContent(refreshing: true) } } final class EmptyBackgroundView: UIView { init(title: String, detail: String) { super.init(frame: .zero) let titleLabel = UILabel() titleLabel.text = title titleLabel.font = .preferredFont(forTextStyle: .title2) titleLabel.textAlignment = .center let detailLabel = UILabel() detailLabel.text = detail detailLabel.font = .preferredFont(forTextStyle: .body) detailLabel.textColor = .secondaryLabel detailLabel.textAlignment = .center detailLabel.numberOfLines = 0 let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel]) stack.axis = .vertical stack.spacing = 8 stack.translatesAutoresizingMaskIntoConstraints = false addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28), stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28), stack.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } } extension UIColor { convenience init?(hex: String) { let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) guard value.count == 6, let rgb = Int(value, radix: 16) else { return nil } self.init( red: CGFloat((rgb >> 16) & 0xff) / 255, green: CGFloat((rgb >> 8) & 0xff) / 255, blue: CGFloat(rgb & 0xff) / 255, alpha: 1 ) } } 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 content.secondaryText = detail content.secondaryTextProperties.numberOfLines = 2 content.image = image content.imageProperties.tintColor = .tintColor cell.contentConfiguration = content cell.backgroundColor = .systemBackground } func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString { let output = NSMutableAttributedString() if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) { let attachment = NSTextAttachment(image: image) attachment.bounds = CGRect(x: 0, y: -2, width: font.pointSize, height: font.pointSize) output.append(NSAttributedString(attachment: attachment)) output.append(NSAttributedString(string: " ")) } output.append(NSAttributedString(string: text, attributes: [ .font: font, .foregroundColor: color, ])) return output }