aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI
diff options
context:
space:
mode:
authorRoopesh Chander <roop@roopc.net>2019-02-01 17:06:42 +0530
committerRoopesh Chander <roop@roopc.net>2019-02-02 19:22:01 +0530
commit4ff6105053b4d0b872b128dba3ac8cbb980e4d80 (patch)
tree422a18cddd49010dbaf8f4a06da8abd25a6c147a /WireGuard/WireGuard/UI
parentiOS: Tunnel detail: Keep track of visible fields with a [Bool] array (diff)
downloadwireguard-apple-4ff6105053b4d0b872b128dba3ac8cbb980e4d80.tar.xz
wireguard-apple-4ff6105053b4d0b872b128dba3ac8cbb980e4d80.zip
iOS: Apply runtime configuration by diff-ing
And apply the diff on the tableView as insert/remove/reloads. Signed-off-by: Roopesh Chander <roop@roopc.net>
Diffstat (limited to 'WireGuard/WireGuard/UI')
-rw-r--r--WireGuard/WireGuard/UI/TunnelViewModel.swift142
-rw-r--r--WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift94
2 files changed, 217 insertions, 19 deletions
diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift
index 886703ed..fcbaef3b 100644
--- a/WireGuard/WireGuard/UI/TunnelViewModel.swift
+++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift
@@ -6,7 +6,7 @@ import Foundation
//swiftlint:disable:next type_body_length
class TunnelViewModel {
- enum InterfaceField {
+ enum InterfaceField: CaseIterable {
case name
case privateKey
case publicKey
@@ -34,7 +34,7 @@ class TunnelViewModel {
.generateKeyPair
]
- enum PeerField {
+ enum PeerField: CaseIterable {
case publicKey
case preSharedKey
case endpoint
@@ -68,6 +68,18 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44
+ struct ChangeHandlers {
+ enum FieldChange {
+ case added
+ case removed
+ case modified
+ }
+ var interfaceChanged: ([InterfaceField: FieldChange]) -> Void
+ var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void
+ var peersRemovedAt: ([Int]) -> Void
+ var peersInsertedAt: ([Int]) -> Void
+ }
+
class InterfaceData {
var scratchpad = [InterfaceField: String]()
var fieldsWithError = Set<InterfaceField>()
@@ -106,6 +118,11 @@ class TunnelViewModel {
func populateScratchpad() {
guard let config = validatedConfiguration else { return }
guard let name = validatedName else { return }
+ scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
+ }
+
+ private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
+ var scratchpad = [InterfaceField: String]()
scratchpad[.name] = name
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
@@ -121,6 +138,7 @@ class TunnelViewModel {
if !config.dns.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
}
+ return scratchpad
}
//swiftlint:disable:next cyclomatic_complexity function_body_length
@@ -199,6 +217,32 @@ class TunnelViewModel {
return !self[field].isEmpty
}
}
+
+ func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
+ var changes = [InterfaceField: ChangeHandlers.FieldChange]()
+ for field in InterfaceField.allCases {
+ switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+ case ("", ""):
+ break
+ case ("", _):
+ changes[field] = .added
+ case (_, ""):
+ changes[field] = .removed
+ case (let this, let other):
+ if this != other {
+ changes[field] = .modified
+ }
+ }
+ }
+ scratchpad = otherScratchPad
+ if !changes.isEmpty {
+ changeHandler(changes)
+ }
+ }
}
class PeerData {
@@ -206,6 +250,15 @@ class TunnelViewModel {
var scratchpad = [PeerField: String]()
var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration?
+ var publicKey: Data? {
+ if let validatedConfiguration = validatedConfiguration {
+ return validatedConfiguration.publicKey
+ }
+ if let scratchPadPublicKey = scratchpad[.publicKey] {
+ return Data(base64Encoded: scratchPadPublicKey)
+ }
+ return nil
+ }
private(set) var shouldAllowExcludePrivateIPsControl = false
private(set) var shouldStronglyRecommendDNS = false
@@ -241,6 +294,12 @@ class TunnelViewModel {
func populateScratchpad() {
guard let config = validatedConfiguration else { return }
+ scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
+ updateExcludePrivateIPsFieldState()
+ }
+
+ private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
+ var scratchpad = [PeerField: String]()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if let preSharedKey = config.preSharedKey {
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
@@ -263,7 +322,7 @@ class TunnelViewModel {
if let lastHandshakeTime = config.lastHandshakeTime {
scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
}
- updateExcludePrivateIPsFieldState()
+ return scratchpad
}
//swiftlint:disable:next cyclomatic_complexity
@@ -381,6 +440,30 @@ class TunnelViewModel {
validatedConfiguration = nil
excludePrivateIPsValue = isOn
}
+
+ func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ let otherScratchPad = PeerData.createScratchPad(from: other)
+ var changes = [PeerField: ChangeHandlers.FieldChange]()
+ for field in PeerField.allCases {
+ switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+ case ("", ""):
+ break
+ case ("", _):
+ changes[field] = .added
+ case (_, ""):
+ changes[field] = .removed
+ case (let this, let other):
+ if this != other {
+ changes[field] = .modified
+ }
+ }
+ }
+ scratchpad = otherScratchPad
+ changeHandler(peerIndex, changes)
+ }
}
enum SaveResult<Configuration> {
@@ -388,8 +471,8 @@ class TunnelViewModel {
case error(String)
}
- var interfaceData: InterfaceData
- var peersData: [PeerData]
+ private(set) var interfaceData: InterfaceData
+ private(set) var peersData: [PeerData]
init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData = InterfaceData()
@@ -462,6 +545,55 @@ class TunnelViewModel {
return .saved(tunnelConfiguration)
}
}
+
+ func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) {
+ // Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
+ // Change handler callbacks are processed in the following order, which is designed to work with both the
+ // UITableView way (modify - delete - insert) and the NSTableView way (indices are based on past operations):
+ // - interfaceChanged
+ // - peerChangedAt
+ // - peersRemovedAt
+ // - peersInsertedAt
+
+ interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "", changeHandler: changeHandlers.interfaceChanged)
+
+ for otherPeer in other.peers {
+ if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
+ let peerData = peersData[peersDataIndex]
+ peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt)
+ }
+ }
+
+ var removedPeerIndices = [Int]()
+ for (index, peerData) in peersData.enumerated().reversed() {
+ if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
+ removedPeerIndices.append(index)
+ peersData.remove(at: index)
+ }
+ }
+ if !removedPeerIndices.isEmpty {
+ changeHandlers.peersRemovedAt(removedPeerIndices)
+ }
+
+ var addedPeerIndices = [Int]()
+ for otherPeer in other.peers {
+ if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
+ addedPeerIndices.append(peersData.count)
+ let peerData = PeerData(index: peersData.count)
+ peerData.validatedConfiguration = otherPeer
+ peersData.append(peerData)
+ }
+ }
+ if !addedPeerIndices.isEmpty {
+ changeHandlers.peersInsertedAt(addedPeerIndices)
+ }
+
+ for (index, peer) in peersData.enumerated() {
+ peer.index = index
+ peer.numberOfPeers = peersData.count
+ peer.updateExcludePrivateIPsFieldState()
+ }
+ }
}
extension TunnelViewModel {
diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift
index 6a2b1b7e..beb5d24e 100644
--- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift
+++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift
@@ -13,12 +13,12 @@ class TunnelDetailTableViewController: UITableViewController {
case delete
}
- let interfaceFields: [TunnelViewModel.InterfaceField] = [
+ static let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
- let peerFields: [TunnelViewModel.PeerField] = [
+ static let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive,
.rxBytes, .txBytes, .lastHandshakeTime
@@ -89,11 +89,11 @@ class TunnelDetailTableViewController: UITableViewController {
}
private func loadVisibleFields() {
- let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)
- interfaceFieldIsVisible = interfaceFields.map { visibleInterfaceFields.contains($0) }
+ let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields)
+ interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) }
peerFieldIsVisible = tunnelViewModel.peersData.map { peer in
- let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: peerFields)
- return peerFields.map { visiblePeerFields.contains($0) }
+ let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields)
+ return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) }
}
}
@@ -172,13 +172,79 @@ class TunnelDetailTableViewController: UITableViewController {
reloadRuntimeConfigurationTimer = nil
}
+ func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
+ // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
+ guard let tableView = self.tableView else { return }
+ let sections = self.sections
+ let interfaceSectionIndex = sections.firstIndex(where: { if case .interface = $0 { return true } else { return false }})!
+ let firstPeerSectionIndex = interfaceSectionIndex + 1
+ var interfaceFieldIsVisible = self.interfaceFieldIsVisible
+ var peerFieldIsVisible = self.peerFieldIsVisible
+
+ func sectionChanged<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
+ var fieldIsVisible = fieldIsVisibleInput
+ var modifiedIndexPaths = [IndexPath]()
+ for (index, field) in fields.enumerated() where changes[field] == .modified {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ modifiedIndexPaths.append(IndexPath(row: row, section: section))
+ }
+ if !modifiedIndexPaths.isEmpty {
+ tableView.reloadRows(at: modifiedIndexPaths, with: .automatic)
+ }
+
+ var removedIndexPaths = [IndexPath]()
+ for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ removedIndexPaths.append(IndexPath(row: row, section: section))
+ fieldIsVisible[index] = false
+ }
+ if !removedIndexPaths.isEmpty {
+ tableView.deleteRows(at: removedIndexPaths, with: .automatic)
+ }
+
+ var addedIndexPaths = [IndexPath]()
+ for (index, field) in fields.enumerated() where changes[field] == .added {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ addedIndexPaths.append(IndexPath(row: row, section: section))
+ fieldIsVisible[index] = true
+ }
+ if !addedIndexPaths.isEmpty {
+ tableView.insertRows(at: addedIndexPaths, with: .automatic)
+ }
+ }
+
+ let changeHandlers = TunnelViewModel.ChangeHandlers(
+ interfaceChanged: { changes in
+ sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
+ tableView: tableView, section: interfaceSectionIndex, changes: changes)
+ },
+ peerChangedAt: { peerIndex, changes in
+ sectionChanged(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex],
+ tableView: tableView, section: firstPeerSectionIndex + peerIndex, changes: changes)
+ },
+ peersRemovedAt: { peerIndices in
+ let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
+ tableView.deleteSections(IndexSet(sectionIndices), with: .automatic)
+ },
+ peersInsertedAt: { peerIndices in
+ let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
+ tableView.insertSections(IndexSet(sectionIndices), with: .automatic)
+ }
+ )
+
+ tableView.beginUpdates()
+ self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
+ self.loadSections()
+ self.loadVisibleFields()
+ tableView.endUpdates()
+ }
+
private func reloadRuntimeConfiguration() {
- tunnel.getRuntimeTunnelConfiguration(completionHandler: {
- guard let tunnelConfiguration = $0 else { return }
- self.tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
- self.loadSections()
- self.tableView.reloadData()
- })
+ tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
+ guard let tunnelConfiguration = tunnelConfiguration else { return }
+ guard let self = self else { return }
+ self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
+ }
}
}
@@ -261,7 +327,7 @@ extension TunnelDetailTableViewController {
}
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
- let visibleInterfaceFields = interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
+ let visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
let field = visibleInterfaceFields[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString
@@ -270,7 +336,7 @@ extension TunnelDetailTableViewController {
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData, peerIndex: Int) -> UITableViewCell {
- let visiblePeerFields = peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
+ let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
let field = visiblePeerFields[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString