iOS: Rewrite applying runtime configuration
To make scrolling smoother while the fields are modified
This commit is contained in:
parent
3355019408
commit
aea253a6e9
|
@ -67,17 +67,17 @@ class TunnelViewModel {
|
||||||
|
|
||||||
static let keyLengthInBase64 = 44
|
static let keyLengthInBase64 = 44
|
||||||
|
|
||||||
struct ChangeHandlers {
|
struct Changes {
|
||||||
enum FieldChange {
|
enum FieldChange: Equatable {
|
||||||
case added
|
case added
|
||||||
case removed
|
case removed
|
||||||
case modified
|
case modified(newValue: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
var interfaceChanged: ([InterfaceField: FieldChange]) -> Void
|
var interfaceChanges: [InterfaceField: FieldChange]
|
||||||
var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void
|
var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
|
||||||
var peersRemovedAt: ([Int]) -> Void
|
var peersRemovedIndices: [Int]
|
||||||
var peersInsertedAt: ([Int]) -> Void
|
var peersInsertedIndices: [Int]
|
||||||
}
|
}
|
||||||
|
|
||||||
class InterfaceData {
|
class InterfaceData {
|
||||||
|
@ -217,12 +217,12 @@ class TunnelViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) {
|
func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
|
||||||
if scratchpad.isEmpty {
|
if scratchpad.isEmpty {
|
||||||
populateScratchpad()
|
populateScratchpad()
|
||||||
}
|
}
|
||||||
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
|
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
|
||||||
var changes = [InterfaceField: ChangeHandlers.FieldChange]()
|
var changes = [InterfaceField: Changes.FieldChange]()
|
||||||
for field in InterfaceField.allCases {
|
for field in InterfaceField.allCases {
|
||||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||||
case ("", ""):
|
case ("", ""):
|
||||||
|
@ -233,14 +233,12 @@ class TunnelViewModel {
|
||||||
changes[field] = .removed
|
changes[field] = .removed
|
||||||
case (let this, let other):
|
case (let this, let other):
|
||||||
if this != other {
|
if this != other {
|
||||||
changes[field] = .modified
|
changes[field] = .modified(newValue: other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scratchpad = otherScratchPad
|
scratchpad = otherScratchPad
|
||||||
if !changes.isEmpty {
|
return changes
|
||||||
changeHandler(changes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,12 +439,12 @@ class TunnelViewModel {
|
||||||
excludePrivateIPsValue = isOn
|
excludePrivateIPsValue = isOn
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) {
|
func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
|
||||||
if scratchpad.isEmpty {
|
if scratchpad.isEmpty {
|
||||||
populateScratchpad()
|
populateScratchpad()
|
||||||
}
|
}
|
||||||
let otherScratchPad = PeerData.createScratchPad(from: other)
|
let otherScratchPad = PeerData.createScratchPad(from: other)
|
||||||
var changes = [PeerField: ChangeHandlers.FieldChange]()
|
var changes = [PeerField: Changes.FieldChange]()
|
||||||
for field in PeerField.allCases {
|
for field in PeerField.allCases {
|
||||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||||
case ("", ""):
|
case ("", ""):
|
||||||
|
@ -457,14 +455,12 @@ class TunnelViewModel {
|
||||||
changes[field] = .removed
|
changes[field] = .removed
|
||||||
case (let this, let other):
|
case (let this, let other):
|
||||||
if this != other {
|
if this != other {
|
||||||
changes[field] = .modified
|
changes[field] = .modified(newValue: other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scratchpad = otherScratchPad
|
scratchpad = otherScratchPad
|
||||||
if !changes.isEmpty {
|
return changes
|
||||||
changeHandler(peerIndex, changes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,21 +544,20 @@ class TunnelViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) {
|
@discardableResult
|
||||||
|
func applyConfiguration(other: TunnelConfiguration) -> Changes {
|
||||||
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
|
// 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)
|
let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
|
||||||
|
|
||||||
|
var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
|
||||||
for otherPeer in other.peers {
|
for otherPeer in other.peers {
|
||||||
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
|
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
|
||||||
let peerData = peersData[peersDataIndex]
|
let peerData = peersData[peersDataIndex]
|
||||||
peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt)
|
let changes = peerData.applyConfiguration(other: otherPeer)
|
||||||
|
if !changes.isEmpty {
|
||||||
|
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,9 +568,6 @@ class TunnelViewModel {
|
||||||
peersData.remove(at: index)
|
peersData.remove(at: index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !removedPeerIndices.isEmpty {
|
|
||||||
changeHandlers.peersRemovedAt(removedPeerIndices)
|
|
||||||
}
|
|
||||||
|
|
||||||
var addedPeerIndices = [Int]()
|
var addedPeerIndices = [Int]()
|
||||||
for otherPeer in other.peers {
|
for otherPeer in other.peers {
|
||||||
|
@ -586,15 +578,14 @@ class TunnelViewModel {
|
||||||
peersData.append(peerData)
|
peersData.append(peerData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !addedPeerIndices.isEmpty {
|
|
||||||
changeHandlers.peersInsertedAt(addedPeerIndices)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, peer) in peersData.enumerated() {
|
for (index, peer) in peersData.enumerated() {
|
||||||
peer.index = index
|
peer.index = index
|
||||||
peer.numberOfPeers = peersData.count
|
peer.numberOfPeers = peersData.count
|
||||||
peer.updateExcludePrivateIPsFieldState()
|
peer.updateExcludePrivateIPsFieldState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,16 +158,21 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||||
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
||||||
var peerFieldIsVisible = self.peerFieldIsVisible
|
var peerFieldIsVisible = self.peerFieldIsVisible
|
||||||
|
|
||||||
func sectionChanged<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
|
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
||||||
|
for (index, field) in fields.enumerated() {
|
||||||
|
guard let change = changes[field] else { continue }
|
||||||
|
if case .modified(let newValue) = change {
|
||||||
|
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
|
||||||
|
let indexPath = IndexPath(row: row, section: section)
|
||||||
|
if let cell = tableView.cellForRow(at: indexPath) as? KeyValueCell {
|
||||||
|
cell.value = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSectionRowsInsertedOrRemoved<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
||||||
var fieldIsVisible = fieldIsVisibleInput
|
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: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
var removedIndexPaths = [IndexPath]()
|
var removedIndexPaths = [IndexPath]()
|
||||||
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
|
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
|
||||||
|
@ -190,30 +195,44 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let changeHandlers = TunnelViewModel.ChangeHandlers(
|
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
|
||||||
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()
|
if !changes.interfaceChanges.isEmpty {
|
||||||
self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
|
handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
|
||||||
self.loadSections()
|
section: interfaceSectionIndex, changes: changes.interfaceChanges)
|
||||||
self.loadVisibleFields()
|
}
|
||||||
tableView.endUpdates()
|
for (peerIndex, peerChanges) in changes.peerChanges {
|
||||||
|
handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
|
||||||
|
let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
|
||||||
|
let peersRemovedSectionIndices = changes.peersRemovedIndices.map { firstPeerSectionIndex + $0 }
|
||||||
|
let peersInsertedSectionIndices = changes.peersInsertedIndices.map { firstPeerSectionIndex + $0 }
|
||||||
|
|
||||||
|
if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !peersRemovedSectionIndices.isEmpty || !peersInsertedSectionIndices.isEmpty {
|
||||||
|
tableView.beginUpdates()
|
||||||
|
if isAnyInterfaceFieldAddedOrRemoved {
|
||||||
|
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, section: interfaceSectionIndex, changes: changes.interfaceChanges)
|
||||||
|
}
|
||||||
|
if isAnyPeerFieldAddedOrRemoved {
|
||||||
|
for (peerIndex, peerChanges) in changes.peerChanges {
|
||||||
|
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !peersRemovedSectionIndices.isEmpty {
|
||||||
|
tableView.deleteSections(IndexSet(peersRemovedSectionIndices), with: .automatic)
|
||||||
|
}
|
||||||
|
if !peersInsertedSectionIndices.isEmpty {
|
||||||
|
tableView.insertSections(IndexSet(peersInsertedSectionIndices), with: .automatic)
|
||||||
|
}
|
||||||
|
self.loadSections()
|
||||||
|
self.loadVisibleFields()
|
||||||
|
tableView.endUpdates()
|
||||||
|
} else {
|
||||||
|
self.loadSections()
|
||||||
|
self.loadVisibleFields()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadRuntimeConfiguration() {
|
private func reloadRuntimeConfiguration() {
|
||||||
|
|
Loading…
Reference in New Issue