mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
perf: Quick-win performance optimizations for node list and Core Data lookups (#1650)
* feat: improve routing performance with split state, fetch batching, node cache, and debounce - Split Router's single @Published navigationState into per-tab properties to reduce spurious re-renders across unrelated views - Add fetchBatchSize=50 and relationshipKeyPathsForPrefetching to node list - Optimize in-body array re-sort from 2 filter passes to single pass - Add in-memory node object ID cache on Router for O(1) lookups - Add fetchLimit=1 to getNodeInfo for early termination - Debounce rapid node selection changes with 100ms Task delay Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/9bfe91f2-8ed7-4d2c-bb2e-4ed3dfd3a16c Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Address code review: add debounce constant and thread-safety comment Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/9bfe91f2-8ed7-4d2c-bb2e-4ed3dfd3a16c Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Tak server improvements (#1603) * added read only mode cot to meshtastic parsing and warning to not enable on pub channel * better icons * fully fixed markers * telemetry support * Update Meshtastic/Helpers/TAK/TAKServerManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixes * fixes * Resolve merge conflicts for PR #1603 (TAK server improvements) (#1645) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net> * Fix merge conflicts * Merge main into tak-server-improvements to resolve PR #1603 conflicts (#1646) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net> * Merge main into tak-server-improvements to resolve PR #1603 conflicts (#1647) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
parent
7886fdc7c0
commit
8032ae0915
11 changed files with 240 additions and 37 deletions
|
|
@ -233,6 +233,12 @@
|
|||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
|
@ -274,6 +280,12 @@
|
|||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
|
@ -52706,6 +52718,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"TAK Cannot Be Used on Public Channel" : {
|
||||
"comment" : "A warning displayed when the user's primary channel is public.",
|
||||
|
|
@ -62950,6 +62963,88 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
": %@" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
": %d" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; };
|
||||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; };
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; };
|
||||
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
|
||||
2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; };
|
||||
300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; };
|
||||
3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; };
|
||||
|
|
@ -412,6 +413,7 @@
|
|||
25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = "<group>"; };
|
||||
AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = "<group>"; };
|
||||
2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = "<group>"; };
|
||||
3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = "<group>"; };
|
||||
3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = "<group>"; };
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public func getNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoE
|
|||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(id))
|
||||
fetchNodeInfoRequest.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,67 @@ import SwiftUI
|
|||
class Router: ObservableObject {
|
||||
|
||||
@Published
|
||||
var navigationState: NavigationState
|
||||
var selectedTab: NavigationState.Tab
|
||||
|
||||
@Published
|
||||
var messagesState: MessagesNavigationState?
|
||||
|
||||
@Published
|
||||
var nodeListSelectedNodeNum: Int64?
|
||||
|
||||
@Published
|
||||
var mapState: MapNavigationState?
|
||||
|
||||
@Published
|
||||
var settingsState: SettingsNavigationState?
|
||||
|
||||
/// Computed property that assembles the individual per-tab properties into a `NavigationState`.
|
||||
/// Provided for backward compatibility (e.g. tests) and convenience.
|
||||
var navigationState: NavigationState {
|
||||
get {
|
||||
NavigationState(
|
||||
selectedTab: selectedTab,
|
||||
messages: messagesState,
|
||||
nodeListSelectedNodeNum: nodeListSelectedNodeNum,
|
||||
map: mapState,
|
||||
settings: settingsState
|
||||
)
|
||||
}
|
||||
set {
|
||||
selectedTab = newValue.selectedTab
|
||||
messagesState = newValue.messages
|
||||
nodeListSelectedNodeNum = newValue.nodeListSelectedNodeNum
|
||||
mapState = newValue.map
|
||||
settingsState = newValue.settings
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Node Object ID Cache
|
||||
|
||||
/// In-memory cache mapping node numbers to their Core Data `NSManagedObjectID` for O(1) lookups.
|
||||
/// Thread-safe by virtue of Router's @MainActor isolation — all access is on the main thread.
|
||||
private var nodeObjectIDCache: [Int64: NSManagedObjectID] = [:]
|
||||
|
||||
/// Updates the node cache from a set of fetched nodes. Call this when the node list changes.
|
||||
func updateNodeIndex<C: Collection>(from nodes: C) where C.Element: NodeInfoEntity {
|
||||
nodeObjectIDCache = Dictionary(
|
||||
nodes.map { ($0.num, $0.objectID) },
|
||||
uniquingKeysWith: { _, new in new }
|
||||
)
|
||||
}
|
||||
|
||||
/// Looks up a node using the in-memory cache for O(1) performance, falling back to a Core Data fetch.
|
||||
func cachedNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoEntity? {
|
||||
if let objectID = nodeObjectIDCache[id] {
|
||||
return try? context.existingObject(with: objectID) as? NodeInfoEntity
|
||||
}
|
||||
// Cache miss — fall back to standard fetch
|
||||
let node = getNodeInfo(id: id, context: context)
|
||||
if let node {
|
||||
nodeObjectIDCache[id] = node.objectID
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
|
|
@ -16,10 +76,14 @@ class Router: ObservableObject {
|
|||
selectedTab: .connect
|
||||
)
|
||||
) {
|
||||
self.navigationState = navigationState
|
||||
self.selectedTab = navigationState.selectedTab
|
||||
self.messagesState = navigationState.messages
|
||||
self.nodeListSelectedNodeNum = navigationState.nodeListSelectedNodeNum
|
||||
self.mapState = navigationState.map
|
||||
self.settingsState = navigationState.settings
|
||||
|
||||
$navigationState.sink { destination in
|
||||
Logger.services.info("🛣 [App] Routed to \(destination.selectedTab.rawValue, privacy: .public)")
|
||||
$selectedTab.sink { tab in
|
||||
Logger.services.info("🛣 [App] Routed to \(tab.rawValue, privacy: .public)")
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +100,7 @@ class Router: ObservableObject {
|
|||
if components.path == "/messages" {
|
||||
routeMessages(components)
|
||||
} else if components.path == "/connect" {
|
||||
navigationState.selectedTab = .connect
|
||||
selectedTab = .connect
|
||||
} else if components.path == "/nodes" {
|
||||
routeNodes(components)
|
||||
} else if components.path == "/map" {
|
||||
|
|
@ -73,8 +137,8 @@ class Router: ObservableObject {
|
|||
} else {
|
||||
nil
|
||||
}
|
||||
navigationState.selectedTab = .messages
|
||||
navigationState.messages = state
|
||||
selectedTab = .messages
|
||||
messagesState = state
|
||||
}
|
||||
|
||||
private func routeNodes(_ components: URLComponents) {
|
||||
|
|
@ -83,13 +147,13 @@ class Router: ObservableObject {
|
|||
.value
|
||||
.flatMap(Int64.init)
|
||||
|
||||
navigationState.selectedTab = .nodes
|
||||
navigationState.nodeListSelectedNodeNum = nodeId
|
||||
selectedTab = .nodes
|
||||
nodeListSelectedNodeNum = nodeId
|
||||
}
|
||||
func navigateToNodeDetail(nodeNum: Int64) {
|
||||
Logger.services.info("🛣 [App] Direct route to node detail \(nodeNum, privacy: .public)")
|
||||
navigationState.selectedTab = .nodes
|
||||
navigationState.nodeListSelectedNodeNum = nodeNum
|
||||
selectedTab = .nodes
|
||||
nodeListSelectedNodeNum = nodeNum
|
||||
}
|
||||
|
||||
private func routeMap(_ components: URLComponents) {
|
||||
|
|
@ -102,8 +166,8 @@ class Router: ObservableObject {
|
|||
.value
|
||||
.flatMap(Int64.init)
|
||||
|
||||
navigationState.selectedTab = .map
|
||||
navigationState.map = if let nodeId {
|
||||
selectedTab = .map
|
||||
mapState = if let nodeId {
|
||||
.selectedNode(nodeId)
|
||||
} else if let waypointId {
|
||||
.waypoint(waypointId)
|
||||
|
|
@ -120,7 +184,7 @@ class Router: ObservableObject {
|
|||
.flatMap(String.init)
|
||||
.flatMap(SettingsNavigationState.init(rawValue:))
|
||||
|
||||
navigationState.selectedTab = .settings
|
||||
navigationState.settings = settingFromPath
|
||||
selectedTab = .settings
|
||||
settingsState = settingFromPath
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $appState.router.navigationState.selectedTab) {
|
||||
TabView(selection: $appState.router.selectedTab) {
|
||||
Messages(
|
||||
router: appState.router,
|
||||
unreadChannelMessages: $appState.unreadChannelMessages,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
|
||||
private func routerIsShowingThisChannel() -> Bool {
|
||||
guard appState.router.navigationState.selectedTab == .messages else { return false }
|
||||
guard appState.router.selectedTab == .messages else { return false }
|
||||
return scenePhase == .active
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct Messages: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
List(selection: $router.navigationState.messages) {
|
||||
List(selection: $router.messagesState) {
|
||||
NavigationLink(value: MessagesNavigationState.channels()) {
|
||||
Spacer()
|
||||
Label {
|
||||
|
|
@ -74,7 +74,7 @@ struct Messages: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
} content: {
|
||||
switch router.navigationState.messages {
|
||||
switch router.messagesState {
|
||||
case .channels(let channelId, let messageId):
|
||||
ChannelList(node: $node, channelSelection: $channelSelection)
|
||||
// Removed navigationTitle and navigationBarTitleDisplayMode here.
|
||||
|
|
@ -91,12 +91,12 @@ struct Messages: View {
|
|||
// The toolbar is now defined inside ChannelMessageList.swift
|
||||
} else if let userSelection {
|
||||
UserMessageList(user: userSelection)
|
||||
} else if case .channels = router.navigationState.messages {
|
||||
} else if case .channels = router.messagesState {
|
||||
Text("Select a channel")
|
||||
} else if case .directMessages = router.navigationState.messages {
|
||||
} else if case .directMessages = router.messagesState {
|
||||
Text("Select a conversation")
|
||||
}
|
||||
}.onChange(of: router.navigationState) {
|
||||
}.onChange(of: router.messagesState) {
|
||||
setupNavigationState()
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ struct Messages: View {
|
|||
node = getNodeInfo(id: nodeId, context: context)
|
||||
}
|
||||
|
||||
guard let state = router.navigationState.messages else {
|
||||
guard let state = router.messagesState else {
|
||||
channelSelection = nil
|
||||
userSelection = nil
|
||||
return
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ struct UserMessageList: View {
|
|||
}
|
||||
|
||||
private func routerIsShowingThisUser() -> Bool {
|
||||
guard appState.router.navigationState.selectedTab == .messages else { return false }
|
||||
guard appState.router.selectedTab == .messages else { return false }
|
||||
return scenePhase == .active
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,8 +134,8 @@ struct MeshMap: View {
|
|||
.sheet(isPresented: $editingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
|
||||
}
|
||||
.onChange(of: router.navigationState) {
|
||||
guard case .map = router.navigationState.selectedTab else { return }
|
||||
.onChange(of: router.mapState) {
|
||||
guard case .map = router.selectedTab else { return }
|
||||
// TODO: handle deep link for waypoints
|
||||
}
|
||||
.onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
|
|
|
|||
|
|
@ -11,10 +11,14 @@ import CoreData
|
|||
import Foundation
|
||||
|
||||
struct NodeList: View {
|
||||
/// Debounce delay for node selection changes (100ms)
|
||||
private static let nodeSelectionDebounceNs: UInt64 = 100_000_000
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@StateObject var router: Router
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var nodeSelectionTask: Task<Void, Never>?
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
@State private var isPresentingPositionSentAlert = false
|
||||
@State private var isPresentingPositionFailedAlert = false
|
||||
|
|
@ -35,6 +39,7 @@ struct NodeList: View {
|
|||
var body: some View {
|
||||
NavigationSplitView {
|
||||
FilteredNodeList(
|
||||
router: router,
|
||||
withFilters: filters,
|
||||
selectedNode: $selectedNode,
|
||||
connectedNode: connectedNode,
|
||||
|
|
@ -122,18 +127,27 @@ struct NodeList: View {
|
|||
ContentUnavailableView("Select a Node", systemImage: "flipphone")
|
||||
}
|
||||
}
|
||||
.onChange(of: router.navigationState.nodeListSelectedNodeNum) { _, newNum in
|
||||
if let num = newNum {
|
||||
self.selectedNode = getNodeInfo(id: num, context: context)
|
||||
} else {
|
||||
self.selectedNode = nil
|
||||
.onChange(of: router.nodeListSelectedNodeNum) { _, newNum in
|
||||
// Debounce rapid route changes — only process the last selection after a short delay
|
||||
nodeSelectionTask?.cancel()
|
||||
nodeSelectionTask = Task {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: Self.nodeSelectionDebounceNs)
|
||||
} catch {
|
||||
return // Cancelled by a newer selection
|
||||
}
|
||||
if let num = newNum {
|
||||
self.selectedNode = router.cachedNodeInfo(id: num, context: context)
|
||||
} else {
|
||||
self.selectedNode = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedNode) { _, node in
|
||||
if let num = node?.num {
|
||||
router.navigationState.nodeListSelectedNodeNum = num
|
||||
router.nodeListSelectedNodeNum = num
|
||||
} else {
|
||||
router.navigationState.nodeListSelectedNodeNum = nil
|
||||
router.nodeListSelectedNodeNum = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +168,7 @@ fileprivate struct FilteredNodeList: View {
|
|||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@FetchRequest private var nodes: FetchedResults<NodeInfoEntity>
|
||||
@Environment(\.managedObjectContext) var context
|
||||
var router: Router
|
||||
|
||||
@Binding var selectedNode: NodeInfoEntity?
|
||||
var connectedNode: NodeInfoEntity?
|
||||
|
|
@ -163,6 +178,7 @@ fileprivate struct FilteredNodeList: View {
|
|||
|
||||
// The initializer for the FetchRequest
|
||||
init(
|
||||
router: Router,
|
||||
withFilters: NodeFilterParameters,
|
||||
selectedNode: Binding<NodeInfoEntity?>,
|
||||
connectedNode: NodeInfoEntity?,
|
||||
|
|
@ -170,6 +186,7 @@ fileprivate struct FilteredNodeList: View {
|
|||
deleteNodeId: Binding<Int64>,
|
||||
shareContactNode: Binding<NodeInfoEntity?>
|
||||
) {
|
||||
self.router = router
|
||||
let request: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(key: "ignored", ascending: true),
|
||||
|
|
@ -178,6 +195,8 @@ fileprivate struct FilteredNodeList: View {
|
|||
NSSortDescriptor(key: "user.longName", ascending: true)
|
||||
]
|
||||
request.predicate = withFilters.buildPredicate()
|
||||
request.fetchBatchSize = 50
|
||||
request.relationshipKeyPathsForPrefetching = ["user"]
|
||||
self._nodes = FetchRequest(fetchRequest: request)
|
||||
|
||||
self._selectedNode = selectedNode
|
||||
|
|
@ -189,8 +208,24 @@ fileprivate struct FilteredNodeList: View {
|
|||
|
||||
// The body of the view
|
||||
var body: some View {
|
||||
// If the connected node passes filters, always show it first
|
||||
let nodesWithConnectedFirst = nodes.filter { $0.num == accessoryManager.activeDeviceNum } + nodes.filter { $0.num != accessoryManager.activeDeviceNum }
|
||||
// If the connected node passes filters, always show it first (single-pass)
|
||||
let nodesWithConnectedFirst: [NodeInfoEntity] = {
|
||||
let activeNum = accessoryManager.activeDeviceNum
|
||||
var result: [NodeInfoEntity] = []
|
||||
result.reserveCapacity(nodes.count)
|
||||
var connectedNode: NodeInfoEntity?
|
||||
for node in nodes {
|
||||
if node.num == activeNum {
|
||||
connectedNode = node
|
||||
} else {
|
||||
result.append(node)
|
||||
}
|
||||
}
|
||||
if let connectedNode {
|
||||
result.insert(connectedNode, at: 0)
|
||||
}
|
||||
return result
|
||||
}()
|
||||
List(nodesWithConnectedFirst, id: \.self, selection: $selectedNode) { node in
|
||||
NavigationLink(value: node) {
|
||||
NodeListItem(
|
||||
|
|
@ -206,6 +241,12 @@ fileprivate struct FilteredNodeList: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
router.updateNodeIndex(from: nodes)
|
||||
}
|
||||
.onChange(of: nodes.count) { _, _ in
|
||||
router.updateNodeIndex(from: nodes)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
|||
|
|
@ -343,10 +343,10 @@ struct Settings: View {
|
|||
NavigationStack(
|
||||
path: Binding<[SettingsNavigationState]>(
|
||||
get: {
|
||||
[router.navigationState.settings].compactMap { $0 }
|
||||
[router.settingsState].compactMap { $0 }
|
||||
},
|
||||
set: { newPath in
|
||||
router.navigationState.settings = newPath.first
|
||||
router.settingsState = newPath.first
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue