From 7a24f18eb753180800f9b44a767b0d59e4e702b7 Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Fri, 14 Dec 2018 17:12:59 -0600 Subject: Most similar views now shared between ViewControllers Signed-off-by: Eric Kuck --- WireGuard/WireGuard/UI/iOS/AppDelegate.swift | 2 +- .../UI/iOS/CopyableLabelTableViewCell.swift | 55 ------ .../UI/iOS/EditTunnel/TunnelEditButtonCell.swift | 55 ------ .../UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift | 158 ---------------- .../TunnelEditReadOnlyKeyValueCell.swift | 35 ++-- .../iOS/EditTunnel/TunnelEditSectionListCell.swift | 30 ---- .../UI/iOS/EditTunnel/TunnelEditSwitchCell.swift | 47 ----- .../EditTunnel/TunnelEditTableViewController.swift | 48 ++--- .../WireGuard/UI/iOS/MainViewController.swift | 14 +- .../WireGuard/UI/iOS/QRScanViewController.swift | 7 +- WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift | 48 ----- .../UI/iOS/Settings/SettingsButtonCell.swift | 47 ----- .../UI/iOS/Settings/SettingsKeyValueCell.swift | 29 --- .../iOS/Settings/SettingsTableViewController.swift | 199 -------------------- .../UI/iOS/SettingsTableViewController.swift | 200 +++++++++++++++++++++ .../UI/iOS/SharedViews/BorderedTextButton.swift | 50 ++++++ .../WireGuard/UI/iOS/SharedViews/ButtonCell.swift | 55 ++++++ .../UI/iOS/SharedViews/CheckmarkCell.swift | 31 ++++ .../SharedViews/CopyableLabelTableViewCell.swift | 55 ++++++ .../UI/iOS/SharedViews/EditableKeyValueCell.swift | 158 ++++++++++++++++ .../UI/iOS/SharedViews/KeyValueCell.swift | 107 +++++++++++ .../UI/iOS/SharedViews/ScrollableLabel.swift | 48 +++++ .../WireGuard/UI/iOS/SharedViews/SwitchCell.swift | 48 +++++ .../TunnelDetailActivateOnDemandCell.swift | 40 ----- .../iOS/TunnelDetail/TunnelDetailButtonCell.swift | 55 ------ .../TunnelDetail/TunnelDetailKeyValueCell.swift | 107 ----------- .../iOS/TunnelDetail/TunnelDetailStatusCell.swift | 85 --------- .../TunnelDetailTableViewController.swift | 85 ++++++--- .../UI/iOS/TunnelList/BorderedTextButton.swift | 50 ------ .../UI/iOS/TunnelList/TunnelListCell.swift | 6 +- .../TunnelsListTableViewController.swift | 16 +- 31 files changed, 881 insertions(+), 1089 deletions(-) delete mode 100644 WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/BorderedTextButton.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/ButtonCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/CheckmarkCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/CopyableLabelTableViewCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/EditableKeyValueCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/KeyValueCell.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/ScrollableLabel.swift create mode 100644 WireGuard/WireGuard/UI/iOS/SharedViews/SwitchCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift (limited to 'WireGuard/WireGuard/UI/iOS') diff --git a/WireGuard/WireGuard/UI/iOS/AppDelegate.swift b/WireGuard/WireGuard/UI/iOS/AppDelegate.swift index 3146346..428f732 100644 --- a/WireGuard/WireGuard/UI/iOS/AppDelegate.swift +++ b/WireGuard/WireGuard/UI/iOS/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path) let window = UIWindow(frame: UIScreen.main.bounds) - window.backgroundColor = UIColor.white + window.backgroundColor = .white self.window = window let mainVC = MainViewController() diff --git a/WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift b/WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift deleted file mode 100644 index daddf0a..0000000 --- a/WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift +++ /dev/null @@ -1,55 +0,0 @@ -// 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(_:))) - self.addGestureRecognizer(gestureRecognizer) - self.isUserInteractionEnabled = true - } - - // MARK: - UIGestureRecognizer - @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) { - if !self.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(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.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() - self.copyableGesture = true - } -} diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift deleted file mode 100644 index af70183..0000000 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditButtonCell.swift +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index 432d75b..0000000 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditKeyValueCell.swift +++ /dev/null @@ -1,158 +0,0 @@ -// 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 index 48c8798..15d58d6 100644 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift +++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditReadOnlyKeyValueCell.swift @@ -6,29 +6,36 @@ import UIKit class TunnelEditReadOnlyKeyValueCell: CopyableLabelTableViewCell { var key: String { get { return keyLabel.text ?? "" } - set(value) {keyLabel.text = value } + set(value) { keyLabel.text = value } } var value: String { get { return valueLabel.text } set(value) { valueLabel.text = value } } - let keyLabel: UILabel - let valueLabel: ScrollableLabel + override var textToCopy: String? { + return valueLabel.text + } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - keyLabel = UILabel() + let keyLabel: UILabel = { + let keyLabel = UILabel() keyLabel.font = UIFont.preferredFont(forTextStyle: .body) keyLabel.adjustsFontForContentSizeCategory = true - valueLabel = ScrollableLabel() + 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) - keyLabel.textColor = UIColor.gray - valueLabel.textColor = UIColor.gray - contentView.addSubview(keyLabel) keyLabel.translatesAutoresizingMaskIntoConstraints = false keyLabel.textAlignment = .right @@ -44,7 +51,7 @@ class TunnelEditReadOnlyKeyValueCell: CopyableLabelTableViewCell { keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), widthRatioConstraint - ]) + ]) contentView.addSubview(valueLabel) valueLabel.translatesAutoresizingMaskIntoConstraints = false @@ -52,11 +59,7 @@ class TunnelEditReadOnlyKeyValueCell: CopyableLabelTableViewCell { valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) - ]) - } - - override var textToCopy: String? { - return self.valueLabel.text + ]) } required init?(coder aDecoder: NSCoder) { diff --git a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift deleted file mode 100644 index ca0352e..0000000 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSectionListCell.swift +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 658fb95..0000000 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditSwitchCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// 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/EditTunnel/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift index 8d055d2..393294e 100644 --- a/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/EditTunnel/TunnelEditTableViewController.swift @@ -72,18 +72,18 @@ class TunnelEditTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - self.title = tunnel == nil ? "New configuration" : "Edit configuration" - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) - - self.tableView.estimatedRowHeight = 44 - self.tableView.rowHeight = UITableView.automaticDimension - - 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) + title = tunnel == nil ? "New configuration" : "Edit configuration" + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) + + tableView.estimatedRowHeight = 44 + tableView.rowHeight = UITableView.automaticDimension + + tableView.register(EditableKeyValueCell.self) + tableView.register(TunnelEditReadOnlyKeyValueCell.self) + tableView.register(ButtonCell.self) + tableView.register(SwitchCell.self) + tableView.register(CheckmarkCell.self) } private func loadSections() { @@ -95,13 +95,13 @@ class TunnelEditTableViewController: UITableViewController { } @objc func saveTapped() { - self.tableView.endEditing(false) + tableView.endEditing(false) let tunnelSaveResult = tunnelViewModel.save() switch tunnelSaveResult { case .error(let errorMessage): let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer" ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self) - self.tableView.reloadData() // Highlight erroring fields + tableView.reloadData() // Highlight erroring fields case .saved(let tunnelConfiguration): if let tunnel = tunnel { // We're modifying an existing tunnel @@ -133,7 +133,7 @@ class TunnelEditTableViewController: UITableViewController { @objc func cancelTapped() { dismiss(animated: true, completion: nil) - self.delegate?.tunnelEditingCancelled() + delegate?.tunnelEditingCancelled() } } @@ -201,7 +201,7 @@ extension TunnelEditTableViewController { } private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { - let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath) + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.rawValue cell.onTapped = { [weak self] in guard let self = self else { return } @@ -225,7 +225,7 @@ extension TunnelEditTableViewController { } private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { - let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + let cell: EditableKeyValueCell = 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: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath) + let cell: ButtonCell = 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: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath) + let cell: SwitchCell = 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: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + let cell: EditableKeyValueCell = 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: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath) + let cell: ButtonCell = 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: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath) + let cell: SwitchCell = 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: TunnelEditSelectionListCell = tableView.dequeueReusableCell(for: indexPath) + let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath) let rowOption = activateOnDemandOptions[indexPath.row - 1] let selectedOption = activateOnDemandSetting.activateOnDemandOption assert(selectedOption != .none) @@ -455,7 +455,7 @@ extension TunnelEditTableViewController { alert.popoverPresentationController?.sourceView = sourceView alert.popoverPresentationController?.sourceRect = sourceView.bounds - self.present(alert, animated: true, completion: nil) + present(alert, animated: true, completion: nil) } } diff --git a/WireGuard/WireGuard/UI/iOS/MainViewController.swift b/WireGuard/WireGuard/UI/iOS/MainViewController.swift index 2fc46b2..82e6f81 100644 --- a/WireGuard/WireGuard/UI/iOS/MainViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/MainViewController.swift @@ -12,20 +12,20 @@ class MainViewController: UISplitViewController { init() { let detailVC = UIViewController() - detailVC.view.backgroundColor = UIColor.white + detailVC.view.backgroundColor = .white let detailNC = UINavigationController(rootViewController: detailVC) let masterVC = TunnelsListTableViewController() let masterNC = UINavigationController(rootViewController: masterVC) - self.tunnelsListVC = masterVC + tunnelsListVC = masterVC super.init(nibName: nil, bundle: nil) - self.viewControllers = [ masterNC, detailNC ] + viewControllers = [ masterNC, detailNC ] // State restoration - self.restorationIdentifier = "MainVC" + restorationIdentifier = "MainVC" masterNC.restorationIdentifier = "MasterNC" detailNC.restorationIdentifier = "DetailNC" } @@ -35,10 +35,10 @@ class MainViewController: UISplitViewController { } override func viewDidLoad() { - self.delegate = self + delegate = self // On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app - self.preferredDisplayMode = .allVisible + preferredDisplayMode = .allVisible // Create the tunnels manager, and when it's ready, inform tunnelsListVC TunnelsManager.create { [weak self] result in @@ -56,7 +56,7 @@ class MainViewController: UISplitViewController { tunnelsManager.activationDelegate = self self.onTunnelsManagerReady?(tunnelsManager) - self.onTunnelsManagerReady = nil + self.onTunnelsManagerReady = nil } } } diff --git a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift index a03b709..1e231ec 100644 --- a/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/QRScanViewController.swift @@ -2,7 +2,6 @@ // Copyright © 2018 WireGuard LLC. All Rights Reserved. import AVFoundation -import CoreData import UIKit protocol QRScanViewControllerDelegate: class { @@ -18,8 +17,8 @@ class QRScanViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - self.title = "Scan QR code" - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) + title = "Scan QR code" + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) let tipLabel = UILabel() tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`" @@ -102,7 +101,7 @@ class QRScanViewController: UIViewController { } } - previewLayer?.frame = self.view.bounds + previewLayer?.frame = view.bounds } func scanDidComplete(withCode code: String) { diff --git a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift deleted file mode 100644 index bd6f547..0000000 --- a/WireGuard/WireGuard/UI/iOS/ScrollableLabel.swift +++ /dev/null @@ -1,48 +0,0 @@ -// 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/Settings/SettingsButtonCell.swift b/WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift deleted file mode 100644 index d795ab4..0000000 --- a/WireGuard/WireGuard/UI/iOS/Settings/SettingsButtonCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 532f1d1..0000000 --- a/WireGuard/WireGuard/UI/iOS/Settings/SettingsKeyValueCell.swift +++ /dev/null @@ -1,29 +0,0 @@ -// 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/Settings/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift deleted file mode 100644 index 5e8aee6..0000000 --- a/WireGuard/WireGuard/UI/iOS/Settings/SettingsTableViewController.swift +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2018 WireGuard LLC. All Rights Reserved. - -import UIKit -import os.log - -class SettingsTableViewController: UITableViewController { - - enum SettingsFields: String { - case iosAppVersion = "WireGuard for iOS" - case goBackendVersion = "WireGuard Go Backend" - case exportZipArchive = "Export zip archive" - case exportLogFile = "Export log file" - } - - let settingsFieldsBySection: [[SettingsFields]] = [ - [.iosAppVersion, .goBackendVersion], - [.exportZipArchive], - [.exportLogFile] - ] - - let tunnelsManager: TunnelsManager? - var wireguardCaptionedImage: (view: UIView, size: CGSize)? - - init(tunnelsManager: TunnelsManager?) { - self.tunnelsManager = tunnelsManager - super.init(style: .grouped) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - title = "Settings" - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) - - tableView.estimatedRowHeight = 44 - tableView.rowHeight = UITableView.automaticDimension - tableView.allowsSelection = false - - tableView.register(SettingsKeyValueCell.self) - tableView.register(SettingsButtonCell.self) - - tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf")) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - guard let logo = self.tableView.tableFooterView else { return } - - let bottomPadding = max(tableView.layoutMargins.bottom, 10) - let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding) - - let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height - - var height = tableView.estimatedRowHeight * 1.5 - var width = height * imageAspectRatio - let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20) - if width > maxWidth { - width = maxWidth - height = width / imageAspectRatio - } - - let needsReload = height != logo.frame.height - - logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height) - - if needsReload { - tableView.tableFooterView = logo - } - } - - @objc func doneTapped() { - dismiss(animated: true, completion: nil) - } - - func exportConfigurationsAsZipFile(sourceView: UIView) { - guard let tunnelsManager = tunnelsManager else { return } - guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - - let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip") - _ = FileManager.deleteFile(at: destinationURL) - - let count = tunnelsManager.numberOfTunnels() - let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() } - ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in - if let error = error { - ErrorPresenter.showErrorAlert(error: error, from: self) - return - } - - let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService) - self?.present(fileExportVC, animated: true, completion: nil) - } - } - - func exportLogForLastActivatedTunnel(sourceView: UIView) { - guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename - let timeStampString = dateFormatter.string(from: Date()) - let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") - - DispatchQueue.global(qos: .userInitiated).async { - - if FileManager.default.fileExists(atPath: destinationURL.path) { - let isDeleted = FileManager.deleteFile(at: destinationURL) - if !isDeleted { - ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self) - return - } - } - - guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { - ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self) - return - } - - let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false - guard isWritten else { - ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self) - return - } - - DispatchQueue.main.async { - let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) - // popoverPresentationController shall be non-nil on the iPad - activityVC.popoverPresentationController?.sourceView = sourceView - activityVC.popoverPresentationController?.sourceRect = sourceView.bounds - activityVC.completionWithItemsHandler = { _, _, _, _ in - // Remove the exported log file after the activity has completed - _ = FileManager.deleteFile(at: destinationURL) - } - self.present(activityVC, animated: true) - } - } - } -} - -// MARK: UITableViewDataSource - -extension SettingsTableViewController { - override func numberOfSections(in tableView: UITableView) -> Int { - return settingsFieldsBySection.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return settingsFieldsBySection[section].count - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch section { - case 0: - return "About" - case 1: - return "Export configurations" - case 2: - return "Tunnel log" - default: - return nil - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let field = settingsFieldsBySection[indexPath.section][indexPath.row] - if field == .iosAppVersion || field == .goBackendVersion { - let cell: SettingsKeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue - if field == .iosAppVersion { - var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" - if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - appVersion += " (\(appBuild))" - } - cell.value = appVersion - } else if field == .goBackendVersion { - cell.value = WIREGUARD_GO_VERSION - } - return cell - } else if field == .exportZipArchive { - let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue - cell.onTapped = { [weak self] in - self?.exportConfigurationsAsZipFile(sourceView: cell.button) - } - return cell - } else { - assert(field == .exportLogFile) - let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue - cell.onTapped = { [weak self] in - self?.exportLogForLastActivatedTunnel(sourceView: cell.button) - } - return cell - } - } -} diff --git a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift new file mode 100644 index 0000000..b583c5b --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import UIKit +import os.log + +class SettingsTableViewController: UITableViewController { + + enum SettingsFields: String { + case iosAppVersion = "WireGuard for iOS" + case goBackendVersion = "WireGuard Go Backend" + case exportZipArchive = "Export zip archive" + case exportLogFile = "Export log file" + } + + let settingsFieldsBySection: [[SettingsFields]] = [ + [.iosAppVersion, .goBackendVersion], + [.exportZipArchive], + [.exportLogFile] + ] + + let tunnelsManager: TunnelsManager? + var wireguardCaptionedImage: (view: UIView, size: CGSize)? + + init(tunnelsManager: TunnelsManager?) { + self.tunnelsManager = tunnelsManager + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = "Settings" + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) + + tableView.estimatedRowHeight = 44 + tableView.rowHeight = UITableView.automaticDimension + tableView.allowsSelection = false + + tableView.register(KeyValueCell.self) + tableView.register(ButtonCell.self) + + tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf")) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + guard let logo = tableView.tableFooterView else { return } + + let bottomPadding = max(tableView.layoutMargins.bottom, 10) + let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding) + + let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height + + var height = tableView.estimatedRowHeight * 1.5 + var width = height * imageAspectRatio + let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20) + if width > maxWidth { + width = maxWidth + height = width / imageAspectRatio + } + + let needsReload = height != logo.frame.height + + logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height) + + if needsReload { + tableView.tableFooterView = logo + } + } + + @objc func doneTapped() { + dismiss(animated: true, completion: nil) + } + + func exportConfigurationsAsZipFile(sourceView: UIView) { + guard let tunnelsManager = tunnelsManager else { return } + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + + let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip") + _ = FileManager.deleteFile(at: destinationURL) + + let count = tunnelsManager.numberOfTunnels() + let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() } + ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in + if let error = error { + ErrorPresenter.showErrorAlert(error: error, from: self) + return + } + + let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService) + self?.present(fileExportVC, animated: true, completion: nil) + } + } + + func exportLogForLastActivatedTunnel(sourceView: UIView) { + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename + let timeStampString = dateFormatter.string(from: Date()) + let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") + + DispatchQueue.global(qos: .userInitiated).async { + + if FileManager.default.fileExists(atPath: destinationURL.path) { + let isDeleted = FileManager.deleteFile(at: destinationURL) + if !isDeleted { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self) + return + } + } + + guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self) + return + } + + let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false + guard isWritten else { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self) + return + } + + DispatchQueue.main.async { + let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) + // popoverPresentationController shall be non-nil on the iPad + activityVC.popoverPresentationController?.sourceView = sourceView + activityVC.popoverPresentationController?.sourceRect = sourceView.bounds + activityVC.completionWithItemsHandler = { _, _, _, _ in + // Remove the exported log file after the activity has completed + _ = FileManager.deleteFile(at: destinationURL) + } + self.present(activityVC, animated: true) + } + } + } +} + +// MARK: UITableViewDataSource + +extension SettingsTableViewController { + override func numberOfSections(in tableView: UITableView) -> Int { + return settingsFieldsBySection.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return settingsFieldsBySection[section].count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "About" + case 1: + return "Export configurations" + case 2: + return "Tunnel log" + default: + return nil + } + } + + 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) + cell.copyableGesture = false + cell.key = field.rawValue + if field == .iosAppVersion { + var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" + if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + appVersion += " (\(appBuild))" + } + cell.value = appVersion + } else if field == .goBackendVersion { + cell.value = WIREGUARD_GO_VERSION + } + return cell + } else if field == .exportZipArchive { + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + self?.exportConfigurationsAsZipFile(sourceView: cell.button) + } + return cell + } else { + assert(field == .exportLogFile) + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + self?.exportLogForLastActivatedTunnel(sourceView: cell.button) + } + return cell + } + } +} diff --git a/WireGuard/WireGuard/UI/iOS/SharedViews/BorderedTextButton.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/BorderedTextButton.swift new file mode 100644 index 0000000..94b76d6 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/ButtonCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/ButtonCell.swift new file mode 100644 index 0000000..4702993 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/CheckmarkCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/CheckmarkCell.swift new file mode 100644 index 0000000..db4b6c9 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/CopyableLabelTableViewCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/CopyableLabelTableViewCell.swift new file mode 100644 index 0000000..93a9ef7 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/EditableKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/EditableKeyValueCell.swift new file mode 100644 index 0000000..48956eb --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/KeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/KeyValueCell.swift new file mode 100644 index 0000000..78026ea --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/ScrollableLabel.swift new file mode 100644 index 0000000..bd6f547 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/SharedViews/SwitchCell.swift b/WireGuard/WireGuard/UI/iOS/SharedViews/SwitchCell.swift new file mode 100644 index 0000000..d0c29aa --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/SharedViews/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/TunnelDetail/TunnelDetailActivateOnDemandCell.swift b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift deleted file mode 100644 index 9507c45..0000000 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailActivateOnDemandCell.swift +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 8710616..0000000 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailButtonCell.swift +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index cbe1c14..0000000 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailKeyValueCell.swift +++ /dev/null @@ -1,107 +0,0 @@ -// 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 deleted file mode 100644 index 0dd1ee9..0000000 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailStatusCell.swift +++ /dev/null @@ -1,85 +0,0 @@ -// 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.asyncAfter(deadline: .now() + .milliseconds(200)) { [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 index af4cf83..ed48d0f 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelDetail/TunnelDetailTableViewController.swift @@ -29,6 +29,8 @@ class TunnelDetailTableViewController: UITableViewController { let tunnel: TunnelContainer var tunnelViewModel: TunnelViewModel private var sections = [Section]() + private var onDemandStatusObservervationToken: AnyObject? + private var statusObservervationToken: AnyObject? init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { self.tunnelsManager = tunnelsManager @@ -41,22 +43,26 @@ class TunnelDetailTableViewController: UITableViewController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + onDemandStatusObservervationToken = nil + statusObservervationToken = nil + } override func viewDidLoad() { super.viewDidLoad() - self.title = tunnelViewModel.interfaceData[.name] - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped)) + title = tunnelViewModel.interfaceData[.name] + 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) + tableView.estimatedRowHeight = 44 + tableView.rowHeight = UITableView.automaticDimension + tableView.allowsSelection = false + tableView.register(SwitchCell.self) + tableView.register(KeyValueCell.self) + tableView.register(ButtonCell.self) // State restoration - self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" + restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" } private func loadSections() { @@ -76,8 +82,7 @@ class TunnelDetailTableViewController: UITableViewController { present(editNC, animated: true) } - func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, - onConfirmed: @escaping (() -> Void)) { + func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) { let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in onConfirmed() } @@ -90,7 +95,7 @@ class TunnelDetailTableViewController: UITableViewController { alert.popoverPresentationController?.sourceView = sourceView alert.popoverPresentationController?.sourceRect = sourceView.bounds - self.present(alert, animated: true, completion: nil) + present(alert, animated: true, completion: nil) } } @@ -100,8 +105,8 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate func tunnelSaved(tunnel: TunnelContainer) { tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) loadSections() - self.title = tunnel.name - self.tableView.reloadData() + title = tunnel.name + tableView.reloadData() } func tunnelEditingCancelled() { // Nothing to do @@ -161,8 +166,40 @@ extension TunnelDetailTableViewController { } private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let cell: TunnelDetailStatusCell = tableView.dequeueReusableCell(for: indexPath) - cell.tunnel = self.tunnel + let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) + + let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in + 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" + } + cell.textLabel?.text = text + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in + cell?.switchView.isOn = !(status == .deactivating || status == .inactive) + cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active) + } + cell.isEnabled = status == .active || status == .inactive + } + + statusUpdate(cell, tunnel.status) + statusObservervationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in + guard let cell = cell else { return } + statusUpdate(cell, tunnel.status) + } + cell.onSwitchToggled = { [weak self] isOn in guard let self = self else { return } if isOn { @@ -176,7 +213,7 @@ extension TunnelDetailTableViewController { 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) + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue cell.value = tunnelViewModel.interfaceData[field] return cell @@ -184,20 +221,24 @@ extension TunnelDetailTableViewController { 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) + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue cell.value = peerData[field] return cell } private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let cell: TunnelDetailActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath) - cell.tunnel = self.tunnel + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.key = "Activate on demand" + cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) + onDemandStatusObservervationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in + cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) + } return cell } private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let cell: TunnelDetailButtonCell = tableView.dequeueReusableCell(for: indexPath) + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = "Delete tunnel" cell.hasDestructiveAction = true cell.onTapped = { [weak self] in diff --git a/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift b/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift deleted file mode 100644 index 5114c09..0000000 --- a/WireGuard/WireGuard/UI/iOS/TunnelList/BorderedTextButton.swift +++ /dev/null @@ -1,50 +0,0 @@ -// 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 index f0a16bf..14a7194 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelListCell.swift @@ -47,14 +47,14 @@ class TunnelListCell: UITableViewCell { NSLayoutConstraint.activate([ statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor) - ]) + ]) contentView.addSubview(busyIndicator) busyIndicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1) - ]) + ]) contentView.addSubview(nameLabel) nameLabel.translatesAutoresizingMaskIntoConstraints = false @@ -66,7 +66,7 @@ class TunnelListCell: UITableViewCell { nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1), busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1), bottomAnchorConstraint - ]) + ]) accessoryType = .disclosureIndicator diff --git a/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift index eda09af..0188c62 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelList/TunnelsListTableViewController.swift @@ -96,7 +96,8 @@ class TunnelsListTableViewController: UIViewController { } @objc func addButtonTapped(sender: AnyObject) { - if self.tunnelsManager == nil { return } // Do nothing until we've loaded the tunnels + guard tunnelsManager != nil else { return } + let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet) let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in self?.presentViewControllerForFileImport() @@ -125,29 +126,30 @@ class TunnelsListTableViewController: UIViewController { alert.popoverPresentationController?.sourceView = sender alert.popoverPresentationController?.sourceRect = sender.bounds } - self.present(alert, animated: true, completion: nil) + present(alert, animated: true, completion: nil) } @objc func settingsButtonTapped(sender: UIBarButtonItem!) { - if self.tunnelsManager == nil { return } // Do nothing until we've loaded the tunnels + guard tunnelsManager != nil else { return } + let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager) let settingsNC = UINavigationController(rootViewController: settingsVC) settingsNC.modalPresentationStyle = .formSheet - self.present(settingsNC, animated: true) + present(settingsNC, animated: true) } func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) { let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration) let editNC = UINavigationController(rootViewController: editVC) editNC.modalPresentationStyle = .formSheet - self.present(editNC, animated: true) + present(editNC, animated: true) } func presentViewControllerForFileImport() { let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)] let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) filePicker.delegate = self - self.present(filePicker, animated: true) + present(filePicker, animated: true) } func presentViewControllerForScanningQRCode() { @@ -155,7 +157,7 @@ class TunnelsListTableViewController: UIViewController { scanQRCodeVC.delegate = self let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC) scanQRCodeNC.modalPresentationStyle = .fullScreen - self.present(scanQRCodeNC, animated: true) + present(scanQRCodeNC, animated: true) } func importFromFile(url: URL, completionHandler: (() -> Void)?) { -- cgit v1.2.3-59-g8ed1b