]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
on-demand: iOS: Support for SSIDs
authorRoopesh Chander <roop@roopc.net>
Wed, 27 Feb 2019 08:00:57 +0000 (13:30 +0530)
committerJason A. Donenfeld <Jason@zx2c4.com>
Mon, 18 Mar 2019 05:46:55 +0000 (06:46 +0100)
Signed-off-by: Roopesh Chander <roop@roopc.net>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/Base.lproj/Localizable.strings
WireGuard/WireGuard/Tunnel/ActivateOnDemandSetting.swift
WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/iOS/View/TextCell.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift

index a499298d781e7e9827122baa9427b617d803d9fe..d9454bb0b8298b2e406d7a121b35fda816c69daa 100644 (file)
@@ -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 */; };
                6BAC16E42216324B00A5FB78 /* AppStorePrivacyNotice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePrivacyNotice.swift; sourceTree = "<group>"; };
                6BD5C979220D1AE100784E08 /* key.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key.c; sourceTree = "<group>"; };
                6BD5C97A220D1AE200784E08 /* key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = key.h; sourceTree = "<group>"; };
+               6F0F44C8222D55BB00B0FF04 /* TextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = "<group>"; };
+               6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextCell.swift; sourceTree = "<group>"; };
                6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = "<group>"; };
                6F4DD16A21DA558800690EAE /* TunnelListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListRow.swift; sourceTree = "<group>"; };
                6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = "<group>"; };
                6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
                6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = "<group>"; };
                6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandViewModel.swift; sourceTree = "<group>"; };
+               6F8F0D7322267AD2000E8335 /* ChevronCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronCell.swift; sourceTree = "<group>"; };
+               6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSIDOptionEditTableViewController.swift; sourceTree = "<group>"; };
                6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
                6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
                6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
                                5F4541A521C4449E00994C13 /* ButtonCell.swift */,
                                5F45419121C2D55800994C13 /* CheckmarkCell.swift */,
                                5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
+                               6F8F0D7322267AD2000E8335 /* ChevronCell.swift */,
+                               6F0F44C8222D55BB00B0FF04 /* TextCell.swift */,
+                               6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */,
                        );
                        path = View;
                        sourceTree = "<group>";
                                6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
                                6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
                                6F7774DF217181B1006A79B3 /* MainViewController.swift */,
+                               6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */,
                        );
                        path = ViewController;
                        sourceTree = "<group>";
                        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 */,
                                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 */,
                                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 */,
index ace96465de64a0453c7396868d421d6ce2ab2c99..41130bb34df5eca9c5ee3bd82a2a410d89d8fa1b 100644 (file)
 "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";
index 28612c70780368b700b4c620412960d2980da21f..89edd777eff33166da72ad81c69a127e1b3a20dc 100644 (file)
@@ -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 (file)
index 0000000..94e4e05
--- /dev/null
@@ -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 (file)
index 0000000..178b200
--- /dev/null
@@ -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 (file)
index 0000000..303f9c7
--- /dev/null
@@ -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 (file)
index 0000000..7027d34
--- /dev/null
@@ -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()
+        }
+    }
+}
index 22c3ec45946a011deb8d92a3d747389838c199b9..ef7fc60002180f6ab8f45974790eb55ea340b5f3 100644 (file)
@@ -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
+    }
+}