summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMojgan <mojgan.jelodar@mullvad.net>2026-04-23 16:47:40 +0200
committerMojgan <mojgan.jelodar@mullvad.net>2026-04-23 16:47:40 +0200
commit7944e9b1982feb3deba8871ab49e05d65886a235 (patch)
tree7b9c2482cf13631a4442b9f1d5a97f77b7eb78eb
parent8ccdcafd4ce312f75ffabafc4ae93f8ef5bad736 (diff)
downloadmullvadvpn-speed-connetcion-hackday.tar.xz
mullvadvpn-speed-connetcion-hackday.zip
Speed connection testspeed-connetcion-hackday
-rw-r--r--ios/Assets/Localizable.xcstrings6
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj104
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved2
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift13
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift3
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/Networking/NetworkSpeedMonitorTests.swift20
-rw-r--r--ios/SpeedConnection/MockNetworkSpeedMonitor.swift41
-rw-r--r--ios/SpeedConnection/NetworkSpeedMonitor.swift119
-rw-r--r--ios/SpeedConnection/SpeedConnectionView.swift66
-rw-r--r--ios/SpeedConnection/SpeedConnectionViewModel.swift41
-rw-r--r--ios/SpeedConnection/TrafficData.swift26
-rw-r--r--ios/SpeedConnection/TrafficPackage.swift22
-rw-r--r--ios/SpeedConnection/TrafficSpeed.swift34
-rw-r--r--ios/SpeedConnection/TrafficStatus.swift17
-rw-r--r--ios/SpeedConnection/TrafficSummery.swift58
-rw-r--r--ios/SpeedConnection/Untitled.swift8
16 files changed, 559 insertions, 21 deletions
diff --git a/ios/Assets/Localizable.xcstrings b/ios/Assets/Localizable.xcstrings
index bd526a0ae6..fd7b4ac168 100644
--- a/ios/Assets/Localizable.xcstrings
+++ b/ios/Assets/Localizable.xcstrings
@@ -21697,6 +21697,9 @@
}
}
},
+ "Download: %lf KB" : {
+
+ },
"Dublin" : {
"localizations" : {
"da" : {
@@ -68167,6 +68170,9 @@
}
}
},
+ "Upload: %lf KB" : {
+
+ },
"USA" : {
"localizations" : {
"da" : {
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 425342cd1d..436aa58b52 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -7,12 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
- E10A0001000000000000AB01 /* GotaTunActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0001000000000000AA01 /* GotaTunActor.swift */; };
- E10A0002000000000000AB01 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
- E10A0002000000000000AB02 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
- E10A0002000000000000AB03 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
- E10A0002000000000000AB04 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
- E10A0002000000000000AB05 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
0107F40B2F5B02580012451B /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 0107F40A2F5B02580012451B /* WireGuardKitTypes */; };
0107F40D2F5B02840012451B /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
0107F4142F5B02D70012451B /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; };
@@ -943,6 +937,12 @@
A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; };
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; };
A9EE85612DF1BE2900F2D769 /* TermsOfServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */; };
+ E10A0001000000000000AB01 /* GotaTunActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0001000000000000AA01 /* GotaTunActor.swift */; };
+ E10A0002000000000000AB01 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
+ E10A0002000000000000AB02 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
+ E10A0002000000000000AB03 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
+ E10A0002000000000000AB04 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
+ E10A0002000000000000AB05 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -965,6 +965,17 @@
F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */; };
F02F41A12B9723AF00625A4F /* AddLocationsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */; };
F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */; };
+ F0341E472EB4D1220054C429 /* NetworkSpeedMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E442EB4D00B0054C429 /* NetworkSpeedMonitorTests.swift */; };
+ F0341E482EB4D1A00054C429 /* NetworkSpeedMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F2666B2EB4ABB300AAD7DA /* NetworkSpeedMonitor.swift */; };
+ F0341E4A2EB4D20F0054C429 /* TrafficData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F2666D2EB4BBFF00AAD7DA /* TrafficData.swift */; };
+ F0341E4C2EB50B850054C429 /* TrafficStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E4B2EB50B830054C429 /* TrafficStatus.swift */; };
+ F0341E4D2EB50B850054C429 /* TrafficStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E4B2EB50B830054C429 /* TrafficStatus.swift */; };
+ F0341E4F2EB50D280054C429 /* TrafficSummery.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E4E2EB50D1E0054C429 /* TrafficSummery.swift */; };
+ F0341E502EB50D280054C429 /* TrafficSummery.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E4E2EB50D1E0054C429 /* TrafficSummery.swift */; };
+ F0341E522EB50F190054C429 /* TrafficPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E512EB50F050054C429 /* TrafficPackage.swift */; };
+ F0341E532EB50F190054C429 /* TrafficPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E512EB50F050054C429 /* TrafficPackage.swift */; };
+ F0341E552EB50F350054C429 /* TrafficSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E542EB50F330054C429 /* TrafficSpeed.swift */; };
+ F0341E562EB50F350054C429 /* TrafficSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0341E542EB50F330054C429 /* TrafficSpeed.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A69F62C2AD2D5000E2E7E /* TimeInterval+Timeout.swift */; };
F03A69F92C2AD414000E2E7E /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A69F82C2AD413000E2E7E /* FormsheetPresentationController.swift */; };
@@ -1043,6 +1054,10 @@
F0A89CB32D9D6C2100580C27 /* MullvadDeviceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */; };
F0A89CB52D9D864B00580C27 /* RustProblemReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */; };
F0A89CB72D9D923300580C27 /* String+UnsafePointer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */; };
+ F0A8B6DE2F9A521D008F29D9 /* SpeedConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8B6DD2F9A521D008F29D9 /* SpeedConnectionViewModel.swift */; };
+ F0A8B6DF2F9A521D008F29D9 /* SpeedConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8B6DD2F9A521D008F29D9 /* SpeedConnectionViewModel.swift */; };
+ F0A8B6E12F9A619C008F29D9 /* MockNetworkSpeedMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8B6E02F9A619A008F29D9 /* MockNetworkSpeedMonitor.swift */; };
+ F0A8B6E22F9A619C008F29D9 /* MockNetworkSpeedMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8B6E02F9A619A008F29D9 /* MockNetworkSpeedMonitor.swift */; };
F0AC643A2F3B4FD40024427C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AC64392F3B4FD40024427C /* NotificationSettings.swift */; };
F0AC643C2F3B50860024427C /* NotificationSettingsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AC643B2F3B50860024427C /* NotificationSettingsObserver.swift */; };
F0AC643D2F3B50860024427C /* NotificationSettingsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AC643B2F3B50860024427C /* NotificationSettingsObserver.swift */; };
@@ -1119,6 +1134,9 @@
F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; };
F0F146942D9462E100BF78E7 /* RustProblemReportRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */; };
F0F266692EB3B71A00AAD7DA /* RelayLocationList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F266682EB3B71A00AAD7DA /* RelayLocationList.swift */; };
+ F0F2666C2EB4AECD00AAD7DA /* NetworkSpeedMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F2666B2EB4ABB300AAD7DA /* NetworkSpeedMonitor.swift */; };
+ F0F2666E2EB4BC0100AAD7DA /* TrafficData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F2666D2EB4BBFF00AAD7DA /* TrafficData.swift */; };
+ F0F266702EB4BC3F00AAD7DA /* SpeedConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F2666F2EB4BC3300AAD7DA /* SpeedConnectionView.swift */; };
F0F313A72E85321D00D55C43 /* RecentConnectionsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F313A62E85320D00D55C43 /* RecentConnectionsRepositoryTests.swift */; };
F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */; };
F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */; };
@@ -1717,8 +1735,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- E10A0001000000000000AA01 /* GotaTunActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GotaTunActor.swift; sourceTree = "<group>"; };
- E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelDebugSettings.swift; sourceTree = "<group>"; };
0107F3F82F56E3ED0012451B /* RelayCacheTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTrackerTests.swift; sourceTree = "<group>"; };
0107F4212F5B97F30012451B /* RelayListCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayListCacheTests.swift; sourceTree = "<group>"; };
014E8C2F2F294FB000837D0A /* relays-test-data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "relays-test-data.json"; sourceTree = "<group>"; };
@@ -2506,6 +2522,8 @@
A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = "<group>"; };
A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceView.swift; sourceTree = "<group>"; };
A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConnectionProtocol.swift; sourceTree = "<group>"; };
+ E10A0001000000000000AA01 /* GotaTunActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GotaTunActor.swift; sourceTree = "<group>"; };
+ E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelDebugSettings.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
@@ -2534,6 +2552,11 @@
F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsViewController.swift; sourceTree = "<group>"; };
F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsDataSource.swift; sourceTree = "<group>"; };
F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsCoordinator.swift; sourceTree = "<group>"; };
+ F0341E442EB4D00B0054C429 /* NetworkSpeedMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedMonitorTests.swift; sourceTree = "<group>"; };
+ F0341E4B2EB50B830054C429 /* TrafficStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficStatus.swift; sourceTree = "<group>"; };
+ F0341E4E2EB50D1E0054C429 /* TrafficSummery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficSummery.swift; sourceTree = "<group>"; };
+ F0341E512EB50F050054C429 /* TrafficPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficPackage.swift; sourceTree = "<group>"; };
+ F0341E542EB50F330054C429 /* TrafficSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficSpeed.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
F03A69F62C2AD2D5000E2E7E /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = "<group>"; };
F03A69F82C2AD413000E2E7E /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = "<group>"; };
@@ -2604,6 +2627,8 @@
F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = "<group>"; };
F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadDeviceProxy.swift; sourceTree = "<group>"; };
F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+UnsafePointer.swift"; sourceTree = "<group>"; };
+ F0A8B6DD2F9A521D008F29D9 /* SpeedConnectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedConnectionViewModel.swift; sourceTree = "<group>"; };
+ F0A8B6E02F9A619A008F29D9 /* MockNetworkSpeedMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSpeedMonitor.swift; sourceTree = "<group>"; };
F0AC64392F3B4FD40024427C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
F0AC643B2F3B50860024427C /* NotificationSettingsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsObserver.swift; sourceTree = "<group>"; };
F0AC643E2F3B51830024427C /* UNUserNotificationCenter+Permission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Permission.swift"; sourceTree = "<group>"; };
@@ -2673,6 +2698,9 @@
F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustProblemReportRequestTests.swift; sourceTree = "<group>"; };
F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = "<group>"; };
F0F266682EB3B71A00AAD7DA /* RelayLocationList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayLocationList.swift; sourceTree = "<group>"; };
+ F0F2666B2EB4ABB300AAD7DA /* NetworkSpeedMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedMonitor.swift; sourceTree = "<group>"; };
+ F0F2666D2EB4BBFF00AAD7DA /* TrafficData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficData.swift; sourceTree = "<group>"; };
+ F0F2666F2EB4BC3300AAD7DA /* SpeedConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedConnectionView.swift; sourceTree = "<group>"; };
F0F313A62E85320D00D55C43 /* RecentConnectionsRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnectionsRepositoryTests.swift; sourceTree = "<group>"; };
F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; };
F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; };
@@ -4071,6 +4099,7 @@
58CE5E57224146200008646E = {
isa = PBXGroup;
children = (
+ F0F2666A2EB4AB9E00AAD7DA /* SpeedConnection */,
58F3C0A824A50C0E003E76BE /* Assets */,
58ECD29023F178FD004298B6 /* Configurations */,
584F991F2902CBDD001F858D /* Frameworks */,
@@ -4096,6 +4125,7 @@
589A454A28DDF59B00565204 /* Shared */,
7A83C3FC2A55B39500DFB83A /* TestPlans */,
58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */,
+ F0A8B6DC2F9A05CE008F29D9 /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -4156,14 +4186,6 @@
path = MullvadVPN;
sourceTree = "<group>";
};
- E10A0001000000000000AC01 /* GotaTunAdapter */ = {
- isa = PBXGroup;
- children = (
- E10A0001000000000000AA01 /* GotaTunActor.swift */,
- );
- path = GotaTunAdapter;
- sourceTree = "<group>";
- };
58CE5E7A224146470008646E /* PacketTunnel */ = {
isa = PBXGroup;
children = (
@@ -4819,6 +4841,14 @@
path = MullvadRustRuntimeTests;
sourceTree = "<group>";
};
+ E10A0001000000000000AC01 /* GotaTunAdapter */ = {
+ isa = PBXGroup;
+ children = (
+ E10A0001000000000000AA01 /* GotaTunActor.swift */,
+ );
+ path = GotaTunAdapter;
+ sourceTree = "<group>";
+ };
F028A5472A336E1900C0CAA3 /* RedeemVoucher */ = {
isa = PBXGroup;
children = (
@@ -4892,6 +4922,14 @@
path = Log;
sourceTree = "<group>";
};
+ F0A8B6DC2F9A05CE008F29D9 /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ F0341E442EB4D00B0054C429 /* NetworkSpeedMonitorTests.swift */,
+ );
+ name = "Recovered References";
+ sourceTree = "<group>";
+ };
F0AC64482F3B52380024427C /* Notification */ = {
isa = PBXGroup;
children = (
@@ -5079,6 +5117,22 @@
path = MullvadApi;
sourceTree = "<group>";
};
+ F0F2666A2EB4AB9E00AAD7DA /* SpeedConnection */ = {
+ isa = PBXGroup;
+ children = (
+ F0A8B6E02F9A619A008F29D9 /* MockNetworkSpeedMonitor.swift */,
+ F0A8B6DD2F9A521D008F29D9 /* SpeedConnectionViewModel.swift */,
+ F0F2666B2EB4ABB300AAD7DA /* NetworkSpeedMonitor.swift */,
+ F0F2666F2EB4BC3300AAD7DA /* SpeedConnectionView.swift */,
+ F0F2666D2EB4BBFF00AAD7DA /* TrafficData.swift */,
+ F0341E512EB50F050054C429 /* TrafficPackage.swift */,
+ F0341E542EB50F330054C429 /* TrafficSpeed.swift */,
+ F0341E4B2EB50B830054C429 /* TrafficStatus.swift */,
+ F0341E4E2EB50D1E0054C429 /* TrafficSummery.swift */,
+ );
+ path = SpeedConnection;
+ sourceTree = "<group>";
+ };
F0FA16072D7F03F8007E2546 /* Filter */ = {
isa = PBXGroup;
children = (
@@ -6179,6 +6233,7 @@
A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */,
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */,
A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */,
+ F0341E482EB4D1A00054C429 /* NetworkSpeedMonitor.swift in Sources */,
7A5468AD2C6B5E4B00590086 /* LocationRelays.swift in Sources */,
F97C38DF2DEEDB0F006DCB08 /* Color+Mullvad.swift in Sources */,
F0AC644D2F3B52F60024427C /* NotificationSettings.swift in Sources */,
@@ -6246,6 +6301,7 @@
A9A5FA022ACB05160083449F /* RelayCacheTracker.swift in Sources */,
A9A5FA032ACB05160083449F /* SimulatorTunnelInfo.swift in Sources */,
A9A5FA042ACB05160083449F /* SimulatorTunnelProvider.swift in Sources */,
+ F0341E472EB4D1220054C429 /* NetworkSpeedMonitorTests.swift in Sources */,
A9A5FA052ACB05160083449F /* SimulatorTunnelProviderHost.swift in Sources */,
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */,
A9A5FA062ACB05160083449F /* SimulatorTunnelProviderManager.swift in Sources */,
@@ -6257,6 +6313,7 @@
A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */,
7A9ED2A92F433D1B005BC0D9 /* NotificationProviderIdentifier.swift in Sources */,
F0FA16092D7F0425007E2546 /* FilterDescriptorTests.swift in Sources */,
+ F0341E4A2EB4D20F0054C429 /* TrafficData.swift in Sources */,
A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */,
7A6811542DC8EC6E009CB61A /* UIFont+Weight.swift in Sources */,
A9A5FA102ACB05160083449F /* PacketTunnelAPITransport.swift in Sources */,
@@ -6267,6 +6324,7 @@
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */,
A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */,
A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */,
+ F0341E552EB50F350054C429 /* TrafficSpeed.swift in Sources */,
7AD63A3D2CD9065D00445268 /* RelayObfuscatorTests.swift in Sources */,
F072D3CF2C07122400906F64 /* SettingsUpdaterTests.swift in Sources */,
7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */,
@@ -6280,6 +6338,7 @@
7A2F410A2EC38FD20013D3C5 /* StoreSubscription.swift in Sources */,
7A4080AE2F5B1F7F00C02173 /* Breadcrumb.swift in Sources */,
A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */,
+ F0341E4F2EB50D280054C429 /* TrafficSummery.swift in Sources */,
7A9BE5A52B90760C00E2A7D0 /* CustomListsDataSourceTests.swift in Sources */,
A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */,
A9A5FA1C2ACB05160083449F /* Tunnel+Messaging.swift in Sources */,
@@ -6288,6 +6347,7 @@
A9A5FA1D2ACB05160083449F /* TunnelBlockObserver.swift in Sources */,
A9A5FA1E2ACB05160083449F /* TunnelConfiguration.swift in Sources */,
A9A5FA1F2ACB05160083449F /* TunnelInteractor.swift in Sources */,
+ F0A8B6E22F9A619C008F29D9 /* MockNetworkSpeedMonitor.swift in Sources */,
A9A5FA202ACB05160083449F /* TunnelManager.swift in Sources */,
A9A5FA212ACB05160083449F /* TunnelManagerErrors.swift in Sources */,
A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */,
@@ -6316,6 +6376,8 @@
A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */,
440870842D809C980038972F /* UIImage+Helpers.swift in Sources */,
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
+ F0341E4D2EB50B850054C429 /* TrafficStatus.swift in Sources */,
+ F0341E522EB50F190054C429 /* TrafficPackage.swift in Sources */,
A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */,
F0FA16112D7F2FE8007E2546 /* RelayFilterViewModel.swift in Sources */,
F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */,
@@ -6333,6 +6395,7 @@
7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */,
A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */,
44E1F75A2D3FDCCA003A60FF /* DestinationDescriberTests.swift in Sources */,
+ F0A8B6DE2F9A521D008F29D9 /* SpeedConnectionViewModel.swift in Sources */,
A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */,
A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */,
F073FCB32C6617D70062EA1D /* TunnelStore+Stubs.swift in Sources */,
@@ -6497,6 +6560,7 @@
files = (
44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */,
F9E3BCF72DD35B78009986C3 /* ListAccessViewModelBridge.swift in Sources */,
+ F0F2666E2EB4BC0100AAD7DA /* TrafficData.swift in Sources */,
7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */,
4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */,
7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */,
@@ -6534,6 +6598,7 @@
7A8A18F92CE34EA8000BCB5B /* SettingsMultihopView.swift in Sources */,
7AFBE3892D089163002335FC /* TunnelViewController.swift in Sources */,
7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */,
+ F0A8B6E12F9A619C008F29D9 /* MockNetworkSpeedMonitor.swift in Sources */,
5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */,
F97C38E52DEEDFD6006DCB08 /* Image+Assets.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
@@ -6547,6 +6612,7 @@
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */,
7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */,
F062000A2CB7EB42002E6DB9 /* CGSize+Helpers.swift in Sources */,
+ F0341E562EB50F350054C429 /* TrafficSpeed.swift in Sources */,
586C0D892B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift in Sources */,
58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */,
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */,
@@ -6601,6 +6667,7 @@
7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */,
F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */,
F92C658C2E7A924E00B8E107 /* MockSelectLocationViewModel.swift in Sources */,
+ F0341E502EB50D280054C429 /* TrafficSummery.swift in Sources */,
58CEB30A2AFD584700E6E088 /* CustomCellDisclosureHandling.swift in Sources */,
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */,
@@ -6677,6 +6744,7 @@
F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */,
582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */,
7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */,
+ F0341E532EB50F190054C429 /* TrafficPackage.swift in Sources */,
F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */,
7AD03E1D2E8E910E00270EAE /* RevokedDeviceView.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
@@ -6743,6 +6811,7 @@
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
7AF822C72DF0664700BA4255 /* DAITAMultihopNotice.swift in Sources */,
7A8A191E2CEF5CF2000BCB5B /* TunnelSettingsObservable.swift in Sources */,
+ F0F266702EB4BC3F00AAD7DA /* SpeedConnectionView.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */,
@@ -6773,6 +6842,7 @@
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */,
F90052562E6EEB290085C80E /* SelectLocationViewModel.swift in Sources */,
+ F0341E4C2EB50B850054C429 /* TrafficStatus.swift in Sources */,
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */,
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */,
7A5869C52B5A899C00640D27 /* MethodSettingsCellConfiguration.swift in Sources */,
@@ -6829,6 +6899,7 @@
586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */,
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
+ F0A8B6DF2F9A521D008F29D9 /* SpeedConnectionViewModel.swift in Sources */,
F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */,
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
A9EE85612DF1BE2900F2D769 /* TermsOfServiceView.swift in Sources */,
@@ -6931,6 +7002,7 @@
F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */,
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
+ F0F2666C2EB4AECD00AAD7DA /* NetworkSpeedMonitor.swift in Sources */,
7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */,
585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
7A8A19122CEF1E68000BCB5B /* SettingsInfoContainerView.swift in Sources */,
diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index d80aca3e5f..6bf0838040 100644
--- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "004c7b9e036775265a3ba321f2643b2be8953ebef355de33e8f9ba4a40a4d960",
+ "originHash" : "fa4c8f47e5b779cddf875f03a820b5084922b27bfbc9ee905e571dadaf4e1e4b",
"pins" : [
{
"identity" : "swift-log",
diff --git a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
index b6b3d34bd5..e9dde36f83 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
@@ -11,6 +11,7 @@ import SwiftUI
struct ConnectionView: View {
@ObservedObject var connectionViewModel: ConnectionViewViewModel
@ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel
+ @ObservedObject var speedConnectionViewModel: SpeedConnectionViewModel
@State private(set) var isExpanded = false
@@ -27,7 +28,7 @@ struct ConnectionView: View {
VStack(alignment: .leading, spacing: 0) {
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
.padding(.bottom, 4)
-
+ SpeedConnectionView(viewModel: speedConnectionViewModel)
Divider()
.background(UIColor.secondaryTextColor.color)
.padding(.top, 4)
@@ -111,12 +112,18 @@ struct ConnectionView: View {
#Preview("ConnectionView (Indicators)") {
ConnectionViewComponentPreview(showIndicators: true) { indicatorModel, viewModel, _ in
- ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
+ ConnectionView(
+ connectionViewModel: viewModel,
+ indicatorsViewModel: indicatorModel,
+ speedConnectionViewModel: SpeedConnectionViewModel(networkSpeedMonitor: MockNetworkSpeedMonitor()))
}
}
#Preview("ConnectionView (No indicators)") {
ConnectionViewComponentPreview(showIndicators: false) { indicatorModel, viewModel, _ in
- ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
+ ConnectionView(
+ connectionViewModel: viewModel,
+ indicatorsViewModel: indicatorModel,
+ speedConnectionViewModel: SpeedConnectionViewModel(networkSpeedMonitor: MockNetworkSpeedMonitor()))
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index 0fc671e992..d1ca6aac2e 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -79,7 +79,8 @@ class TunnelViewController: UIViewController, RootContainment {
connectionView = ConnectionView(
connectionViewModel: connectionViewViewModel,
- indicatorsViewModel: indicatorsViewViewModel
+ indicatorsViewModel: indicatorsViewViewModel,
+ speedConnectionViewModel: SpeedConnectionViewModel(networkSpeedMonitor: NetworkSpeedMonitor())
)
super.init(nibName: nil, bundle: nil)
diff --git a/ios/MullvadVPNTests/MullvadVPN/Networking/NetworkSpeedMonitorTests.swift b/ios/MullvadVPNTests/MullvadVPN/Networking/NetworkSpeedMonitorTests.swift
new file mode 100644
index 0000000000..4cd8489992
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadVPN/Networking/NetworkSpeedMonitorTests.swift
@@ -0,0 +1,20 @@
+//
+// NetworkSpeedMonitorTests.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import Testing
+
+@Suite("NetworkSpeedMonitorTests")
+struct NetworkSpeedMonitorTests {
+
+ @Test
+ func start() {
+ let monitor = NetworkSpeedMonitor()
+ monitor.start()
+
+ }
+}
diff --git a/ios/SpeedConnection/MockNetworkSpeedMonitor.swift b/ios/SpeedConnection/MockNetworkSpeedMonitor.swift
new file mode 100644
index 0000000000..941dec19fe
--- /dev/null
+++ b/ios/SpeedConnection/MockNetworkSpeedMonitor.swift
@@ -0,0 +1,41 @@
+//
+// MockNetworkSpeedMonitor.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2026-04-23.
+// Copyright © 2026 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+
+final class MockNetworkSpeedMonitor: NetworkSpeedMonitorProtocol, @unchecked Sendable {
+
+ var onUpdateTrafficSummery: (@Sendable (TrafficSummery) -> Void)?
+
+ private var timer: Timer?
+
+ func start(timeInterval: TimeInterval) {
+ timer?.invalidate()
+
+ timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { [weak self] _ in
+ guard let self else { return }
+
+ let sent = UInt64.random(in: 10_000...100_000)
+ let received = UInt64.random(in: 10_000...100_000)
+
+ let new = TrafficPackage(wifi: TrafficData(sent: sent, received: received))
+
+ self.onUpdateTrafficSummery?(
+ TrafficSummery.make(self.previousTraffic, new: new, interval: timeInterval)
+ )
+
+ self.previousTraffic = new
+ }
+ }
+
+ func stop() {
+ timer?.invalidate()
+ timer = nil
+ }
+
+ private var previousTraffic = TrafficPackage()
+}
diff --git a/ios/SpeedConnection/NetworkSpeedMonitor.swift b/ios/SpeedConnection/NetworkSpeedMonitor.swift
new file mode 100644
index 0000000000..8986bae361
--- /dev/null
+++ b/ios/SpeedConnection/NetworkSpeedMonitor.swift
@@ -0,0 +1,119 @@
+//
+// NetworkSpeedMonitor.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+import SystemConfiguration
+
+protocol NetworkSpeedMonitorProtocol {
+ var onUpdateTrafficSummery: (@Sendable (TrafficSummery) -> Void)? { get set }
+ func start(timeInterval: TimeInterval)
+ func stop()
+}
+
+final class NetworkSpeedMonitor : NetworkSpeedMonitorProtocol {
+ private var timer: DispatchSourceTimer? = nil
+ private var previousTrafficPackage = TrafficPackage()
+ private var lock = NSLock()
+ private let timerQueue = DispatchQueue(label: "NetworkSpeedMonitorTimerQueue")
+
+ var onUpdateTrafficSummery: (@Sendable (TrafficSummery) -> Void)?
+
+ func start(timeInterval: TimeInterval = 1.0) {
+ timer?.cancel()
+ timer = DispatchSource.makeTimerSource(queue: timerQueue)
+ timer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ self.measureSpeed()
+ }
+ timer?.schedule(wallDeadline: .now(), repeating: timeInterval)
+ timer?.resume()
+ }
+
+ func stop() {
+ timer?.cancel()
+ timer = nil
+ }
+
+ private func measureSpeed() {
+ lock.lock()
+ defer {
+ lock.unlock()
+ }
+ let newTrafficPacket = getTrafficPackage()
+ onUpdateTrafficSummery?(TrafficSummery.make(previousTrafficPackage, new: newTrafficPacket, interval: 1))
+ previousTrafficPackage = newTrafficPacket
+ }
+
+ private func getTrafficPackage() -> TrafficPackage {
+ var result = TrafficPackage()
+ var address: UnsafeMutablePointer<ifaddrs>? = nil
+
+ guard getifaddrs(&address) == 0, let first = address else {
+ return result
+ }
+
+ defer {
+ freeifaddrs(first)
+ }
+
+ var pointer: UnsafeMutablePointer<ifaddrs>? = first
+
+ while let current = pointer?.pointee {
+
+ defer {
+ pointer = current.ifa_next
+ }
+
+ let interfaceName = String(cString: current.ifa_name)
+ let interface = mapInterface(interfaceName)
+
+ guard
+ let dataPtr = current.ifa_data
+ else {
+ continue
+ }
+
+ let data = dataPtr.assumingMemoryBound(to: if_data.self).pointee
+
+ let sent = UInt64(data.ifi_obytes)
+ let received = UInt64(data.ifi_ibytes)
+
+ switch interface {
+ case .cellular:
+ result.cellular.sent += sent
+ result.cellular.received += received
+
+ case .wifi:
+ result.wifi.sent += sent
+ result.wifi.received += received
+
+ case .vpn:
+ result.vpn.sent += sent
+ result.vpn.received += received
+
+ default:
+ break
+ }
+ }
+
+ return result
+ }
+
+ func mapInterface(_ name: String) -> InterfaceType {
+ if name.hasPrefix("en") {
+ return .wifi
+ } else if name.hasPrefix("pdp_ip") {
+ return .cellular
+ } else if name.hasPrefix("utun") || name.hasPrefix("ppp") {
+ return .vpn
+ } else if name == "lo0" {
+ return .loopback
+ } else {
+ return .other
+ }
+ }
+}
diff --git a/ios/SpeedConnection/SpeedConnectionView.swift b/ios/SpeedConnection/SpeedConnectionView.swift
new file mode 100644
index 0000000000..0071f9cbc1
--- /dev/null
+++ b/ios/SpeedConnection/SpeedConnectionView.swift
@@ -0,0 +1,66 @@
+//
+// SpeedConnectionView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct SpeedConnectionView<ViewModel: SpeedConnectionViewModelProtocol>: View {
+ @State private var uploadValue: Double = 0.0
+ @State private var downloadValue: Double = 0.0
+
+ var viewModel: ViewModel
+
+ init(viewModel: ViewModel) {
+ self.viewModel = viewModel
+ }
+
+ var body: some View {
+ VStack {
+ Text("Download: \(uploadValue / 1024.0) KB")
+ .font(.mullvadTiny)
+ Text("Upload: \(downloadValue / 1024.0) KB")
+ .font(.mullvadTiny)
+ }
+ .padding(8.0)
+ .onAppear {
+ viewModel.startMonitoring()
+ }
+ .onDisappear {
+ viewModel.stopMonitoring()
+ }
+ }
+}
+
+#Preview {
+ SpeedConnectionView(viewModel: MockSpeedConnectionViewModel())
+}
+
+final class MockSpeedConnectionViewModel: SpeedConnectionViewModelProtocol,@unchecked Sendable {
+
+ var uploadValue: Double = 0.0
+ var downloadValue: Double = 0.0
+
+ private var timer: Timer?
+
+ func startMonitoring() {
+ timer?.invalidate()
+
+ timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
+ let upload = Double.random(in: 10_000...100_000)
+ let download = Double.random(in: 10_000...100_000)
+ Task { @MainActor in
+ self.uploadValue = upload
+ self.downloadValue = download
+ }
+ }
+ }
+
+ func stopMonitoring() {
+ timer?.invalidate()
+ timer = nil
+ }
+}
diff --git a/ios/SpeedConnection/SpeedConnectionViewModel.swift b/ios/SpeedConnection/SpeedConnectionViewModel.swift
new file mode 100644
index 0000000000..a06e857cc4
--- /dev/null
+++ b/ios/SpeedConnection/SpeedConnectionViewModel.swift
@@ -0,0 +1,41 @@
+//
+// SpeedConnectionViewModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2026-04-23.
+// Copyright © 2026 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+
+protocol SpeedConnectionViewModelProtocol: ObservableObject {
+ var uploadValue: Double { get }
+ var downloadValue: Double { get }
+
+ func startMonitoring()
+ func stopMonitoring()
+}
+
+
+class SpeedConnectionViewModel: SpeedConnectionViewModelProtocol,@unchecked Sendable {
+ @Published var uploadValue: Double = 0.0
+ @Published var downloadValue: Double = 0.0
+ var networkSpeedMonitor : NetworkSpeedMonitorProtocol
+
+ init(networkSpeedMonitor : NetworkSpeedMonitorProtocol) {
+ self.networkSpeedMonitor = networkSpeedMonitor
+ self.networkSpeedMonitor.onUpdateTrafficSummery = { [weak self] trafficSummery in
+ Task { @MainActor in
+ self?.uploadValue = trafficSummery.speed.sent
+ self?.downloadValue = trafficSummery.speed.received
+ }
+ }
+ }
+ func startMonitoring() {
+ self.networkSpeedMonitor.start(timeInterval: 1.0)
+ }
+
+ func stopMonitoring() {
+ self.networkSpeedMonitor.stop()
+ }
+}
diff --git a/ios/SpeedConnection/TrafficData.swift b/ios/SpeedConnection/TrafficData.swift
new file mode 100644
index 0000000000..ce855a833a
--- /dev/null
+++ b/ios/SpeedConnection/TrafficData.swift
@@ -0,0 +1,26 @@
+//
+// TrafficData.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+
+struct TrafficData {
+ var sent: UInt64 = 0// in bytes
+ var received: UInt64 = 0 // in bytes
+}
+
+extension TrafficData {
+ static var zero: TrafficData {
+ TrafficData()
+ }
+}
+
+func +(lhs: TrafficData, rhs: TrafficData) -> TrafficData {
+ var result = lhs
+ result.received += rhs.received
+ result.sent += rhs.sent
+ return result
+}
diff --git a/ios/SpeedConnection/TrafficPackage.swift b/ios/SpeedConnection/TrafficPackage.swift
new file mode 100644
index 0000000000..62ac1d2d62
--- /dev/null
+++ b/ios/SpeedConnection/TrafficPackage.swift
@@ -0,0 +1,22 @@
+//
+// TrafficPackage.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+
+enum InterfaceType {
+ case wifi
+ case cellular
+ case vpn
+ case loopback
+ case other
+}
+
+struct TrafficPackage {
+ var wifi = TrafficData()
+ var cellular = TrafficData()
+ var vpn = TrafficData()
+}
diff --git a/ios/SpeedConnection/TrafficSpeed.swift b/ios/SpeedConnection/TrafficSpeed.swift
new file mode 100644
index 0000000000..1ba81f9ed0
--- /dev/null
+++ b/ios/SpeedConnection/TrafficSpeed.swift
@@ -0,0 +1,34 @@
+//
+// TrafficSpeed.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+struct TrafficSpeed {
+ var received: Double
+ var sent: Double
+
+ private init(received: Double, sent: Double) {
+ self.received = received
+ self.sent = sent
+ }
+
+ public init(old: TrafficData, new: TrafficData, interval: Double) {
+ self.received = Double((new.received - old.received)) / interval
+ self.sent = Double((new.sent - old.sent)) / interval
+ }
+}
+extension TrafficSpeed {
+ static var zero: TrafficSpeed {
+ .init(received: 0, sent: 0)
+ }
+}
+
+func +(lhs: TrafficSpeed, rhs: TrafficSpeed) -> TrafficSpeed {
+ var result = lhs
+ result.received += rhs.received
+ result.sent += rhs.sent
+ return result
+}
diff --git a/ios/SpeedConnection/TrafficStatus.swift b/ios/SpeedConnection/TrafficStatus.swift
new file mode 100644
index 0000000000..ebd3776989
--- /dev/null
+++ b/ios/SpeedConnection/TrafficStatus.swift
@@ -0,0 +1,17 @@
+//
+// TrafficStatus.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+struct TrafficStatus {
+ var speed: TrafficSpeed
+ var data: TrafficData
+
+ init(speed: TrafficSpeed = .zero, data: TrafficData = .zero) {
+ self.speed = speed
+ self.data = data
+ }
+}
diff --git a/ios/SpeedConnection/TrafficSummery.swift b/ios/SpeedConnection/TrafficSummery.swift
new file mode 100644
index 0000000000..7ef20ab29b
--- /dev/null
+++ b/ios/SpeedConnection/TrafficSummery.swift
@@ -0,0 +1,58 @@
+//
+// TrafficSummery.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+
+struct TrafficSummery {
+ let wifi: TrafficStatus
+ let cellular: TrafficStatus
+ let vpn: TrafficStatus?
+
+ init(
+ wifi: TrafficStatus = TrafficStatus(),
+ cellular: TrafficStatus = TrafficStatus(),
+ vpn: TrafficStatus? = nil
+ ) {
+ self.wifi = wifi
+ self.cellular = cellular
+ self.vpn = vpn
+ }
+
+ var speed: TrafficSpeed {
+ wifi.speed + cellular.speed + (vpn?.speed ?? .zero)
+ }
+
+ private var data: TrafficData {
+ if let vpn = vpn {
+ return vpn.data
+ }
+ return wifi.data + cellular.data
+ }
+
+ static func make(_ old: TrafficPackage, new: TrafficPackage, interval: TimeInterval) -> Self {
+ TrafficSummery(
+ wifi: TrafficStatus(
+ speed: TrafficSpeed(
+ old: old.wifi,
+ new: new.wifi,
+ interval: interval),
+ data: new.wifi),
+ cellular: TrafficStatus(
+ speed: TrafficSpeed(
+ old: old.cellular,
+ new: new.cellular,
+ interval: interval),
+ data: new.cellular),
+ vpn: TrafficStatus(
+ speed: TrafficSpeed(
+ old: old.vpn,
+ new: new.vpn,
+ interval: interval),
+ data: new.vpn))
+ }
+
+}
diff --git a/ios/SpeedConnection/Untitled.swift b/ios/SpeedConnection/Untitled.swift
new file mode 100644
index 0000000000..23d31d97a8
--- /dev/null
+++ b/ios/SpeedConnection/Untitled.swift
@@ -0,0 +1,8 @@
+//
+// Untitled.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+