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>
This commit is contained in:
parent
4134baced1
commit
4ff6105053
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue