diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index a499298..d9454bb 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ 6BD5C97C220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97D220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97E220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; + 6F0F44C9222D55BB00B0FF04 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0F44C8222D55BB00B0FF04 /* TextCell.swift */; }; + 6F0F44CB222D55FD00B0FF04 /* EditableTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */; }; 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */; }; 6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */; }; 6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */; }; @@ -79,6 +81,8 @@ 6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */; }; 6F8F0D7122258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */; }; 6F8F0D7222258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */; }; + 6F8F0D7422267AD2000E8335 /* ChevronCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7322267AD2000E8335 /* ChevronCell.swift */; }; + 6F8F0D7722267C57000E8335 /* SSIDOptionEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */; }; 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; }; 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; }; 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; }; @@ -262,6 +266,8 @@ 6BAC16E42216324B00A5FB78 /* AppStorePrivacyNotice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePrivacyNotice.swift; sourceTree = ""; }; 6BD5C979220D1AE100784E08 /* key.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key.c; sourceTree = ""; }; 6BD5C97A220D1AE200784E08 /* key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = key.h; sourceTree = ""; }; + 6F0F44C8222D55BB00B0FF04 /* TextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = ""; }; + 6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextCell.swift; sourceTree = ""; }; 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = ""; }; 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListRow.swift; sourceTree = ""; }; 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = ""; }; @@ -297,6 +303,8 @@ 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = ""; }; 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = ""; }; 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandViewModel.swift; sourceTree = ""; }; + 6F8F0D7322267AD2000E8335 /* ChevronCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronCell.swift; sourceTree = ""; }; + 6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSIDOptionEditTableViewController.swift; sourceTree = ""; }; 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = ""; }; 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = ""; }; 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = ""; }; @@ -402,6 +410,9 @@ 5F4541A521C4449E00994C13 /* ButtonCell.swift */, 5F45419121C2D55800994C13 /* CheckmarkCell.swift */, 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */, + 6F8F0D7322267AD2000E8335 /* ChevronCell.swift */, + 6F0F44C8222D55BB00B0FF04 /* TextCell.swift */, + 6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */, ); path = View; sourceTree = ""; @@ -415,6 +426,7 @@ 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */, 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */, 6F7774DF217181B1006A79B3 /* MainViewController.swift */, + 6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */, ); path = ViewController; sourceTree = ""; @@ -1231,6 +1243,7 @@ files = ( 6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */, 6FF3527221C2616C0008484E /* ringlogger.c in Sources */, + 6F0F44CB222D55FD00B0FF04 /* EditableTextCell.swift in Sources */, 6FF3527321C2616C0008484E /* Logger.swift in Sources */, 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, @@ -1238,12 +1251,14 @@ 5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */, 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */, 6F8F0D7122258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */, + 6F8F0D7722267C57000E8335 /* SSIDOptionEditTableViewController.swift in Sources */, 6B586C53220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */, 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */, + 6F8F0D7422267AD2000E8335 /* ChevronCell.swift in Sources */, 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, @@ -1265,6 +1280,7 @@ 6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */, + 6F0F44C9222D55BB00B0FF04 /* TextCell.swift in Sources */, 5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */, 5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */, 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */, diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings index ace9646..41130bb 100644 --- a/WireGuard/WireGuard/Base.lproj/Localizable.strings +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -92,6 +92,11 @@ "tunnelOnDemandOnlySelectedSSIDs" = "Only selected SSIDs"; "tunnelOnDemandExceptSelectedSSIDs" = "Except selected SSIDs"; +"tunnelOnDemandSelectionViewTitle" = "Select SSIDs"; +"tunnelOnDemandSectionTitleSelectedSSIDs" = "Selected SSIDs"; +"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs"; +"tunnelOnDemandAddMessageAddNew" = "Add manually"; + "tunnelOnDemandKey" = "Activate on demand"; "tunnelOnDemandOptionOff" = "Off"; "tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only"; diff --git a/WireGuard/WireGuard/Tunnel/ActivateOnDemandSetting.swift b/WireGuard/WireGuard/Tunnel/ActivateOnDemandSetting.swift index 28612c7..89edd77 100644 --- a/WireGuard/WireGuard/Tunnel/ActivateOnDemandSetting.swift +++ b/WireGuard/WireGuard/Tunnel/ActivateOnDemandSetting.swift @@ -132,3 +132,13 @@ private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDeman NEOnDemandRuleConnect(interfaceType: .wiFi)] } } + +extension ActivateOnDemandSetting { + init(with option: ActivateOnDemandOption) { + if option == .none { + self = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: option) + } else { + self = ActivateOnDemandSetting(isActivateOnDemandEnabled: true, activateOnDemandOption: option) + } + } +} diff --git a/WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift b/WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift new file mode 100644 index 0000000..94e4e05 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import UIKit + +class ChevronCell: UITableViewCell { + var message: String { + get { return textLabel?.text ?? "" } + set(value) { textLabel?.text = value } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .default, reuseIdentifier: reuseIdentifier) + accessoryType = .disclosureIndicator + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + message = "" + } +} diff --git a/WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift b/WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift new file mode 100644 index 0000000..178b200 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import UIKit + +class EditableTextCell: UITableViewCell { + var message: String { + get { return valueTextField.text ?? "" } + set(value) { valueTextField.text = value } + } + + let valueTextField: UITextField = { + let valueTextField = UITextField() + valueTextField.textAlignment = .left + valueTextField.isEnabled = true + valueTextField.font = UIFont.preferredFont(forTextStyle: .body) + valueTextField.adjustsFontForContentSizeCategory = true + valueTextField.autocapitalizationType = .none + valueTextField.autocorrectionType = .no + valueTextField.spellCheckingType = .no + return valueTextField + }() + + var onValueBeingEdited: ((String) -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + valueTextField.delegate = self + contentView.addSubview(valueTextField) + valueTextField.translatesAutoresizingMaskIntoConstraints = false + let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1) + bottomAnchorConstraint.priority = .defaultLow + NSLayoutConstraint.activate([ + valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1), + contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1), + valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1), + bottomAnchorConstraint + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func beginEditing() { + valueTextField.becomeFirstResponder() + } + + override func prepareForReuse() { + super.prepareForReuse() + message = "" + } +} + +extension EditableTextCell: UITextFieldDelegate { + 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/TextCell.swift b/WireGuard/WireGuard/UI/iOS/View/TextCell.swift new file mode 100644 index 0000000..303f9c7 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/View/TextCell.swift @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import UIKit + +class TextCell: UITableViewCell { + var message: String { + get { return textLabel?.text ?? "" } + set(value) { textLabel!.text = value } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .default, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + message = "" + } +} diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift new file mode 100644 index 0000000..7027d34 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import UIKit + +protocol SSIDOptionEditTableViewControllerDelegate: class { + func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) +} + +class SSIDOptionEditTableViewController: UITableViewController { + private enum Section { + case ssidOption + case selectedSSIDs + case addSSIDs + } + + weak var delegate: SSIDOptionEditTableViewControllerDelegate? + + private var sections = [Section]() + + let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [ + .anySSID, + .onlySpecificSSIDs, + .exceptSpecificSSIDs + ] + + var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption + var selectedSSIDs: [String] + + init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) { + selectedOption = option + selectedSSIDs = ssids + super.init(style: .grouped) + loadSections() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = tr("tunnelOnDemandSelectionViewTitle") + + tableView.estimatedRowHeight = 44 + tableView.rowHeight = UITableView.automaticDimension + + tableView.register(CheckmarkCell.self) + tableView.register(EditableTextCell.self) + tableView.register(TextCell.self) + tableView.isEditing = true + tableView.allowsSelectionDuringEditing = true + } + + func loadSections() { + sections.removeAll() + sections.append(.ssidOption) + if selectedOption != .anySSID { + if !selectedSSIDs.isEmpty { + sections.append(.selectedSSIDs) + } + sections.append(.addSSIDs) + } + } + + override func viewWillDisappear(_ animated: Bool) { + delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs) + } +} + +extension SSIDOptionEditTableViewController { + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch sections[section] { + case .ssidOption: + return ssidOptionFields.count + case .selectedSSIDs: + return selectedSSIDs.count + case .addSSIDs: + return 1 + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch sections[indexPath.section] { + case .ssidOption: + return ssidOptionCell(for: tableView, at: indexPath) + case .selectedSSIDs: + return selectedSSIDCell(for: tableView, at: indexPath) + case .addSSIDs: + return addSSIDCell(for: tableView, at: indexPath) + } + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + switch sections[indexPath.section] { + case .ssidOption: + return false + case .selectedSSIDs, .addSSIDs: + return true + } + } + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + switch sections[indexPath.section] { + case .ssidOption: + return .none + case .selectedSSIDs: + return .delete + case .addSSIDs: + return .insert + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch sections[section] { + case .ssidOption: + return nil + case .selectedSSIDs: + return tr("tunnelOnDemandSectionTitleSelectedSSIDs") + case .addSSIDs: + return tr("tunnelOnDemandSectionTitleAddSSIDs") + } + } + + private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let field = ssidOptionFields[indexPath.row] + let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath) + cell.message = field.localizedUIString + cell.isChecked = selectedOption == field + cell.isEditing = false + return cell + } + + private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath) + cell.message = selectedSSIDs[indexPath.row] + cell.isEditing = true + cell.onValueBeingEdited = { [weak self, weak cell] text in + guard let self = self, let cell = cell else { return } + if let row = self.tableView.indexPath(for: cell)?.row { + self.selectedSSIDs[row] = text + } + } + return cell + } + + private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { + let cell: TextCell = tableView.dequeueReusableCell(for: indexPath) + cell.message = tr("tunnelOnDemandAddMessageAddNew") + cell.isEditing = true + return cell + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + switch sections[indexPath.section] { + case .ssidOption: + assertionFailure() + case .selectedSSIDs: + assert(editingStyle == .delete) + selectedSSIDs.remove(at: indexPath.row) + loadSections() + let hasSelectedSSIDsSection = sections.contains(.selectedSSIDs) + if hasSelectedSSIDsSection { + tableView.deleteRows(at: [indexPath], with: .automatic) + } else { + tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic) + } + case .addSSIDs: + assert(editingStyle == .insert) + let hasSelectedSSIDsSection = sections.contains(.selectedSSIDs) + selectedSSIDs.append("") + loadSections() + let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)! + let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection) + if !hasSelectedSSIDsSection { + tableView.insertSections(IndexSet(integer: selectedSSIDsSection), with: .automatic) + } else { + tableView.insertRows(at: [indexPath], with: .automatic) + } + if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell { + selectedSSIDCell.beginEditing() + } + } + } + + func lastSelectedSSIDItemIndexPath() -> IndexPath? { + guard !selectedSSIDs.isEmpty else { return nil } + guard let section = sections.firstIndex(of: .selectedSSIDs) else { return nil } + return IndexPath(row: selectedSSIDs.count - 1, section: section) + } +} + +extension SSIDOptionEditTableViewController { + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + switch sections[indexPath.section] { + case .ssidOption: + return indexPath + case .selectedSSIDs, .addSSIDs: + return nil + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch sections[indexPath.section] { + case .ssidOption: + let previousOption = selectedOption + let previousSectionCount = sections.count + selectedOption = ssidOptionFields[indexPath.row] + loadSections() + if previousOption == .anySSID { + let indexSet = selectedSSIDs.isEmpty ? IndexSet(integer: 1) : IndexSet(1 ... 2) + tableView.insertSections(indexSet, with: .fade) + } + if selectedOption == .anySSID { + let indexSet = previousSectionCount == 2 ? IndexSet(integer: 1) : IndexSet(1 ... 2) + tableView.deleteSections(indexSet, with: .fade) + } + tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none) + case .selectedSSIDs, .addSSIDs: + assertionFailure() + } + } +} diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift index 22c3ec4..ef7fc60 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift @@ -43,16 +43,16 @@ class TunnelEditTableViewController: UITableViewController { .deletePeer ] - let activateOnDemandOptions: [ActivateOnDemandOption] = [ - .anyInterface(.anySSID), - .wiFiInterfaceOnly(.anySSID), - .nonWiFiInterfaceOnly + let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [ + .nonWiFiInterface, + .wiFiInterface, + .ssidEdit ] let tunnelsManager: TunnelsManager let tunnel: TunnelContainer? let tunnelViewModel: TunnelViewModel - var activateOnDemandSetting: ActivateOnDemandSetting + var onDemandViewModel: ActivateOnDemandViewModel private var sections = [Section]() // Use this initializer to edit an existing tunnel. @@ -60,7 +60,8 @@ class TunnelEditTableViewController: UITableViewController { self.tunnelsManager = tunnelsManager self.tunnel = tunnel tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) - activateOnDemandSetting = tunnel.activateOnDemandSetting + let onDemandOption = tunnel.activateOnDemandSetting.isActivateOnDemandEnabled ? tunnel.activateOnDemandSetting.activateOnDemandOption : .none + onDemandViewModel = ActivateOnDemandViewModel(from: onDemandOption) super.init(style: .grouped) loadSections() } @@ -70,7 +71,7 @@ class TunnelEditTableViewController: UITableViewController { self.tunnelsManager = tunnelsManager tunnel = nil tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil) - activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting + onDemandViewModel = ActivateOnDemandViewModel() super.init(style: .grouped) loadSections() } @@ -92,7 +93,7 @@ class TunnelEditTableViewController: UITableViewController { tableView.register(TunnelEditEditableKeyValueCell.self) tableView.register(ButtonCell.self) tableView.register(SwitchCell.self) - tableView.register(CheckmarkCell.self) + tableView.register(ChevronCell.self) } private func loadSections() { @@ -113,6 +114,7 @@ class TunnelEditTableViewController: UITableViewController { ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self) tableView.reloadData() // Highlight erroring fields case .saved(let tunnelConfiguration): + let activateOnDemandSetting = ActivateOnDemandSetting(with: onDemandViewModel.toOnDemandOption()) if let tunnel = tunnel { // We're modifying an existing tunnel tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in @@ -161,10 +163,10 @@ extension TunnelEditTableViewController { case .addPeer: return 1 case .onDemand: - if activateOnDemandSetting.isActivateOnDemandEnabled { - return 4 + if onDemandViewModel.isWiFiInterfaceEnabled { + return 3 } else { - return 1 + return 2 } } } @@ -419,36 +421,28 @@ extension TunnelEditTableViewController { } private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - if indexPath.row == 0 { + let field = onDemandFields[indexPath.row] + if indexPath.row < 2 { let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) - cell.message = tr("tunnelOnDemandKey") - cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled + cell.message = field.localizedUIString + cell.isOn = onDemandViewModel.isEnabled(field: field) cell.onSwitchToggled = { [weak self] isOn in guard let self = self else { return } - guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return } - - self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn - self.loadSections() - + self.onDemandViewModel.setEnabled(field: field, isEnabled: isOn) let section = self.sections.firstIndex { $0 == .onDemand }! - let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) } - if isOn { - if self.activateOnDemandSetting.activateOnDemandOption == .none { - self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption() + let indexPath = IndexPath(row: 2, section: section) + if field == .wiFiInterface { + if isOn { + tableView.insertRows(at: [indexPath], with: .fade) + } else { + tableView.deleteRows(at: [indexPath], with: .fade) } - self.tableView.insertRows(at: indexPaths, with: .fade) - } else { - self.tableView.deleteRows(at: indexPaths, with: .fade) } } return cell } else { - let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath) - let rowOption = activateOnDemandOptions[indexPath.row - 1] - let selectedOption = activateOnDemandSetting.activateOnDemandOption - assert(selectedOption != .none) - cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption) - cell.isChecked = selectedOption == rowOption + let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath) + cell.message = field.localizedUIString return cell } } @@ -484,7 +478,7 @@ extension TunnelEditTableViewController { extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - if case .onDemand = sections[indexPath.section], indexPath.row > 0 { + if case .onDemand = sections[indexPath.section], indexPath.row == 2 { return indexPath } else { return nil @@ -494,16 +488,21 @@ extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch sections[indexPath.section] { case .onDemand: - let option = activateOnDemandOptions[indexPath.row - 1] - assert(option != .none) - activateOnDemandSetting.activateOnDemandOption = option - - let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) } - UIView.performWithoutAnimation { - tableView.reloadRows(at: indexPaths, with: .none) - } + assert(indexPath.row == 2) + tableView.deselectRow(at: indexPath, animated: true) + let ssidOptionVC = SSIDOptionEditTableViewController(option: onDemandViewModel.ssidOption, ssids: onDemandViewModel.selectedSSIDs) + ssidOptionVC.delegate = self + navigationController?.pushViewController(ssidOptionVC, animated: true) default: assertionFailure() } } } + +extension TunnelEditTableViewController: SSIDOptionEditTableViewControllerDelegate { + func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) { + let validSSIDs = ssids.filter { !$0.isEmpty } + onDemandViewModel.selectedSSIDs = validSSIDs + onDemandViewModel.ssidOption = validSSIDs.isEmpty ? .anySSID : option + } +}