aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/iOS/TunnelDetail
diff options
context:
space:
mode:
authorEric Kuck <eric@bluelinelabs.com>2018-12-13 12:58:50 -0600
committerEric Kuck <eric@bluelinelabs.com>2018-12-13 12:58:50 -0600
commit05d750539b91eff582ff6a789fcdcab73bb5f7bb (patch)
tree0a59939a0805567ea1c4b310d78e4d4c9394cb96 /WireGuard/WireGuard/UI/iOS/TunnelDetail
parentAvoid escaping heap allocation (diff)
downloadwireguard-apple-05d750539b91eff582ff6a789fcdcab73bb5f7bb.tar.xz
wireguard-apple-05d750539b91eff582ff6a789fcdcab73bb5f7bb.zip
Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)
All swiftlint warnings except one fixed up Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
Diffstat (limited to '')
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift40
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift55
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift107
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift85
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift219
5 files changed, 506 insertions, 0 deletions
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift
new file mode 100644
index 0000000..9507c45
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelDetailActivateOnDemandCell: UITableViewCell {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ update(from: tunnel?.activateOnDemandSetting())
+ onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
+ self?.update(from: tunnel.activateOnDemandSetting())
+ }
+ }
+ }
+
+ var onDemandStatusObservervationToken: AnyObject?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .value1, reuseIdentifier: reuseIdentifier)
+ textLabel?.text = "Activate on demand"
+ textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ textLabel?.adjustsFontForContentSizeCategory = true
+ detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ detailTextLabel?.adjustsFontForContentSizeCategory = true
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(from activateOnDemandSetting: ActivateOnDemandSetting?) {
+ detailTextLabel?.text = TunnelViewModel.activateOnDemandDetailText(for: activateOnDemandSetting)
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ textLabel?.text = "Activate on demand"
+ detailTextLabel?.text = ""
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift
new file mode 100644
index 0000000..8710616
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelDetailButtonCell: UITableViewCell {
+ var buttonText: String {
+ get { return button.title(for: .normal) ?? "" }
+ set(value) { button.setTitle(value, for: .normal) }
+ }
+ var hasDestructiveAction: Bool {
+ get { return button.tintColor == UIColor.red }
+ set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
+ }
+ var onTapped: (() -> Void)?
+
+ let button: UIButton = {
+ let button = UIButton(type: .system)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
+ return button
+ }()
+
+ var buttonStandardTintColor: UIColor
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ buttonStandardTintColor = button.tintColor
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ 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)
+ }
+
+ @objc func buttonTapped() {
+ onTapped?()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ buttonText = ""
+ onTapped = nil
+ hasDestructiveAction = false
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift
new file mode 100644
index 0000000..cbe1c14
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelDetailKeyValueCell: CopyableLabelTableViewCell {
+ var key: String {
+ get { return keyLabel.text ?? "" }
+ set(value) { keyLabel.text = value }
+ }
+ var value: String {
+ get { return valueLabel.text }
+ set(value) { valueLabel.text = value }
+ }
+
+ override var textToCopy: String? {
+ return self.valueLabel.text
+ }
+
+ let keyLabel: UILabel = {
+ let keyLabel = UILabel()
+ keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ keyLabel.adjustsFontForContentSizeCategory = true
+ keyLabel.textColor = .black
+ return keyLabel
+ }()
+
+ let valueLabel: ScrollableLabel = {
+ let valueLabel = ScrollableLabel()
+ valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
+ valueLabel.label.adjustsFontForContentSizeCategory = true
+ valueLabel.textColor = .gray
+ return valueLabel
+ }()
+
+ var isStackedHorizontally = false
+ var isStackedVertically = false
+ var contentSizeBasedConstraints = [NSLayoutConstraint]()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(keyLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ keyLabel.textAlignment = .left
+ 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)
+ ])
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ configureForContentSize()
+ }
+
+ func configureForContentSize() {
+ var constraints = [NSLayoutConstraint]()
+ if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
+ // Stack vertically
+ if !isStackedVertically {
+ constraints = [
+ valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
+ valueLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
+ keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
+ ]
+ isStackedVertically = true
+ isStackedHorizontally = false
+ }
+ } else {
+ // Stack horizontally
+ if !isStackedHorizontally {
+ constraints = [
+ contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
+ valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
+ valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
+ ]
+ isStackedHorizontally = true
+ isStackedVertically = false
+ }
+ }
+ if !constraints.isEmpty {
+ NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
+ NSLayoutConstraint.activate(constraints)
+ self.contentSizeBasedConstraints = constraints
+ }
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ key = ""
+ value = ""
+ configureForContentSize()
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift
new file mode 100644
index 0000000..855e3ed
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelDetailStatusCell: UITableViewCell {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ update(from: tunnel?.status)
+ statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
+ self?.update(from: tunnel.status)
+ }
+ }
+ }
+ var isSwitchInteractionEnabled: Bool {
+ get { return statusSwitch.isUserInteractionEnabled }
+ set(value) { statusSwitch.isUserInteractionEnabled = value }
+ }
+ var onSwitchToggled: ((Bool) -> Void)?
+ private var isOnSwitchToggledHandlerEnabled = true
+
+ let statusSwitch: UISwitch
+ private var statusObservervationToken: AnyObject?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ statusSwitch = UISwitch()
+ super.init(style: .default, reuseIdentifier: TunnelDetailKeyValueCell.reuseIdentifier)
+ accessoryView = statusSwitch
+
+ statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ @objc func switchToggled() {
+ if isOnSwitchToggledHandlerEnabled {
+ onSwitchToggled?(statusSwitch.isOn)
+ }
+ }
+
+ private func update(from status: TunnelStatus?) {
+ guard let status = status else {
+ reset()
+ return
+ }
+ let text: String
+ switch status {
+ case .inactive:
+ text = "Inactive"
+ case .activating:
+ text = "Activating"
+ case .active:
+ text = "Active"
+ case .deactivating:
+ text = "Deactivating"
+ case .reasserting:
+ text = "Reactivating"
+ case .restarting:
+ text = "Restarting"
+ case .waiting:
+ text = "Waiting"
+ }
+ textLabel?.text = text
+ DispatchQueue.main.async { [weak statusSwitch] in
+ guard let statusSwitch = statusSwitch else { return }
+ statusSwitch.isOn = !(status == .deactivating || status == .inactive)
+ statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
+ }
+ textLabel?.textColor = (status == .active || status == .inactive) ? UIColor.black : UIColor.gray
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func reset() {
+ textLabel?.text = "Invalid"
+ statusSwitch.isOn = false
+ textLabel?.textColor = UIColor.gray
+ statusSwitch.isUserInteractionEnabled = false
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ reset()
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift
new file mode 100644
index 0000000..af4cf83
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift
@@ -0,0 +1,219 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+// MARK: TunnelDetailTableViewController
+
+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
+ ]
+
+ let peerFields: [TunnelViewModel.PeerField] = [
+ .publicKey, .preSharedKey, .endpoint,
+ .allowedIPs, .persistentKeepAlive
+ ]
+
+ 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) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.title = tunnelViewModel.interfaceData[.name]
+ self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
+
+ self.tableView.estimatedRowHeight = 44
+ self.tableView.rowHeight = UITableView.automaticDimension
+ self.tableView.allowsSelection = false
+ self.tableView.register(TunnelDetailStatusCell.self)
+ self.tableView.register(TunnelDetailKeyValueCell.self)
+ self.tableView.register(TunnelDetailButtonCell.self)
+ self.tableView.register(TunnelDetailActivateOnDemandCell.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
+ let editNC = UINavigationController(rootViewController: editVC)
+ editNC.modalPresentationStyle = .formSheet
+ present(editNC, animated: true)
+ }
+
+ func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
+ onConfirmed: @escaping (() -> Void)) {
+ let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
+ onConfirmed()
+ }
+ let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
+ let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
+ alert.addAction(destroyAction)
+ alert.addAction(cancelAction)
+
+ // popoverPresentationController will be nil on iPhone and non-nil on iPad
+ alert.popoverPresentationController?.sourceView = sourceView
+ alert.popoverPresentationController?.sourceRect = sourceView.bounds
+
+ self.present(alert, animated: true, completion: nil)
+ }
+}
+
+// MARK: TunnelEditTableViewControllerDelegate
+
+extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
+ func tunnelSaved(tunnel: TunnelContainer) {
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
+ loadSections()
+ self.title = tunnel.name
+ self.tableView.reloadData()
+ }
+ func tunnelEditingCancelled() {
+ // Nothing to do
+ }
+}
+
+// MARK: UITableViewDataSource
+
+extension TunnelDetailTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sections[section] {
+ case .status:
+ return 1
+ case .interface:
+ return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
+ case .peer(let peerData):
+ return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
+ case .onDemand:
+ return 1
+ case .delete:
+ return 1
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch sections[section] {
+ case .status:
+ return "Status"
+ case .interface:
+ return "Interface"
+ case .peer:
+ return "Peer"
+ case .onDemand:
+ return "On-Demand Activation"
+ case .delete:
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ 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: TunnelDetailStatusCell = 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)
+ } else {
+ self.tunnelsManager.startDeactivation(of: self.tunnel)
+ }
+ }
+ return cell
+ }
+
+ private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
+ let cell: TunnelDetailKeyValueCell = 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: TunnelDetailKeyValueCell = 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: TunnelDetailActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.tunnel = self.tunnel
+ return cell
+ }
+
+ private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: TunnelDetailButtonCell = 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)
+ }
+ }
+ return cell
+ }
+
+}