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>2024-12-13 14:31:41 +0100
commitc61b7d4e5a3a891562416692ee8d5b94de1e8549 (patch)
treee85b0edcfb08238a5714f59ddaa1a4b03662359e /ios
parent9573f3ed7a449f2aeb9f8efe0c6f4335ed0c326c (diff)
downloadmullvadvpn-c61b7d4e5a3a891562416692ee8d5b94de1e8549.tar.xz
mullvadvpn-c61b7d4e5a3a891562416692ee8d5b94de1e8549.zip
Add state to new connection view
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj29
-rw-r--r--ios/MullvadVPN/Coordinators/TunnelCoordinator.swift10
-rw-r--r--ios/MullvadVPN/Extensions/Color+Helpers.swift16
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState+UI.swift97
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift16
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ActivityIndicator.swift48
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift135
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift135
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift198
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift37
-rw-r--r--ios/MullvadVPN/Views/BlurView.swift22
-rw-r--r--ios/MullvadVPN/Views/MainButton.swift7
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift16
-rw-r--r--ios/MullvadVPN/Views/SplitMainButton.swift8
-rw-r--r--ios/MullvadVPN/Views/VisualEffectView.swift4
15 files changed, 661 insertions, 117 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 4924105424..fe09f04daf 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -464,6 +464,9 @@
7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */; };
7A0EAE9A2D01B41500D3EB8B /* MainButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAE992D01B41500D3EB8B /* MainButtonStyle.swift */; };
7A0EAE9E2D01BCBF00D3EB8B /* View+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */; };
+ 7A0EAEA02D0333CE00D3EB8B /* Color+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAE9F2D0333CB00D3EB8B /* Color+Helpers.swift */; };
+ 7A0EAEA22D033D5D00D3EB8B /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */; };
+ 7A0EAEA42D06DF8C00D3EB8B /* ConnectionViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */; };
7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */; };
7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; };
7A12D0772B062D6500E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; };
@@ -658,6 +661,8 @@
7AF9BE972A41C71F00DBFEDB /* ChipViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */; };
7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */; };
7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */; };
+ 7AFBE3872D084C9D002335FC /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */; };
+ 7AFBE3892D089163002335FC /* FI_TunnelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */; };
850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DA2B503D7700EF8C96 /* RelayTests.swift */; };
850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */; };
850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */; };
@@ -1843,6 +1848,9 @@
7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = "<group>"; };
7A0EAE992D01B41500D3EB8B /* MainButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButtonStyle.swift; sourceTree = "<group>"; };
7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Size.swift"; sourceTree = "<group>"; };
+ 7A0EAE9F2D0333CB00D3EB8B /* Color+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Helpers.swift"; sourceTree = "<group>"; };
+ 7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = "<group>"; };
+ 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewViewModel.swift; sourceTree = "<group>"; };
7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = "<group>"; };
7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAlertPresenter.swift; sourceTree = "<group>"; };
7A1A26442A29CEF700B978AA /* RelayFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewController.swift; sourceTree = "<group>"; };
@@ -2014,6 +2022,8 @@
7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewCell.swift; sourceTree = "<group>"; };
7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglehopPicker.swift; sourceTree = "<group>"; };
7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPicker.swift; sourceTree = "<group>"; };
+ 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
+ 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FI_TunnelViewController.swift; sourceTree = "<group>"; };
85006A8E2B73EF67004AD8FB /* MullvadVPNUITestsSmoke.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MullvadVPNUITestsSmoke.xctestplan; sourceTree = "<group>"; };
850201DA2B503D7700EF8C96 /* RelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayTests.swift; sourceTree = "<group>"; };
850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationPage.swift; sourceTree = "<group>"; };
@@ -3015,6 +3025,7 @@
isa = PBXGroup;
children = (
7A5869962B32EA4500640D27 /* AppButton.swift */,
+ 7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */,
7A9FA1412A2E3306000B728D /* CheckboxView.swift */,
5868585424054096000B8131 /* CustomButton.swift */,
58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */,
@@ -3085,6 +3096,7 @@
587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */,
58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */,
7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */,
+ 7A0EAE9F2D0333CB00D3EB8B /* Color+Helpers.swift */,
7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */,
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */,
58DFF7CF2B02560400F864E0 /* NSAttributedString+Extensions.swift */,
@@ -3612,6 +3624,7 @@
A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
+ 7A0EAE982D01B29E00D3EB8B /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -3912,6 +3925,14 @@
path = Edit;
sourceTree = "<group>";
};
+ 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ 7AA1309C2D0072F900640DF9 /* View+Size.swift */,
+ );
+ name = "Recovered References";
+ sourceTree = "<group>";
+ };
7A2960F72A964A3500389B82 /* Alert */ = {
isa = PBXGroup;
children = (
@@ -4046,7 +4067,10 @@
7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
isa = PBXGroup;
children = (
+ 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
+ 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
+ 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */,
);
path = FeatureIndicators;
sourceTree = "<group>";
@@ -5817,6 +5841,7 @@
F0ADC3742CD3C47400A1AD97 /* ChipFlowLayout.swift in Sources */,
587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */,
587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */,
+ 7AFBE3872D084C9D002335FC /* ActivityIndicator.swift in Sources */,
F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */,
58DFF7D82B02774C00F864E0 /* ListItemPickerViewController.swift in Sources */,
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
@@ -5838,6 +5863,7 @@
7A8A19142CEF2548000BCB5B /* DAITATunnelSettingsViewModel.swift in Sources */,
7A8A18F92CE34EA8000BCB5B /* SettingsMultihopView.swift in Sources */,
44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */,
+ 7AFBE3892D089163002335FC /* FI_TunnelViewController.swift in Sources */,
7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */,
5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */,
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
@@ -5872,6 +5898,7 @@
7AA1309B2D0048D800640DF9 /* MainButton.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */,
+ 7A0EAEA02D0333CE00D3EB8B /* Color+Helpers.swift in Sources */,
7A8A19052CE4E9A9000BCB5B /* SwitchRowView.swift in Sources */,
A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */,
7A5869972B32EA4500640D27 /* AppButton.swift in Sources */,
@@ -6061,6 +6088,8 @@
7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
+ 7A0EAEA42D06DF8C00D3EB8B /* ConnectionViewViewModel.swift in Sources */,
+ 7A0EAEA22D033D5D00D3EB8B /* BlurView.swift in Sources */,
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
58FF9FF02B07C4D300E4C97D /* PersistentAccessMethod+ViewModel.swift in Sources */,
7A8A19162CEF269E000BCB5B /* MultihopTunnelSettingsViewModel.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
index 5fa71da17c..1e3e48e64c 100644
--- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
@@ -11,7 +11,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?
@@ -35,7 +40,12 @@ class TunnelCoordinator: Coordinator, Presenting {
tunnelManager: tunnelManager,
outgoingConnectionService: outgoingConnectionService
)
+
+ #if DEBUG
+ controller = FI_TunnelViewController(interactor: interactor)
+ #else
controller = TunnelViewController(interactor: interactor)
+ #endif
super.init()
diff --git a/ios/MullvadVPN/Extensions/Color+Helpers.swift b/ios/MullvadVPN/Extensions/Color+Helpers.swift
new file mode 100644
index 0000000000..89c886fc24
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/Color+Helpers.swift
@@ -0,0 +1,16 @@
+//
+// Color.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension Color {
+ /// Returns the color darker by the given percent (in range from 0..1)
+ func darkened(by percent: CGFloat) -> Color? {
+ UIColor(self).darkened(by: percent)?.color
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
index e87afd87b3..f02bf005e1 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
@@ -9,14 +9,18 @@
import UIKit
extension TunnelState {
+ enum TunnelControlActionButton {
+ case connect
+ case disconnect
+ case cancel
+ }
+
var textColorForSecureLabel: UIColor {
switch self {
case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer:
.white
-
case .connected:
.successColor
-
case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
.dangerColor
}
@@ -65,6 +69,7 @@ extension TunnelState {
comment: ""
)
}
+
case let .connected(_, isPostQuantum, _):
if isPostQuantum {
NSLocalizedString(
@@ -77,7 +82,7 @@ extension TunnelState {
NSLocalizedString(
"TUNNEL_STATE_CONNECTED",
tableName: "Main",
- value: "Secure connection",
+ value: "Connected",
comment: ""
)
}
@@ -89,6 +94,7 @@ extension TunnelState {
value: "Disconnecting",
comment: ""
)
+
case .disconnecting(.reconnect), .pendingReconnect:
NSLocalizedString(
"TUNNEL_STATE_PENDING_RECONNECT",
@@ -123,7 +129,7 @@ extension TunnelState {
}
}
- var localizedTitleForSelectLocationButton: String? {
+ var localizedTitleForSelectLocationButton: String {
switch self {
case .disconnecting(.reconnect), .pendingReconnect:
NSLocalizedString(
@@ -159,24 +165,6 @@ extension TunnelState {
}
}
- func secureConnectionLabel(isPostQuantum: Bool) -> String {
- if isPostQuantum {
- NSLocalizedString(
- "TUNNEL_STATE_PQ_CONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Creating quantum secure connection",
- comment: ""
- )
- } else {
- NSLocalizedString(
- "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Creating secure connection",
- comment: ""
- )
- }
- }
-
var localizedAccessibilityLabel: String {
switch self {
case let .connecting(_, isPostQuantum, _):
@@ -263,4 +251,69 @@ extension TunnelState {
)
}
}
+
+ var actionButton: TunnelControlActionButton {
+ switch self {
+ case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork):
+ .connect
+ case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection):
+ .cancel
+ case .negotiatingEphemeralPeer:
+ .cancel
+ case .connected, .reconnecting, .error:
+ .disconnect
+ }
+ }
+
+ var titleForCountryAndCity: String? {
+ guard isSecured, let tunnelRelays = relays else {
+ return nil
+ }
+
+ return "\(tunnelRelays.exit.location.country), \(tunnelRelays.exit.location.city)"
+ }
+
+ func titleForServer(daitaEnabled: Bool) -> String? {
+ guard isSecured, let tunnelRelays = relays else {
+ return nil
+ }
+
+ let exitName = tunnelRelays.exit.hostname
+ let entryName = tunnelRelays.entry?.hostname
+ let usingDaita = daitaEnabled == true
+
+ return if let entryName {
+ String(format: NSLocalizedString(
+ "CONNECT_PANEL_TITLE",
+ tableName: "Main",
+ value: "%@ via %@\(usingDaita ? " using DAITA" : "")",
+ comment: ""
+ ), exitName, entryName)
+ } else {
+ String(format: NSLocalizedString(
+ "CONNECT_PANEL_TITLE",
+ tableName: "Main",
+ value: "%@\(usingDaita ? " using DAITA" : "")",
+ comment: ""
+ ), exitName)
+ }
+ }
+
+ func secureConnectionLabel(isPostQuantum: Bool) -> String {
+ if isPostQuantum {
+ NSLocalizedString(
+ "TUNNEL_STATE_PQ_CONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Creating quantum secure connection",
+ comment: ""
+ )
+ } else {
+ NSLocalizedString(
+ "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Creating secure connection",
+ comment: ""
+ )
+ }
+ }
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index ea55431d5a..b235f8255c 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -84,8 +84,8 @@ enum TunnelState: Equatable, CustomStringConvertible {
case let .connecting(tunnelRelays, isPostQuantum, isDaita):
if let tunnelRelays {
"""
- connecting \(isPostQuantum ? "(PQ) " : "")\
- daita: \(isDaita) \
+ connecting \(isPostQuantum ? "(PQ) " : ""), \
+ daita: \(isDaita), \
to \(tunnelRelays.exit.hostname)\
\(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")
"""
@@ -94,8 +94,8 @@ enum TunnelState: Equatable, CustomStringConvertible {
}
case let .connected(tunnelRelays, isPostQuantum, isDaita):
"""
- connected \(isPostQuantum ? "(PQ) " : "")\
- daita: \(isDaita) \
+ connected \(isPostQuantum ? "(PQ) " : ""), \
+ daita: \(isDaita), \
to \(tunnelRelays.exit.hostname)\
\(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")
"""
@@ -105,8 +105,8 @@ enum TunnelState: Equatable, CustomStringConvertible {
"disconnected"
case let .reconnecting(tunnelRelays, isPostQuantum, isDaita):
"""
- reconnecting \(isPostQuantum ? "(PQ) " : "")\
- daita: \(isDaita) \
+ reconnecting \(isPostQuantum ? "(PQ) " : ""), \
+ daita: \(isDaita), \
to \(tunnelRelays.exit.hostname)\
\(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")
"""
@@ -117,8 +117,8 @@ enum TunnelState: Equatable, CustomStringConvertible {
case let .negotiatingEphemeralPeer(tunnelRelays, _, isPostQuantum, isDaita):
"""
negotiating key with exit relay: \(tunnelRelays.exit.hostname)\
- \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")\
- , isPostQuantum: \(isPostQuantum), isDaita: \(isDaita)
+ \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? ""), \
+ isPostQuantum: \(isPostQuantum), isDaita: \(isDaita)
"""
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ActivityIndicator.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ActivityIndicator.swift
new file mode 100644
index 0000000000..9b42bab8e6
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ActivityIndicator.swift
@@ -0,0 +1,48 @@
+//
+// ActivityIndicator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-10.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct CustomProgressView: View {
+ var style: Style
+ @State private var angle: Double = 0
+
+ var body: some View {
+ Image(.iconSpinner)
+ .resizable()
+ .frame(width: style.size.width, height: style.size.height)
+ .rotationEffect(.degrees(angle))
+ .onAppear {
+ withAnimation(Animation.linear(duration: 0.6).repeatForever(autoreverses: false)) {
+ angle = 360
+ }
+ }
+ }
+}
+
+#Preview {
+ CustomProgressView(style: .large)
+ .background(UIColor.secondaryColor.color)
+}
+
+extension CustomProgressView {
+ enum Style {
+ case small, medium, large
+
+ var size: CGSize {
+ switch self {
+ case .small:
+ CGSize(width: 16, height: 16)
+ case .medium:
+ CGSize(width: 20, height: 20)
+ case .large:
+ CGSize(width: 60, height: 60)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
index 3f3d4e473b..03980fb361 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
@@ -8,80 +8,133 @@
import SwiftUI
-// TODO: Replace all hardcoded values with real values dependent on tunnel state. To be addressed in upcoming PR.
+typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void
struct ConnectionView: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+
+ var action: ButtonAction?
+ var onContentUpdate: (() -> Void)?
+
var body: some View {
- ZStack {
- BlurView()
+ VStack(spacing: 22) {
+ if viewModel.showsActivityIndicator {
+ CustomProgressView(style: .large)
+ }
+
+ ZStack {
+ BlurView(style: .dark)
- VStack(alignment: .leading, spacing: 16) {
- ConnectionPanel()
- ButtonPanel()
+ VStack(alignment: .leading, spacing: 16) {
+ ConnectionPanel(viewModel: viewModel)
+ ButtonPanel(viewModel: viewModel, action: action)
+ }
+ .padding(16)
}
+ .cornerRadius(12)
.padding(16)
}
- .cornerRadius(12)
- .padding(16)
- // Importing UIView in SwitftUI (see BlurView) has sizing limitations, so we need to help the view
- // understand its width constraints.
- .frame(maxWidth: UIScreen.main.bounds.width)
+ .onReceive(viewModel.$tunnelState, perform: { _ in
+ onContentUpdate?()
+ })
+ .onReceive(viewModel.$showsActivityIndicator, perform: { _ in
+ onContentUpdate?()
+ })
}
}
#Preview {
- ConnectionView()
- .background(UIColor.secondaryColor.color)
-}
-
-private struct BlurView: View {
- var body: some View {
- Spacer()
- .overlay {
- VisualEffectView(effect: UIBlurEffect(style: .dark))
- .opacity(0.8)
- }
+ ConnectionView(viewModel: ConnectionViewViewModel(tunnelState: .disconnected)) { action in
+ print(action)
}
+ .background(UIColor.secondaryColor.color)
}
private struct ConnectionPanel: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+
var body: some View {
VStack(alignment: .leading) {
- Text("Connected")
+ Text(viewModel.localizedTitleForSecureLabel)
.textCase(.uppercase)
.font(.title3.weight(.semibold))
- .foregroundStyle(UIColor.successColor.color)
+ .foregroundStyle(viewModel.textColorForSecureLabel.color)
.padding(.bottom, 4)
- Text("Country, City")
- .font(.title3.weight(.semibold))
- .foregroundStyle(UIColor.primaryTextColor.color)
- Text("Server")
- .font(.body)
- .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+
+ if let countryAndCity = viewModel.titleForCountryAndCity, let server = viewModel.titleForServer {
+ Text(countryAndCity)
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ Text(server)
+ .font(.body)
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ }
}
+ .accessibilityLabel(viewModel.localizedAccessibilityLabel)
}
}
private struct ButtonPanel: View {
+ @StateObject var viewModel: ConnectionViewViewModel
+ var action: ButtonAction?
+
var body: some View {
VStack(spacing: 16) {
+ locationButton(with: action)
+ actionButton(with: action)
+ }
+ }
+
+ @ViewBuilder
+ private func locationButton(with action: ButtonAction?) -> some View {
+ switch viewModel.tunnelState {
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
SplitMainButton(
- text: "Switch location",
+ text: viewModel.localizedTitleForSelectLocationButton,
image: .iconReload,
style: .default,
- primaryAction: {
- print("Switch location tapped")
- }, secondaryAction: {
- print("Reload tapped")
- }
+ disabled: viewModel.disableButtons,
+ primaryAction: { action?(.selectLocation) },
+ secondaryAction: { action?(.reconnect) }
)
+ case .disconnecting, .pendingReconnect, .disconnected:
+ MainButton(
+ text: viewModel.localizedTitleForSelectLocationButton,
+ style: .default,
+ disabled: viewModel.disableButtons,
+ action: { action?(.selectLocation) }
+ )
+ }
+ }
+ @ViewBuilder
+ private func actionButton(with action: ButtonAction?) -> some View {
+ switch viewModel.actionButton {
+ case .connect:
MainButton(
- text: "Cancel",
- style: .danger
- ) {
- print("Cancel tapped")
- }
+ text: LocalizedStringKey("Connect"),
+ style: .success,
+ disabled: viewModel.disableButtons,
+ action: { action?(.connect) }
+ )
+ case .disconnect:
+ MainButton(
+ text: LocalizedStringKey("Disconnect"),
+ style: .danger,
+ disabled: viewModel.disableButtons,
+ action: { action?(.disconnect) }
+ )
+ case .cancel:
+ MainButton(
+ text: LocalizedStringKey(
+ viewModel.tunnelState == .waitingForConnectivity(.noConnection)
+ ? "Disconnect"
+ : "Cancel"
+ ),
+ style: .danger,
+ disabled: viewModel.disableButtons,
+ action: { action?(.cancel) }
+ )
}
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
new file mode 100644
index 0000000000..29a4748b41
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
@@ -0,0 +1,135 @@
+//
+// ConnectionViewViewModel.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-09.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+class ConnectionViewViewModel: ObservableObject {
+ enum TunnelControlActionButton {
+ case connect
+ case disconnect
+ case cancel
+ }
+
+ enum TunnelControlAction {
+ case connect
+ case disconnect
+ case cancel
+ case reconnect
+ case selectLocation
+ }
+
+ @Published var tunnelState: TunnelState
+ @Published var showsActivityIndicator = false
+
+ init(tunnelState: TunnelState) {
+ self.tunnelState = tunnelState
+ }
+}
+
+extension ConnectionViewViewModel {
+ var textColorForSecureLabel: UIColor {
+ switch tunnelState {
+ case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer:
+ .white
+ case .connected:
+ .successColor
+ case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
+ .dangerColor
+ }
+ }
+
+ var disableButtons: Bool {
+ if case .waitingForConnectivity(.noNetwork) = tunnelState {
+ return true
+ }
+
+ return false
+ }
+
+ var localizedTitleForSecureLabel: LocalizedStringKey {
+ switch tunnelState {
+ case .connecting, .reconnecting, .negotiatingEphemeralPeer:
+ LocalizedStringKey("Connecting")
+ case .connected:
+ LocalizedStringKey("Connected")
+ case .disconnecting(.nothing):
+ LocalizedStringKey("Disconnecting")
+ case .disconnecting(.reconnect), .pendingReconnect:
+ LocalizedStringKey("Reconnecting")
+ case .disconnected:
+ LocalizedStringKey("Disconnected")
+ case .waitingForConnectivity(.noConnection), .error:
+ LocalizedStringKey("Blocked connection")
+ case .waitingForConnectivity(.noNetwork):
+ LocalizedStringKey("No network")
+ }
+ }
+
+ var localizedTitleForSelectLocationButton: LocalizedStringKey {
+ switch tunnelState {
+ case .disconnecting, .pendingReconnect, .disconnected:
+ LocalizedStringKey("Select location")
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
+ LocalizedStringKey("Switch location")
+ }
+ }
+
+ var localizedAccessibilityLabel: LocalizedStringKey {
+ switch tunnelState {
+ case .disconnected, .waitingForConnectivity, .disconnecting, .pendingReconnect, .error:
+ localizedTitleForSecureLabel
+ case let .connected(tunnelInfo, _, _):
+ LocalizedStringKey("Connected to \(tunnelInfo.exit.location.city), \(tunnelInfo.exit.location.country)")
+ case let .connecting(tunnelInfo, _, _):
+ if let tunnelInfo {
+ LocalizedStringKey(
+ "Connecting to \(tunnelInfo.exit.location.city), \(tunnelInfo.exit.location.country)"
+ )
+ } else {
+ localizedTitleForSecureLabel
+ }
+ case let .reconnecting(tunnelInfo, _, _), let .negotiatingEphemeralPeer(tunnelInfo, _, _, _):
+ LocalizedStringKey("Reconnecting to \(tunnelInfo.exit.location.city), \(tunnelInfo.exit.location.country)")
+ }
+ }
+
+ var actionButton: TunnelControlActionButton {
+ switch tunnelState {
+ case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork):
+ .connect
+ case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection),
+ .negotiatingEphemeralPeer:
+ .cancel
+ case .connected, .reconnecting, .error:
+ .disconnect
+ }
+ }
+
+ var titleForCountryAndCity: LocalizedStringKey? {
+ guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else {
+ return nil
+ }
+
+ return LocalizedStringKey("\(tunnelRelays.exit.location.country), \(tunnelRelays.exit.location.city)")
+ }
+
+ var titleForServer: LocalizedStringKey? {
+ guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else {
+ return nil
+ }
+
+ let exitName = tunnelRelays.exit.hostname
+ let entryName = tunnelRelays.entry?.hostname
+
+ return if let entryName {
+ LocalizedStringKey("\(exitName) via \(entryName)")
+ } else {
+ LocalizedStringKey("\(exitName)")
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
new file mode 100644
index 0000000000..b70c3a9ffa
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
@@ -0,0 +1,198 @@
+//
+// FI_TunnelViewController.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-10.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MapKit
+import MullvadLogging
+import MullvadTypes
+import SwiftUI
+
+// NOTE: This ViewController will replace TunnelViewController once feature indicators work is done.
+
+class FI_TunnelViewController: UIViewController, RootContainment {
+ private let logger = Logger(label: "TunnelViewController")
+ private let interactor: TunnelViewControllerInteractor
+ private var tunnelState: TunnelState = .disconnected
+ private var viewModel = ConnectionViewViewModel(tunnelState: .disconnected)
+ private var connectionView: ConnectionView
+ private var connectionController: UIHostingController<ConnectionView>?
+
+ var shouldShowSelectLocationPicker: (() -> Void)?
+ var shouldShowCancelTunnelAlert: (() -> Void)?
+
+ private let mapViewController = MapViewController()
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ .lightContent
+ }
+
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ switch interactor.deviceState {
+ case .loggedIn, .revoked:
+ return HeaderBarPresentation(
+ style: tunnelState.isSecured ? .secured : .unsecured,
+ showsDivider: false
+ )
+ case .loggedOut:
+ return HeaderBarPresentation(style: .default, showsDivider: true)
+ }
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ false
+ }
+
+ init(interactor: TunnelViewControllerInteractor) {
+ self.interactor = interactor
+ connectionView = ConnectionView(viewModel: self.viewModel)
+
+ super.init(nibName: nil, bundle: nil)
+
+ // When content size is updated in SwiftUI we need to explicitly tell UIKit to
+ // update its view size. This is not necessary on iOS 16 where we can set
+ // hostingController.sizingOptions instead.
+ connectionView.onContentUpdate = { [weak self] in
+ self?.connectionController?.view.setNeedsUpdateConstraints()
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ interactor.didUpdateDeviceState = { [weak self] _, _ in
+ self?.setNeedsHeaderBarStyleAppearanceUpdate()
+ }
+
+ interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in
+ self?.setTunnelState(tunnelStatus.state, animated: true)
+ self?.viewModel.tunnelState = tunnelStatus.state
+ self?.view.setNeedsLayout()
+ }
+
+ connectionView.action = { [weak self] action in
+ switch action {
+ case .connect:
+ self?.interactor.startTunnel()
+
+ case .cancel:
+ if case .waitingForConnectivity(.noConnection) = self?.interactor.tunnelStatus.state {
+ self?.shouldShowCancelTunnelAlert?()
+ } else {
+ self?.interactor.stopTunnel()
+ }
+
+ case .disconnect:
+ self?.interactor.stopTunnel()
+
+ case .reconnect:
+ self?.interactor.reconnectTunnel(selectNewRelay: true)
+
+ case .selectLocation:
+ self?.shouldShowSelectLocationPicker?()
+ }
+ }
+
+ addMapController()
+ addContentView()
+
+ tunnelState = interactor.tunnelStatus.state
+ viewModel.tunnelState = tunnelState
+
+ updateMap(animated: false)
+ }
+
+ func setMainContentHidden(_ isHidden: Bool, animated: Bool) {
+ let actions = {
+ _ = self.connectionView.opacity(isHidden ? 0 : 1)
+ }
+
+ if animated {
+ UIView.animate(withDuration: 0.25, animations: actions)
+ } else {
+ actions()
+ }
+ }
+
+ // MARK: - Private
+
+ private func setTunnelState(_ tunnelState: TunnelState, animated: Bool) {
+ self.tunnelState = tunnelState
+ setNeedsHeaderBarStyleAppearanceUpdate()
+
+ guard isViewLoaded else { return }
+
+ updateMap(animated: animated)
+ }
+
+ private func updateMap(animated: Bool) {
+ switch tunnelState {
+ case let .connecting(tunnelRelays, _, _):
+ mapViewController.removeLocationMarker()
+ mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated)
+ viewModel.showsActivityIndicator = true
+
+ case let .reconnecting(tunnelRelays, _, _), let .negotiatingEphemeralPeer(tunnelRelays, _, _, _):
+ mapViewController.removeLocationMarker()
+ mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated)
+ viewModel.showsActivityIndicator = true
+
+ case let .connected(tunnelRelays, _, _):
+ let center = tunnelRelays.exit.location.geoCoordinate
+ mapViewController.setCenter(center, animated: animated) {
+ self.viewModel.showsActivityIndicator = false
+
+ // Connection can change during animation, so make sure we're still connected before adding marker.
+ if case .connected = self.tunnelState {
+ self.mapViewController.addLocationMarker(coordinate: center)
+ }
+ }
+
+ case .pendingReconnect:
+ mapViewController.removeLocationMarker()
+ viewModel.showsActivityIndicator = true
+
+ case .waitingForConnectivity, .error:
+ mapViewController.removeLocationMarker()
+ viewModel.showsActivityIndicator = false
+
+ case .disconnected, .disconnecting:
+ mapViewController.removeLocationMarker()
+ mapViewController.setCenter(nil, animated: animated)
+ viewModel.showsActivityIndicator = false
+ }
+ }
+
+ private func addMapController() {
+ let mapView = mapViewController.view!
+
+ addChild(mapViewController)
+ mapViewController.didMove(toParent: self)
+
+ view.addConstrainedSubviews([mapView]) {
+ mapView.pinEdgesToSuperview()
+ }
+ }
+
+ private func addContentView() {
+ let connectionController = UIHostingController(rootView: connectionView)
+ self.connectionController = connectionController
+
+ let connectionViewProxy = connectionController.view!
+ connectionViewProxy.backgroundColor = .clear
+
+ addChild(connectionController)
+ connectionController.didMove(toParent: self)
+
+ view.addConstrainedSubviews([connectionViewProxy]) {
+ connectionViewProxy.pinEdgesToSuperview(.all().excluding(.top))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 5c9d6970f5..88c933493b 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
@@ -20,13 +20,6 @@ enum TunnelControlAction {
case selectLocation
}
-private enum TunnelControlActionButton {
- case connect
- case disconnect
- case cancel
- case selectLocation
-}
-
final class TunnelControlView: UIView {
private let secureLabel = makeBoldTextLabel(ofSize: 20, numberOfLines: 0)
private let cityLabel = makeBoldTextLabel(ofSize: 34)
@@ -158,12 +151,11 @@ final class TunnelControlView: UIView {
}
private func updateActionButtons(tunnelState: TunnelState) {
- let actionButtons = tunnelState.actionButtons(traitCollection: traitCollection)
- let views = actionButtons.map { self.view(forActionButton: $0) }
+ let view = view(forActionButton: tunnelState.actionButton)
updateButtonTitles(tunnelState: tunnelState)
updateButtonEnabledStates(shouldEnableButtons: tunnelState.shouldEnableButtons)
- setArrangedButtons(views)
+ setArrangedButtons([selectLocationButtonBlurView, view])
}
private func updateSecureLabel(tunnelState: TunnelState) {
@@ -351,7 +343,7 @@ final class TunnelControlView: UIView {
}
}
- private func view(forActionButton actionButton: TunnelControlActionButton) -> UIView {
+ private func view(forActionButton actionButton: TunnelState.TunnelControlActionButton) -> UIView {
switch actionButton {
case .connect:
return connectButton
@@ -359,8 +351,6 @@ final class TunnelControlView: UIView {
return splitDisconnectButton
case .cancel:
return cancelButtonBlurView
- case .selectLocation:
- return selectLocationButtonBlurView
}
}
@@ -406,24 +396,3 @@ final class TunnelControlView: UIView {
actionHandler?(.selectLocation)
}
}
-
-private extension TunnelState {
- func actionButtons(traitCollection: UITraitCollection) -> [TunnelControlActionButton] {
- switch self {
- case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork):
- [.selectLocation, .connect]
-
- case .connecting, .pendingReconnect, .disconnecting(.reconnect),
- .waitingForConnectivity(.noConnection):
- [.selectLocation, .cancel]
-
- case .negotiatingEphemeralPeer:
- [.selectLocation, .cancel]
-
- case .connected, .reconnecting, .error:
- [.selectLocation, .disconnect]
- }
- }
-
- // swiftlint:disable:next file_length
-}
diff --git a/ios/MullvadVPN/Views/BlurView.swift b/ios/MullvadVPN/Views/BlurView.swift
new file mode 100644
index 0000000000..eb976efb74
--- /dev/null
+++ b/ios/MullvadVPN/Views/BlurView.swift
@@ -0,0 +1,22 @@
+//
+// BlurView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+/// Blurred (background) view using a `UIBlurEffect`.
+struct BlurView: View {
+ var style: UIBlurEffect.Style
+
+ var body: some View {
+ Spacer()
+ .overlay {
+ VisualEffectView(effect: UIBlurEffect(style: style))
+ .opacity(0.8)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift
index a5d6f0f718..679b34a2cd 100644
--- a/ios/MullvadVPN/Views/MainButton.swift
+++ b/ios/MullvadVPN/Views/MainButton.swift
@@ -9,8 +9,9 @@
import SwiftUI
struct MainButton: View {
- var text: String
+ var text: LocalizedStringKey
var style: MainButtonStyle.Style
+ var disabled = false
var action: () -> Void
@@ -22,13 +23,13 @@ struct MainButton: View {
Spacer()
}
})
- .buttonStyle(MainButtonStyle(style))
+ .buttonStyle(MainButtonStyle(style, disabled: disabled))
.cornerRadius(UIMetrics.MainButton.cornerRadius)
}
}
#Preview {
- MainButton(text: "Connect", style: .default) {
+ MainButton(text: "Connect", style: .success) {
print("Tapped")
}
}
diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift
index 06a32b5606..f638c87ac2 100644
--- a/ios/MullvadVPN/Views/MainButtonStyle.swift
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -9,10 +9,12 @@
import SwiftUI
struct MainButtonStyle: ButtonStyle {
- @State var style: Style
+ var style: Style
+ @State var disabled: Bool
- init(_ style: Style) {
+ init(_ style: Style, disabled: Bool = false) {
self.style = style
+ self.disabled = disabled
}
func makeBody(configuration: Configuration) -> some View {
@@ -22,9 +24,15 @@ struct MainButtonStyle: ButtonStyle {
.foregroundColor(
configuration.isPressed
? UIColor.secondaryTextColor.color
- : UIColor.primaryTextColor.color
+ : disabled
+ ? UIColor.primaryTextColor.withAlphaComponent(0.2).color
+ : UIColor.primaryTextColor.color
+ )
+ .background(
+ disabled
+ ? style.color.darkened(by: 0.6)
+ : style.color
)
- .background(style.color)
.font(.body.weight(.semibold))
}
}
diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift
index 59a5d1b2ea..11336f424b 100644
--- a/ios/MullvadVPN/Views/SplitMainButton.swift
+++ b/ios/MullvadVPN/Views/SplitMainButton.swift
@@ -9,9 +9,10 @@
import SwiftUI
struct SplitMainButton: View {
- var text: String
+ var text: LocalizedStringKey
var image: ImageResource
var style: MainButtonStyle.Style
+ var disabled = false
var primaryAction: () -> Void
var secondaryAction: () -> Void
@@ -37,14 +38,14 @@ struct SplitMainButton: View {
.aspectRatio(1, contentMode: .fit)
.sizeOfView { width = $0.width }
}
- .buttonStyle(MainButtonStyle(style))
+ .buttonStyle(MainButtonStyle(style, disabled: disabled))
.cornerRadius(UIMetrics.MainButton.cornerRadius)
}
}
#Preview {
SplitMainButton(
- text: "Connect",
+ text: "Select location",
image: .iconReload,
style: .default,
primaryAction: {
@@ -54,5 +55,4 @@ struct SplitMainButton: View {
print("Tapped secondary")
}
)
- .frame(maxWidth: .infinity)
}
diff --git a/ios/MullvadVPN/Views/VisualEffectView.swift b/ios/MullvadVPN/Views/VisualEffectView.swift
index 0cad1b06d7..f28b715065 100644
--- a/ios/MullvadVPN/Views/VisualEffectView.swift
+++ b/ios/MullvadVPN/Views/VisualEffectView.swift
@@ -12,7 +12,9 @@ struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
- UIVisualEffectView()
+ let view = UIVisualEffectView(effect: effect)
+ view.translatesAutoresizingMaskIntoConstraints = false
+ return view
}
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {