Add first-class Actions management (#63)
This commit is contained in:
661
ios/Sources/ActionsScreens.swift
Normal file
661
ios/Sources/ActionsScreens.swift
Normal file
@@ -0,0 +1,661 @@
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class ActionsViewController: RefreshingTableViewController {
|
||||
private enum Mode: Int { case workflows, runs }
|
||||
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let defaultBranch: String
|
||||
private let modeControl = UISegmentedControl(items: ["Workflows", "Runs"])
|
||||
private var mode = Mode.workflows
|
||||
private var workflows: [ActionWorkflowRow] = []
|
||||
private var runs: [ActionRunRow] = []
|
||||
private var currentPage: UInt32 = 0
|
||||
private var autoRefreshTask: Task<Void, Never>?
|
||||
private var loading = false
|
||||
private var requestGeneration = 0
|
||||
|
||||
init(context: AppContext, owner: String, repository: String, defaultBranch: String) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
self.defaultBranch = defaultBranch
|
||||
super.init()
|
||||
title = "Actions"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
deinit { autoRefreshTask?.cancel() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
modeControl.selectedSegmentIndex = mode.rawValue
|
||||
modeControl.addTarget(self, action: #selector(modeChanged), for: .valueChanged)
|
||||
navigationItem.titleView = modeControl
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
startAutoRefresh()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
autoRefreshTask?.cancel()
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
loadPage(1, refreshing: refreshing)
|
||||
}
|
||||
|
||||
override func loadMoreContent() {
|
||||
guard mode == .runs else { return }
|
||||
loadPage(currentPage + 1, refreshing: false)
|
||||
}
|
||||
|
||||
@objc private func modeChanged() {
|
||||
guard let mode = Mode(rawValue: modeControl.selectedSegmentIndex) else { return }
|
||||
self.mode = mode
|
||||
loadingTask?.cancel()
|
||||
loading = false
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
private func loadPage(_ page: UInt32, refreshing: Bool) {
|
||||
guard !loading else {
|
||||
refreshControl?.endRefreshing()
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
requestGeneration += 1
|
||||
let generation = requestGeneration
|
||||
let requestedMode = mode
|
||||
if page == 1 {
|
||||
resetPagination()
|
||||
beginLoading(refreshing: refreshing)
|
||||
}
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
defer {
|
||||
if requestGeneration == generation {
|
||||
loading = false
|
||||
if page == 1 { endLoading() }
|
||||
}
|
||||
}
|
||||
do {
|
||||
switch requestedMode {
|
||||
case .workflows:
|
||||
let result = try await context.core.actionWorkflows(
|
||||
owner: owner,
|
||||
repository: repository
|
||||
)
|
||||
guard !Task.isCancelled, mode == requestedMode else { return }
|
||||
workflows = result
|
||||
currentPage = 1
|
||||
finishPagination(hasMore: false)
|
||||
case .runs:
|
||||
let result = try await context.core.actionRuns(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
page: page
|
||||
)
|
||||
guard !Task.isCancelled, mode == requestedMode else { return }
|
||||
runs = page == 1 ? result.rows : runs + result.rows
|
||||
currentPage = page
|
||||
finishPagination(hasMore: result.hasMore)
|
||||
}
|
||||
tableView.reloadData()
|
||||
updateEmptyState()
|
||||
} catch {
|
||||
if !Task.isCancelled {
|
||||
show(error: error)
|
||||
failPagination()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startAutoRefresh() {
|
||||
autoRefreshTask?.cancel()
|
||||
autoRefreshTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
guard let self, self.mode == .runs, !self.loading else { continue }
|
||||
self.loadPage(1, refreshing: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateEmptyState() {
|
||||
let empty = mode == .workflows ? workflows.isEmpty : runs.isEmpty
|
||||
tableView.backgroundView = empty
|
||||
? EmptyBackgroundView(
|
||||
title: mode == .workflows ? "No workflows" : "No workflow runs",
|
||||
detail: mode == .workflows
|
||||
? "This repository has no Actions workflows."
|
||||
: "Dispatch a workflow to create the first run."
|
||||
)
|
||||
: nil
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
mode == .workflows ? workflows.count : runs.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "action")
|
||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "action")
|
||||
if mode == .workflows {
|
||||
let row = workflows[indexPath.row]
|
||||
cell.accessoryView = nil
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: row.name,
|
||||
detail: "\(row.state) · \(row.path)",
|
||||
image: context.symbol("play.square")
|
||||
)
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
let row = runs[indexPath.row]
|
||||
configureActionCell(cell, row: row, context: context)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if mode == .workflows {
|
||||
let workflow = workflows[indexPath.row]
|
||||
navigationController?.pushViewController(
|
||||
WorkflowDispatchViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
workflow: workflow,
|
||||
defaultBranch: defaultBranch
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
} else {
|
||||
navigationController?.pushViewController(
|
||||
ActionRunViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
run: runs[indexPath.row]
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WorkflowDispatchViewController: UITableViewController, UITextViewDelegate {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let workflow: ActionWorkflowRow
|
||||
private let referenceField = UITextField()
|
||||
private let inputsView = UITextView()
|
||||
private lazy var runButton = UIBarButtonItem(
|
||||
title: "Run Workflow",
|
||||
primaryAction: UIAction { [weak self] _ in self?.dispatch() }
|
||||
)
|
||||
private var dispatchTask: Task<Void, Never>?
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
owner: String,
|
||||
repository: String,
|
||||
workflow: ActionWorkflowRow,
|
||||
defaultBranch: String
|
||||
) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
self.workflow = workflow
|
||||
referenceField.text = defaultBranch
|
||||
super.init(style: .insetGrouped)
|
||||
title = "Dispatch"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
deinit { dispatchTask?.cancel() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
referenceField.placeholder = "Branch or tag"
|
||||
referenceField.autocapitalizationType = .none
|
||||
referenceField.autocorrectionType = .no
|
||||
referenceField.clearButtonMode = .whileEditing
|
||||
referenceField.addTarget(self, action: #selector(referenceChanged), for: .editingChanged)
|
||||
inputsView.font = .preferredFont(forTextStyle: .body)
|
||||
inputsView.adjustsFontForContentSizeCategory = true
|
||||
inputsView.autocapitalizationType = .none
|
||||
inputsView.autocorrectionType = .no
|
||||
inputsView.accessibilityLabel = "Workflow dispatch inputs"
|
||||
navigationItem.rightBarButtonItem = runButton
|
||||
referenceChanged()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { 3 }
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
[workflow.name, "Git Reference", "Inputs"][section]
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
section == 2 ? "Enter one workflow_dispatch input per line as KEY=VALUE. Leave blank when the workflow has no inputs." : nil
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
||||
if indexPath.section == 0 {
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = workflow.path
|
||||
content.secondaryText = workflow.state
|
||||
cell.contentConfiguration = content
|
||||
cell.selectionStyle = .none
|
||||
} else {
|
||||
let view = indexPath.section == 1 ? referenceField : inputsView
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
cell.contentView.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 16),
|
||||
view.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -16),
|
||||
view.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8),
|
||||
view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8),
|
||||
])
|
||||
cell.selectionStyle = .none
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
indexPath.section == 2 ? 140 : UITableView.automaticDimension
|
||||
}
|
||||
|
||||
private func dispatch() {
|
||||
navigationItem.rightBarButtonItem?.isEnabled = false
|
||||
dispatchTask?.cancel()
|
||||
dispatchTask = Task {
|
||||
do {
|
||||
try await context.core.dispatchActionWorkflow(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
workflow: workflow.id,
|
||||
reference: referenceField.text ?? "",
|
||||
inputs: [inputsView.text]
|
||||
)
|
||||
navigationController?.popViewController(animated: true)
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
navigationItem.rightBarButtonItem?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func referenceChanged() {
|
||||
runButton.isEnabled = !(referenceField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ActionRunViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let runID: Int64
|
||||
private var page: ActionRunPage?
|
||||
private var autoRefreshTask: Task<Void, Never>?
|
||||
private var loading = false
|
||||
|
||||
init(context: AppContext, owner: String, repository: String, run: ActionRunRow) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
runID = run.id
|
||||
super.init()
|
||||
title = "Run #\(run.number)"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
deinit { autoRefreshTask?.cancel() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
autoRefreshTask?.cancel()
|
||||
autoRefreshTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
guard let self, !self.loading else { continue }
|
||||
self.loadContent(refreshing: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
autoRefreshTask?.cancel()
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
guard !loading else {
|
||||
refreshControl?.endRefreshing()
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
defer {
|
||||
loading = false
|
||||
endLoading()
|
||||
}
|
||||
do {
|
||||
page = try await context.core.actionRun(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
run: runID
|
||||
)
|
||||
tableView.reloadData()
|
||||
tableView.backgroundView = page?.jobs.isEmpty == true
|
||||
? EmptyBackgroundView(
|
||||
title: "No jobs yet",
|
||||
detail: "Pull to refresh while the run is queued."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 1 : page?.jobs.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 0 ? "Run" : "Jobs"
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "actionDetail")
|
||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "actionDetail")
|
||||
if indexPath.section == 0 {
|
||||
let run = page!.run
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: run.title,
|
||||
detail: "\(actionStateLabel(run.state)) · \(run.event) · \(run.branch)\n\(run.meta)",
|
||||
image: context.symbol(actionStateSymbolName(run.state))
|
||||
)
|
||||
tintActionIcon(in: cell, state: run.state)
|
||||
cell.selectionStyle = .none
|
||||
cell.accessoryType = .none
|
||||
} else {
|
||||
let job = page!.jobs[indexPath.row]
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: job.name,
|
||||
detail: job.meta,
|
||||
image: context.symbol(actionStateSymbolName(job.state))
|
||||
)
|
||||
tintActionIcon(in: cell, state: job.state)
|
||||
cell.selectionStyle = .default
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 1 else { return }
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let job = page!.jobs[indexPath.row]
|
||||
navigationController?.pushViewController(
|
||||
ActionJobViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
runID: runID,
|
||||
job: job
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ActionJobViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let runID: Int64
|
||||
private let job: ActionJobRow
|
||||
private var page: ActionJobLogPage?
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
owner: String,
|
||||
repository: String,
|
||||
runID: Int64,
|
||||
job: ActionJobRow
|
||||
) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
self.runID = runID
|
||||
self.job = job
|
||||
super.init()
|
||||
title = job.name
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
defer { endLoading() }
|
||||
do {
|
||||
page = try await context.core.actionJobLog(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
run: runID,
|
||||
job: job.id
|
||||
)
|
||||
tableView.reloadData()
|
||||
tableView.backgroundView = page?.groups.isEmpty == true
|
||||
? EmptyBackgroundView(
|
||||
title: "No log groups",
|
||||
detail: "This job has no log output yet. Pull to refresh."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 1 : page?.groups.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 0 ? "Job" : "Tasks"
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "jobDetail")
|
||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "jobDetail")
|
||||
if indexPath.section == 0 {
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: page!.job.name,
|
||||
detail: page!.job.meta,
|
||||
image: context.symbol(actionStateSymbolName(page!.job.state))
|
||||
)
|
||||
tintActionIcon(in: cell, state: page!.job.state)
|
||||
cell.selectionStyle = .none
|
||||
cell.accessoryType = .none
|
||||
} else {
|
||||
let group = page!.groups[indexPath.row]
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: group.name,
|
||||
detail: group.duration.isEmpty
|
||||
? actionStateLabel(group.state)
|
||||
: "\(actionStateLabel(group.state)) · \(group.duration)",
|
||||
image: context.symbol(actionStateSymbolName(group.state))
|
||||
)
|
||||
tintActionIcon(in: cell, state: group.state)
|
||||
cell.selectionStyle = .default
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 1 else { return }
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let group = page!.groups[indexPath.row]
|
||||
navigationController?.pushViewController(
|
||||
ActionLogTextViewController(title: group.name, text: group.text),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ActionLogTextViewController: UIViewController {
|
||||
private let logTitle: String
|
||||
private let text: String
|
||||
|
||||
init(title: String, text: String) {
|
||||
logTitle = title
|
||||
self.text = text
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.title = title
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(
|
||||
for: .monospacedSystemFont(ofSize: 15, weight: .regular)
|
||||
)
|
||||
textView.adjustsFontForContentSizeCategory = true
|
||||
textView.text = text
|
||||
textView.accessibilityLabel = "\(logTitle) log"
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(textView)
|
||||
NSLayoutConstraint.activate([
|
||||
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func configureActionCell(_ cell: UITableViewCell, row: ActionRunRow, context: AppContext) {
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: "#\(row.number) \(row.title)",
|
||||
detail: "\(actionStateLabel(row.state)) · \(row.event) · \(row.branch)\n\(row.meta)",
|
||||
image: context.symbol(actionStateSymbolName(row.state))
|
||||
)
|
||||
tintActionIcon(in: cell, state: row.state)
|
||||
cell.accessoryView = nil
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
|
||||
private func tintActionIcon(in cell: UITableViewCell, state: ActionState) {
|
||||
guard var content = cell.contentConfiguration as? UIListContentConfiguration else { return }
|
||||
content.imageProperties.tintColor = actionStateColor(state)
|
||||
cell.contentConfiguration = content
|
||||
}
|
||||
|
||||
private func actionStateColor(_ state: ActionState) -> UIColor {
|
||||
switch state {
|
||||
case .succeeded: return .systemGreen
|
||||
case .failed: return .systemRed
|
||||
case .cancelled: return .systemGray
|
||||
case .skipped: return .tertiaryLabel
|
||||
case .queued: return .systemOrange
|
||||
case .waiting: return .systemOrange
|
||||
case .inProgress: return .systemBlue
|
||||
case .unknown: return .secondaryLabel
|
||||
}
|
||||
}
|
||||
|
||||
private func actionStateLabel(_ state: ActionState) -> String {
|
||||
switch state {
|
||||
case .queued: return "Queued"
|
||||
case .waiting: return "Waiting"
|
||||
case .inProgress: return "In Progress"
|
||||
case .succeeded: return "Succeeded"
|
||||
case .failed: return "Failed"
|
||||
case .cancelled: return "Cancelled"
|
||||
case .skipped: return "Skipped"
|
||||
case .unknown: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func actionStateSymbolName(_ state: ActionState) -> String {
|
||||
switch state {
|
||||
case .succeeded: return "checkmark.circle.fill"
|
||||
case .failed: return "xmark.circle.fill"
|
||||
case .cancelled: return "slash.circle.fill"
|
||||
case .skipped: return "minus.circle.fill"
|
||||
case .queued: return "clock.fill"
|
||||
case .waiting: return "hourglass.circle.fill"
|
||||
case .inProgress: return "circle.fill"
|
||||
case .unknown: return "questionmark.circle.fill"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user