aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-02-05 16:38:12 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2021-03-09 09:10:20 -0700
commit6447a845082416add981bd36d92864933635d984 (patch)
treeffb9109f93702256ac6114a3f8a4793a149e1c2c
parentUI: iOS: backport DiffableDataSource (diff)
downloadwireguard-apple-am/diffable-data-source-ssid.tar.xz
wireguard-apple-am/diffable-data-source-ssid.zip
UI: iOS: use DiffableDataSource for SSID editoram/diffable-data-source-ssid
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift33
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift393
2 files changed, 243 insertions, 183 deletions
diff --git a/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift b/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift
index 20c29724..b1550adc 100644
--- a/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift
+++ b/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift
@@ -5,13 +5,21 @@ import UIKit
class EditableTextCell: UITableViewCell {
var message: String {
- get { return valueTextField.text ?? "" }
- set(value) { valueTextField.text = value }
+ get {
+ return valueTextField.text ?? ""
+ }
+ set {
+ valueTextField.text = newValue
+ }
}
var placeholder: String? {
- get { return valueTextField.placeholder }
- set(value) { valueTextField.placeholder = value }
+ get {
+ return valueTextField.placeholder
+ }
+ set {
+ valueTextField.placeholder = newValue
+ }
}
let valueTextField: UITextField = {
@@ -26,23 +34,26 @@ class EditableTextCell: UITableViewCell {
return valueTextField
}()
- var onValueBeingEdited: ((String) -> Void)?
+ var onValueBeingEdited: ((EditableTextCell, String) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
- valueTextField.delegate = self
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
+
// Reduce the bottom margin by 0.5pt to maintain the default cell height (44pt)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: valueTextField.bottomAnchor, constant: -0.5)
bottomAnchorConstraint.priority = .defaultLow
+
NSLayoutConstraint.activate([
valueTextField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: valueTextField.trailingAnchor),
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: valueTextField.topAnchor),
bottomAnchorConstraint
])
+
+ NotificationCenter.default.addObserver(self, selector: #selector(textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: valueTextField)
}
required init?(coder aDecoder: NSCoder) {
@@ -58,14 +69,8 @@ class EditableTextCell: UITableViewCell {
message = ""
placeholder = nil
}
-}
-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
+ @objc private func textFieldDidChangeText(_ notification: Notification) {
+ onValueBeingEdited?(self, valueTextField.text ?? "")
}
}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift
index 256ff1c0..87a61938 100644
--- a/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift
@@ -10,45 +10,71 @@ protocol SSIDOptionEditTableViewControllerDelegate: class {
}
class SSIDOptionEditTableViewController: UITableViewController {
- private enum Section {
+ private enum Section: Hashable {
case ssidOption
case selectedSSIDs
case addSSIDs
}
- private enum AddSSIDRow {
- case addConnectedSSID(connectedSSID: String)
+ private struct SSIDEntry: Equatable, Hashable {
+ let uuid = UUID().uuidString
+ var string: String
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(uuid)
+ }
+
+ static func == (lhs: SSIDEntry, rhs: SSIDEntry) -> Bool {
+ return lhs.uuid == rhs.uuid
+ }
+ }
+
+ private enum Item: Hashable {
+ case ssidOption(ActivateOnDemandViewModel.OnDemandSSIDOption)
+ case selectedSSID(SSIDEntry)
+ case noSSID
+ case addConnectedSSID(String)
case addNewSSID
}
weak var delegate: SSIDOptionEditTableViewControllerDelegate?
- private var sections = [Section]()
- private var addSSIDRows = [AddSSIDRow]()
+ private var dataSource: TableViewDiffableDataSource<Section, Item>?
- let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
+ private let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
.anySSID,
.onlySpecificSSIDs,
.exceptSpecificSSIDs
]
- var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
- var selectedSSIDs: [String]
- var connectedSSID: String?
+ private var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
+ private var selectedSSIDs: [SSIDEntry]
+ private var connectedSSID: String?
init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
selectedOption = option
- selectedSSIDs = ssids
+ selectedSSIDs = ssids.map { SSIDEntry(string: $0) }
super.init(style: .grouped)
- loadSections()
- addSSIDRows.removeAll()
- addSSIDRows.append(.addNewSSID)
+ }
- getConnectedSSID { [weak self] ssid in
- guard let self = self else { return }
- self.connectedSSID = ssid
- self.updateCurrentSSIDEntry()
- self.updateTableViewAddSSIDRows()
+ private func makeCell(for itemIdentifier: Item, at indexPath: IndexPath) -> UITableViewCell? {
+ guard let dataSource = self.dataSource else { return nil }
+
+ let sectionIdentifier = dataSource.snapshot().sectionIdentifiers[indexPath.section]
+ switch sectionIdentifier {
+ case .ssidOption:
+ return ssidOptionCell(for: tableView, itemIdentifier: itemIdentifier, at: indexPath)
+ case .selectedSSIDs:
+ switch itemIdentifier {
+ case .noSSID:
+ return noSSIDsCell(for: tableView, at: indexPath)
+ case .selectedSSID(let ssidEntry):
+ return selectedSSIDCell(for: tableView, ssidEntry: ssidEntry, at: indexPath)
+ default:
+ fatalError()
+ }
+ case .addSSIDs:
+ return addSSIDCell(for: tableView, itemIdentifier: itemIdentifier, at: indexPath)
}
}
@@ -60,6 +86,11 @@ class SSIDOptionEditTableViewController: UITableViewController {
super.viewDidLoad()
title = tr("tunnelOnDemandSSIDViewTitle")
+ dataSource = TableViewDiffableDataSource(tableView: tableView) { [weak self] _, indexPath, item -> UITableViewCell? in
+ return self?.makeCell(for: item, at: indexPath)
+ }
+
+ tableView.dataSource = self
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
@@ -69,79 +100,93 @@ class SSIDOptionEditTableViewController: UITableViewController {
tableView.isEditing = true
tableView.allowsSelectionDuringEditing = true
tableView.keyboardDismissMode = .onDrag
+
+ updateDataSource()
+
+ updateConnectedSSID()
}
- func loadSections() {
- sections.removeAll()
- sections.append(.ssidOption)
- if selectedOption != .anySSID {
- sections.append(.selectedSSIDs)
- sections.append(.addSSIDs)
+ private func updateConnectedSSID() {
+ getConnectedSSID { [weak self] ssid in
+ guard let self = self else { return }
+
+ self.connectedSSID = ssid
+ self.updateDataSource()
}
}
- func updateCurrentSSIDEntry() {
- if let connectedSSID = connectedSSID, !selectedSSIDs.contains(connectedSSID) {
- if let first = addSSIDRows.first, case .addNewSSID = first {
- addSSIDRows.insert(.addConnectedSSID(connectedSSID: connectedSSID), at: 0)
+ private func getConnectedSSID(completionHandler: @escaping (String?) -> Void) {
+ #if targetEnvironment(simulator)
+ completionHandler("Simulator Wi-Fi")
+ #else
+ if #available(iOS 14, *) {
+ NEHotspotNetwork.fetchCurrent { hotspotNetwork in
+ completionHandler(hotspotNetwork?.ssid)
+ }
+ } else {
+ if let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] {
+ for interface in supportedInterfaces {
+ if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
+ if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
+ completionHandler(!ssid.isEmpty ? ssid : nil)
+ return
+ }
+ }
+ }
}
- } else if let first = addSSIDRows.first, case .addConnectedSSID = first {
- addSSIDRows.removeFirst()
+
+ completionHandler(nil)
}
+ #endif
}
- func updateTableViewAddSSIDRows() {
- guard let addSSIDSection = sections.firstIndex(of: .addSSIDs) else { return }
- let numberOfAddSSIDRows = addSSIDRows.count
- let numberOfAddSSIDRowsInTableView = tableView.numberOfRows(inSection: addSSIDSection)
- switch (numberOfAddSSIDRowsInTableView, numberOfAddSSIDRows) {
- case (1, 2):
- tableView.insertRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
- case (2, 1):
- tableView.deleteRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
- default:
- break
+ private func updateDataSource(completion: (() -> Void)? = nil) {
+ var snapshot = DiffableDataSourceSnapshot<Section, Item>()
+ snapshot.appendSections([.ssidOption])
+ snapshot.appendItems(ssidOptionFields.map { .ssidOption($0) }, toSection: .ssidOption)
+
+ if selectedOption != .anySSID {
+ snapshot.appendSections([.selectedSSIDs, .addSSIDs])
+
+ if selectedSSIDs.isEmpty {
+ snapshot.appendItems([.noSSID], toSection: .selectedSSIDs)
+ } else {
+ snapshot.appendItems(selectedSSIDs.map { .selectedSSID($0) }, toSection: .selectedSSIDs)
+ }
+
+ if let connectedSSID = connectedSSID, !selectedSSIDs.contains(where: { $0.string == connectedSSID }) {
+ snapshot.appendItems([.addConnectedSSID(connectedSSID)], toSection: .addSSIDs)
+ }
+ snapshot.appendItems([.addNewSSID], toSection: .addSSIDs)
}
+
+ dataSource?.apply(snapshot, animatingDifferences: true, completion: completion)
}
override func viewWillDisappear(_ animated: Bool) {
- delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs)
+ delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs.map { $0.string })
}
}
extension SSIDOptionEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
- return sections.count
+ return dataSource?.numberOfSections(in: tableView) ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch sections[section] {
- case .ssidOption:
- return ssidOptionFields.count
- case .selectedSSIDs:
- return selectedSSIDs.isEmpty ? 1 : selectedSSIDs.count
- case .addSSIDs:
- return addSSIDRows.count
- }
+ return dataSource?.tableView(tableView, numberOfRowsInSection: section) ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- switch sections[indexPath.section] {
- case .ssidOption:
- return ssidOptionCell(for: tableView, at: indexPath)
- case .selectedSSIDs:
- if !selectedSSIDs.isEmpty {
- return selectedSSIDCell(for: tableView, at: indexPath)
- } else {
- return noSSIDsCell(for: tableView, at: indexPath)
- }
- case .addSSIDs:
- return addSSIDCell(for: tableView, at: indexPath)
- }
+ return dataSource!.tableView(tableView, cellForRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- switch sections[indexPath.section] {
+ guard let dataSource = dataSource else { return false }
+
+ let sectionIdentifier = dataSource.snapshot().sectionIdentifiers[indexPath.section]
+
+ switch sectionIdentifier {
case .ssidOption:
return false
case .selectedSSIDs:
@@ -152,7 +197,11 @@ extension SSIDOptionEditTableViewController {
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
- switch sections[indexPath.section] {
+ guard let dataSource = dataSource else { return .none }
+
+ let sectionIdentifier = dataSource.snapshot().sectionIdentifiers[indexPath.section]
+
+ switch sectionIdentifier {
case .ssidOption:
return .none
case .selectedSSIDs:
@@ -163,7 +212,11 @@ extension SSIDOptionEditTableViewController {
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- switch sections[section] {
+ guard let dataSource = dataSource else { return nil }
+
+ let sectionIdentifier = dataSource.snapshot().sectionIdentifiers[section]
+
+ switch sectionIdentifier {
case .ssidOption:
return nil
case .selectedSSIDs:
@@ -173,8 +226,38 @@ extension SSIDOptionEditTableViewController {
}
}
- private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
- let field = ssidOptionFields[indexPath.row]
+ override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
+ guard let dataSource = dataSource else { return }
+
+ let sectionIdentifier = dataSource.snapshot().sectionIdentifiers[indexPath.section]
+
+ switch sectionIdentifier {
+ case .ssidOption:
+ assertionFailure()
+
+ case .selectedSSIDs:
+ assert(editingStyle == .delete)
+ selectedSSIDs.remove(at: indexPath.row)
+ updateDataSource()
+
+ case .addSSIDs:
+ assert(editingStyle == .insert)
+
+ let itemIdentifier = dataSource.itemIdentifier(for: indexPath)
+ switch itemIdentifier {
+ case .addConnectedSSID(let connectedSSID):
+ appendSSID(connectedSSID, beginEditing: false)
+ case .addNewSSID:
+ appendSSID("", beginEditing: true)
+ default:
+ fatalError()
+ }
+ }
+ }
+
+ private func ssidOptionCell(for tableView: UITableView, itemIdentifier: Item, at indexPath: IndexPath) -> UITableViewCell {
+ guard case .ssidOption(let field) = itemIdentifier else { fatalError() }
+
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.localizedUIString
cell.isChecked = selectedOption == field
@@ -194,134 +277,106 @@ extension SSIDOptionEditTableViewController {
return cell
}
- private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ private func selectedSSIDCell(for tableView: UITableView, ssidEntry: SSIDEntry, at indexPath: IndexPath) -> UITableViewCell {
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
- cell.message = selectedSSIDs[indexPath.row]
+ cell.message = ssidEntry.string
cell.placeholder = tr("tunnelOnDemandSSIDTextFieldPlaceholder")
cell.isEditing = true
- cell.onValueBeingEdited = { [weak self, weak cell] text in
- guard let self = self, let cell = cell else { return }
+ cell.onValueBeingEdited = { [weak self] cell, text in
+ guard let self = self else { return }
+
if let row = self.tableView.indexPath(for: cell)?.row {
- self.selectedSSIDs[row] = text
- self.updateCurrentSSIDEntry()
- self.updateTableViewAddSSIDRows()
+ self.selectedSSIDs[row].string = text
+ self.updateDataSource()
}
}
return cell
}
- private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ private func addSSIDCell(for tableView: UITableView, itemIdentifier: Item, at indexPath: IndexPath) -> UITableViewCell {
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
- switch addSSIDRows[indexPath.row] {
- case .addConnectedSSID:
- cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID!)
+ cell.isEditing = true
+
+ switch itemIdentifier {
+ case .addConnectedSSID(let connectedSSID):
+ cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID)
case .addNewSSID:
cell.message = tr("tunnelOnDemandAddMessageAddNewSSID")
+ default:
+ fatalError()
}
- cell.isEditing = true
+
return cell
}
+}
- override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- switch sections[indexPath.section] {
- case .ssidOption:
- assertionFailure()
+extension SSIDOptionEditTableViewController {
+ override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
+ guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[indexPath.section] else { return nil }
+
+ switch sectionIdentifier {
+ case .ssidOption, .addSSIDs:
+ return indexPath
case .selectedSSIDs:
- assert(editingStyle == .delete)
- selectedSSIDs.remove(at: indexPath.row)
- if !selectedSSIDs.isEmpty {
- tableView.deleteRows(at: [indexPath], with: .automatic)
- } else {
- tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- updateCurrentSSIDEntry()
- updateTableViewAddSSIDRows()
- case .addSSIDs:
- assert(editingStyle == .insert)
- let newSSID: String
- switch addSSIDRows[indexPath.row] {
- case .addConnectedSSID(let connectedSSID):
- newSSID = connectedSSID
- case .addNewSSID:
- newSSID = ""
- }
- selectedSSIDs.append(newSSID)
- loadSections()
- let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)!
- let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection)
- if selectedSSIDs.count == 1 {
- tableView.reloadRows(at: [indexPath], with: .automatic)
- } else {
- tableView.insertRows(at: [indexPath], with: .automatic)
- }
- updateCurrentSSIDEntry()
- updateTableViewAddSSIDRows()
- if newSSID.isEmpty {
- if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
- selectedSSIDCell.beginEditing()
- }
- }
+ return nil
}
}
- private func getConnectedSSID(completionHandler: @escaping (String?) -> Void) {
- #if targetEnvironment(simulator)
- completionHandler("Simulator Wi-Fi")
- #else
- if #available(iOS 14, *) {
- NEHotspotNetwork.fetchCurrent { hotspotNetwork in
- completionHandler(hotspotNetwork?.ssid)
- }
- } else {
- if let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] {
- for interface in supportedInterfaces {
- if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
- if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
- completionHandler(!ssid.isEmpty ? ssid : nil)
- return
- }
- }
- }
- }
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ guard let dataSource = dataSource else { return }
- completionHandler(nil)
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let itemIdentifier = dataSource.itemIdentifier(for: indexPath)
+
+ switch itemIdentifier {
+ case .ssidOption(let newOption):
+ setSSIDOption(newOption)
+
+ case .addConnectedSSID(let connectedSSID):
+ appendSSID(connectedSSID, beginEditing: false)
+
+ case .addNewSSID:
+ appendSSID("", beginEditing: true)
+
+ default:
+ break
}
- #endif
+
}
-}
-extension SSIDOptionEditTableViewController {
- override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- switch sections[indexPath.section] {
- case .ssidOption:
- return indexPath
- case .selectedSSIDs, .addSSIDs:
- return nil
+ private func appendSSID(_ newSSID: String, beginEditing: Bool) {
+ guard let dataSource = dataSource else { return }
+
+ let newEntry = SSIDEntry(string: newSSID)
+ selectedSSIDs.append(newEntry)
+ updateDataSource {
+ let indexPath = dataSource.indexPath(for: .selectedSSID(newEntry))!
+
+ if let cell = self.tableView.cellForRow(at: indexPath) as? EditableTextCell, beginEditing {
+ cell.beginEditing()
+ }
}
}
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- switch sections[indexPath.section] {
- case .ssidOption:
- let previousOption = selectedOption
- selectedOption = ssidOptionFields[indexPath.row]
- guard previousOption != selectedOption else {
- tableView.deselectRow(at: indexPath, animated: true)
- return
- }
- loadSections()
- if previousOption == .anySSID {
- let indexSet = IndexSet(1 ... 2)
- tableView.insertSections(indexSet, with: .fade)
- }
- if selectedOption == .anySSID {
- let indexSet = IndexSet(1 ... 2)
- tableView.deleteSections(indexSet, with: .fade)
- }
- tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none)
- case .selectedSSIDs, .addSSIDs:
- assertionFailure()
+ private func setSSIDOption(_ ssidOption: ActivateOnDemandViewModel.OnDemandSSIDOption) {
+ guard let dataSource = dataSource, ssidOption != selectedOption else { return }
+
+ let prevOption = selectedOption
+ selectedOption = ssidOption
+
+ // Manually update cells
+ let indexPathForPrevItem = dataSource.indexPath(for: .ssidOption(prevOption))!
+ let indexPathForSelectedItem = dataSource.indexPath(for: .ssidOption(selectedOption))!
+
+ if let cell = tableView.cellForRow(at: indexPathForPrevItem) as? CheckmarkCell {
+ cell.isChecked = false
+ }
+
+ if let cell = tableView.cellForRow(at: indexPathForSelectedItem) as? CheckmarkCell {
+ cell.isChecked = true
}
+
+ updateDataSource()
}
}
-