import UIKit extension UIViewController { func promptForSearchText(title: String, current: String, apply: @escaping (String) -> Void) { let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert) alert.addTextField { field in field.text = current field.placeholder = "Search text" field.accessibilityLabel = "Search text" field.clearButtonMode = .whileEditing field.returnKeyType = .search } alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alert.addAction(UIAlertAction(title: "Apply", style: .default) { [weak alert] _ in apply(alert?.textFields?.first?.text ?? "") }) present(alert, animated: true) } } @MainActor protocol IssueSwipeActionHost: AnyObject { var context: AppContext { get } var owner: String { get } var repository: String { get } var issueMutationTask: Task? { get set } func reloadIssuesAfterMutation() } extension IssueSwipeActionHost where Self: UIViewController { func issueSwipeActions(for row: IssueRow) -> UISwipeActionsConfiguration { let close = row.state != .closed let stateAction = UIContextualAction( style: .normal, title: close ? "Close" : "Open" ) { [weak self] _, _, completion in self?.setIssue(row, closed: close, completion: completion) ?? completion(false) } stateAction.image = context.symbol(close ? "checkmark.circle" : "arrow.uturn.left.circle") stateAction.backgroundColor = close ? .systemPurple : .systemGreen let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completion in self?.confirmDeleteIssue(row, completion: completion) ?? completion(false) } deleteAction.image = context.symbol("trash") let configuration = UISwipeActionsConfiguration(actions: [stateAction, deleteAction]) configuration.performsFirstActionWithFullSwipe = true return configuration } private func setIssue( _ row: IssueRow, closed: Bool, completion: @escaping (Bool) -> Void ) { issueMutationTask?.cancel() issueMutationTask = Task { do { try await context.core.setIssueClosed( owner: owner, repository: repository, number: row.number, closed: closed ) guard !Task.isCancelled else { completion(false) return } completion(true) reloadIssuesAfterMutation() } catch { completion(false) if !Task.isCancelled { show(error: error) } } } } private func confirmDeleteIssue(_ row: IssueRow, completion: @escaping (Bool) -> Void) { let alert = UIAlertController( title: "Delete Issue #\(row.number)?", message: "“\(row.title)” will be permanently deleted. This can’t be undone.", preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completion(false) }) alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in guard let self else { completion(false) return } self.issueMutationTask?.cancel() self.issueMutationTask = Task { do { try await self.context.core.deleteIssue( owner: self.owner, repository: self.repository, number: row.number ) guard !Task.isCancelled else { completion(false) return } completion(true) self.reloadIssuesAfterMutation() } catch { completion(false) if !Task.isCancelled { self.show(error: error) } } } }) present(alert, animated: true) } }