iOS: Tunnels list: Ability to remove multiple tunnels at a time

Signed-off-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
Roopesh Chander 2019-03-18 18:17:34 +05:30 committed by Jason A. Donenfeld
parent 0dd22ca45a
commit adc5a7cac2
3 changed files with 145 additions and 4 deletions

View File

@ -13,6 +13,10 @@
"tunnelsListSettingsButtonTitle" = "Settings"; "tunnelsListSettingsButtonTitle" = "Settings";
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel"; "tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
"tunnelsListSwipeDeleteButtonTitle" = "Delete"; "tunnelsListSwipeDeleteButtonTitle" = "Delete";
"tunnelsListSelectButtonTitle" = "Select";
"tunnelsListSelectAllButtonTitle" = "Select All";
"tunnelsListDeleteButtonTitle" = "Delete";
"tunnelsListSelectedTitle (%d)" = "%d selected";
// Tunnels list menu // Tunnels list menu
@ -32,6 +36,10 @@
"alertBadConfigImportTitle" = "Unable to import tunnel"; "alertBadConfigImportTitle" = "Unable to import tunnel";
"alertBadConfigImportMessage (%@)" = "The file %@ does not contain a valid WireGuard configuration"; "alertBadConfigImportMessage (%@)" = "The file %@ does not contain a valid WireGuard configuration";
"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Delete %d tunnel";
"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Delete %d tunnels";
// Tunnel detail and edit UI // Tunnel detail and edit UI
"newTunnelViewTitle" = "New configuration"; "newTunnelViewTitle" = "New configuration";

View File

@ -98,6 +98,11 @@ class TunnelListCell: UITableViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
statusSwitch.isEnabled = !editing
}
private func reset() { private func reset() {
statusSwitch.isOn = false statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false statusSwitch.isUserInteractionEnabled = false

View File

@ -9,6 +9,12 @@ class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager? var tunnelsManager: TunnelsManager?
enum TableState: Equatable {
case normal
case rowSwiped
case multiSelect(selectionCount: Int)
}
let tableView: UITableView = { let tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain) let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.estimatedRowHeight = 60 tableView.estimatedRowHeight = 60
@ -32,6 +38,11 @@ class TunnelsListTableViewController: UIViewController {
}() }()
var detailDisplayedTunnel: TunnelContainer? var detailDisplayedTunnel: TunnelContainer?
var tableState: TableState = .normal {
didSet {
handleTableStateChange()
}
}
override func loadView() { override func loadView() {
view = UIView() view = UIView()
@ -74,13 +85,39 @@ class TunnelsListTableViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = tr("tunnelsListTitle") tableState = .normal
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC" restorationIdentifier = "TunnelsListVC"
} }
func handleTableStateChange() {
switch tableState {
case .normal:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
case .rowSwiped:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectButtonTitle"), style: .plain, target: self, action: #selector(selectButtonTapped))
case .multiSelect(let selectionCount):
if selectionCount > 0 {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListDeleteButtonTitle"), style: .plain, target: self, action: #selector(deleteButtonTapped(sender:)))
} else {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectAllButtonTitle"), style: .plain, target: self, action: #selector(selectAllButtonTapped))
}
}
if case .multiSelect(let selectionCount) = tableState, selectionCount > 0 {
navigationItem.title = tr(format: "tunnelsListSelectedTitle (%d)", selectionCount)
} else {
navigationItem.title = tr("tunnelsListTitle")
}
if case .multiSelect = tableState {
tableView.allowsMultipleSelectionDuringEditing = true
} else {
tableView.allowsMultipleSelectionDuringEditing = false
}
}
func setTunnelsManager(tunnelsManager: TunnelsManager) { func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self tunnelsManager.tunnelsListDelegate = self
@ -159,6 +196,74 @@ class TunnelsListTableViewController: UIViewController {
scanQRCodeNC.modalPresentationStyle = .fullScreen scanQRCodeNC.modalPresentationStyle = .fullScreen
present(scanQRCodeNC, animated: true) present(scanQRCodeNC, animated: true)
} }
@objc func selectButtonTapped() {
let shouldCancelSwipe = tableState == .rowSwiped
tableState = .multiSelect(selectionCount: 0)
if shouldCancelSwipe {
tableView.setEditing(false, animated: false)
}
tableView.setEditing(true, animated: true)
}
@objc func doneButtonTapped() {
tableState = .normal
tableView.setEditing(false, animated: true)
}
@objc func selectAllButtonTapped() {
guard tableView.isEditing else { return }
guard let tunnelsManager = tunnelsManager else { return }
for index in 0 ..< tunnelsManager.numberOfTunnels() {
tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)
}
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
}
@objc func cancelButtonTapped() {
tableState = .normal
tableView.setEditing(false, animated: true)
}
@objc func deleteButtonTapped(sender: AnyObject?) {
guard let sender = sender as? UIBarButtonItem else { return }
guard let tunnelsManager = tunnelsManager else { return }
let selectedTunnelIndices = tableView.indexPathsForSelectedRows?.map { $0.row } ?? []
let selectedTunnels = selectedTunnelIndices.compactMap { tunnelIndex in
tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() ? tunnelsManager.tunnel(at: tunnelIndex) : nil
}
guard !selectedTunnels.isEmpty else { return }
let message = selectedTunnels.count == 1 ?
tr(format: "deleteTunnelConfirmationAlertButtonMessage (%d)", selectedTunnels.count) :
tr(format: "deleteTunnelsConfirmationAlertButtonMessage (%d)", selectedTunnels.count)
let title = tr("deleteTunnelsConfirmationAlertButtonTitle")
self.showConfirmationAlert(message: message, buttonTitle: title, from: sender) { [weak self] in
self?.tunnelsManager?.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
guard let self = self else { return }
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
self.tableState = .normal
self.tableView.setEditing(false, animated: true)
}
}
}
func showConfirmationAlert(message: String, buttonTitle: String, from barButtonItem: UIBarButtonItem, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.barButtonItem = barButtonItem
present(alert, animated: true, completion: nil)
}
} }
extension TunnelsListTableViewController: UIDocumentPickerDelegate { extension TunnelsListTableViewController: UIDocumentPickerDelegate {
@ -210,6 +315,10 @@ extension TunnelsListTableViewController: UITableViewDataSource {
extension TunnelsListTableViewController: UITableViewDelegate { extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard !tableView.isEditing else {
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
return
}
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
@ -220,6 +329,13 @@ extension TunnelsListTableViewController: UITableViewDelegate {
detailDisplayedTunnel = tunnel detailDisplayedTunnel = tunnel
} }
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard !tableView.isEditing else {
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
return
}
}
func tableView(_ tableView: UITableView, func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in
@ -236,6 +352,18 @@ extension TunnelsListTableViewController: UITableViewDelegate {
} }
return UISwipeActionsConfiguration(actions: [deleteAction]) return UISwipeActionsConfiguration(actions: [deleteAction])
} }
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
if tableState == .normal {
tableState = .rowSwiped
}
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
if tableState == .rowSwiped {
tableState = .normal
}
}
} }
extension TunnelsListTableViewController: TunnelsManagerListDelegate { extension TunnelsListTableViewController: TunnelsManagerListDelegate {