aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/iOS
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift55
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift158
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift71
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift30
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift47
-rw-r--r--WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift (renamed from WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift)358
-rw-r--r--WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift2
-rw-r--r--WireGuard/WireGuard/UI/iOS/QRScanViewController.swift2
-rw-r--r--WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift30
-rw-r--r--WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift47
-rw-r--r--WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift29
-rw-r--r--WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift (renamed from WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift)74
-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
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift50
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift111
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift (renamed from WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift)280
20 files changed, 1205 insertions, 645 deletions
diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift
new file mode 100644
index 0000000..af70183
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditButtonCell: 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/EditTunnel/TunnelEditKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift
new file mode 100644
index 0000000..432d75b
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditKeyValueCell: UITableViewCell {
+ var key: String {
+ get { return keyLabel.text ?? "" }
+ set(value) {keyLabel.text = value }
+ }
+ var value: String {
+ get { return valueTextField.text ?? "" }
+ set(value) { valueTextField.text = value }
+ }
+ var placeholderText: String {
+ get { return valueTextField.placeholder ?? "" }
+ set(value) { valueTextField.placeholder = value }
+ }
+ var isValueValid = true {
+ didSet {
+ if isValueValid {
+ keyLabel.textColor = .black
+ } else {
+ keyLabel.textColor = .red
+ }
+ }
+ }
+ var keyboardType: UIKeyboardType {
+ get { return valueTextField.keyboardType }
+ set(value) { valueTextField.keyboardType = value }
+ }
+
+ var onValueChanged: ((String) -> Void)?
+ var onValueBeingEdited: ((String) -> Void)?
+
+ let keyLabel: UILabel = {
+ let keyLabel = UILabel()
+ keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ keyLabel.adjustsFontForContentSizeCategory = true
+ return keyLabel
+ }()
+
+ let valueTextField: UITextField = {
+ let valueTextField = UITextField()
+ valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
+ valueTextField.adjustsFontForContentSizeCategory = true
+ valueTextField.autocapitalizationType = .none
+ valueTextField.autocorrectionType = .no
+ valueTextField.spellCheckingType = .no
+ return valueTextField
+ }()
+
+ var isStackedHorizontally = false
+ var isStackedVertically = false
+ var contentSizeBasedConstraints = [NSLayoutConstraint]()
+
+ private var textFieldValueOnBeginEditing: String = ""
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(keyLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ keyLabel.textAlignment = .right
+ let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
+ relatedBy: .equal,
+ toItem: self, attribute: .width,
+ multiplier: 0.4, constant: 0)
+ // The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
+ // so set a CR priority > the 0.4-constraint's priority.
+ widthRatioConstraint.priority = .defaultHigh + 1
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ NSLayoutConstraint.activate([
+ 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
+
+ configureForContentSize()
+ }
+
+ func configureForContentSize() {
+ var constraints = [NSLayoutConstraint]()
+ if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
+ // Stack vertically
+ if !isStackedVertically {
+ constraints = [
+ valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
+ valueTextField.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),
+ valueTextField.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
+ valueTextField.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 = ""
+ placeholderText = ""
+ isValueValid = true
+ keyboardType = .default
+ onValueChanged = nil
+ onValueBeingEdited = nil
+ configureForContentSize()
+ }
+}
+
+extension TunnelEditKeyValueCell: UITextFieldDelegate {
+ func textFieldDidBeginEditing(_ textField: UITextField) {
+ textFieldValueOnBeginEditing = textField.text ?? ""
+ isValueValid = true
+ }
+ func textFieldDidEndEditing(_ textField: UITextField) {
+ let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
+ guard isModified else { return }
+ if let onValueChanged = onValueChanged {
+ onValueChanged(textField.text ?? "")
+ }
+ }
+ func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+ if let onValueBeingEdited = onValueBeingEdited {
+ let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
+ onValueBeingEdited(modifiedText)
+ }
+ return true
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift
new file mode 100644
index 0000000..48c8798
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditReadOnlyKeyValueCell: 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 }
+ }
+
+ let keyLabel: UILabel
+ let valueLabel: ScrollableLabel
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ keyLabel = UILabel()
+ keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ keyLabel.adjustsFontForContentSizeCategory = true
+ valueLabel = ScrollableLabel()
+ valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
+ valueLabel.label.adjustsFontForContentSizeCategory = true
+
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ keyLabel.textColor = UIColor.gray
+ valueLabel.textColor = UIColor.gray
+
+ contentView.addSubview(keyLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ keyLabel.textAlignment = .right
+ let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
+ relatedBy: .equal,
+ toItem: self, attribute: .width,
+ multiplier: 0.4, constant: 0)
+ // In case the key doesn't fit into 0.4 * width,
+ // so set a CR priority > the 0.4-constraint's priority.
+ widthRatioConstraint.priority = .defaultHigh + 1
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ NSLayoutConstraint.activate([
+ keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+ keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
+ widthRatioConstraint
+ ])
+
+ contentView.addSubview(valueLabel)
+ valueLabel.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ 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? {
+ return self.valueLabel.text
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ key = ""
+ value = ""
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift
new file mode 100644
index 0000000..ca0352e
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditSelectionListCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel!.text = value }
+ }
+ var isChecked: Bool {
+ didSet {
+ accessoryType = isChecked ? .checkmark : .none
+ }
+ }
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ isChecked = false
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ isChecked = false
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift
new file mode 100644
index 0000000..658fb95
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditSwitchCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel!.text = value }
+ }
+ var isOn: Bool {
+ get { return switchView.isOn }
+ set(value) { switchView.isOn = value }
+ }
+ var isEnabled: Bool {
+ get { return switchView.isEnabled }
+ set(value) {
+ switchView.isEnabled = value
+ textLabel?.textColor = value ? UIColor.black : UIColor.gray
+ }
+ }
+
+ var onSwitchToggled: ((Bool) -> Void)?
+
+ let switchView: UISwitch
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ switchView = UISwitch()
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+ accessoryView = switchView
+ switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ @objc func switchToggled() {
+ onSwitchToggled?(switchView.isOn)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ isOn = false
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift
index 0386b0a..8d055d2 100644
--- a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift
+++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift
@@ -79,11 +79,11 @@ class TunnelEditTableViewController: UITableViewController {
self.tableView.estimatedRowHeight = 44
self.tableView.rowHeight = UITableView.automaticDimension
- 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)
+ self.tableView.register(TunnelEditKeyValueCell.self)
+ self.tableView.register(TunnelEditReadOnlyKeyValueCell.self)
+ self.tableView.register(TunnelEditButtonCell.self)
+ self.tableView.register(TunnelEditSwitchCell.self)
+ self.tableView.register(TunnelEditSelectionListCell.self)
}
private func loadSections() {
@@ -201,7 +201,7 @@ extension TunnelEditTableViewController {
}
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
- let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
guard let self = self else { return }
@@ -218,14 +218,14 @@ extension TunnelEditTableViewController {
}
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
- let cell: ReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditReadOnlyKeyValueCell = 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)
+ let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
@@ -287,7 +287,7 @@ extension TunnelEditTableViewController {
}
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
- let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
@@ -313,7 +313,7 @@ extension TunnelEditTableViewController {
}
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
- let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue
@@ -328,7 +328,7 @@ extension TunnelEditTableViewController {
}
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
- let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
@@ -377,7 +377,7 @@ extension TunnelEditTableViewController {
}
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
- let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in
guard let self = self else { return }
@@ -398,7 +398,7 @@ extension TunnelEditTableViewController {
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
- let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in
@@ -419,7 +419,7 @@ extension TunnelEditTableViewController {
}
return cell
} else {
- let cell: SelectionListCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelEditSelectionListCell = tableView.dequeueReusableCell(for: indexPath)
let rowOption = activateOnDemandOptions[indexPath.row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none)
@@ -486,333 +486,3 @@ extension TunnelEditTableViewController {
}
}
}
-
-private class KeyValueCell: UITableViewCell {
- var key: String {
- get { return keyLabel.text ?? "" }
- set(value) {keyLabel.text = value }
- }
- var value: String {
- get { return valueTextField.text ?? "" }
- set(value) { valueTextField.text = value }
- }
- var placeholderText: String {
- get { return valueTextField.placeholder ?? "" }
- set(value) { valueTextField.placeholder = value }
- }
- var isValueValid: Bool = true {
- didSet {
- if isValueValid {
- keyLabel.textColor = UIColor.black
- } else {
- keyLabel.textColor = UIColor.red
- }
- }
- }
- var keyboardType: UIKeyboardType {
- get { return valueTextField.keyboardType }
- set(value) { valueTextField.keyboardType = value }
- }
-
- var onValueChanged: ((String) -> Void)?
- var onValueBeingEdited: ((String) -> Void)?
-
- let keyLabel: UILabel
- let valueTextField: UITextField
-
- var isStackedHorizontally: Bool = false
- var isStackedVertically: Bool = false
- var contentSizeBasedConstraints = [NSLayoutConstraint]()
-
- private var textFieldValueOnBeginEditing: String = ""
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- keyLabel = UILabel()
- keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
- keyLabel.adjustsFontForContentSizeCategory = true
- valueTextField = UITextField()
- valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
- valueTextField.adjustsFontForContentSizeCategory = true
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- contentView.addSubview(keyLabel)
- keyLabel.translatesAutoresizingMaskIntoConstraints = false
- keyLabel.textAlignment = .right
- let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
- relatedBy: .equal,
- toItem: self, attribute: .width,
- multiplier: 0.4, constant: 0)
- // The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
- // so set a CR priority > the 0.4-constraint's priority.
- widthRatioConstraint.priority = .defaultHigh + 1
- keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
- NSLayoutConstraint.activate([
- 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
- valueTextField.autocorrectionType = .no
- valueTextField.spellCheckingType = .no
-
- configureForContentSize()
- }
-
- func configureForContentSize() {
- var constraints = [NSLayoutConstraint]()
- if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
- // Stack vertically
- if !isStackedVertically {
- constraints = [
- valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
- valueTextField.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),
- valueTextField.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
- valueTextField.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 = ""
- placeholderText = ""
- isValueValid = true
- keyboardType = .default
- onValueChanged = nil
- onValueBeingEdited = nil
- configureForContentSize()
- }
-}
-
-extension KeyValueCell: UITextFieldDelegate {
- func textFieldDidBeginEditing(_ textField: UITextField) {
- textFieldValueOnBeginEditing = textField.text ?? ""
- isValueValid = true
- }
- func textFieldDidEndEditing(_ textField: UITextField) {
- let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
- guard isModified else { return }
- if let onValueChanged = onValueChanged {
- onValueChanged(textField.text ?? "")
- }
- }
- func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
- if let onValueBeingEdited = onValueBeingEdited {
- let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
- onValueBeingEdited(modifiedText)
- }
- return true
- }
-}
-
-private class ReadOnlyKeyValueCell: 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 }
- }
-
- let keyLabel: UILabel
- let valueLabel: ScrollableLabel
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- keyLabel = UILabel()
- keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
- keyLabel.adjustsFontForContentSizeCategory = true
- valueLabel = ScrollableLabel()
- valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
- valueLabel.label.adjustsFontForContentSizeCategory = true
-
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- keyLabel.textColor = UIColor.gray
- valueLabel.textColor = UIColor.gray
-
- contentView.addSubview(keyLabel)
- keyLabel.translatesAutoresizingMaskIntoConstraints = false
- keyLabel.textAlignment = .right
- let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
- relatedBy: .equal,
- toItem: self, attribute: .width,
- multiplier: 0.4, constant: 0)
- // In case the key doesn't fit into 0.4 * width,
- // so set a CR priority > the 0.4-constraint's priority.
- widthRatioConstraint.priority = .defaultHigh + 1
- keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
- NSLayoutConstraint.activate([
- keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
- keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
- widthRatioConstraint
- ])
-
- contentView.addSubview(valueLabel)
- valueLabel.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- 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? {
- return self.valueLabel.text
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func prepareForReuse() {
- super.prepareForReuse()
- key = ""
- value = ""
- }
-}
-
-private class ButtonCell: 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
- var buttonStandardTintColor: UIColor
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- button = UIButton(type: .system)
- button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
- button.titleLabel?.adjustsFontForContentSizeCategory = true
- 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
- }
-}
-
-private class SwitchCell: UITableViewCell {
- var message: String {
- get { return textLabel?.text ?? "" }
- set(value) { textLabel!.text = value }
- }
- var isOn: Bool {
- get { return switchView.isOn }
- set(value) { switchView.isOn = value }
- }
- var isEnabled: Bool {
- get { return switchView.isEnabled }
- set(value) {
- switchView.isEnabled = value
- textLabel?.textColor = value ? UIColor.black : UIColor.gray
- }
- }
-
- var onSwitchToggled: ((Bool) -> Void)?
-
- let switchView: UISwitch
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- switchView = UISwitch()
- super.init(style: .default, reuseIdentifier: reuseIdentifier)
- accessoryView = switchView
- switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
- }
-
- @objc func switchToggled() {
- onSwitchToggled?(switchView.isOn)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func prepareForReuse() {
- super.prepareForReuse()
- message = ""
- isOn = false
- }
-}
-
-private class SelectionListCell: UITableViewCell {
- var message: String {
- get { return textLabel?.text ?? "" }
- set(value) { textLabel!.text = value }
- }
- var isChecked: Bool {
- didSet {
- accessoryType = isChecked ? .checkmark : .none
- }
- }
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- isChecked = false
- super.init(style: .default, reuseIdentifier: reuseIdentifier)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func prepareForReuse() {
- super.prepareForReuse()
- message = ""
- isChecked = false
- }
-}
diff --git a/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift b/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift
index ccbd00d..6d57006 100644
--- a/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift
+++ b/WireGuard/WireGuard/UI/iOS/ErrorPresenter.swift
@@ -8,7 +8,7 @@ class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
- let (title, message) = error.alertText()
+ let (title, message) = error.alertText
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
onDismissal?()
}
diff --git a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift
index ad0fe79..a03b709 100644
--- a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift
+++ b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift
@@ -24,7 +24,7 @@ class QRScanViewController: UIViewController {
let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.adjustsFontSizeToFitWidth = true
- tipLabel.textColor = UIColor.lightGray
+ tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center
view.addSubview(tipLabel)
diff --git a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift
index f2d0f58..bd6f547 100644
--- a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift
+++ b/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift
@@ -13,33 +13,31 @@ class ScrollableLabel: UIScrollView {
set(value) { label.textColor = value }
}
- let label: UILabel
-
- init() {
+ let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
- self.label = label
+ return label
+ }()
+ init() {
super.init(frame: CGRect.zero)
- self.isDirectionalLockEnabled = true
- self.showsHorizontalScrollIndicator = false
- self.showsVerticalScrollIndicator = false
+ isDirectionalLockEnabled = true
+ showsHorizontalScrollIndicator = false
+ showsVerticalScrollIndicator = false
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
- label.leftAnchor.constraint(equalTo: self.contentLayoutGuide.leftAnchor),
- label.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor),
- label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor),
- label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor),
- label.heightAnchor.constraint(equalTo: self.heightAnchor)
+ label.leftAnchor.constraint(equalTo: contentLayoutGuide.leftAnchor),
+ label.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
+ label.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
+ label.rightAnchor.constraint(equalTo: contentLayoutGuide.rightAnchor),
+ label.heightAnchor.constraint(equalTo: 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,
- toItem: self, attribute: .width, multiplier: 1, constant: 0)
+
+ let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
}
diff --git a/WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift b/WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift
new file mode 100644
index 0000000..d795ab4
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class SettingsButtonCell: UITableViewCell {
+ var buttonText: String {
+ get { return button.title(for: .normal) ?? "" }
+ set(value) { button.setTitle(value, for: .normal) }
+ }
+ var onTapped: (() -> Void)?
+
+ let button: UIButton = {
+ let button = UIButton(type: .system)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
+ return button
+ }()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ 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
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift
new file mode 100644
index 0000000..532f1d1
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class SettingsKeyValueCell: UITableViewCell {
+ var key: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel?.text = value }
+ }
+ var value: String {
+ get { return detailTextLabel?.text ?? "" }
+ set(value) { detailTextLabel?.text = value }
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .value1, reuseIdentifier: SettingsKeyValueCell.reuseIdentifier)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ key = ""
+ value = ""
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift
index af9893d..c87d452 100644
--- a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift
+++ b/WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift
@@ -40,8 +40,8 @@ class SettingsTableViewController: UITableViewController {
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
- tableView.register(KeyValueCell.self)
- tableView.register(ButtonCell.self)
+ tableView.register(SettingsKeyValueCell.self)
+ tableView.register(SettingsButtonCell.self)
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
}
@@ -167,7 +167,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: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: SettingsKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
@@ -180,7 +180,7 @@ extension SettingsTableViewController {
}
return cell
} else if field == .exportZipArchive {
- let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
@@ -188,7 +188,7 @@ extension SettingsTableViewController {
return cell
} else {
assert(field == .exportLogFile)
- let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
@@ -197,67 +197,3 @@ extension SettingsTableViewController {
}
}
}
-
-private class KeyValueCell: UITableViewCell {
- var key: String {
- get { return textLabel?.text ?? "" }
- set(value) { textLabel?.text = value }
- }
- var value: String {
- get { return detailTextLabel?.text ?? "" }
- set(value) { detailTextLabel?.text = value }
- }
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: .value1, reuseIdentifier: KeyValueCell.reuseIdentifier)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func prepareForReuse() {
- super.prepareForReuse()
- key = ""
- value = ""
- }
-}
-
-private class ButtonCell: UITableViewCell {
- var buttonText: String {
- get { return button.title(for: .normal) ?? "" }
- set(value) { button.setTitle(value, for: .normal) }
- }
- var onTapped: (() -> Void)?
-
- let button: UIButton
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- button = UIButton(type: .system)
- button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
- button.titleLabel?.adjustsFontForContentSizeCategory = true
- 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
- }
-}
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
+ }
+
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift b/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift
new file mode 100644
index 0000000..5114c09
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class BorderedTextButton: UIView {
+ let button: UIButton = {
+ let button = UIButton(type: .system)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
+ return button
+ }()
+
+ override var intrinsicContentSize: CGSize {
+ let buttonSize = button.intrinsicContentSize
+ return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
+ }
+
+ var title: String {
+ get { return button.title(for: .normal) ?? "" }
+ set(value) { button.setTitle(value, for: .normal) }
+ }
+
+ var onTapped: (() -> Void)?
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ layer.borderWidth = 1
+ layer.cornerRadius = 5
+ layer.borderColor = button.tintColor.cgColor
+
+ addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
+ button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
+ ])
+
+ button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ }
+
+ @objc func buttonTapped() {
+ onTapped?()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift
new file mode 100644
index 0000000..95055c5
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelListCell: UITableViewCell {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ // Bind to the tunnel's name
+ nameLabel.text = tunnel?.name ?? ""
+ 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
+ self?.update(from: tunnel.status)
+ }
+ }
+ }
+ var onSwitchToggled: ((Bool) -> Void)?
+
+ let nameLabel: UILabel = {
+ let nameLabel = UILabel()
+ nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ nameLabel.adjustsFontForContentSizeCategory = true
+ nameLabel.numberOfLines = 0
+ return nameLabel
+ }()
+
+ let busyIndicator: UIActivityIndicatorView = {
+ let busyIndicator = UIActivityIndicatorView(style: .gray)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ }()
+
+ let statusSwitch = UISwitch()
+
+ private var statusObservervationToken: AnyObject?
+ private var nameObservervationToken: AnyObject?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(statusSwitch)
+ statusSwitch.translatesAutoresizingMaskIntoConstraints = false
+ 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.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
+ bottomAnchorConstraint.priority = .defaultLow
+ NSLayoutConstraint.activate([
+ nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
+ nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
+ busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
+ bottomAnchorConstraint
+ ])
+
+ accessoryType = .disclosureIndicator
+
+ statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ @objc func switchToggled() {
+ onSwitchToggled?(statusSwitch.isOn)
+ }
+
+ private func update(from status: TunnelStatus?) {
+ guard let status = status else {
+ reset()
+ return
+ }
+ DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
+ guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
+ statusSwitch.isOn = !(status == .deactivating || status == .inactive)
+ statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
+ if status == .inactive || status == .active {
+ busyIndicator.stopAnimating()
+ } else {
+ busyIndicator.startAnimating()
+ }
+ }
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func reset() {
+ statusSwitch.isOn = false
+ statusSwitch.isUserInteractionEnabled = false
+ busyIndicator.stopAnimating()
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ reset()
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift
index efa85e6..eda09af 100644
--- a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift
+++ b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift
@@ -9,95 +9,88 @@ class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
- var busyIndicator: UIActivityIndicatorView?
- var centeredAddButton: BorderedTextButton?
- var tableView: UITableView?
-
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = UIColor.white
-
- // Set up the navigation bar
- self.title = "WireGuard"
- let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
- self.navigationItem.rightBarButtonItem = addButtonItem
- let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
- self.navigationItem.leftBarButtonItem = settingsButtonItem
-
- // Set up the busy indicator
+ let tableView: UITableView = {
+ let tableView = UITableView(frame: CGRect.zero, style: .plain)
+ tableView.estimatedRowHeight = 60
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.separatorStyle = .none
+ tableView.register(TunnelListCell.self)
+ return tableView
+ }()
+
+ let centeredAddButton: BorderedTextButton = {
+ let button = BorderedTextButton()
+ button.title = "Add a tunnel"
+ button.isHidden = true
+ return button
+ }()
+
+ let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ }()
+
+ override func loadView() {
+ view = UIView()
+ view.backgroundColor = .white
+
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ view.addSubview(tableView)
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
+ tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
+ tableView.topAnchor.constraint(equalTo: view.topAnchor),
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+ ])
- // Add the busyIndicator, centered
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
- busyIndicator.startAnimating()
- self.busyIndicator = busyIndicator
-
- // State restoration
- self.restorationIdentifier = "TunnelsListVC"
- }
-
- func setTunnelsManager(tunnelsManager: TunnelsManager) {
- if self.tunnelsManager != nil {
- // If a tunnels manager is already set, do nothing
- return
- }
-
- // Create the table view
-
- let tableView = UITableView(frame: CGRect.zero, style: .plain)
- tableView.estimatedRowHeight = 60
- tableView.rowHeight = UITableView.automaticDimension
- tableView.separatorStyle = .none
- tableView.register(TunnelCell.self)
-
- self.view.addSubview(tableView)
- tableView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
- 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
-
- // Add button at the center
-
- let centeredAddButton = BorderedTextButton()
- centeredAddButton.title = "Add a tunnel"
- centeredAddButton.isHidden = true
- self.view.addSubview(centeredAddButton)
+
+ view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
- centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
- centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
+ centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
+
centeredAddButton.onTapped = { [weak self] in
- self?.addButtonTapped(sender: centeredAddButton)
+ guard let self = self else { return }
+ self.addButtonTapped(sender: self.centeredAddButton)
}
- centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
- self.centeredAddButton = centeredAddButton
-
- // Hide the busy indicator
+
+ busyIndicator.startAnimating()
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
- self.busyIndicator?.stopAnimating()
+ title = "WireGuard"
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
- // Keep track of the tunnels manager
+ restorationIdentifier = "TunnelsListVC"
+ }
+ func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self
+
+ busyIndicator.stopAnimating()
+ tableView.reloadData()
+ centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
}
override func viewWillAppear(_: Bool) {
// Remove selection when getting back to the list view on iPhone
- if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
+ if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
}
}
@@ -241,7 +234,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell: TunnelCell = tableView.dequeueReusableCell(for: indexPath)
+ let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel
@@ -293,161 +286,20 @@ extension TunnelsListTableViewController: UITableViewDelegate {
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) {
- tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
- centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
+ tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
func tunnelModified(at index: Int) {
- tableView?.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
- tableView?.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
+ tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
}
func tunnelRemoved(at index: Int) {
- tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
- centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
- }
-}
-
-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
- self?.nameLabel.text = tunnel.name
- }
- // Bind to the tunnel's status
- update(from: tunnel?.status)
- statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
- self?.update(from: tunnel.status)
- }
- }
- }
- var onSwitchToggled: ((Bool) -> Void)?
-
- let nameLabel: UILabel
- let busyIndicator: UIActivityIndicatorView
- let statusSwitch: UISwitch
-
- private var statusObservervationToken: AnyObject?
- private var nameObservervationToken: AnyObject?
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- nameLabel = UILabel()
- nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
- nameLabel.adjustsFontForContentSizeCategory = true
- busyIndicator = UIActivityIndicatorView(style: .gray)
- busyIndicator.hidesWhenStopped = true
- statusSwitch = UISwitch()
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- contentView.addSubview(statusSwitch)
- statusSwitch.translatesAutoresizingMaskIntoConstraints = false
- 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
- nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(
- equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
- bottomAnchorConstraint.priority = .defaultLow // Allow this constraint to be broken when animating a cell away during deletion
- NSLayoutConstraint.activate([
- nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
- nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
- busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
- bottomAnchorConstraint
- ])
-
- self.accessoryType = .disclosureIndicator
-
- statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
- }
-
- @objc func switchToggled() {
- onSwitchToggled?(statusSwitch.isOn)
- }
-
- private func update(from status: TunnelStatus?) {
- guard let status = status else {
- reset()
- return
- }
- DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
- guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
- statusSwitch.isOn = !(status == .deactivating || status == .inactive)
- statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
- if status == .inactive || status == .active {
- busyIndicator.stopAnimating()
- } else {
- busyIndicator.startAnimating()
- }
- }
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func reset() {
- statusSwitch.isOn = false
- statusSwitch.isUserInteractionEnabled = false
- busyIndicator.stopAnimating()
- }
-
- override func prepareForReuse() {
- super.prepareForReuse()
- reset()
- }
-}
-
-class BorderedTextButton: UIView {
- let button: UIButton
-
- override var intrinsicContentSize: CGSize {
- let buttonSize = button.intrinsicContentSize
- return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
- }
-
- var title: String {
- get { return button.title(for: .normal) ?? "" }
- set(value) { button.setTitle(value, for: .normal) }
- }
-
- var onTapped: (() -> Void)?
-
- init() {
- button = UIButton(type: .system)
- button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
- button.titleLabel?.adjustsFontForContentSizeCategory = true
- super.init(frame: CGRect.zero)
- addSubview(button)
- button.translatesAutoresizingMaskIntoConstraints = false
- 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
- button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
- }
-
- @objc func buttonTapped() {
- onTapped?()
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
+ tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
}
}