diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift index efca5c0..0657a16 100644 --- a/WireGuard/WireGuard/UI/TunnelViewModel.swift +++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift @@ -67,17 +67,17 @@ class TunnelViewModel { static let keyLengthInBase64 = 44 - struct ChangeHandlers { - enum FieldChange { + struct Changes { + enum FieldChange: Equatable { case added case removed - case modified + case modified(newValue: String) } - var interfaceChanged: ([InterfaceField: FieldChange]) -> Void - var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void - var peersRemovedAt: ([Int]) -> Void - var peersInsertedAt: ([Int]) -> Void + var interfaceChanges: [InterfaceField: FieldChange] + var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])] + var peersRemovedIndices: [Int] + var peersInsertedIndices: [Int] } 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 { populateScratchpad() } let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName) - var changes = [InterfaceField: ChangeHandlers.FieldChange]() + var changes = [InterfaceField: Changes.FieldChange]() for field in InterfaceField.allCases { switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { case ("", ""): @@ -233,14 +233,12 @@ class TunnelViewModel { changes[field] = .removed case (let this, let other): if this != other { - changes[field] = .modified + changes[field] = .modified(newValue: other) } } } scratchpad = otherScratchPad - if !changes.isEmpty { - changeHandler(changes) - } + return changes } } @@ -441,12 +439,12 @@ class TunnelViewModel { excludePrivateIPsValue = isOn } - func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) { + func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] { if scratchpad.isEmpty { populateScratchpad() } let otherScratchPad = PeerData.createScratchPad(from: other) - var changes = [PeerField: ChangeHandlers.FieldChange]() + var changes = [PeerField: Changes.FieldChange]() for field in PeerField.allCases { switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { case ("", ""): @@ -457,14 +455,12 @@ class TunnelViewModel { changes[field] = .removed case (let this, let other): if this != other { - changes[field] = .modified + changes[field] = .modified(newValue: other) } } } scratchpad = otherScratchPad - if !changes.isEmpty { - changeHandler(peerIndex, changes) - } + return 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. - // 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 { if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) { 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) } } - if !removedPeerIndices.isEmpty { - changeHandlers.peersRemovedAt(removedPeerIndices) - } var addedPeerIndices = [Int]() for otherPeer in other.peers { @@ -586,15 +578,14 @@ class TunnelViewModel { peersData.append(peerData) } } - if !addedPeerIndices.isEmpty { - changeHandlers.peersInsertedAt(addedPeerIndices) - } for (index, peer) in peersData.enumerated() { peer.index = index peer.numberOfPeers = peersData.count peer.updateExcludePrivateIPsFieldState() } + + return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices) } } diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift index 1eb6461..d45f846 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift @@ -158,16 +158,21 @@ class TunnelDetailTableViewController: UITableViewController { var interfaceFieldIsVisible = self.interfaceFieldIsVisible var peerFieldIsVisible = self.peerFieldIsVisible - func sectionChanged(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) { + func handleSectionFieldsModified(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(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.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: .none) - } var removedIndexPaths = [IndexPath]() for (index, field) in fields.enumerated().reversed() where changes[field] == .removed { @@ -190,30 +195,44 @@ class TunnelDetailTableViewController: UITableViewController { } } - 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) - } - ) + let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration) - tableView.beginUpdates() - self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers) - self.loadSections() - self.loadVisibleFields() - tableView.endUpdates() + if !changes.interfaceChanges.isEmpty { + handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, + section: interfaceSectionIndex, changes: changes.interfaceChanges) + } + 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() {