diff options
Diffstat (limited to 'WireGuard/WireGuard/UI/iOS')
9 files changed, 436 insertions, 394 deletions
diff --git a/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift b/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift index 6cae1e6..7c28495 100644 --- a/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift +++ b/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift @@ -9,7 +9,7 @@ class ErrorPresenter { onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) { guard let sourceVC = sourceVC else { return } guard let (title, message) = error.alertText() else { return } - let okAction = UIAlertAction(title: "OK", style: .default) { (_) in + let okAction = UIAlertAction(title: "OK", style: .default) { _ in onDismissal?() } let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -21,7 +21,7 @@ class ErrorPresenter { static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) { guard let sourceVC = sourceVC else { return } - let okAction = UIAlertAction(title: "OK", style: .default) { (_) in + let okAction = UIAlertAction(title: "OK", style: .default) { _ in onDismissal?() } let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) diff --git a/WireGuard/WireGuard/UI/iOS/MainViewController.swift b/WireGuard/WireGuard/UI/iOS/MainViewController.swift index 70d838e..6822263 100644 --- a/WireGuard/WireGuard/UI/iOS/MainViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/MainViewController.swift @@ -75,7 +75,7 @@ extension MainViewController { } func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) { - let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] (tunnelsManager) in + let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in if let tunnel = tunnelsManager.tunnel(named: tunnelName) { let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) diff --git a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift index 3849f70..ad0fe79 100644 --- a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift @@ -33,7 +33,7 @@ class QRScanViewController: UIViewController { tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32) - ]) + ]) guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), @@ -114,10 +114,10 @@ class QRScanViewController: UIViewController { let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert) alert.addTextField(configurationHandler: nil) - alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { [weak self] _ in + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { [weak self] _ in self?.dismiss(animated: true, completion: nil) - })) - alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default, handler: { [weak self] _ in + }) + alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default) { [weak self] _ in guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return } tunnelConfiguration.interface.name = title if let self = self { @@ -125,15 +125,15 @@ class QRScanViewController: UIViewController { self.dismiss(animated: true, completion: nil) } } - })) + }) present(alert, animated: true) } func scanDidEncounterError(title: String, message: String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in + alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in self?.dismiss(animated: true, completion: nil) - })) + }) present(alertController, animated: true) captureSession = nil } diff --git a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift index c0cc86b..f2d0f58 100644 --- a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift +++ b/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift @@ -35,7 +35,7 @@ class ScrollableLabel: UIScrollView { label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor), label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor), label.heightAnchor.constraint(equalTo: self.heightAnchor) - ]) + ]) // If label has less content, it should expand to fit the scrollView, // so that right-alignment works in the label. let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, diff --git a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift index 3107579..8f248c9 100644 --- a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift @@ -40,8 +40,8 @@ class SettingsTableViewController: UITableViewController { self.tableView.rowHeight = UITableView.automaticDimension self.tableView.allowsSelection = false - self.tableView.register(TunnelSettingsTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelSettingsTableViewKeyValueCell.reuseIdentifier) - self.tableView.register(TunnelSettingsTableViewButtonCell.self, forCellReuseIdentifier: TunnelSettingsTableViewButtonCell.reuseIdentifier) + self.tableView.register(KeyValueCell.self) + self.tableView.register(ButtonCell.self) let logo = UIImageView(image: UIImage(named: "wireguard.pdf", in: Bundle.main, compatibleWith: nil)!) logo.contentMode = .scaleAspectFit @@ -76,7 +76,7 @@ class SettingsTableViewController: UITableViewController { let count = tunnelsManager.numberOfTunnels() let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() } - ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] (error) in + ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in if let error = error { ErrorPresenter.showErrorAlert(error: error, from: self) return @@ -127,7 +127,7 @@ class SettingsTableViewController: UITableViewController { // popoverPresentationController shall be non-nil on the iPad activityVC.popoverPresentationController?.sourceView = sourceView activityVC.popoverPresentationController?.sourceRect = sourceView.bounds - activityVC.completionWithItemsHandler = { (_, _, _, _) in + activityVC.completionWithItemsHandler = { _, _, _, _ in // Remove the exported log file after the activity has completed _ = FileManager.deleteFile(at: destinationURL) } @@ -164,7 +164,7 @@ extension SettingsTableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let field = settingsFieldsBySection[indexPath.section][indexPath.row] if field == .iosAppVersion || field == .goBackendVersion { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelSettingsTableViewKeyValueCell + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue if field == .iosAppVersion { var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" @@ -177,7 +177,7 @@ extension SettingsTableViewController { } return cell } else if field == .exportZipArchive { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelSettingsTableViewButtonCell + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.rawValue cell.onTapped = { [weak self] in self?.exportConfigurationsAsZipFile(sourceView: cell.button) @@ -185,7 +185,7 @@ extension SettingsTableViewController { return cell } else { assert(field == .exportLogFile) - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelSettingsTableViewButtonCell + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.rawValue cell.onTapped = { [weak self] in self?.exportLogForLastActivatedTunnel(sourceView: cell.button) @@ -195,8 +195,7 @@ extension SettingsTableViewController { } } -class TunnelSettingsTableViewKeyValueCell: UITableViewCell { - static let reuseIdentifier = "TunnelSettingsTableViewKeyValueCell" +private class KeyValueCell: UITableViewCell { var key: String { get { return textLabel?.text ?? "" } set(value) { textLabel?.text = value } @@ -207,7 +206,7 @@ class TunnelSettingsTableViewKeyValueCell: UITableViewCell { } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .value1, reuseIdentifier: TunnelSettingsTableViewKeyValueCell.reuseIdentifier) + super.init(style: .value1, reuseIdentifier: KeyValueCell.reuseIdentifier) } required init?(coder aDecoder: NSCoder) { @@ -221,8 +220,7 @@ class TunnelSettingsTableViewKeyValueCell: UITableViewCell { } } -class TunnelSettingsTableViewButtonCell: UITableViewCell { - static let reuseIdentifier = "TunnelSettingsTableViewButtonCell" +private class ButtonCell: UITableViewCell { var buttonText: String { get { return button.title(for: .normal) ?? "" } set(value) { button.setTitle(value, for: .normal) } @@ -242,7 +240,7 @@ class TunnelSettingsTableViewButtonCell: UITableViewCell { button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor), button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) - ]) + ]) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) } diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift index 2ab3c26..e6ad024 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift @@ -7,6 +7,14 @@ import UIKit class TunnelDetailTableViewController: UITableViewController { + private enum Section { + case status + case interface + case peer(_ peer: TunnelViewModel.PeerData) + case onDemand + case delete + } + let interfaceFields: [TunnelViewModel.InterfaceField] = [ .name, .publicKey, .addresses, .listenPort, .mtu, .dns @@ -20,12 +28,14 @@ class TunnelDetailTableViewController: UITableViewController { let tunnelsManager: TunnelsManager let tunnel: TunnelContainer var tunnelViewModel: TunnelViewModel + private var sections = [Section]() init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { self.tunnelsManager = tunnelsManager self.tunnel = tunnel tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) super.init(style: .grouped) + loadSections() } required init?(coder aDecoder: NSCoder) { @@ -40,15 +50,24 @@ class TunnelDetailTableViewController: UITableViewController { self.tableView.estimatedRowHeight = 44 self.tableView.rowHeight = UITableView.automaticDimension self.tableView.allowsSelection = false - self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.reuseIdentifier) - self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.reuseIdentifier) - self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.reuseIdentifier) - self.tableView.register(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.reuseIdentifier) + self.tableView.register(StatusCell.self) + self.tableView.register(KeyValueCell.self) + self.tableView.register(ButtonCell.self) + self.tableView.register(ActivateOnDemandCell.self) // State restoration self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" } + private func loadSections() { + sections.removeAll() + sections.append(.status) + sections.append(.interface) + tunnelViewModel.peersData.forEach { sections.append(.peer($0)) } + sections.append(.onDemand) + sections.append(.delete) + } + @objc func editTapped() { let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) editVC.delegate = self @@ -59,7 +78,7 @@ class TunnelDetailTableViewController: UITableViewController { func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) { - let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in + let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in onConfirmed() } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) @@ -80,6 +99,7 @@ class TunnelDetailTableViewController: UITableViewController { extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate { func tunnelSaved(tunnel: TunnelContainer) { tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) + loadSections() self.title = tunnel.name self.tableView.reloadData() } @@ -92,136 +112,125 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate extension TunnelDetailTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { - return 4 + tunnelViewModel.peersData.count + return sections.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let interfaceData = tunnelViewModel.interfaceData - let numberOfPeerSections = tunnelViewModel.peersData.count - - if section == 0 { - // Status + switch sections[section] { + case .status: return 1 - } else if section == 1 { - // Interface - return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count - } else if (numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections)) { - // Peer - let peerData = tunnelViewModel.peersData[section - 2] + case .interface: + return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count + case .peer(let peerData): return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count - } else if section < (3 + numberOfPeerSections) { - // Activate on demand + case .onDemand: return 1 - } else { - assert(section == (3 + numberOfPeerSections)) - // Delete tunnel + case .delete: return 1 } } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let numberOfPeerSections = tunnelViewModel.peersData.count - - if section == 0 { - // Status + switch sections[section] { + case .status: return "Status" - } else if section == 1 { - // Interface + case .interface: return "Interface" - } else if (numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections)) { - // Peer + case .peer: return "Peer" - } else if section < (3 + numberOfPeerSections) { - // On-Demand Activation + case .onDemand: return "On-Demand Activation" - } else { - // Delete tunnel + case .delete: return nil } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let interfaceData = tunnelViewModel.interfaceData - let numberOfPeerSections = tunnelViewModel.peersData.count - - let section = indexPath.section - let row = indexPath.row - - if section == 0 { - // Status - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewStatusCell.reuseIdentifier, for: indexPath) as! TunnelDetailTableViewStatusCell - cell.tunnel = self.tunnel - cell.onSwitchToggled = { [weak self] isOn in - guard let self = self else { return } - if isOn { - self.tunnelsManager.startActivation(of: self.tunnel) { [weak self] error in - if let error = error { - ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: { - DispatchQueue.main.async { - cell.statusSwitch.isOn = false - } - }) + switch sections[indexPath.section] { + case .status: + return statusCell(for: tableView, at: indexPath) + case .interface: + return interfaceCell(for: tableView, at: indexPath) + case .peer(let peer): + return peerCell(for: tableView, at: indexPath, with: peer) + case .onDemand: + return onDemandCell(for: tableView, at: indexPath) + case .delete: + return deleteConfigurationCell(for: tableView, at: indexPath) + } + } + + private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let cell: StatusCell = tableView.dequeueReusableCell(for: indexPath) + cell.tunnel = self.tunnel + cell.onSwitchToggled = { [weak self] isOn in + guard let self = self else { return } + if isOn { + self.tunnelsManager.startActivation(of: self.tunnel) { [weak self] error in + if let error = error { + ErrorPresenter.showErrorAlert(error: error, from: self) { + DispatchQueue.main.async { + cell.statusSwitch.isOn = false + } } } - } else { - self.tunnelsManager.startDeactivation(of: self.tunnel) } + } else { + self.tunnelsManager.startDeactivation(of: self.tunnel) } - return cell - } else if section == 1 { - // Interface - let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row] - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelDetailTableViewKeyValueCell - // Set key and value - cell.key = field.rawValue - cell.value = interfaceData[field] - return cell - } else if (numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections)) { - // Peer - let peerData = tunnelViewModel.peersData[section - 2] - let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row] - - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelDetailTableViewKeyValueCell - // Set key and value - cell.key = field.rawValue - cell.value = peerData[field] - return cell - } else if section < (3 + numberOfPeerSections) { - // On-Demand Activation - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewActivateOnDemandCell.reuseIdentifier, for: indexPath) as! TunnelDetailTableViewActivateOnDemandCell - cell.tunnel = self.tunnel - return cell - } else { - assert(section == (3 + numberOfPeerSections)) - // Delete configuration - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelDetailTableViewButtonCell - cell.buttonText = "Delete tunnel" - cell.hasDestructiveAction = true - cell.onTapped = { [weak self] in - guard let self = self else { return } - self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in - guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return } - tunnelsManager.remove(tunnel: tunnel) { (error) in - if error != nil { - print("Error removing tunnel: \(String(describing: error))") - return - } + } + return cell + } + + private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row] + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = field.rawValue + cell.value = tunnelViewModel.interfaceData[field] + return cell + } + + private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell { + let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row] + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = field.rawValue + cell.value = peerData[field] + return cell + } + + private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let cell: ActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath) + cell.tunnel = self.tunnel + return cell + } + + private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = "Delete tunnel" + cell.hasDestructiveAction = true + cell.onTapped = { [weak self] in + guard let self = self else { return } + self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in + guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return } + tunnelsManager.remove(tunnel: tunnel) { error in + if error != nil { + print("Error removing tunnel: \(String(describing: error))") + return } - self?.navigationController?.navigationController?.popToRootViewController(animated: true) } + self?.navigationController?.navigationController?.popToRootViewController(animated: true) } - return cell } + return cell } -} -class TunnelDetailTableViewStatusCell: UITableViewCell { - static let reuseIdentifier = "TunnelDetailTableViewStatusCell" +} +private class StatusCell: UITableViewCell { var tunnel: TunnelContainer? { didSet(value) { update(from: tunnel?.status) - statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in + statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in self?.update(from: tunnel.status) } } @@ -238,7 +247,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { statusSwitch = UISwitch() - super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.reuseIdentifier) + super.init(style: .default, reuseIdentifier: KeyValueCell.reuseIdentifier) accessoryView = statusSwitch statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged) @@ -296,8 +305,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell { } } -class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell { - static let reuseIdentifier = "TunnelDetailTableViewKeyValueCell" +private class KeyValueCell: CopyableLabelTableViewCell { var key: String { get { return keyLabel.text ?? "" } set(value) { keyLabel.text = value } @@ -337,14 +345,14 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell { NSLayoutConstraint.activate([ keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) - ]) + ]) contentView.addSubview(valueLabel) valueLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabel.bottomAnchor, multiplier: 0.5) - ]) + ]) // Key label should never appear truncated keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) @@ -399,8 +407,7 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell { } } -class TunnelDetailTableViewButtonCell: UITableViewCell { - static let reuseIdentifier = "TunnelDetailTableViewButtonCell" +private class ButtonCell: UITableViewCell { var buttonText: String { get { return button.title(for: .normal) ?? "" } set(value) { button.setTitle(value, for: .normal) } @@ -426,7 +433,7 @@ class TunnelDetailTableViewButtonCell: UITableViewCell { button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor), button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) - ]) + ]) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) } @@ -446,13 +453,11 @@ class TunnelDetailTableViewButtonCell: UITableViewCell { } } -class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell { - static let reuseIdentifier = "TunnelDetailTableViewActivateOnDemandCell" - +private class ActivateOnDemandCell: UITableViewCell { var tunnel: TunnelContainer? { didSet(value) { update(from: tunnel?.activateOnDemandSetting()) - onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] (tunnel, _) in + onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in self?.update(from: tunnel.activateOnDemandSetting()) } } diff --git a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift index 330a3cc..b26992d 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift @@ -12,6 +12,13 @@ protocol TunnelEditTableViewControllerDelegate: class { class TunnelEditTableViewController: UITableViewController { + private enum Section { + case interface + case peer(_ peer: TunnelViewModel.PeerData) + case addPeer + case onDemand + } + weak var delegate: TunnelEditTableViewControllerDelegate? let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [ @@ -36,9 +43,7 @@ class TunnelEditTableViewController: UITableViewController { let tunnel: TunnelContainer? let tunnelViewModel: TunnelViewModel var activateOnDemandSetting: ActivateOnDemandSetting - - private var interfaceSectionCount: Int { return interfaceFieldsBySection.count } - private var peerSectionCount: Int { return tunnelViewModel.peersData.count } + private var sections = [Section]() init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { // Use this initializer to edit an existing tunnel. @@ -47,6 +52,7 @@ class TunnelEditTableViewController: UITableViewController { tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) activateOnDemandSetting = tunnel.activateOnDemandSetting() super.init(style: .grouped) + loadSections() } init(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) { @@ -57,6 +63,7 @@ class TunnelEditTableViewController: UITableViewController { tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration) activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting super.init(style: .grouped) + loadSections() } required init?(coder aDecoder: NSCoder) { @@ -72,11 +79,19 @@ class TunnelEditTableViewController: UITableViewController { self.tableView.estimatedRowHeight = 44 self.tableView.rowHeight = UITableView.automaticDimension - self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.reuseIdentifier) - self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.reuseIdentifier) - self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.reuseIdentifier) - self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.reuseIdentifier) - self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.reuseIdentifier) + self.tableView.register(KeyValueCell.self) + self.tableView.register(ReadOnlyKeyValueCell.self) + self.tableView.register(ButtonCell.self) + self.tableView.register(SwitchCell.self) + self.tableView.register(SelectionListCell.self) + } + + private func loadSections() { + sections.removeAll() + interfaceFieldsBySection.forEach { _ in sections.append(.interface) } + tunnelViewModel.peersData.forEach { sections.append(.peer($0)) } + sections.append(.addPeer) + sections.append(.onDemand) } @objc func saveTapped() { @@ -92,7 +107,7 @@ class TunnelEditTableViewController: UITableViewController { // We're modifying an existing tunnel tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, - activateOnDemandSetting: activateOnDemandSetting) { [weak self] (error) in + activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in if let error = error { ErrorPresenter.showErrorAlert(error: error, from: self) } else { @@ -126,24 +141,19 @@ class TunnelEditTableViewController: UITableViewController { extension TunnelEditTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { - return interfaceSectionCount + peerSectionCount + 1 /* Add Peer */ + 1 /* On-Demand */ + return sections.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section < interfaceSectionCount { - // Interface + switch sections[section] { + case .interface: return interfaceFieldsBySection[section].count - } else if (peerSectionCount > 0) && (section < (interfaceSectionCount + peerSectionCount)) { - // Peer - let peerIndex = (section - interfaceSectionCount) - let peerData = tunnelViewModel.peersData[peerIndex] + case .peer(let peerData): let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs } return peerFieldsToShow.count - } else if section < (interfaceSectionCount + peerSectionCount + 1) { - // Add peer + case .addPeer: return 1 - } else { - // On-Demand Rules + case .onDemand: if activateOnDemandSetting.isActivateOnDemandEnabled { return 4 } else { @@ -153,222 +163,239 @@ extension TunnelEditTableViewController { } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if section < interfaceSectionCount { - // Interface - return (section == 0) ? "Interface" : nil - } else if (peerSectionCount > 0) && (section < (interfaceSectionCount + peerSectionCount)) { - // Peer + switch sections[section] { + case .interface: + return section == 0 ? "Interface" : nil + case .peer: return "Peer" - } else if section == (interfaceSectionCount + peerSectionCount) { - // Add peer + case .addPeer: return nil - } else { - assert(section == (interfaceSectionCount + peerSectionCount + 1)) + case .onDemand: return "On-Demand Activation" } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section < interfaceSectionCount { + switch sections[indexPath.section] { + case .interface: return interfaceFieldCell(for: tableView, at: indexPath) - } else if (peerSectionCount > 0) && (indexPath.section < (interfaceSectionCount + peerSectionCount)) { - return peerCell(for: tableView, at: indexPath) - } else if indexPath.section == (interfaceSectionCount + peerSectionCount) { + case .peer(let peerData): + return peerCell(for: tableView, at: indexPath, with: peerData) + case .addPeer: return addPeerCell(for: tableView, at: indexPath) - } else { + case .onDemand: return onDemandCell(for: tableView, at: indexPath) } } private func interfaceFieldCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let interfaceData = tunnelViewModel.interfaceData let field = interfaceFieldsBySection[indexPath.section][indexPath.row] - if field == .generateKeyPair { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewButtonCell - cell.buttonText = field.rawValue - cell.onTapped = { [weak self, weak interfaceData] in - if let interfaceData = interfaceData, let self = self { - interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString() - if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey), - let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) { - let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section) - let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section) - self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .automatic) - } - } - } - return cell - } else if field == .publicKey { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewReadOnlyKeyValueCell - cell.key = field.rawValue - cell.value = interfaceData[field] - return cell - } else { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewKeyValueCell - // Set key - cell.key = field.rawValue - // Set placeholder text - switch field { - case .name: - cell.placeholderText = "Required" - case .privateKey: - cell.placeholderText = "Required" - case .addresses: - cell.placeholderText = "Optional" - case .listenPort: - cell.placeholderText = "Automatic" - case .mtu: - cell.placeholderText = "Automatic" - case .dns: - cell.placeholderText = "Optional" - case .publicKey: break - case .generateKeyPair: break + switch field { + case .generateKeyPair: + return generateKeyPairCell(for: tableView, at: indexPath, with: field) + case .publicKey: + return publicKeyCell(for: tableView, at: indexPath, with: field) + default: + return interfaceFieldKeyValueCell(for: tableView, at: indexPath, with: field) + } + } + + private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + guard let self = self else { return } + + self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString() + if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey), + let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) { + let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section) + let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section) + self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .fade) } - // Set keyboardType - if field == .mtu || field == .listenPort { - cell.keyboardType = .numberPad - } else if field == .addresses || field == .dns { - cell.keyboardType = .numbersAndPunctuation + } + return cell + } + + private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { + let cell: ReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = field.rawValue + cell.value = tunnelViewModel.interfaceData[field] + return cell + } + + private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = field.rawValue + + switch field { + case .name, .privateKey: + cell.placeholderText = "Required" + cell.keyboardType = .default + case .addresses, .dns: + cell.placeholderText = "Optional" + cell.keyboardType = .numbersAndPunctuation + case .listenPort, .mtu: + cell.placeholderText = "Automatic" + cell.keyboardType = .numberPad + case .publicKey, .generateKeyPair: + cell.keyboardType = .default + } + + cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field)) + // Bind values to view model + cell.value = tunnelViewModel.interfaceData[field] + if field == .dns { // While editing DNS, you might directly set exclude private IPs + cell.onValueChanged = nil + cell.onValueBeingEdited = { [weak self] value in + self?.tunnelViewModel.interfaceData[field] = value } - // Show erroring fields - cell.isValueValid = (!interfaceData.fieldsWithError.contains(field)) - // Bind values to view model - cell.value = interfaceData[field] - if field == .dns { // While editing DNS, you might directly set exclude private IPs - cell.onValueBeingEdited = { [weak interfaceData] value in - interfaceData?[field] = value - } - } else { - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[field] = value - } + } else { + cell.onValueChanged = { [weak self] value in + self?.tunnelViewModel.interfaceData[field] = value } - // Compute public key live - if field == .privateKey { - cell.onValueBeingEdited = { [weak self, weak interfaceData] value in - if let interfaceData = interfaceData, let self = self { - interfaceData[.privateKey] = value - if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) { - self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none) - } - } + cell.onValueBeingEdited = nil + } + // Compute public key live + if field == .privateKey { + cell.onValueBeingEdited = { [weak self] value in + guard let self = self else { return } + + self.tunnelViewModel.interfaceData[.privateKey] = value + if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) { + self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none) } } - return cell + } else { + cell.onValueBeingEdited = nil } + return cell } - private func peerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let peerIndex = indexPath.section - interfaceFieldsBySection.count - let peerData = tunnelViewModel.peersData[peerIndex] + private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell { let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs } let field = peerFieldsToShow[indexPath.row] - if field == .deletePeer { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewButtonCell - cell.buttonText = field.rawValue - cell.hasDestructiveAction = true - cell.onTapped = { [weak self, weak peerData] in - guard let peerData = peerData else { return } + + switch field { + case .deletePeer: + return deletePeerCell(for: tableView, at: indexPath, peerData: peerData, field: field) + case .excludePrivateIPs: + return excludePrivateIPsCell(for: tableView, at: indexPath, peerData: peerData, field: field) + default: + return peerFieldKeyValueCell(for: tableView, at: indexPath, peerData: peerData, field: field) + } + } + + private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.hasDestructiveAction = true + cell.onTapped = { [weak self, weak peerData] in + guard let peerData = peerData else { return } + guard let self = self else { return } + self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in guard let self = self else { return } - self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in - guard let self = self else { return } - let removedSectionIndices = self.deletePeer(peer: peerData) - let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) - tableView.performBatchUpdates({ - self.tableView.deleteSections(removedSectionIndices, with: .automatic) - if shouldShowExcludePrivateIPs { - if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { - let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */) - self.tableView.insertRows(at: [rowIndexPath], with: .automatic) - } - + let removedSectionIndices = self.deletePeer(peer: peerData) + let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) + tableView.performBatchUpdates({ + self.tableView.deleteSections(removedSectionIndices, with: .fade) + if shouldShowExcludePrivateIPs { + if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { + let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */) + self.tableView.insertRows(at: [rowIndexPath], with: .fade) } - }) - } - } - return cell - } else if field == .excludePrivateIPs { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewSwitchCell - cell.message = field.rawValue - cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl - cell.isOn = peerData.excludePrivateIPsValue - cell.onSwitchToggled = { [weak self] (isOn) in - guard let self = self else { return } - peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns]) - if let row = self.peerFields.firstIndex(of: .allowedIPs) { - self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none) - } - } - return cell - } else { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewKeyValueCell - // Set key - cell.key = field.rawValue - // Set placeholder text - switch field { - case .publicKey: - cell.placeholderText = "Required" - case .preSharedKey: - cell.placeholderText = "Optional" - case .endpoint: - cell.placeholderText = "Optional" - case .allowedIPs: - cell.placeholderText = "Optional" - case .persistentKeepAlive: - cell.placeholderText = "Off" - case .excludePrivateIPs: break - case .deletePeer: break + + } + }) } - // Set keyboardType - if field == .persistentKeepAlive { - cell.keyboardType = .numberPad - } else if field == .allowedIPs { - cell.keyboardType = .numbersAndPunctuation + } + return cell + } + + private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { + let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) + cell.message = field.rawValue + cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl + cell.isOn = peerData.excludePrivateIPsValue + cell.onSwitchToggled = { [weak self] isOn in + guard let self = self else { return } + peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns]) + if let row = self.peerFields.firstIndex(of: .allowedIPs) { + self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none) } - // Show erroring fields - cell.isValueValid = (!peerData.fieldsWithError.contains(field)) - // Bind values to view model - cell.value = peerData[field] - if field != .allowedIPs { - cell.onValueChanged = { [weak peerData] value in - peerData?[field] = value - } + } + return cell + } + + private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = field.rawValue + + switch field { + case .publicKey: + cell.placeholderText = "Required" + case .preSharedKey, .endpoint, .allowedIPs: + cell.placeholderText = "Optional" + case .persistentKeepAlive: + cell.placeholderText = "Off" + case .excludePrivateIPs, .deletePeer: + break + } + + switch field { + case .persistentKeepAlive: + cell.keyboardType = .numberPad + case .allowedIPs: + cell.keyboardType = .numbersAndPunctuation + default: + cell.keyboardType = .default + } + + // Show erroring fields + cell.isValueValid = (!peerData.fieldsWithError.contains(field)) + // Bind values to view model + cell.value = peerData[field] + if field != .allowedIPs { + cell.onValueChanged = { [weak peerData] value in + peerData?[field] = value } - // Compute state of exclude private IPs live - if field == .allowedIPs { - cell.onValueBeingEdited = { [weak self, weak peerData] value in - if let peerData = peerData, let self = self { - let oldValue = peerData.shouldAllowExcludePrivateIPsControl - peerData[.allowedIPs] = value - if oldValue != peerData.shouldAllowExcludePrivateIPsControl { - if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { - if peerData.shouldAllowExcludePrivateIPsControl { - self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .automatic) - } else { - self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .automatic) - } + } + // Compute state of exclude private IPs live + if field == .allowedIPs { + cell.onValueBeingEdited = { [weak self, weak peerData] value in + if let peerData = peerData, let self = self { + let oldValue = peerData.shouldAllowExcludePrivateIPsControl + peerData[.allowedIPs] = value + if oldValue != peerData.shouldAllowExcludePrivateIPsControl { + if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { + if peerData.shouldAllowExcludePrivateIPsControl { + self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade) + } else { + self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade) } } } } } - return cell + } else { + cell.onValueBeingEdited = nil } + return cell } private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewButtonCell + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = "Add peer" cell.onTapped = { [weak self] in guard let self = self else { return } let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let addedSectionIndices = self.appendEmptyPeer() tableView.performBatchUpdates({ - tableView.insertSections(addedSectionIndices, with: .automatic) + tableView.insertSections(addedSectionIndices, with: .fade) if shouldHideExcludePrivateIPs { if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */) - self.tableView.deleteRows(at: [rowIndexPath], with: .automatic) + self.tableView.deleteRows(at: [rowIndexPath], with: .fade) } } }, completion: nil) @@ -377,12 +404,11 @@ extension TunnelEditTableViewController { } private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - assert(indexPath.section == interfaceSectionCount + peerSectionCount + 1) if indexPath.row == 0 { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewSwitchCell + let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) cell.message = "Activate on demand" cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled - cell.onSwitchToggled = { [weak self] (isOn) in + cell.onSwitchToggled = { [weak self] isOn in guard let self = self else { return } let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) } if isOn { @@ -390,16 +416,17 @@ extension TunnelEditTableViewController { if self.activateOnDemandSetting.activateOnDemandOption == .none { self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption() } - self.tableView.insertRows(at: indexPaths, with: .automatic) + self.loadSections() + self.tableView.insertRows(at: indexPaths, with: .fade) } else { self.activateOnDemandSetting.isActivateOnDemandEnabled = false - self.tableView.deleteRows(at: indexPaths, with: .automatic) + self.loadSections() + self.tableView.deleteRows(at: indexPaths, with: .fade) } } return cell } else { - assert(indexPath.row < 4) - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSelectionListCell.reuseIdentifier, for: indexPath) as! TunnelEditTableViewSelectionListCell + let cell: SelectionListCell = tableView.dequeueReusableCell(for: indexPath) let rowOption = activateOnDemandOptions[indexPath.row - 1] let selectedOption = activateOnDemandSetting.activateOnDemandOption assert(selectedOption != .none) @@ -411,22 +438,19 @@ extension TunnelEditTableViewController { func appendEmptyPeer() -> IndexSet { tunnelViewModel.appendEmptyPeer() + loadSections() let addedPeerIndex = tunnelViewModel.peersData.count - 1 - - let addedSectionIndices = IndexSet(integer: interfaceSectionCount + addedPeerIndex) - return addedSectionIndices + return IndexSet(integer: interfaceFieldsBySection.count + addedPeerIndex) } func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet { - assert(peer.index < tunnelViewModel.peersData.count) tunnelViewModel.deletePeer(peer: peer) - - let removedSectionIndices = IndexSet(integer: (interfaceSectionCount + peer.index)) - return removedSectionIndices + loadSections() + return IndexSet(integer: interfaceFieldsBySection.count + peer.index) } func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) { - let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in + let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in onConfirmed() } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) @@ -446,31 +470,31 @@ extension TunnelEditTableViewController { extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - if indexPath.section == (interfaceSectionCount + peerSectionCount + 1) { - return (indexPath.row > 0) ? indexPath : nil + if case .onDemand = sections[indexPath.section], indexPath.row > 0 { + return indexPath } else { return nil } } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let section = indexPath.section - let row = indexPath.row - - assert(section == (interfaceSectionCount + peerSectionCount + 1)) - assert(row > 0) - - let option = activateOnDemandOptions[row - 1] - assert(option != .none) - activateOnDemandSetting.activateOnDemandOption = option - - let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) } - tableView.reloadRows(at: indexPaths, with: .automatic) + switch sections[indexPath.section] { + case .onDemand: + let option = activateOnDemandOptions[indexPath.row - 1] + assert(option != .none) + activateOnDemandSetting.activateOnDemandOption = option + + let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) } + UIView.performWithoutAnimation { + tableView.reloadRows(at: indexPaths, with: .none) + } + default: + assertionFailure() + } } } -class TunnelEditTableViewKeyValueCell: UITableViewCell { - static let reuseIdentifier = "TunnelEditTableViewKeyValueCell" +private class KeyValueCell: UITableViewCell { var key: String { get { return keyLabel.text ?? "" } set(value) {keyLabel.text = value } @@ -532,13 +556,13 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell { keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5), widthRatioConstraint - ]) + ]) contentView.addSubview(valueTextField) valueTextField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 0.5) - ]) + ]) valueTextField.delegate = self valueTextField.autocapitalizationType = .none @@ -597,7 +621,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell { } } -extension TunnelEditTableViewKeyValueCell: UITextFieldDelegate { +extension KeyValueCell: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { textFieldValueOnBeginEditing = textField.text ?? "" isValueValid = true @@ -618,8 +642,7 @@ extension TunnelEditTableViewKeyValueCell: UITextFieldDelegate { } } -class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell { - static let reuseIdentifier = "TunnelEditTableViewReadOnlyKeyValueCell" +private class ReadOnlyKeyValueCell: CopyableLabelTableViewCell { var key: String { get { return keyLabel.text ?? "" } set(value) {keyLabel.text = value } @@ -660,7 +683,7 @@ class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell { keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), widthRatioConstraint - ]) + ]) contentView.addSubview(valueLabel) valueLabel.translatesAutoresizingMaskIntoConstraints = false @@ -668,7 +691,7 @@ class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell { valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) - ]) + ]) } override var textToCopy: String? { @@ -686,8 +709,7 @@ class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell { } } -class TunnelEditTableViewButtonCell: UITableViewCell { - static let reuseIdentifier = "TunnelEditTableViewButtonCell" +private class ButtonCell: UITableViewCell { var buttonText: String { get { return button.title(for: .normal) ?? "" } set(value) { button.setTitle(value, for: .normal) } @@ -713,7 +735,7 @@ class TunnelEditTableViewButtonCell: UITableViewCell { button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor), button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) - ]) + ]) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) } @@ -733,8 +755,7 @@ class TunnelEditTableViewButtonCell: UITableViewCell { } } -class TunnelEditTableViewSwitchCell: UITableViewCell { - static let reuseIdentifier = "TunnelEditTableViewSwitchCell" +private class SwitchCell: UITableViewCell { var message: String { get { return textLabel?.text ?? "" } set(value) { textLabel!.text = value } @@ -759,7 +780,6 @@ class TunnelEditTableViewSwitchCell: UITableViewCell { switchView = UISwitch() super.init(style: .default, reuseIdentifier: reuseIdentifier) accessoryView = switchView - switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged) } @@ -778,8 +798,7 @@ class TunnelEditTableViewSwitchCell: UITableViewCell { } } -class TunnelEditTableViewSelectionListCell: UITableViewCell { - static let reuseIdentifier = "TunnelEditTableViewSelectionListCell" +private class SelectionListCell: UITableViewCell { var message: String { get { return textLabel?.text ?? "" } set(value) { textLabel!.text = value } diff --git a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift index bd5f972..e5d5af1 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift @@ -34,7 +34,7 @@ class TunnelsListTableViewController: UIViewController { NSLayoutConstraint.activate([ busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) + ]) busyIndicator.startAnimating() self.busyIndicator = busyIndicator @@ -54,7 +54,7 @@ class TunnelsListTableViewController: UIViewController { tableView.estimatedRowHeight = 60 tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none - tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.reuseIdentifier) + tableView.register(TunnelCell.self) self.view.addSubview(tableView) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -63,7 +63,7 @@ class TunnelsListTableViewController: UIViewController { tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor), tableView.topAnchor.constraint(equalTo: self.view.topAnchor), tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) - ]) + ]) tableView.dataSource = self tableView.delegate = self self.tableView = tableView @@ -78,7 +78,7 @@ class TunnelsListTableViewController: UIViewController { NSLayoutConstraint.activate([ centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor) - ]) + ]) centeredAddButton.onTapped = { [weak self] in self?.addButtonTapped(sender: centeredAddButton) } @@ -105,17 +105,17 @@ class TunnelsListTableViewController: UIViewController { @objc func addButtonTapped(sender: AnyObject) { if self.tunnelsManager == nil { return } // Do nothing until we've loaded the tunnels let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet) - let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] (_) in + let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in self?.presentViewControllerForFileImport() } alert.addAction(importFileAction) - let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] (_) in + let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in self?.presentViewControllerForScanningQRCode() } alert.addAction(scanQRCodeAction) - let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] (_) in + let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in if let self = self, let tunnelsManager = self.tunnelsManager { self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil) } @@ -174,7 +174,7 @@ class TunnelsListTableViewController: UIViewController { return } let configs: [TunnelConfiguration?] = result.value! - tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] (numberSuccessful) in + tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in if numberSuccessful == configs.count { completionHandler?() return @@ -241,7 +241,7 @@ extension TunnelsListTableViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsListTableViewCell.reuseIdentifier, for: indexPath) as! TunnelsListTableViewCell + let cell: TunnelCell = tableView.dequeueReusableCell(for: indexPath) if let tunnelsManager = tunnelsManager { let tunnel = tunnelsManager.tunnel(at: indexPath.row) cell.tunnel = tunnel @@ -250,11 +250,11 @@ extension TunnelsListTableViewController: UITableViewDataSource { if isOn { tunnelsManager.startActivation(of: tunnel) { [weak self] error in if let error = error { - ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: { + ErrorPresenter.showErrorAlert(error: error, from: self) { DispatchQueue.main.async { cell.statusSwitch.isOn = false } - }) + } } } } else { @@ -281,18 +281,18 @@ extension TunnelsListTableViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let deleteAction = UIContextualAction(style: .destructive, title: "Delete", handler: { [weak self] (_, _, completionHandler) in + let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in guard let tunnelsManager = self?.tunnelsManager else { return } let tunnel = tunnelsManager.tunnel(at: indexPath.row) - tunnelsManager.remove(tunnel: tunnel, completionHandler: { (error) in + tunnelsManager.remove(tunnel: tunnel) { error in if error != nil { ErrorPresenter.showErrorAlert(error: error!, from: self) completionHandler(false) } else { completionHandler(true) } - }) - }) + } + } return UISwipeActionsConfiguration(actions: [deleteAction]) } } @@ -319,18 +319,17 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate { } } -class TunnelsListTableViewCell: UITableViewCell { - static let reuseIdentifier = "TunnelsListTableViewCell" +private class TunnelCell: UITableViewCell { var tunnel: TunnelContainer? { didSet(value) { // Bind to the tunnel's name nameLabel.text = tunnel?.name ?? "" - nameObservervationToken = tunnel?.observe(\.name) { [weak self] (tunnel, _) in + nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in self?.nameLabel.text = tunnel.name } // Bind to the tunnel's status update(from: tunnel?.status) - statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in + statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in self?.update(from: tunnel.status) } } @@ -357,13 +356,13 @@ class TunnelsListTableViewCell: UITableViewCell { NSLayoutConstraint.activate([ statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor) - ]) + ]) contentView.addSubview(busyIndicator) busyIndicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1) - ]) + ]) contentView.addSubview(nameLabel) nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.numberOfLines = 0 @@ -376,7 +375,7 @@ class TunnelsListTableViewCell: UITableViewCell { nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1), busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1), bottomAnchorConstraint - ]) + ]) self.accessoryType = .disclosureIndicator @@ -445,7 +444,7 @@ class BorderedTextButton: UIView { NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: self.centerXAnchor), button.centerYAnchor.constraint(equalTo: self.centerYAnchor) - ]) + ]) layer.borderWidth = 1 layer.cornerRadius = 5 layer.borderColor = button.tintColor.cgColor diff --git a/WireGuard/WireGuard/UI/iOS/UITableViewCell+Reuse.swift b/WireGuard/WireGuard/UI/iOS/UITableViewCell+Reuse.swift new file mode 100644 index 0000000..587f7b1 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/UITableViewCell+Reuse.swift @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import UIKit + +extension UITableViewCell { + static var reuseIdentifier: String { + return NSStringFromClass(self) + } +} + +extension UITableView { + func register<T: UITableViewCell>(_: T.Type) { + register(T.self, forCellReuseIdentifier: T.reuseIdentifier) + } + + func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T { + //swiftlint:disable:next force_cast + return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T + } +} |