aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/iOS
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift4
-rw-r--r--WireGuard/WireGuard/UI/iOS/MainViewController.swift2
-rw-r--r--WireGuard/WireGuard/UI/iOS/QRScanViewController.swift14
-rw-r--r--WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift2
-rw-r--r--WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift24
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift231
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift487
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift45
-rw-r--r--WireGuard/WireGuard/UI/iOS/UITableViewCell+Reuse.swift21
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
+ }
+}