summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2024-12-04 10:18:22 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-01-13 09:34:43 +0100
commit0a4111becc952dee83b4c7052ea3f91df6cb174a (patch)
tree7cd6f320bc70bf7427e2d431b5adab209fa3d3e8 /ios
parenta330c48b39db414b38332f5dd45de77337d53a89 (diff)
downloadmullvadvpn-0a4111becc952dee83b4c7052ea3f91df6cb174a.tar.xz
mullvadvpn-0a4111becc952dee83b4c7052ea3f91df6cb174a.zip
Add toggle in connection view
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift36
-rw-r--r--ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift3
-rw-r--r--ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift3
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj24
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift5
-rw-r--r--ios/MullvadVPN/Coordinators/TunnelCoordinator.swift9
-rw-r--r--ios/MullvadVPN/Extensions/String+Helpers.swift6
-rw-r--r--ios/MullvadVPN/Extensions/View+TapAreaSize.swift4
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json2
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf71
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg10
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift88
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift68
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift123
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift10
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift59
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift217
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift81
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift139
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift12
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift24
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift6
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift4
-rw-r--r--ios/MullvadVPN/Views/MainButton.swift3
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift24
-rw-r--r--ios/MullvadVPN/Views/SplitMainButton.swift14
-rw-r--r--ios/MullvadVPNUITests/Pages/TunnelControlPage.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift2
-rwxr-xr-xios/convert-assets.rb1
31 files changed, 710 insertions, 349 deletions
diff --git a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
new file mode 100644
index 0000000000..adc7492230
--- /dev/null
+++ b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
@@ -0,0 +1,36 @@
+//
+// SelectedRelaysStub+Stubs.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-18.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadREST
+import MullvadTypes
+import Network
+
+public struct SelectedRelaysStub {
+ public static let selectedRelays = SelectedRelays(
+ entry: nil,
+ exit: SelectedRelay(
+ endpoint: MullvadEndpoint(
+ ipv4Relay: IPv4Endpoint(ip: .loopback, port: 42),
+ ipv6Relay: IPv6Endpoint(ip: .loopback, port: 42),
+ ipv4Gateway: IPv4Address.loopback,
+ ipv6Gateway: IPv6Address.loopback,
+ publicKey: Data()
+ ),
+ hostname: "se-got-wg-001",
+ location: Location(
+ country: "Sweden",
+ countryCode: "se",
+ city: "Gothenburg",
+ cityCode: "got",
+ latitude: 42,
+ longitude: 42
+ )
+ ),
+ retryAttempt: 0
+ )
+}
diff --git a/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift b/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift
index 8346b2686d..85503c8bf4 100644
--- a/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift
+++ b/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift
@@ -11,7 +11,6 @@ import MullvadTypes
import NetworkExtension
import WireGuardKitTypes
-// swiftlint:disable function_parameter_count
public protocol EphemeralPeerNegotiating {
func startNegotiation(
devicePublicKey: PublicKey,
@@ -70,5 +69,3 @@ public class EphemeralPeerNegotiator: EphemeralPeerNegotiating {
drop_ephemeral_peer_exchange_token(cancelToken)
}
}
-
-// swiftlint:enable function_parameter_count
diff --git a/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift
index 52e4742c21..ff7a3f74a4 100644
--- a/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift
+++ b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift
@@ -12,7 +12,6 @@ import NetworkExtension
@testable import PacketTunnelCore
@testable import WireGuardKitTypes
-// swiftlint:disable function_parameter_count
class NWTCPConnectionStub: NWTCPConnection {
var _isViable = false
override var isViable: Bool {
@@ -104,5 +103,3 @@ class SuccessfulNegotiatorStub: EphemeralPeerNegotiating {
onCancelKeyNegotiation?()
}
}
-
-// swiftlint:enable function_parameter_count
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 95ae9dc0ec..d15e9a7680 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -655,6 +655,8 @@
7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */; };
7AF36A9A2CA2964200E1D497 /* AnyIPAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */; };
7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; };
+ 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */; };
+ 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */; };
7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */; };
7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */; };
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */; };
@@ -1007,7 +1009,7 @@
F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; };
F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495752D02025200CFEC2A /* ChipContainerView.swift */; };
F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */; };
- F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */; };
+ F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeature.swift */; };
F0B4957C2D03154200CFEC2A /* FeatureIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */; };
F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */; };
F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F02BF751E300817A42 /* RelayWithDistance.swift */; };
@@ -2028,6 +2030,8 @@
7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = "<group>"; };
7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddressTests.swift; sourceTree = "<group>"; };
7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = "<group>"; };
+ 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedRelaysStub+Stubs.swift"; sourceTree = "<group>"; };
+ 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewPreview.swift; sourceTree = "<group>"; };
7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilter.swift; sourceTree = "<group>"; };
7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewModel.swift; sourceTree = "<group>"; };
7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = "<group>"; };
@@ -2257,7 +2261,7 @@
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; };
F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = "<group>"; };
F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = "<group>"; };
- F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeatures.swift; sourceTree = "<group>"; };
+ F0B495792D02F41F00CFEC2A /* ChipFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeature.swift; sourceTree = "<group>"; };
F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsView.swift; sourceTree = "<group>"; };
F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = "<group>"; };
F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = "<group>"; };
@@ -3648,7 +3652,6 @@
A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
- 7A0EAE982D01B29E00D3EB8B /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -3949,13 +3952,6 @@
path = Edit;
sourceTree = "<group>";
};
- 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = {
- isa = PBXGroup;
- children = (
- );
- name = "Recovered References";
- sourceTree = "<group>";
- };
7A2960F72A964A3500389B82 /* Alert */ = {
isa = PBXGroup;
children = (
@@ -4092,8 +4088,8 @@
children = (
F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
- F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */,
7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
+ 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */,
7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */,
F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
@@ -4408,6 +4404,7 @@
F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */,
58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */,
A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */,
+ 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */,
);
path = MullvadREST;
sourceTree = "<group>";
@@ -4425,6 +4422,7 @@
isa = PBXGroup;
children = (
F0B495752D02025200CFEC2A /* ChipContainerView.swift */,
+ F0B495792D02F41F00CFEC2A /* ChipFeature.swift */,
F0ADF1D02D01B55C00299F09 /* ChipModel.swift */,
F0ADF1D42D01DCFD00299F09 /* ChipView.swift */,
F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */,
@@ -5947,6 +5945,7 @@
7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */,
588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */,
7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */,
+ 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */,
58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */,
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */,
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */,
@@ -6142,7 +6141,7 @@
588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */,
5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */,
588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */,
- F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */,
+ F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */,
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */,
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
@@ -6530,6 +6529,7 @@
F0ACE3332BE516F1006D5333 /* RESTRequestExecutor+Stubs.swift in Sources */,
F0ACE32D2BE4E784006D5333 /* AccountMock.swift in Sources */,
7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */,
+ 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */,
F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */,
F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */,
);
diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
index 60b0c22fe9..73d9a2d140 100644
--- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
+++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
@@ -40,7 +40,7 @@ public enum AccessibilityIdentifier: Equatable {
case purchaseButton
case redeemVoucherButton
case restorePurchasesButton
- case secureConnectionButton
+ case connectButton
case selectLocationButton
case closeSelectLocationButton
case settingsButton
@@ -132,7 +132,7 @@ public enum AccessibilityIdentifier: Equatable {
case selectLocationTableView
case settingsTableView
case vpnSettingsTableView
- case tunnelControlView
+ case connectionView
case problemReportView
case problemReportSubmittedView
case revokedDeviceView
@@ -156,6 +156,7 @@ public enum AccessibilityIdentifier: Equatable {
case logOutSpinnerAlertView
case connectionPanelInAddressRow
case connectionPanelOutAddressRow
+ case connectionPanelOutIpv6AddressRow
case customSwitch
case customWireGuardPortTextField
case dnsContentBlockersHeaderView
diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
index 7a8145ddca..0c55f7e1af 100644
--- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
@@ -12,7 +12,12 @@ import UIKit
class TunnelCoordinator: Coordinator, Presenting {
private let tunnelManager: TunnelManager
+
+ #if DEBUG
+ private let controller: FI_TunnelViewController
+ #else
private let controller: TunnelViewController
+ #endif
private var tunnelObserver: TunnelObserver?
@@ -39,7 +44,11 @@ class TunnelCoordinator: Coordinator, Presenting {
ipOverrideRepository: ipOverrideRepository
)
+ #if DEBUG
+ controller = FI_TunnelViewController(interactor: interactor)
+ #else
controller = TunnelViewController(interactor: interactor)
+ #endif
super.init()
diff --git a/ios/MullvadVPN/Extensions/String+Helpers.swift b/ios/MullvadVPN/Extensions/String+Helpers.swift
index a311281940..512adaa6f7 100644
--- a/ios/MullvadVPN/Extensions/String+Helpers.swift
+++ b/ios/MullvadVPN/Extensions/String+Helpers.swift
@@ -6,7 +6,6 @@
// Copyright © 2020 Mullvad VPN AB. All rights reserved.
//
-import Foundation
import UIKit
extension String {
@@ -19,4 +18,9 @@ extension String {
return (0 ..< resultCount)
.map { dropFirst($0 * length).prefix(length) }
}
+
+ func width(using font: UIFont) -> CGFloat {
+ let fontAttributes = [NSAttributedString.Key.font: font]
+ return self.size(withAttributes: fontAttributes).width
+ }
}
diff --git a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift
index 1e4ed64d37..bf81cd57be 100644
--- a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift
+++ b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift
@@ -17,8 +17,8 @@ extension View {
}
private struct TappablePadding: ViewModifier {
- @State var actualViewSize: CGSize = .zero
- let tappableViewSize = UIMetrics.Button.minimumTappableAreaSize
+ @State private var actualViewSize: CGSize = .zero
+ private let tappableViewSize = UIMetrics.Button.minimumTappableAreaSize
func body(content: Content) -> some View {
content
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json
index ff6e723432..fc394e1bfb 100644
--- a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json
@@ -1,7 +1,7 @@
{
"images" : [
{
- "filename" : "IconReload.pdf",
+ "filename" : "icon-reload.svg",
"idiom" : "universal"
}
],
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf
deleted file mode 100644
index d58fb05aa5..0000000000
--- a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf
+++ /dev/null
@@ -1,71 +0,0 @@
-%PDF-1.5
-%
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xURKNE1wl@,p %9PxS(pI Q}1_' FEd3$Y)W kJ.^t#_S|rt'OdzW(t}.H܆"atk~kmۖyXMk7BqH)pjgH
-lBRT,hseXJ֙%P/y#\ͯ YR[<<4Ɓ(d>n3<q5M!V=)7Za
-h0ȵp? ~p{80
-i)M kdq!wma*lE!uqI A
-endstream
-endobj
-5 0 obj
- 360
-endobj
-3 0 obj
-<<
- /ExtGState <<
- /a0 << /CA 1 /ca 1 >>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 512 512 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (20200729113941+00'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000764 00000 n
-0000000546 00000 n
-0000000474 00000 n
-0000000015 00000 n
-0000000452 00000 n
-0000000829 00000 n
-0000000943 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-995
-%%EOF
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg
new file mode 100644
index 0000000000..6d443ac8b4
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="icon">
+<mask id="mask0_774_19809" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
+<rect id="box" width="24" height="24" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_774_19809)">
+<path id="vector" fill-rule="evenodd" clip-rule="evenodd" d="M6 12C6 8.68629 8.68629 6 12 6C13.7762 6 15.3729 6.77144 16.4724 8H15C14.4477 8 14 8.44772 14 9C14 9.55228 14.4477 10 15 10H19C19.5523 10 20 9.55228 20 9V5C20 4.44772 19.5523 4 19 4C18.4477 4 18 4.44772 18 5V6.70853C16.535 5.04867 14.3903 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.13 20 16.0674 19.1663 17.5001 17.8094C17.9011 17.4296 17.9183 16.7967 17.5386 16.3957C17.1588 15.9947 16.5259 15.9775 16.1249 16.3572C15.0487 17.3764 13.5983 18 12 18C8.68629 18 6 15.3137 6 12Z" fill="white"/>
+</g>
+</g>
+</svg>
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 1d0f98e8f0..8eeb56c159 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -138,6 +138,11 @@ enum UIMetrics {
enum MainButton {
static let cornerRadius: CGFloat = 4
}
+
+ enum FeatureIndicators {
+ static let chipViewHorisontalPadding: CGFloat = 8
+ static let chipViewTrailingMargin: CGFloat = 6
+ }
}
extension UIMetrics {
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift
deleted file mode 100644
index c005b3f080..0000000000
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-//
-// ChipFeatures.swift
-// MullvadVPN
-//
-// Created by Mojgan on 2024-12-06.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-import Foundation
-import MullvadSettings
-import SwiftUI
-
-protocol ChipFeature {
- var isEnabled: Bool { get }
- var name: LocalizedStringKey { get }
-}
-
-struct DaitaFeature: ChipFeature {
- let settings: LatestTunnelSettings
-
- var isEnabled: Bool {
- settings.daita.daitaState.isEnabled
- }
-
- var name: LocalizedStringKey {
- LocalizedStringKey("DAITA")
- }
-}
-
-struct QuantumResistanceFeature: ChipFeature {
- let settings: LatestTunnelSettings
- var isEnabled: Bool {
- settings.tunnelQuantumResistance.isEnabled
- }
-
- var name: LocalizedStringKey {
- LocalizedStringKey("Quantum resistance")
- }
-}
-
-struct MultihopFeature: ChipFeature {
- let settings: LatestTunnelSettings
- var isEnabled: Bool {
- settings.tunnelMultihopState.isEnabled
- }
-
- var name: LocalizedStringKey {
- LocalizedStringKey("Multihop")
- }
-}
-
-struct ObfuscationFeature: ChipFeature {
- let settings: LatestTunnelSettings
-
- var isEnabled: Bool {
- settings.wireGuardObfuscation.state.isEnabled
- }
-
- var name: LocalizedStringKey {
- LocalizedStringKey("Obfuscation")
- }
-}
-
-struct DNSFeature: ChipFeature {
- let settings: LatestTunnelSettings
-
- var isEnabled: Bool {
- settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty
- }
-
- var name: LocalizedStringKey {
- if !settings.dnsSettings.blockingOptions.isEmpty {
- return LocalizedStringKey("DNS content blockers")
- }
- return LocalizedStringKey("Custom DNS")
- }
-}
-
-struct IPOverrideFeature: ChipFeature {
- let overrides: [IPOverride]
-
- var isEnabled: Bool {
- !overrides.isEmpty
- }
-
- var name: LocalizedStringKey {
- LocalizedStringKey("Server IP override")
- }
-}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
index f06124ed48..e64874ad22 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
@@ -10,46 +10,42 @@ import SwiftUI
struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
@ObservedObject var viewModel: ViewModel
+ @Binding var isExpanded: Bool
- @State var chipHeight: CGFloat = 0
- @State var fullContainerHeight: CGFloat = 0
- @State var visibleContainerHeight: CGFloat = 0
+ @State private var chipContainerHeight: CGFloat = .zero
+ private let verticalPadding: CGFloat = 6
var body: some View {
GeometryReader { geo in
let containerWidth = geo.size.width
- let chipsOverflow = !viewModel.isExpanded && (fullContainerHeight > chipHeight)
- let numberOfChips = chipsOverflow ? 2 : viewModel.chips.count
+
+ let (chipsToAdd, showMoreButton) = if isExpanded {
+ (viewModel.chips, false)
+ } else {
+ viewModel.chipsToAdd(forContainerWidth: containerWidth)
+ }
HStack {
ZStack(alignment: .topLeading) {
- createChipViews(chips: Array(viewModel.chips.prefix(numberOfChips)), containerWidth: containerWidth)
+ createChipViews(chips: chipsToAdd, containerWidth: containerWidth)
}
- .sizeOfView { visibleContainerHeight = $0.height }
- if chipsOverflow {
- Text(LocalizedStringKey("\(viewModel.chips.count - numberOfChips) more..."))
+ if showMoreButton {
+ Text(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more..."))
.font(.subheadline)
.lineLimit(1)
.foregroundStyle(UIColor.primaryTextColor.color)
- .padding(.bottom, 12)
+ .onTapGesture {
+ isExpanded.toggle()
+ }
}
Spacer()
}
- .background(preRenderViewSize(containerWidth: containerWidth))
- }.frame(height: visibleContainerHeight)
- }
-
- // Renders all chips on screen, in this case specifically to get their combined height.
- // Used to determine if content would overflow if view was not expanded and should
- // only be called from a background modifier.
- private func preRenderViewSize(containerWidth: CGFloat) -> some View {
- ZStack(alignment: .topLeading) {
- createChipViews(chips: viewModel.chips, containerWidth: containerWidth)
+ .sizeOfView { chipContainerHeight = $0.height }
}
- .hidden()
- .sizeOfView { fullContainerHeight = $0.height }
+ .frame(height: chipContainerHeight)
+ .padding(.vertical, -(verticalPadding - 1)) // Remove extra padding from chip views on top and bottom.
}
private func createChipViews(chips: [ChipModel], containerWidth: CGFloat) -> some View {
@@ -58,14 +54,21 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
return ForEach(chips) { data in
ChipView(item: data)
- .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 8))
+ .padding(
+ EdgeInsets(
+ top: verticalPadding,
+ leading: 0,
+ bottom: verticalPadding,
+ trailing: UIMetrics.FeatureIndicators.chipViewTrailingMargin
+ )
+ )
.alignmentGuide(.leading) { dimension in
if abs(width - dimension.width) > containerWidth {
width = 0
height -= dimension.height
}
let result = width
- if data.id == chips.last!.id {
+ if data.id == chips.last?.id {
width = 0
} else {
width -= dimension.width
@@ -74,22 +77,27 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
}
.alignmentGuide(.top) { _ in
let result = height
- if data.id == chips.last!.id {
+ if data.id == chips.last?.id {
height = 0
}
return result
}
- .sizeOfView { chipHeight = $0.height }
}
}
}
#Preview("Normal") {
- ChipContainerView(viewModel: MockFeatureIndicatorsViewModel())
- .background(UIColor.secondaryColor.color)
+ ChipContainerView(
+ viewModel: MockFeatureIndicatorsViewModel(),
+ isExpanded: .constant(false)
+ )
+ .background(UIColor.secondaryColor.color)
}
#Preview("Expanded") {
- ChipContainerView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: true))
- .background(UIColor.secondaryColor.color)
+ ChipContainerView(
+ viewModel: MockFeatureIndicatorsViewModel(),
+ isExpanded: .constant(true)
+ )
+ .background(UIColor.secondaryColor.color)
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift
new file mode 100644
index 0000000000..ea1d130dbe
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift
@@ -0,0 +1,123 @@
+//
+// ChipFeature.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+import MullvadSettings
+import SwiftUI
+
+protocol ChipFeature {
+ var isEnabled: Bool { get }
+ var name: String { get }
+}
+
+struct DaitaFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.daita.daitaState.isEnabled
+ }
+
+ var name: String {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_DAITA",
+ tableName: "FeatureIndicatorsChip",
+ value: "DAITA",
+ comment: ""
+ )
+ }
+}
+
+struct QuantumResistanceFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+ var isEnabled: Bool {
+ settings.tunnelQuantumResistance.isEnabled
+ }
+
+ var name: String {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_QUANTUM_RESISTANCE",
+ tableName: "FeatureIndicatorsChip",
+ value: "Quantum resistance",
+ comment: ""
+ )
+ }
+}
+
+struct MultihopFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+ var isEnabled: Bool {
+ settings.tunnelMultihopState.isEnabled
+ }
+
+ var name: String {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_MULTIHOP",
+ tableName: "FeatureIndicatorsChip",
+ value: "Multihop",
+ comment: ""
+ )
+ }
+}
+
+struct ObfuscationFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.wireGuardObfuscation.state.isEnabled
+ }
+
+ var name: String {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_OBFUSCATION",
+ tableName: "FeatureIndicatorsChip",
+ value: "Obfuscation",
+ comment: ""
+ )
+ }
+}
+
+struct DNSFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty
+ }
+
+ var name: String {
+ if !settings.dnsSettings.blockingOptions.isEmpty {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_CONTENT_BLOCKERS",
+ tableName: "FeatureIndicatorsChip",
+ value: "DNS content blockers",
+ comment: ""
+ )
+ } else {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_CUSTOM_DNS",
+ tableName: "FeatureIndicatorsChip",
+ value: "Custom DNS",
+ comment: ""
+ )
+ }
+ }
+}
+
+struct IPOverrideFeature: ChipFeature {
+ let overrides: [IPOverride]
+
+ var isEnabled: Bool {
+ !overrides.isEmpty
+ }
+
+ var name: String {
+ NSLocalizedString(
+ "FEATURE_INDICATORS_CHIP_IP_OVERRIDE",
+ tableName: "FeatureIndicatorsChip",
+ value: "Server IP Override",
+ comment: ""
+ )
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift
index c1e990a1b1..a746897c06 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift
@@ -11,5 +11,5 @@ import SwiftUI
struct ChipModel: Identifiable {
let id = UUID()
- let name: LocalizedStringKey
+ let name: String
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift
index 6d6614973f..1c6a5eb522 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift
@@ -11,20 +11,20 @@ import SwiftUI
struct ChipView: View {
let item: ChipModel
var body: some View {
- Text(item.name)
+ Text(LocalizedStringKey(item.name))
.font(.subheadline)
.lineLimit(1)
.foregroundStyle(UIColor.primaryTextColor.color)
- .padding(.horizontal, 8)
+ .padding(.horizontal, UIMetrics.FeatureIndicators.chipViewHorisontalPadding)
.padding(.vertical, 4)
.background(
- RoundedRectangle(cornerRadius: 8.0)
+ RoundedRectangle(cornerRadius: 8)
.stroke(
UIColor.primaryColor.color,
lineWidth: 1
)
.background(
- RoundedRectangle(cornerRadius: 8.0)
+ RoundedRectangle(cornerRadius: 8)
.fill(UIColor.secondaryColor.color)
)
)
@@ -33,7 +33,7 @@ struct ChipView: View {
#Preview {
ZStack {
- ChipView(item: ChipModel(name: LocalizedStringKey("Example")))
+ ChipView(item: ChipModel(name: "Example"))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(UIColor.secondaryColor.color)
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift
index 65e3b0ccef..fabec7f8c9 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift
@@ -10,23 +10,54 @@ import SwiftUI
protocol ChipViewModelProtocol: ObservableObject {
var chips: [ChipModel] { get }
- var isExpanded: Bool { get set }
}
-class MockFeatureIndicatorsViewModel: ChipViewModelProtocol {
- @Published var chips: [ChipModel] = [
- ChipModel(name: LocalizedStringKey("DAITA")),
- ChipModel(name: LocalizedStringKey("Obfuscation")),
- ChipModel(name: LocalizedStringKey("Quantum resistance")),
- ChipModel(name: LocalizedStringKey("Multihop")),
- ChipModel(name: LocalizedStringKey("DNS content blockers")),
- ChipModel(name: LocalizedStringKey("Custom DNS")),
- ChipModel(name: LocalizedStringKey("Server IP override")),
- ]
+extension ChipViewModelProtocol {
+ func chipsToAdd(forContainerWidth containerWidth: CGFloat) -> (chips: [ChipModel], chipsWillOverflow: Bool) {
+ var chipsToAdd = [ChipModel]()
+ var chipsWillOverflow = false
+
+ let moreTextWidth = "\(chips.count) more..."
+ .width(using: .preferredFont(forTextStyle: .subheadline)) + 4 // Some extra to be safe.
+ var totalChipsWidth: CGFloat = 0
+
+ for (index, chip) in chips.enumerated() {
+ let textWidth = chip.name.width(using: .preferredFont(forTextStyle: .subheadline))
+ let chipWidth = textWidth
+ + UIMetrics.FeatureIndicators.chipViewHorisontalPadding * 2
+ + UIMetrics.FeatureIndicators.chipViewTrailingMargin
+ let isLastChip = index == chips.count - 1
+
+ totalChipsWidth += chipWidth
- @Published var isExpanded: Bool
+ let chipWillFitWithMoreText = (totalChipsWidth + moreTextWidth) <= containerWidth
+ let chipWillFit = totalChipsWidth <= containerWidth
- init(isExpanded: Bool = false) {
- self.isExpanded = isExpanded
+ if chipWillFitWithMoreText {
+ // If a chip can fit together with the "more" text, add it.
+ chipsToAdd.append(chip)
+ chipsWillOverflow = !isLastChip
+ } else if chipWillFit && isLastChip {
+ // If a chip can fit and it's the last one, add it.
+ chipsToAdd.append(chip)
+ chipsWillOverflow = false
+ } else {
+ break
+ }
+ }
+
+ return (chipsToAdd, chipsWillOverflow)
}
}
+
+class MockFeatureIndicatorsViewModel: ChipViewModelProtocol {
+ @Published var chips: [ChipModel] = [
+ ChipModel(name: "DAITA"),
+ ChipModel(name: "Obfuscation"),
+ ChipModel(name: "Quantum resistance"),
+ ChipModel(name: "Multihop"),
+ ChipModel(name: "DNS content blockers"),
+ ChipModel(name: "Custom DNS"),
+ ChipModel(name: "Server IP override"),
+ ]
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
index 3a1bf7d9af..d066bb47f2 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
@@ -6,21 +6,23 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
-import MullvadSettings
import SwiftUI
-typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void
+typealias ButtonAction = (ConnectionViewViewModel.TunnelAction) -> Void
struct ConnectionView: View {
- @StateObject var viewModel: ConnectionViewViewModel
+ @StateObject var connectionViewModel: ConnectionViewViewModel
@StateObject var indicatorsViewModel: FeatureIndicatorsViewModel
+ @State private(set) var isExpanded = false
+
var action: ButtonAction?
var onContentUpdate: (() -> Void)?
var body: some View {
+ Spacer()
VStack(spacing: 22) {
- if viewModel.showsActivityIndicator {
+ if connectionViewModel.showsActivityIndicator {
CustomProgressView(style: .large)
}
@@ -28,63 +30,184 @@ struct ConnectionView: View {
BlurView(style: .dark)
VStack(alignment: .leading, spacing: 16) {
- ConnectionPanel(viewModel: viewModel)
+ ConnectionHeader(viewModel: connectionViewModel, isExpanded: $isExpanded)
- if !indicatorsViewModel.chips.isEmpty {
- FeatureIndicatorsView(viewModel: indicatorsViewModel)
+ if connectionViewModel.showConnectionDetails {
+ ConnectionDetailsContainer(
+ viewModel: connectionViewModel,
+ indicatorsViewModel: indicatorsViewModel,
+ isExpanded: $isExpanded
+ )
}
- ButtonPanel(viewModel: viewModel, action: action)
+ ButtonPanel(viewModel: connectionViewModel, action: action)
}
.padding(16)
}
.cornerRadius(12)
.padding(16)
}
- .padding(.bottom, 8) // Adding some spacing so to not overlap with the map legal link.
- .onReceive(
- indicatorsViewModel.$isExpanded
- .combineLatest(
- viewModel.$tunnelState,
- viewModel.$showsActivityIndicator
- )
- ) { _ in
+ .padding(.bottom, 8) // Adding some spacing so as not to overlap with the map legal link.
+ .accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
+ .onChange(of: isExpanded) { _ in
+ onContentUpdate?()
+ }
+ .onReceive(connectionViewModel.combinedState) { _, _ in
onContentUpdate?()
+
+ if !connectionViewModel.showConnectionDetails {
+ isExpanded = false
+ }
}
}
}
-#Preview {
- ConnectionView(
- viewModel: ConnectionViewViewModel(tunnelState: .disconnected),
- indicatorsViewModel: FeatureIndicatorsViewModel(tunnelSettings: LatestTunnelSettings(), ipOverrides: [])
- ) { action in
- print(action)
+#Preview("ConnectionView (Normal)") {
+ ConnectionViewPreview(configuration: .normal).make()
+}
+
+#Preview("ConnectionView (Normal, no indicators)") {
+ ConnectionViewPreview(configuration: .normalNoIndicators).make()
+}
+
+#Preview("ConnectionView (Expanded)") {
+ ConnectionViewPreview(configuration: .expanded).make()
+}
+
+#Preview("ConnectionView (Expanded, no indicators)") {
+ ConnectionViewPreview(configuration: .expandedNoIndicators).make()
+}
+
+private struct ConnectionHeader: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ HStack(alignment: .top) {
+ VStack(alignment: .leading, spacing: 0) {
+ Text(viewModel.localizedTitleForSecureLabel)
+ .textCase(.uppercase)
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(viewModel.textColorForSecureLabel.color)
+ .accessibilityIdentifier(viewModel.accessibilityIdForSecureLabel.asString)
+
+ if let countryAndCity = viewModel.titleForCountryAndCity {
+ Text(countryAndCity)
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ .padding(.top, 4)
+ }
+
+ if let server = viewModel.titleForServer {
+ Text(server)
+ .font(.body)
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ .padding(.top, 2)
+ }
+ }
+ .accessibilityLabel(viewModel.localizedAccessibilityLabelForSecureLabel)
+
+ if viewModel.showConnectionDetails {
+ Spacer()
+ Image(.iconChevron)
+ .renderingMode(.template)
+ .rotationEffect(isExpanded ? .degrees(-90) : .degrees(90))
+ .foregroundStyle(.white)
+ .transaction { transaction in
+ transaction.animation = nil
+ }
+ }
+ }
+ .accessibilityIdentifier(AccessibilityIdentifier.relayStatusCollapseButton.asString)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ isExpanded.toggle()
+ }
}
- .background(UIColor.secondaryColor.color)
}
-private struct ConnectionPanel: View {
+private struct ConnectionDetailsContainer: View {
@StateObject var viewModel: ConnectionViewViewModel
+ @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel
+ @Binding var isExpanded: Bool
+
+ @State private var scrollViewHeight: CGFloat = 0
var body: some View {
- VStack(alignment: .leading) {
- Text(viewModel.localizedTitleForSecureLabel)
- .textCase(.uppercase)
- .font(.title3.weight(.semibold))
- .foregroundStyle(viewModel.textColorForSecureLabel.color)
- .padding(.bottom, 4)
+ if isExpanded {
+ Divider()
+ .background(UIColor.secondaryTextColor.color)
+ }
- if let countryAndCity = viewModel.titleForCountryAndCity, let server = viewModel.titleForServer {
- Text(countryAndCity)
- .font(.title3.weight(.semibold))
- .foregroundStyle(UIColor.primaryTextColor.color)
- Text(server)
- .font(.body)
+ // This geometry reader is somewhat of a workaround. It's "smart" in that it takes up as much
+ // space as it can and thereby helps the view to understand the maximum allowed height when
+ // placed in a UIKit context. If ConnectionView would ever be placed as a subview of SwiftUI
+ // parent, this reader could probably be removed.
+ GeometryReader { _ in
+ ScrollView {
+ VStack(spacing: 16) {
+ if !indicatorsViewModel.chips.isEmpty {
+ FeatureIndicatorsView(
+ viewModel: indicatorsViewModel,
+ isExpanded: $isExpanded
+ )
+ }
+
+ if isExpanded {
+ ConnectionDetails(viewModel: viewModel)
+ }
+ }
+ .sizeOfView { scrollViewHeight = $0.height }
+ }
+ }
+ .frame(maxHeight: scrollViewHeight)
+ }
+}
+
+private struct ConnectionDetails: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+ @State private var columnWidth: CGFloat = 0
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack {
+ Text(LocalizedStringKey("Connection details"))
+ .font(.footnote.weight(.semibold))
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ Spacer()
+ }
+
+ VStack(alignment: .leading, spacing: 0) {
+ if let inAddress = viewModel.inAddress {
+ connectionDetailRow(title: LocalizedStringKey("In"), value: inAddress)
+ .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelInAddressRow.asString)
+ }
+ if viewModel.tunnelIsConnected {
+ if let outAddressIpv4 = viewModel.outAddressIpv4 {
+ connectionDetailRow(title: LocalizedStringKey("Out IPv4"), value: outAddressIpv4)
+ .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelOutAddressRow.asString)
+ }
+ if let outAddressIpv6 = viewModel.outAddressIpv6 {
+ connectionDetailRow(title: LocalizedStringKey("Out IPv6"), value: outAddressIpv6)
+ .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelOutAddressRow.asString)
+ }
+ }
}
}
- .accessibilityLabel(viewModel.localizedAccessibilityLabel)
+ }
+
+ @ViewBuilder
+ private func connectionDetailRow(title: LocalizedStringKey, value: String) -> some View {
+ HStack(alignment: .top, spacing: 8) {
+ Text(title)
+ .font(.subheadline)
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ .frame(minWidth: columnWidth, alignment: .leading)
+ .sizeOfView { columnWidth = max(columnWidth, $0.width) }
+ Text(value)
+ .font(.subheadline)
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ }
}
}
@@ -95,29 +218,31 @@ private struct ButtonPanel: View {
var body: some View {
VStack(spacing: 16) {
locationButton(with: action)
+ .disabled(viewModel.disableButtons)
actionButton(with: action)
+ .disabled(viewModel.disableButtons)
}
}
@ViewBuilder
private func locationButton(with action: ButtonAction?) -> some View {
- switch viewModel.tunnelState {
+ switch viewModel.tunnelStatus.state {
case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
SplitMainButton(
text: viewModel.localizedTitleForSelectLocationButton,
image: .iconReload,
style: .default,
- disabled: viewModel.disableButtons,
primaryAction: { action?(.selectLocation) },
secondaryAction: { action?(.reconnect) }
)
+ .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
case .disconnecting, .pendingReconnect, .disconnected:
MainButton(
text: viewModel.localizedTitleForSelectLocationButton,
style: .default,
- disabled: viewModel.disableButtons,
action: { action?(.selectLocation) }
)
+ .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
}
}
@@ -128,27 +253,31 @@ private struct ButtonPanel: View {
MainButton(
text: LocalizedStringKey("Connect"),
style: .success,
- disabled: viewModel.disableButtons,
action: { action?(.connect) }
)
+ .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString)
case .disconnect:
MainButton(
text: LocalizedStringKey("Disconnect"),
style: .danger,
- disabled: viewModel.disableButtons,
action: { action?(.disconnect) }
)
+ .accessibilityIdentifier(AccessibilityIdentifier.disconnectButton.asString)
case .cancel:
MainButton(
text: LocalizedStringKey(
- viewModel.tunnelState == .waitingForConnectivity(.noConnection)
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
? "Disconnect"
: "Cancel"
),
style: .danger,
- disabled: viewModel.disableButtons,
action: { action?(.cancel) }
)
+ .accessibilityIdentifier(
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
+ ? AccessibilityIdentifier.disconnectButton.asString
+ : AccessibilityIdentifier.cancelButton.asString
+ )
}
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift
new file mode 100644
index 0000000000..0575d4658f
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift
@@ -0,0 +1,81 @@
+//
+// ConnectionViewPreview.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-18.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadMockData
+import MullvadSettings
+import MullvadTypes
+import PacketTunnelCore
+import SwiftUI
+
+struct ConnectionViewPreview {
+ enum Configuration {
+ case normal, normalNoIndicators, expanded, expandedNoIndicators
+ }
+
+ private let configuration: Configuration
+
+ private let populatedTunnelSettings = LatestTunnelSettings(
+ wireGuardObfuscation: WireGuardObfuscationSettings(state: .udpOverTcp),
+ tunnelQuantumResistance: .on,
+ tunnelMultihopState: .on,
+ daita: DAITASettings(daitaState: .on)
+ )
+
+ private let viewModel = ConnectionViewViewModel(
+ tunnelStatus: TunnelStatus(
+ observedState: .connected(ObservedConnectionState(
+ selectedRelays: SelectedRelaysStub.selectedRelays,
+ relayConstraints: RelayConstraints(entryLocations: .any, exitLocations: .any, port: .any, filter: .any),
+ networkReachability: .reachable,
+ connectionAttemptCount: 0,
+ transportLayer: .udp,
+ remotePort: 80,
+ isPostQuantum: true,
+ isDaitaEnabled: true
+ )),
+ state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true)
+ )
+ )
+
+ init(configuration: Configuration) {
+ self.configuration = configuration
+ }
+
+ @ViewBuilder
+ func make() -> some View {
+ VStack {
+ switch configuration {
+ case .normal:
+ connectionView(with: populatedTunnelSettings, viewModel: viewModel)
+ case .normalNoIndicators:
+ connectionView(with: LatestTunnelSettings(), viewModel: viewModel)
+ case .expanded:
+ connectionView(with: populatedTunnelSettings, viewModel: viewModel, isExpanded: true)
+ case .expandedNoIndicators:
+ connectionView(with: LatestTunnelSettings(), viewModel: viewModel, isExpanded: true)
+ }
+ }
+ .background(UIColor.secondaryColor.color)
+ }
+
+ @ViewBuilder
+ private func connectionView(
+ with settings: LatestTunnelSettings,
+ viewModel: ConnectionViewViewModel,
+ isExpanded: Bool = false
+ ) -> some View {
+ ConnectionView(
+ connectionViewModel: viewModel,
+ indicatorsViewModel: FeatureIndicatorsViewModel(
+ tunnelSettings: settings,
+ ipOverrides: []
+ ),
+ isExpanded: isExpanded
+ )
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
index 29a4748b41..2f7af8a3b5 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
@@ -6,16 +6,17 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import Combine
import SwiftUI
class ConnectionViewViewModel: ObservableObject {
- enum TunnelControlActionButton {
+ enum TunnelActionButton {
case connect
case disconnect
case cancel
}
- enum TunnelControlAction {
+ enum TunnelAction {
case connect
case disconnect
case cancel
@@ -23,42 +24,69 @@ class ConnectionViewViewModel: ObservableObject {
case selectLocation
}
- @Published var tunnelState: TunnelState
+ @Published var tunnelStatus: TunnelStatus
+ @Published var outgoingConnectionInfo: OutgoingConnectionInfo?
@Published var showsActivityIndicator = false
- init(tunnelState: TunnelState) {
- self.tunnelState = tunnelState
+ var combinedState: Publishers.CombineLatest<
+ Published<TunnelStatus>.Publisher,
+ Published<Bool>.Publisher
+ > {
+ $tunnelStatus.combineLatest($showsActivityIndicator)
+ }
+
+ var tunnelIsConnected: Bool {
+ if case .connected = tunnelStatus.state {
+ true
+ } else {
+ false
+ }
+ }
+
+ init(tunnelStatus: TunnelStatus) {
+ self.tunnelStatus = tunnelStatus
}
}
extension ConnectionViewViewModel {
+ var showConnectionDetails: Bool {
+ switch tunnelStatus.state {
+ case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer,
+ .connected, .pendingReconnect, .waitingForConnectivity(.noNetwork):
+ true
+ case .disconnecting, .disconnected, .error:
+ false
+ }
+ }
+
var textColorForSecureLabel: UIColor {
- switch tunnelState {
- case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer:
+ switch tunnelStatus.state {
+ case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer,
+ .pendingReconnect, .disconnecting:
.white
case .connected:
.successColor
- case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
+ case .disconnected, .waitingForConnectivity(.noNetwork), .error:
.dangerColor
}
}
var disableButtons: Bool {
- if case .waitingForConnectivity(.noNetwork) = tunnelState {
- return true
+ if case .waitingForConnectivity(.noNetwork) = tunnelStatus.state {
+ true
+ } else {
+ false
}
-
- return false
}
var localizedTitleForSecureLabel: LocalizedStringKey {
- switch tunnelState {
+ switch tunnelStatus.state {
case .connecting, .reconnecting, .negotiatingEphemeralPeer:
- LocalizedStringKey("Connecting")
+ LocalizedStringKey("Connecting...")
case .connected:
LocalizedStringKey("Connected")
case .disconnecting(.nothing):
- LocalizedStringKey("Disconnecting")
+ LocalizedStringKey("Disconnecting...")
case .disconnecting(.reconnect), .pendingReconnect:
LocalizedStringKey("Reconnecting")
case .disconnected:
@@ -70,17 +98,19 @@ extension ConnectionViewViewModel {
}
}
- var localizedTitleForSelectLocationButton: LocalizedStringKey {
- switch tunnelState {
- case .disconnecting, .pendingReconnect, .disconnected:
- LocalizedStringKey("Select location")
- case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
- LocalizedStringKey("Switch location")
+ var accessibilityIdForSecureLabel: AccessibilityIdentifier {
+ switch tunnelStatus.state {
+ case .connected:
+ .connectionStatusConnectedLabel
+ case .connecting:
+ .connectionStatusConnectingLabel
+ default:
+ .connectionStatusNotConnectedLabel
}
}
- var localizedAccessibilityLabel: LocalizedStringKey {
- switch tunnelState {
+ var localizedAccessibilityLabelForSecureLabel: LocalizedStringKey {
+ switch tunnelStatus.state {
case .disconnected, .waitingForConnectivity, .disconnecting, .pendingReconnect, .error:
localizedTitleForSecureLabel
case let .connected(tunnelInfo, _, _):
@@ -98,8 +128,17 @@ extension ConnectionViewViewModel {
}
}
- var actionButton: TunnelControlActionButton {
- switch tunnelState {
+ var localizedTitleForSelectLocationButton: LocalizedStringKey {
+ switch tunnelStatus.state {
+ case .disconnecting, .pendingReconnect, .disconnected:
+ LocalizedStringKey("Select location")
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
+ LocalizedStringKey("Switch location")
+ }
+ }
+
+ var actionButton: TunnelActionButton {
+ switch tunnelStatus.state {
case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork):
.connect
case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection),
@@ -111,7 +150,7 @@ extension ConnectionViewViewModel {
}
var titleForCountryAndCity: LocalizedStringKey? {
- guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else {
+ guard let tunnelRelays = tunnelStatus.state.relays else {
return nil
}
@@ -119,7 +158,7 @@ extension ConnectionViewViewModel {
}
var titleForServer: LocalizedStringKey? {
- guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else {
+ guard let tunnelRelays = tunnelStatus.state.relays else {
return nil
}
@@ -132,4 +171,50 @@ extension ConnectionViewViewModel {
LocalizedStringKey("\(exitName)")
}
}
+
+ var inAddress: String? {
+ guard let tunnelRelays = tunnelStatus.state.relays else {
+ return nil
+ }
+
+ let observedTunnelState = tunnelStatus.observedState
+
+ var portAndTransport = ""
+ if let inPort = observedTunnelState.connectionState?.remotePort {
+ let protocolLayer = observedTunnelState.connectionState?.transportLayer == .tcp ? "TCP" : "UDP"
+ portAndTransport = ":\(inPort) \(protocolLayer)"
+ }
+
+ guard
+ let address = tunnelRelays.entry?.endpoint.ipv4Relay.ip
+ ?? tunnelStatus.state.relays?.exit.endpoint.ipv4Relay.ip
+ else {
+ return nil
+ }
+
+ return "\(address)\(portAndTransport)"
+ }
+
+ var outAddressIpv4: String? {
+ guard
+ let outgoingConnectionInfo,
+ let address = outgoingConnectionInfo.ipv4.exitIP ? outgoingConnectionInfo.ipv4.ip : nil
+ else {
+ return nil
+ }
+
+ return "\(address)"
+ }
+
+ var outAddressIpv6: String? {
+ guard
+ let outgoingConnectionInfo,
+ let ipv6 = outgoingConnectionInfo.ipv6,
+ let address = ipv6.exitIP ? ipv6.ip : nil
+ else {
+ return nil
+ }
+
+ return "\(address)"
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
index 9aed890041..8d3f4c78d5 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
@@ -53,14 +53,14 @@ class FI_TunnelViewController: UIViewController, RootContainment {
self.interactor = interactor
tunnelState = interactor.tunnelStatus.state
- connectionViewViewModel = ConnectionViewViewModel(tunnelState: tunnelState)
+ connectionViewViewModel = ConnectionViewViewModel(tunnelStatus: interactor.tunnelStatus)
indicatorsViewViewModel = FeatureIndicatorsViewModel(
tunnelSettings: interactor.tunnelSettings,
ipOverrides: interactor.ipOverrides
)
connectionView = ConnectionView(
- viewModel: self.connectionViewViewModel,
+ connectionViewModel: self.connectionViewViewModel,
indicatorsViewModel: self.indicatorsViewViewModel
)
@@ -86,10 +86,15 @@ class FI_TunnelViewController: UIViewController, RootContainment {
}
interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in
+ self?.connectionViewViewModel.tunnelStatus = tunnelStatus
self?.setTunnelState(tunnelStatus.state, animated: true)
self?.view.setNeedsLayout()
}
+ interactor.didGetOutGoingAddress = { [weak self] connectionInfo in
+ self?.connectionViewViewModel.outgoingConnectionInfo = connectionInfo
+ }
+
interactor.didUpdateTunnelSettings = { [weak self] tunnelSettings in
self?.indicatorsViewViewModel.tunnelSettings = tunnelSettings
}
@@ -142,7 +147,6 @@ class FI_TunnelViewController: UIViewController, RootContainment {
private func setTunnelState(_ tunnelState: TunnelState, animated: Bool) {
self.tunnelState = tunnelState
- connectionViewViewModel.tunnelState = tunnelState
setNeedsHeaderBarStyleAppearanceUpdate()
@@ -211,7 +215,7 @@ class FI_TunnelViewController: UIViewController, RootContainment {
connectionController.didMove(toParent: self)
view.addConstrainedSubviews([connectionViewProxy]) {
- connectionViewProxy.pinEdgesToSuperview(.all().excluding(.top))
+ connectionViewProxy.pinEdgesToSuperview(.all())
}
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift
index eb1a29ea81..b1a369f99e 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift
@@ -10,22 +10,26 @@ import SwiftUI
struct FeatureIndicatorsView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
@ObservedObject var viewModel: ViewModel
+ @Binding var isExpanded: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
- Text(LocalizedStringKey("Active features"))
- .font(.footnote.weight(.semibold))
- .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ if isExpanded {
+ Text(LocalizedStringKey("Active features"))
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ .padding(.bottom, 8)
+ }
- ChipContainerView(viewModel: viewModel)
- .onTapGesture {
- viewModel.isExpanded.toggle()
- }
+ ChipContainerView(viewModel: viewModel, isExpanded: $isExpanded)
}
}
}
-#Preview("FeatureIndicatorsView") {
- FeatureIndicatorsView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: true))
- .background(UIColor.secondaryColor.color)
+#Preview {
+ FeatureIndicatorsView(
+ viewModel: MockFeatureIndicatorsViewModel(),
+ isExpanded: .constant(true)
+ )
+ .background(UIColor.secondaryColor.color)
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift
index 42376b4560..97eac59ca8 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift
@@ -6,18 +6,16 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
-import Foundation
import MullvadSettings
+import SwiftUI
class FeatureIndicatorsViewModel: ChipViewModelProtocol {
@Published var tunnelSettings: LatestTunnelSettings
@Published var ipOverrides: [IPOverride]
- @Published var isExpanded = false
- init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride], isExpanded: Bool = false) {
+ init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride]) {
self.tunnelSettings = tunnelSettings
self.ipOverrides = ipOverrides
- self.isExpanded = isExpanded
}
var chips: [ChipModel] {
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 88c933493b..cff5222976 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
@@ -60,7 +60,7 @@ final class TunnelControlView: UIView {
private let connectButton: AppButton = {
let button = AppButton(style: .success)
- button.setAccessibilityIdentifier(.secureConnectionButton)
+ button.setAccessibilityIdentifier(.connectButton)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
@@ -115,7 +115,7 @@ final class TunnelControlView: UIView {
backgroundColor = .clear
directionalLayoutMargins = UIMetrics.contentLayoutMargins
accessibilityContainerType = .semanticGroup
- setAccessibilityIdentifier(.tunnelControlView)
+ setAccessibilityIdentifier(.connectionView)
addSubviews()
addButtonHandlers()
diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift
index 679b34a2cd..a4240c433d 100644
--- a/ios/MullvadVPN/Views/MainButton.swift
+++ b/ios/MullvadVPN/Views/MainButton.swift
@@ -11,7 +11,6 @@ import SwiftUI
struct MainButton: View {
var text: LocalizedStringKey
var style: MainButtonStyle.Style
- var disabled = false
var action: () -> Void
@@ -23,7 +22,7 @@ struct MainButton: View {
Spacer()
}
})
- .buttonStyle(MainButtonStyle(style, disabled: disabled))
+ .buttonStyle(MainButtonStyle(style))
.cornerRadius(UIMetrics.MainButton.cornerRadius)
}
}
diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift
index f32a27fa06..ceabd7761b 100644
--- a/ios/MullvadVPN/Views/MainButtonStyle.swift
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -10,7 +10,8 @@ import SwiftUI
struct MainButtonStyle: ButtonStyle {
var style: Style
- @State var disabled: Bool
+ var disabled: Bool
+ @Environment(\.isEnabled) private var isEnabled: Bool
init(_ style: Style, disabled: Bool = false) {
self.style = style
@@ -18,20 +19,19 @@ struct MainButtonStyle: ButtonStyle {
}
func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .padding(.horizontal, 8)
+ return configuration.label
.frame(height: 44)
.foregroundColor(
- disabled
- ? UIColor.primaryTextColor.withAlphaComponent(0.2).color
- : UIColor.primaryTextColor.color
+ isEnabled
+ ? UIColor.primaryTextColor.color
+ : UIColor.primaryTextColor.withAlphaComponent(0.2).color
)
.background(
- disabled
- ? style.disabledColor
- : configuration.isPressed
+ isEnabled
+ ? configuration.isPressed
? style.pressedColor
: style.color
+ : style.disabledColor
)
.font(.body.weight(.semibold))
}
@@ -46,11 +46,11 @@ extension MainButtonStyle {
var color: Color {
switch self {
case .default:
- Color(UIColor.primaryColor)
+ UIColor.primaryColor.color
case .danger:
- Color(UIColor.dangerColor)
+ UIColor.dangerColor.color
case .success:
- Color(UIColor.successColor)
+ UIColor.successColor.color
}
}
diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift
index 11336f424b..6f36b839be 100644
--- a/ios/MullvadVPN/Views/SplitMainButton.swift
+++ b/ios/MullvadVPN/Views/SplitMainButton.swift
@@ -12,13 +12,12 @@ struct SplitMainButton: View {
var text: LocalizedStringKey
var image: ImageResource
var style: MainButtonStyle.Style
- var disabled = false
+
+ @State private var secondaryButtonWidth: CGFloat = 0
var primaryAction: () -> Void
var secondaryAction: () -> Void
- @State private var width: CGFloat = 0
-
var body: some View {
HStack(spacing: 1) {
Button(action: primaryAction, label: {
@@ -27,18 +26,19 @@ struct SplitMainButton: View {
Text(text)
Spacer()
}
- .padding(.trailing, -width)
+ .padding(.trailing, -secondaryButtonWidth)
})
Button(action: secondaryAction, label: {
Image(image)
.resizable()
.scaledToFit()
- .padding(4)
+ .frame(width: 24, height: 24)
+ .padding(10)
})
.aspectRatio(1, contentMode: .fit)
- .sizeOfView { width = $0.width }
+ .sizeOfView { secondaryButtonWidth = $0.width }
}
- .buttonStyle(MainButtonStyle(style, disabled: disabled))
+ .buttonStyle(MainButtonStyle(style))
.cornerRadius(UIMetrics.MainButton.cornerRadius)
}
}
diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
index 6929ee1f5a..da47a2c334 100644
--- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
+++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
@@ -74,7 +74,7 @@ class TunnelControlPage: Page {
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
- self.pageElement = app.otherElements[.tunnelControlView]
+ self.pageElement = app.otherElements[.connectionView]
waitForPageToBeShown()
}
@@ -84,7 +84,7 @@ class TunnelControlPage: Page {
}
@discardableResult func tapSecureConnectionButton() -> Self {
- app.buttons[AccessibilityIdentifier.secureConnectionButton].tap()
+ app.buttons[AccessibilityIdentifier.connectButton].tap()
return self
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
index 469bf0fa20..9fbc650d70 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
@@ -99,6 +99,7 @@ public actor PacketTunnelActor {
}
}
+ // swiftlint:disable:next function_body_length
func executeEffect(_ effect: Effect) async {
switch effect {
case .startDefaultPathObserver:
@@ -136,7 +137,6 @@ public actor PacketTunnelActor {
state = .disconnected
case let .configureForErrorState(reason):
await setErrorStateInternal(with: reason)
-
case let .cacheActiveKey(lastKeyRotation):
cacheActiveKey(lastKeyRotation: lastKeyRotation)
case let .reconfigureForEphemeralPeer(configuration, configurationSemaphore):
diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb
index 5419e6282d..637068cfe6 100755
--- a/ios/convert-assets.rb
+++ b/ios/convert-assets.rb
@@ -32,7 +32,6 @@ GRAPHICAL_ASSETS = [
"icon-extLink.svg",
"icon-fail.svg",
"icon-info.svg",
- "icon-reload.svg",
"icon-settings.svg",
"icon-spinner.svg",
"icon-success.svg",