From 2dcb23a998c68421a4f7bc10dc430480bc36f5f0 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Mon, 18 Mar 2019 18:17:34 +0530 Subject: [PATCH] iOS: Tunnels list: Ability to remove multiple tunnels at a time --- .../WireGuard/Base.lproj/Localizable.strings | 8 ++ .../UI/iOS/View/TunnelListCell.swift | 5 + .../TunnelsListTableViewController.swift | 136 +++++++++++++++++- 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings index fc17428..abf7236 100644 --- a/WireGuard/WireGuard/Base.lproj/Localizable.strings +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -13,6 +13,10 @@ "tunnelsListSettingsButtonTitle" = "Settings"; "tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel"; "tunnelsListSwipeDeleteButtonTitle" = "Delete"; +"tunnelsListSelectButtonTitle" = "Select"; +"tunnelsListSelectAllButtonTitle" = "Select All"; +"tunnelsListDeleteButtonTitle" = "Delete"; +"tunnelsListSelectedTitle (%d)" = "%d selected"; // Tunnels list menu @@ -32,6 +36,10 @@ "alertBadConfigImportTitle" = "Unable to import tunnel"; "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 "newTunnelViewTitle" = "New configuration"; diff --git a/WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift b/WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift index 81607de..914acc2 100644 --- a/WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift +++ b/WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift @@ -98,6 +98,11 @@ class TunnelListCell: UITableViewCell { 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() { statusSwitch.isOn = false statusSwitch.isUserInteractionEnabled = false diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift index e8e0a52..c09bb34 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift @@ -9,6 +9,12 @@ class TunnelsListTableViewController: UIViewController { var tunnelsManager: TunnelsManager? + enum TableState: Equatable { + case normal + case rowSwiped + case multiSelect(selectionCount: Int) + } + let tableView: UITableView = { let tableView = UITableView(frame: CGRect.zero, style: .plain) tableView.estimatedRowHeight = 60 @@ -32,6 +38,11 @@ class TunnelsListTableViewController: UIViewController { }() var detailDisplayedTunnel: TunnelContainer? + var tableState: TableState = .normal { + didSet { + handleTableStateChange() + } + } override func loadView() { view = UIView() @@ -74,13 +85,39 @@ class TunnelsListTableViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = tr("tunnelsListTitle") - 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:))) - + tableState = .normal 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) { self.tunnelsManager = tunnelsManager tunnelsManager.tunnelsListDelegate = self @@ -159,6 +196,74 @@ class TunnelsListTableViewController: UIViewController { scanQRCodeNC.modalPresentationStyle = .fullScreen 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 { @@ -210,6 +315,10 @@ extension TunnelsListTableViewController: UITableViewDataSource { extension TunnelsListTableViewController: UITableViewDelegate { 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 } let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, @@ -220,6 +329,13 @@ extension TunnelsListTableViewController: UITableViewDelegate { 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, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in @@ -236,6 +352,18 @@ extension TunnelsListTableViewController: UITableViewDelegate { } 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 {