summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrew Bulhak <andrew.bulhak@mullvad.net>2025-01-03 16:56:55 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-01-13 09:34:43 +0100
commit3b90cc8957ac8fc749dafac2f3338e3d8dcda0cd (patch)
tree5d4efff228f7b146e0378c990e236e92b58375bf /ios
parent0a4111becc952dee83b4c7052ea3f91df6cb174a (diff)
downloadmullvadvpn-3b90cc8957ac8fc749dafac2f3338e3d8dcda0cd.tar.xz
mullvadvpn-3b90cc8957ac8fc749dafac2f3338e3d8dcda0cd.zip
Split ConnectionView, improve previews, attempt animation
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj42
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift22
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift283
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift90
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift71
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift (renamed from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift61
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift64
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift67
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift69
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift (renamed from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift)6
11 files changed, 470 insertions, 305 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index d15e9a7680..9ba658df15 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -43,6 +43,9 @@
44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */; };
440E5AB02CDBD67D00B09614 /* StatefulPreviewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */; };
440E5AB42CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */; };
+ 4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA882D282687001B13C9 /* DetailsContainer.swift */; };
+ 4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */; };
+ 4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */; };
4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */; };
4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */; };
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */; };
@@ -52,6 +55,8 @@
4495ECD52D131A4800A7358B /* ShadowsocksObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */; };
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
+ 449E9A6D2D283A2500F8574A /* ConnectionViewComponentPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */; };
+ 449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */; };
449EBA262B975B9700DFA4EB /* EphemeralPeerReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */; };
44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; };
44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
@@ -1443,6 +1448,9 @@
44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPreviewWrapper.swift; sourceTree = "<group>"; };
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationSettingsWatchingObservableObject.swift; sourceTree = "<group>"; };
+ 4419AA882D282687001B13C9 /* DetailsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsContainer.swift; sourceTree = "<group>"; };
+ 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = "<group>"; };
+ 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsView.swift; sourceTree = "<group>"; };
4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChoiceList.swift; sourceTree = "<group>"; };
447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
@@ -1453,6 +1461,8 @@
4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsPage.swift; sourceTree = "<group>"; };
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
+ 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewComponentPreview.swift; sourceTree = "<group>"; };
+ 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonPanel.swift; sourceTree = "<group>"; };
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; };
449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerReceiving.swift; sourceTree = "<group>"; };
@@ -2694,6 +2704,20 @@
path = Protocols;
sourceTree = "<group>";
};
+ 4419AA862D28264D001B13C9 /* ConnectionView */ = {
+ isa = PBXGroup;
+ children = (
+ 449E9A6B2D2839FD00F8574A /* Preview */,
+ 7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
+ 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
+ 4419AA882D282687001B13C9 /* DetailsContainer.swift */,
+ 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */,
+ 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */,
+ 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */,
+ );
+ path = ConnectionView;
+ sourceTree = "<group>";
+ };
4422C06F2CCFF6520001A385 /* Obfuscation */ = {
isa = PBXGroup;
children = (
@@ -2734,6 +2758,15 @@
path = MullvadSettings;
sourceTree = "<group>";
};
+ 449E9A6B2D2839FD00F8574A /* Preview */ = {
+ isa = PBXGroup;
+ children = (
+ 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */,
+ 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */,
+ );
+ path = Preview;
+ sourceTree = "<group>";
+ };
449EBA242B975B7C00DFA4EB /* Protocols */ = {
isa = PBXGroup;
children = (
@@ -4086,11 +4119,9 @@
7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
isa = PBXGroup;
children = (
+ 4419AA862D28264D001B13C9 /* ConnectionView */,
F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
- 7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
- 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */,
- 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */,
F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */,
@@ -5908,6 +5939,7 @@
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
7A8A190A2CE5FFE9000BCB5B /* SettingsDAITAView.swift in Sources */,
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */,
+ 449E9A6D2D283A2500F8574A /* ConnectionViewComponentPreview.swift in Sources */,
7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */,
F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
@@ -6042,6 +6074,7 @@
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */,
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */,
+ 4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */,
@@ -6105,6 +6138,7 @@
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */,
5888AD83227B11080051EB06 /* LocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
+ 4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */,
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
@@ -6156,6 +6190,8 @@
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */,
+ 449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */,
+ 4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */,
5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */,
7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */,
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */,
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
index e64874ad22..333761fa30 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
@@ -86,18 +86,12 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
}
}
-#Preview("Normal") {
- ChipContainerView(
- viewModel: MockFeatureIndicatorsViewModel(),
- isExpanded: .constant(false)
- )
- .background(UIColor.secondaryColor.color)
-}
-
-#Preview("Expanded") {
- ChipContainerView(
- viewModel: MockFeatureIndicatorsViewModel(),
- isExpanded: .constant(true)
- )
- .background(UIColor.secondaryColor.color)
+#Preview("Tap to expand") {
+ StatefulPreviewWrapper(false) { isExpanded in
+ ChipContainerView(
+ viewModel: MockFeatureIndicatorsViewModel(),
+ isExpanded: isExpanded
+ )
+ .background(UIColor.secondaryColor.color)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
deleted file mode 100644
index d066bb47f2..0000000000
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
+++ /dev/null
@@ -1,283 +0,0 @@
-//
-// ConnectionView.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 2024-12-03.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-
-import SwiftUI
-
-typealias ButtonAction = (ConnectionViewViewModel.TunnelAction) -> Void
-
-struct ConnectionView: View {
- @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 connectionViewModel.showsActivityIndicator {
- CustomProgressView(style: .large)
- }
-
- ZStack {
- BlurView(style: .dark)
-
- VStack(alignment: .leading, spacing: 16) {
- ConnectionHeader(viewModel: connectionViewModel, isExpanded: $isExpanded)
-
- if connectionViewModel.showConnectionDetails {
- ConnectionDetailsContainer(
- viewModel: connectionViewModel,
- indicatorsViewModel: indicatorsViewModel,
- isExpanded: $isExpanded
- )
- }
-
- ButtonPanel(viewModel: connectionViewModel, action: action)
- }
- .padding(16)
- }
- .cornerRadius(12)
- .padding(16)
- }
- .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 (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()
- }
- }
-}
-
-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 {
- if isExpanded {
- Divider()
- .background(UIColor.secondaryTextColor.color)
- }
-
- // 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)
- }
- }
- }
- }
- }
-
- @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)
- }
- }
-}
-
-private struct ButtonPanel: View {
- @StateObject var viewModel: ConnectionViewViewModel
- var action: ButtonAction?
-
- 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.tunnelStatus.state {
- case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
- SplitMainButton(
- text: viewModel.localizedTitleForSelectLocationButton,
- image: .iconReload,
- style: .default,
- primaryAction: { action?(.selectLocation) },
- secondaryAction: { action?(.reconnect) }
- )
- .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
- case .disconnecting, .pendingReconnect, .disconnected:
- MainButton(
- text: viewModel.localizedTitleForSelectLocationButton,
- style: .default,
- action: { action?(.selectLocation) }
- )
- .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
- }
- }
-
- @ViewBuilder
- private func actionButton(with action: ButtonAction?) -> some View {
- switch viewModel.actionButton {
- case .connect:
- MainButton(
- text: LocalizedStringKey("Connect"),
- style: .success,
- action: { action?(.connect) }
- )
- .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString)
- case .disconnect:
- MainButton(
- text: LocalizedStringKey("Disconnect"),
- style: .danger,
- action: { action?(.disconnect) }
- )
- .accessibilityIdentifier(AccessibilityIdentifier.disconnectButton.asString)
- case .cancel:
- MainButton(
- text: LocalizedStringKey(
- viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
- ? "Disconnect"
- : "Cancel"
- ),
- style: .danger,
- action: { action?(.cancel) }
- )
- .accessibilityIdentifier(
- viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
- ? AccessibilityIdentifier.disconnectButton.asString
- : AccessibilityIdentifier.cancelButton.asString
- )
- }
- }
-}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift
new file mode 100644
index 0000000000..bfbe437041
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift
@@ -0,0 +1,90 @@
+//
+// ButtonPanel.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct ButtonPanel: View {
+ typealias Action = (ConnectionViewViewModel.TunnelAction) -> Void
+
+ @StateObject var viewModel: ConnectionViewViewModel
+ var action: Action?
+
+ 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: Action?) -> some View {
+ switch viewModel.tunnelStatus.state {
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
+ SplitMainButton(
+ text: viewModel.localizedTitleForSelectLocationButton,
+ image: .iconReload,
+ style: .default,
+ primaryAction: { action?(.selectLocation) },
+ secondaryAction: { action?(.reconnect) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
+ case .disconnecting, .pendingReconnect, .disconnected:
+ MainButton(
+ text: viewModel.localizedTitleForSelectLocationButton,
+ style: .default,
+ action: { action?(.selectLocation) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
+ }
+ }
+
+ @ViewBuilder
+ private func actionButton(with action: Action?) -> some View {
+ switch viewModel.actionButton {
+ case .connect:
+ MainButton(
+ text: LocalizedStringKey("Connect"),
+ style: .success,
+ action: { action?(.connect) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString)
+ case .disconnect:
+ MainButton(
+ text: LocalizedStringKey("Disconnect"),
+ style: .danger,
+ action: { action?(.disconnect) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.disconnectButton.asString)
+ case .cancel:
+ MainButton(
+ text: LocalizedStringKey(
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
+ ? "Disconnect"
+ : "Cancel"
+ ),
+ style: .danger,
+ action: { action?(.cancel) }
+ )
+ .accessibilityIdentifier(
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
+ ? AccessibilityIdentifier.disconnectButton.asString
+ : AccessibilityIdentifier.cancelButton.asString
+ )
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
+ ConnectionView.ButtonPanel(viewModel: vm, action: nil)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift
new file mode 100644
index 0000000000..929e0b17a3
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift
@@ -0,0 +1,71 @@
+//
+// ConnectionView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct ConnectionView: View {
+ @StateObject var connectionViewModel: ConnectionViewViewModel
+ @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel
+
+ @State private(set) var isExpanded = false
+
+ var action: ButtonPanel.Action?
+ var onContentUpdate: (() -> Void)?
+
+ var body: some View {
+ Spacer()
+ VStack(spacing: 22) {
+ if connectionViewModel.showsActivityIndicator {
+ CustomProgressView(style: .large)
+ }
+
+ ZStack {
+ BlurView(style: .dark)
+
+ VStack(alignment: .leading, spacing: 16) {
+ HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
+
+ if connectionViewModel.showConnectionDetails {
+ DetailsContainer(
+ viewModel: connectionViewModel,
+ indicatorsViewModel: indicatorsViewModel,
+ isExpanded: $isExpanded
+ )
+ }
+
+ ButtonPanel(viewModel: connectionViewModel, action: action)
+ }
+ .padding(16)
+ }
+ .cornerRadius(12)
+ .padding(16)
+ }
+ .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 {
+// withAnimation {
+ isExpanded = false
+// }
+ }
+ }
+ }
+}
+
+#Preview("ConnectionView (Indicators)") {
+ ConnectionViewPreview(configuration: .normal).make()
+}
+
+#Preview("ConnectionView (No indicators)") {
+ ConnectionViewPreview(configuration: .normalNoIndicators).make()
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift
index 2f7af8a3b5..2f7af8a3b5 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift
new file mode 100644
index 0000000000..7da9aef5b8
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift
@@ -0,0 +1,61 @@
+//
+// ConnectionDetailsContainer.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct DetailsContainer: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+ @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel
+ @Binding var isExpanded: Bool
+
+ @State private var scrollViewHeight: CGFloat = 0
+
+ var body: some View {
+// if isExpanded {
+ Divider()
+ .background(UIColor.secondaryTextColor.color)
+ .opacity(isExpanded ? 1.0 : 0.0)
+// }
+
+ // 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 {
+ DetailsView(viewModel: viewModel)
+ .transition(.move(edge: .bottom))
+ }
+ }
+ .sizeOfView { scrollViewHeight = $0.height }
+ }
+ }
+ .frame(maxHeight: scrollViewHeight)
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, isExpanded in
+ ConnectionView.DetailsContainer(
+ viewModel: viewModel,
+ indicatorsViewModel: indicatorModel,
+ isExpanded: isExpanded
+ )
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift
new file mode 100644
index 0000000000..25559757a1
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift
@@ -0,0 +1,64 @@
+//
+// ConnectionDetails.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct DetailsView: 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)
+ }
+ }
+ }
+ }
+ }
+
+ @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)
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
+ ConnectionView.DetailsView(viewModel: vm)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift
new file mode 100644
index 0000000000..5ec0eb9316
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift
@@ -0,0 +1,67 @@
+//
+// HeaderView.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct HeaderView: 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 {
+// withAnimation {
+ isExpanded.toggle()
+// }
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, isExpanded in
+ ConnectionView.HeaderView(viewModel: vm, isExpanded: isExpanded)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift
new file mode 100644
index 0000000000..89b3fb0aaf
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift
@@ -0,0 +1,69 @@
+//
+// ConnectionViewComponentPreview.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadMockData
+import MullvadSettings
+import MullvadTypes
+import PacketTunnelCore
+import SwiftUI
+
+struct ConnectionViewComponentPreview<Content: View>: View {
+ let showIndicators: Bool
+
+ private var tunnelSettings: LatestTunnelSettings {
+ LatestTunnelSettings(
+ wireGuardObfuscation: WireGuardObfuscationSettings(state: showIndicators ? .udpOverTcp : .off),
+ tunnelQuantumResistance: showIndicators ? .on : .off,
+ tunnelMultihopState: showIndicators ? .on : .off,
+ daita: DAITASettings(daitaState: showIndicators ? .on : .off)
+ )
+ }
+
+ 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)
+ )
+ )
+
+ var content: (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
+
+ @State var isExpanded: Bool
+
+ init(
+ showIndicators: Bool,
+ isExpanded: Bool,
+ content: @escaping (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
+ ) {
+ self.showIndicators = showIndicators
+ self._isExpanded = State(wrappedValue: isExpanded)
+ self.content = content
+ }
+
+ var body: some View {
+ VStack {
+ content(
+ FeatureIndicatorsViewModel(
+ tunnelSettings: tunnelSettings,
+ ipOverrides: []
+ ),
+ viewModel,
+ $isExpanded
+ )
+ }.background(UIColor.secondaryColor.color)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift
index 0575d4658f..4f2fd49a92 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift
@@ -14,7 +14,7 @@ import SwiftUI
struct ConnectionViewPreview {
enum Configuration {
- case normal, normalNoIndicators, expanded, expandedNoIndicators
+ case normal, normalNoIndicators
}
private let configuration: Configuration
@@ -54,10 +54,6 @@ struct ConnectionViewPreview {
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)