summaryrefslogtreecommitdiffhomepage
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-11 14:39:54 +0100
commit0526bd6517349cc34fd48241dfe8eb9dd6018d0a (patch)
tree1bb3e157fa73ecacb6f89522fcdb9da2d525d1e6
parent28cb21840c6877de67a74a96b7eaca3f76651d72 (diff)
downloadmullvadvpn-0526bd6517349cc34fd48241dfe8eb9dd6018d0a.tar.xz
mullvadvpn-0526bd6517349cc34fd48241dfe8eb9dd6018d0a.zip
Add foundation for new connection view
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj36
-rw-r--r--ios/MullvadVPN/Extensions/UIColor+Helpers.swift6
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift87
-rw-r--r--ios/MullvadVPN/Views/MainButton.swift34
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift49
-rw-r--r--ios/MullvadVPN/Views/SplitMainButton.swift58
-rw-r--r--ios/MullvadVPN/Views/VisualEffectView.swift21
8 files changed, 290 insertions, 5 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 05a800581d..36ca682853 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -462,6 +462,8 @@
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; };
7A0B311E2B303A0D004B12E0 /* AccessbilityIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0B311D2B303A0D004B12E0 /* AccessbilityIdentifier.swift */; };
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 */; };
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 */; };
@@ -563,7 +565,6 @@
7A8A19052CE4E9A9000BCB5B /* SwitchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19042CE4E9A5000BCB5B /* SwitchRowView.swift */; };
7A8A19072CE4E9D3000BCB5B /* SettingsInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19062CE4E9CC000BCB5B /* SettingsInfoView.swift */; };
7A8A190A2CE5FFE9000BCB5B /* SettingsDAITAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19092CE5FFDF000BCB5B /* SettingsDAITAView.swift */; };
- 7A8A190C2CE618D3000BCB5B /* View+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A190B2CE618CE000BCB5B /* View+Size.swift */; };
7A8A190E2CEB77C1000BCB5B /* SettingsRowViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A190D2CEB77B7000BCB5B /* SettingsRowViewFooter.swift */; };
7A8A19102CEE391B000BCB5B /* RowSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A190F2CEE3918000BCB5B /* RowSeparator.swift */; };
7A8A19122CEF1E68000BCB5B /* SettingsInfoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19112CEF1E58000BCB5B /* SettingsInfoContainerView.swift */; };
@@ -605,6 +606,10 @@
7A9F293D2CAD2FD5005F2089 /* InfoModalConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */; };
7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1412A2E3306000B728D /* CheckboxView.swift */; };
7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */; };
+ 7AA130992CFF365D00640DF9 /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA130982CFF365A00640DF9 /* ConnectionView.swift */; };
+ 7AA1309B2D0048D800640DF9 /* MainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA1309A2D0048D800640DF9 /* MainButton.swift */; };
+ 7AA1309F2D007B2500640DF9 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */; };
+ 7AA130A12D01B1E200640DF9 /* SplitMainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */; };
7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; };
7AA7046A2C8EFE2B0045699D /* StoredRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA704682C8EFE050045699D /* StoredRelays.swift */; };
7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; };
@@ -1834,6 +1839,8 @@
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
7A0B311D2B303A0D004B12E0 /* AccessbilityIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessbilityIdentifier.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@@ -1923,7 +1930,6 @@
7A8A19042CE4E9A5000BCB5B /* SwitchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchRowView.swift; sourceTree = "<group>"; };
7A8A19062CE4E9CC000BCB5B /* SettingsInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInfoView.swift; sourceTree = "<group>"; };
7A8A19092CE5FFDF000BCB5B /* SettingsDAITAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDAITAView.swift; sourceTree = "<group>"; };
- 7A8A190B2CE618CE000BCB5B /* View+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Size.swift"; sourceTree = "<group>"; };
7A8A190D2CEB77B7000BCB5B /* SettingsRowViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowViewFooter.swift; sourceTree = "<group>"; };
7A8A190F2CEE3918000BCB5B /* RowSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowSeparator.swift; sourceTree = "<group>"; };
7A8A19112CEF1E58000BCB5B /* SettingsInfoContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInfoContainerView.swift; sourceTree = "<group>"; };
@@ -1962,6 +1968,10 @@
7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoModalConfig.swift; sourceTree = "<group>"; };
7A9FA1412A2E3306000B728D /* CheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = "<group>"; };
7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckableSettingsCell.swift; sourceTree = "<group>"; };
+ 7AA130982CFF365A00640DF9 /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = "<group>"; };
+ 7AA1309A2D0048D800640DF9 /* MainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButton.swift; sourceTree = "<group>"; };
+ 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; };
+ 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitMainButton.swift; sourceTree = "<group>"; };
7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = "<group>"; };
7AA704682C8EFE050045699D /* StoredRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredRelays.swift; sourceTree = "<group>"; };
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; };
@@ -2982,6 +2992,7 @@
583FE01E29C197D5006E85F9 /* Tunnel */ = {
isa = PBXGroup;
children = (
+ 7AA130972CFF364F00640DF9 /* FeatureIndicators */,
58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */,
5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */,
58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */,
@@ -3012,10 +3023,14 @@
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */,
7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */,
7A5869942B32E9C700640D27 /* LinkButton.swift */,
+ 7AA1309A2D0048D800640DF9 /* MainButton.swift */,
+ 7A0EAE992D01B41500D3EB8B /* MainButtonStyle.swift */,
7A8A190F2CEE3918000BCB5B /* RowSeparator.swift */,
58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */,
+ 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */,
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */,
58EF581025D69DB400AEBA94 /* StatusImageView.swift */,
+ 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -3091,7 +3106,7 @@
7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */,
5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */,
7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */,
- 7A8A190B2CE618CE000BCB5B /* View+Size.swift */,
+ 7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */,
7A8A18FA2CE4B66C000BCB5B /* View+TapAreaSize.swift */,
);
path = Extensions;
@@ -4024,6 +4039,14 @@
path = SelectLocation;
sourceTree = "<group>";
};
+ 7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
+ isa = PBXGroup;
+ children = (
+ 7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
+ );
+ path = FeatureIndicators;
+ sourceTree = "<group>";
+ };
7AD63A422CDA661B00445268 /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -5830,6 +5853,7 @@
7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
7A8A19072CE4E9D3000BCB5B /* SettingsInfoView.swift in Sources */,
+ 7AA1309B2D0048D800640DF9 /* MainButton.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */,
7A8A19052CE4E9A9000BCB5B /* SwitchRowView.swift in Sources */,
@@ -5900,6 +5924,7 @@
586C0D872B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift in Sources */,
7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */,
7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */,
+ 7A0EAE9E2D01BCBF00D3EB8B /* View+Size.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */,
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
@@ -5924,7 +5949,10 @@
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
+ 7A0EAE9A2D01B41500D3EB8B /* MainButtonStyle.swift in Sources */,
58CEB3022AFD365600E6E088 /* SwitchCellContentConfiguration.swift in Sources */,
+ 7AA130A12D01B1E200640DF9 /* SplitMainButton.swift in Sources */,
+ 7AA1309F2D007B2500640DF9 /* VisualEffectView.swift in Sources */,
7A9CCCB52A96302800DD6A34 /* AddCreditSucceededCoordinator.swift in Sources */,
7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */,
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */,
@@ -6038,7 +6066,6 @@
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */,
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
- 7A8A190C2CE618D3000BCB5B /* View+Size.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */,
@@ -6068,6 +6095,7 @@
58EFC7712AFB45E500E9F4CB /* SettingsChildCoordinator.swift in Sources */,
7A8A19102CEE391B000BCB5B /* RowSeparator.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
+ 7AA130992CFF365D00640DF9 /* ConnectionView.swift in Sources */,
F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */,
581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */,
5827B0BB2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift in Sources */,
diff --git a/ios/MullvadVPN/Extensions/UIColor+Helpers.swift b/ios/MullvadVPN/Extensions/UIColor+Helpers.swift
index f3c89ba889..0157058b89 100644
--- a/ios/MullvadVPN/Extensions/UIColor+Helpers.swift
+++ b/ios/MullvadVPN/Extensions/UIColor+Helpers.swift
@@ -6,9 +6,13 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import UIKit
+import SwiftUI
extension UIColor {
+ var color: Color {
+ Color(self)
+ }
+
/// Returns the color lighter by the given percent (in range from 0..1)
func lightened(by percent: CGFloat) -> UIColor? {
darkened(by: -percent)
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index e68d8b0c32..1d0f98e8f0 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -134,6 +134,10 @@ enum UIMetrics {
static let inRowHeight: CGFloat = 22
static let outRowHeight: CGFloat = 44
}
+
+ enum MainButton {
+ static let cornerRadius: CGFloat = 4
+ }
}
extension UIMetrics {
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
new file mode 100644
index 0000000000..3f3d4e473b
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
@@ -0,0 +1,87 @@
+//
+// ConnectionView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+// TODO: Replace all hardcoded values with real values dependent on tunnel state. To be addressed in upcoming PR.
+
+struct ConnectionView: View {
+ var body: some View {
+ ZStack {
+ BlurView()
+
+ VStack(alignment: .leading, spacing: 16) {
+ ConnectionPanel()
+ ButtonPanel()
+ }
+ .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)
+ }
+}
+
+#Preview {
+ ConnectionView()
+ .background(UIColor.secondaryColor.color)
+}
+
+private struct BlurView: View {
+ var body: some View {
+ Spacer()
+ .overlay {
+ VisualEffectView(effect: UIBlurEffect(style: .dark))
+ .opacity(0.8)
+ }
+ }
+}
+
+private struct ConnectionPanel: View {
+ var body: some View {
+ VStack(alignment: .leading) {
+ Text("Connected")
+ .textCase(.uppercase)
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(UIColor.successColor.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))
+ }
+ }
+}
+
+private struct ButtonPanel: View {
+ var body: some View {
+ VStack(spacing: 16) {
+ SplitMainButton(
+ text: "Switch location",
+ image: .iconReload,
+ style: .default,
+ primaryAction: {
+ print("Switch location tapped")
+ }, secondaryAction: {
+ print("Reload tapped")
+ }
+ )
+
+ MainButton(
+ text: "Cancel",
+ style: .danger
+ ) {
+ print("Cancel tapped")
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift
new file mode 100644
index 0000000000..a5d6f0f718
--- /dev/null
+++ b/ios/MullvadVPN/Views/MainButton.swift
@@ -0,0 +1,34 @@
+//
+// MainButton.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-04.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct MainButton: View {
+ var text: String
+ var style: MainButtonStyle.Style
+
+ var action: () -> Void
+
+ var body: some View {
+ Button(action: action, label: {
+ HStack {
+ Spacer()
+ Text(text)
+ Spacer()
+ }
+ })
+ .buttonStyle(MainButtonStyle(style))
+ .cornerRadius(UIMetrics.MainButton.cornerRadius)
+ }
+}
+
+#Preview {
+ MainButton(text: "Connect", style: .default) {
+ print("Tapped")
+ }
+}
diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift
new file mode 100644
index 0000000000..06a32b5606
--- /dev/null
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -0,0 +1,49 @@
+//
+// MainButtonStyle.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct MainButtonStyle: ButtonStyle {
+ @State var style: Style
+
+ init(_ style: Style) {
+ self.style = style
+ }
+
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .padding(.horizontal, 8)
+ .frame(height: 44)
+ .foregroundColor(
+ configuration.isPressed
+ ? UIColor.secondaryTextColor.color
+ : UIColor.primaryTextColor.color
+ )
+ .background(style.color)
+ .font(.body.weight(.semibold))
+ }
+}
+
+extension MainButtonStyle {
+ enum Style {
+ case `default`
+ case danger
+ case success
+
+ var color: Color {
+ switch self {
+ case .default:
+ Color(UIColor.primaryColor)
+ case .danger:
+ Color(UIColor.dangerColor)
+ case .success:
+ Color(UIColor.successColor)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift
new file mode 100644
index 0000000000..59a5d1b2ea
--- /dev/null
+++ b/ios/MullvadVPN/Views/SplitMainButton.swift
@@ -0,0 +1,58 @@
+//
+// SplitMainButton.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct SplitMainButton: View {
+ var text: String
+ var image: ImageResource
+ var style: MainButtonStyle.Style
+
+ var primaryAction: () -> Void
+ var secondaryAction: () -> Void
+
+ @State private var width: CGFloat = 0
+
+ var body: some View {
+ HStack(spacing: 1) {
+ Button(action: primaryAction, label: {
+ HStack {
+ Spacer()
+ Text(text)
+ Spacer()
+ }
+ .padding(.trailing, -width)
+ })
+ Button(action: secondaryAction, label: {
+ Image(image)
+ .resizable()
+ .scaledToFit()
+ .padding(4)
+ })
+ .aspectRatio(1, contentMode: .fit)
+ .sizeOfView { width = $0.width }
+ }
+ .buttonStyle(MainButtonStyle(style))
+ .cornerRadius(UIMetrics.MainButton.cornerRadius)
+ }
+}
+
+#Preview {
+ SplitMainButton(
+ text: "Connect",
+ image: .iconReload,
+ style: .default,
+ primaryAction: {
+ print("Tapped primary")
+ },
+ secondaryAction: {
+ print("Tapped secondary")
+ }
+ )
+ .frame(maxWidth: .infinity)
+}
diff --git a/ios/MullvadVPN/Views/VisualEffectView.swift b/ios/MullvadVPN/Views/VisualEffectView.swift
new file mode 100644
index 0000000000..0cad1b06d7
--- /dev/null
+++ b/ios/MullvadVPN/Views/VisualEffectView.swift
@@ -0,0 +1,21 @@
+//
+// VisualEffectView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-04.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct VisualEffectView: UIViewRepresentable {
+ var effect: UIVisualEffect?
+
+ func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
+ UIVisualEffectView()
+ }
+
+ func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
+ uiView.effect = effect
+ }
+}