aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/iOS/View
diff options
context:
space:
mode:
authorEric Kuck <eric@bluelinelabs.com>2018-12-14 17:27:11 -0600
committerEric Kuck <eric@bluelinelabs.com>2018-12-14 17:27:11 -0600
commitcb051f695db44e7a52e3f423fa27de00c493a9ac (patch)
tree64b583809b17825a0660211f7f381dfc892ccf61 /WireGuard/WireGuard/UI/iOS/View
parentMost similar views now shared between ViewControllers (diff)
downloadwireguard-apple-cb051f695db44e7a52e3f423fa27de00c493a9ac.tar.xz
wireguard-apple-cb051f695db44e7a52e3f423fa27de00c493a9ac.zip
Reorganized project structure
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
Diffstat (limited to 'WireGuard/WireGuard/UI/iOS/View')
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift50
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift55
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/CheckmarkCell.swift31
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift55
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift158
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift107
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift48
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift48
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift74
-rw-r--r--WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift111
10 files changed, 737 insertions, 0 deletions
diff --git a/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift b/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift
new file mode 100644
index 0000000..94b76d6
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/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: centerXAnchor),
+ button.centerYAnchor.constraint(equalTo: 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/View/ButtonCell.swift b/WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift
new file mode 100644
index 0000000..4702993
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+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 == .red }
+ set(value) { button.tintColor = value ? .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/View/CheckmarkCell.swift b/WireGuard/WireGuard/UI/iOS/View/CheckmarkCell.swift
new file mode 100644
index 0000000..db4b6c9
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/CheckmarkCell.swift
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class CheckmarkCell: 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/View/CopyableLabelTableViewCell.swift b/WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift
new file mode 100644
index 0000000..93a9ef7
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class CopyableLabelTableViewCell: UITableViewCell {
+ var copyableGesture = true
+
+ var textToCopy: String? {
+ fatalError("textToCopy must be implemented by subclass")
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
+ addGestureRecognizer(gestureRecognizer)
+ isUserInteractionEnabled = true
+ }
+
+ // MARK: - UIGestureRecognizer
+ @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
+ if !copyableGesture {
+ return
+ }
+ guard recognizer.state == .recognized else { return }
+
+ if let recognizerView = recognizer.view,
+ let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
+ let menuController = UIMenuController.shared
+ menuController.setTargetRect(detailTextLabel?.frame ?? recognizerView.frame, in: detailTextLabel?.superview ?? recognizerSuperView)
+ menuController.setMenuVisible(true, animated: true)
+ }
+ }
+
+ override var canBecomeFirstResponder: Bool {
+ return true
+ }
+
+ override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+ return (action == #selector(UIResponderStandardEditActions.copy(_:)))
+ }
+
+ override func copy(_ sender: Any?) {
+ UIPasteboard.general.string = textToCopy
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ copyableGesture = true
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift
new file mode 100644
index 0000000..48956eb
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class EditableKeyValueCell: 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 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(contentSizeBasedConstraints)
+ NSLayoutConstraint.activate(constraints)
+ 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 EditableKeyValueCell: 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/View/KeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift
new file mode 100644
index 0000000..78026ea
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class KeyValueCell: 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 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 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(contentSizeBasedConstraints)
+ NSLayoutConstraint.activate(constraints)
+ 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/View/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift
new file mode 100644
index 0000000..bd6f547
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class ScrollableLabel: UIScrollView {
+ var text: String {
+ get { return label.text ?? "" }
+ set(value) { label.text = value }
+ }
+ var textColor: UIColor {
+ get { return label.textColor }
+ set(value) { label.textColor = value }
+ }
+
+ let label: UILabel = {
+ let label = UILabel()
+ label.translatesAutoresizingMaskIntoConstraints = false
+ label.textAlignment = .right
+ return label
+ }()
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ isDirectionalLockEnabled = true
+ showsHorizontalScrollIndicator = false
+ showsVerticalScrollIndicator = false
+
+ addSubview(label)
+ label.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ 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)
+ ])
+
+ let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
+ expandToFitValueLabelConstraint.priority = .defaultLow + 1
+ expandToFitValueLabelConstraint.isActive = true
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift b/WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift
new file mode 100644
index 0000000..d0c29aa
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+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 ? .black : .gray
+ }
+ }
+
+ var onSwitchToggled: ((Bool) -> Void)?
+
+ let switchView = UISwitch()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+
+ accessoryView = switchView
+ switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func switchToggled() {
+ onSwitchToggled?(switchView.isOn)
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ isEnabled = true
+ message = ""
+ isOn = false
+ }
+}
diff --git a/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift
new file mode 100644
index 0000000..15d58d6
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift
@@ -0,0 +1,74 @@
+// 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 }
+ }
+
+ override var textToCopy: String? {
+ return valueLabel.text
+ }
+
+ let keyLabel: UILabel = {
+ let keyLabel = UILabel()
+ keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ keyLabel.adjustsFontForContentSizeCategory = true
+ keyLabel.textColor = .gray
+ return keyLabel
+ }()
+
+ let valueLabel: ScrollableLabel = {
+ let valueLabel = ScrollableLabel()
+ valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
+ valueLabel.label.adjustsFontForContentSizeCategory = true
+ valueLabel.textColor = .gray
+ return valueLabel
+ }()
+
+ 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)
+ // 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)
+ ])
+ }
+
+ 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/View/TunnelListCell.swift b/WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift
new file mode 100644
index 0000000..14a7194
--- /dev/null
+++ b/WireGuard/WireGuard/UI/iOS/View/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.asyncAfter(deadline: .now() + .milliseconds(200)) { [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()
+ }
+}