From cb051f695db44e7a52e3f423fa27de00c493a9ac Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Fri, 14 Dec 2018 17:27:11 -0600 Subject: Reorganized project structure Signed-off-by: Eric Kuck --- .../WireGuard/UI/iOS/View/BorderedTextButton.swift | 50 +++++++ WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift | 55 +++++++ .../WireGuard/UI/iOS/View/CheckmarkCell.swift | 31 ++++ .../UI/iOS/View/CopyableLabelTableViewCell.swift | 55 +++++++ .../UI/iOS/View/EditableKeyValueCell.swift | 158 +++++++++++++++++++++ WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift | 107 ++++++++++++++ .../WireGuard/UI/iOS/View/ScrollableLabel.swift | 48 +++++++ WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift | 48 +++++++ .../iOS/View/TunnelEditReadOnlyKeyValueCell.swift | 74 ++++++++++ .../WireGuard/UI/iOS/View/TunnelListCell.swift | 111 +++++++++++++++ 10 files changed, 737 insertions(+) create mode 100644 WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/CheckmarkCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift (limited to 'WireGuard/WireGuard/UI/iOS/View') 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() + } +} -- cgit v1.2.3-59-g8ed1b