summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-01-20 13:58:15 +0100
committerBug Magnet <marco.nikic@mullvad.net>2025-01-20 13:58:15 +0100
commit7cc4ba8b2e3359150835e765a097070f3792606d (patch)
treecf1730ef72f05ac7c16ea6e83125d7e3ec931130
parent5b3b433809f9fcadba1cc12d616a3edc67592356 (diff)
parent1cfba069c0e066a34c98b2c80bc0a91174d3f7ef (diff)
downloadmullvadvpn-7cc4ba8b2e3359150835e765a097070f3792606d.tar.xz
mullvadvpn-7cc4ba8b2e3359150835e765a097070f3792606d.zip
Merge branch 'position-location-marker-on-the-map-correctly-ios-993'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ActivityIndicator.swift48
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift3
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift221
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift45
5 files changed, 41 insertions, 280 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 1a1df0918b..5af100575d 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -663,7 +663,6 @@
7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */; };
7AF9BE952A40461100DBFEDB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */; };
7AF9BE972A41C71F00DBFEDB /* ChipViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */; };
- 7AFBE3872D084C9D002335FC /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */; };
7AFBE3892D089163002335FC /* TunnelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3882D08915D002335FC /* TunnelViewController.swift */; };
7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */; };
7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */; };
@@ -2045,7 +2044,6 @@
7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = "<group>"; };
7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewCell.swift; sourceTree = "<group>"; };
- 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
7AFBE3882D08915D002335FC /* TunnelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewController.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>"; };
@@ -3059,7 +3057,6 @@
isa = PBXGroup;
children = (
4419AA862D28264D001B13C9 /* ConnectionView */,
- 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */,
58C3F4F82964B08300D72515 /* MapViewController.swift */,
F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */,
@@ -5897,7 +5894,6 @@
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 */,
diff --git a/ios/MullvadVPN/View controllers/Tunnel/ActivityIndicator.swift b/ios/MullvadVPN/View controllers/Tunnel/ActivityIndicator.swift
deleted file mode 100644
index 9b42bab8e6..0000000000
--- a/ios/MullvadVPN/View controllers/Tunnel/ActivityIndicator.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// 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/ConnectionView/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
index 6dfee3abed..51a6b99eff 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift
@@ -22,9 +22,6 @@ struct ConnectionView: View {
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
VStack(spacing: 22) {
- CustomProgressView(style: .large)
- .showIf(connectionViewModel.showsActivityIndicator)
-
ZStack {
BlurView(style: .dark)
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
deleted file mode 100644
index bb754a0c25..0000000000
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
+++ /dev/null
@@ -1,221 +0,0 @@
-//
-// FI_TunnelViewController.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 2024-12-10.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-
-import Combine
-import MapKit
-import MullvadLogging
-import MullvadSettings
-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 connectionViewViewModel: ConnectionViewViewModel
- private var indicatorsViewViewModel: FeatureIndicatorsViewModel
- 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
-
- tunnelState = interactor.tunnelStatus.state
- connectionViewViewModel = ConnectionViewViewModel(tunnelStatus: interactor.tunnelStatus)
- indicatorsViewViewModel = FeatureIndicatorsViewModel(
- tunnelSettings: interactor.tunnelSettings,
- ipOverrides: interactor.ipOverrides
- )
-
- connectionView = ConnectionView(
- connectionViewModel: self.connectionViewViewModel,
- indicatorsViewModel: self.indicatorsViewViewModel
- )
-
- 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?.connectionViewViewModel.tunnelStatus = tunnelStatus
- self?.setTunnelState(tunnelStatus.state, animated: true)
- self?.view.setNeedsLayout()
- }
-
- interactor.didGetOutgoingAddress = { [weak self] connectionInfo in
- self?.connectionViewViewModel.outgoingConnectionInfo = connectionInfo
- }
-
- interactor.didUpdateTunnelSettings = { [weak self] tunnelSettings in
- self?.indicatorsViewViewModel.tunnelSettings = tunnelSettings
- }
-
- interactor.didUpdateIpOverrides = { [weak self] overrides in
- self?.indicatorsViewViewModel.ipOverrides = overrides
- }
-
- 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()
- 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)
- connectionViewViewModel.showsActivityIndicator = true
-
- case let .reconnecting(tunnelRelays, _, _), let .negotiatingEphemeralPeer(tunnelRelays, _, _, _):
- mapViewController.removeLocationMarker()
- mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated)
- connectionViewViewModel.showsActivityIndicator = true
-
- case let .connected(tunnelRelays, _, _):
- let center = tunnelRelays.exit.location.geoCoordinate
- mapViewController.setCenter(center, animated: animated) {
- self.connectionViewViewModel.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()
- connectionViewViewModel.showsActivityIndicator = true
-
- case .waitingForConnectivity, .error:
- mapViewController.removeLocationMarker()
- connectionViewViewModel.showsActivityIndicator = false
-
- case .disconnected, .disconnecting:
- mapViewController.removeLocationMarker()
- mapViewController.setCenter(nil, animated: animated)
- connectionViewViewModel.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())
- }
- }
-}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index a486014e72..6eb7369288 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -25,6 +25,15 @@ class TunnelViewController: UIViewController, RootContainment {
var shouldShowSelectLocationPicker: (() -> Void)?
var shouldShowCancelTunnelAlert: (() -> Void)?
+ let activityIndicator: SpinnerActivityIndicatorView = {
+ let activityIndicator = SpinnerActivityIndicatorView(style: .large)
+ activityIndicator.translatesAutoresizingMaskIntoConstraints = false
+ activityIndicator.tintColor = .white
+ activityIndicator.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ activityIndicator.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
+ return activityIndicator
+ }()
+
private let mapViewController = MapViewController()
override var preferredStatusBarStyle: UIStatusBarStyle {
@@ -125,7 +134,8 @@ class TunnelViewController: UIViewController, RootContainment {
}
addMapController()
- addContentView()
+ addConnectionView()
+ addActivityIndicator()
updateMap(animated: false)
}
@@ -159,8 +169,10 @@ class TunnelViewController: UIViewController, RootContainment {
mapViewController.removeLocationMarker()
mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated)
connectionViewViewModel.showsActivityIndicator = true
+ activityIndicator.startAnimating()
case let .reconnecting(tunnelRelays, _, _), let .negotiatingEphemeralPeer(tunnelRelays, _, _, _):
+ activityIndicator.startAnimating()
mapViewController.removeLocationMarker()
mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated)
connectionViewViewModel.showsActivityIndicator = true
@@ -173,18 +185,22 @@ class TunnelViewController: UIViewController, RootContainment {
// 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)
+ self.activityIndicator.stopAnimating()
}
}
case .pendingReconnect:
+ activityIndicator.startAnimating()
mapViewController.removeLocationMarker()
connectionViewViewModel.showsActivityIndicator = true
case .waitingForConnectivity, .error:
+ activityIndicator.stopAnimating()
mapViewController.removeLocationMarker()
connectionViewViewModel.showsActivityIndicator = false
case .disconnected, .disconnecting:
+ activityIndicator.stopAnimating()
mapViewController.removeLocationMarker()
mapViewController.setCenter(nil, animated: animated)
connectionViewViewModel.showsActivityIndicator = false
@@ -195,6 +211,7 @@ class TunnelViewController: UIViewController, RootContainment {
let mapView = mapViewController.view!
addChild(mapViewController)
+ mapViewController.alignmentView = activityIndicator
mapViewController.didMove(toParent: self)
view.addConstrainedSubviews([mapView]) {
@@ -202,7 +219,28 @@ class TunnelViewController: UIViewController, RootContainment {
}
}
- private func addContentView() {
+ /// Computes a constraint multiplier based on the screen size
+ private func computeHeightBreakpointMultiplier() -> CGFloat {
+ let screenBounds = UIWindow().screen.coordinateSpace.bounds
+ return screenBounds.height < 700 ? 2.0 : 1.5
+ }
+
+ private func addActivityIndicator() {
+ // If the device doesn't have a lot of vertical screen estate, center the progress view higher on the map
+ // so the connection view details do not shadow it unless fully expanded if possible
+ let heightConstraintMultiplier = computeHeightBreakpointMultiplier()
+
+ let verticalCenteredAnchor = activityIndicator.centerYAnchor.anchorWithOffset(to: view.centerYAnchor)
+ view.addConstrainedSubviews([activityIndicator]) {
+ activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor)
+ verticalCenteredAnchor.constraint(
+ equalTo: activityIndicator.heightAnchor,
+ multiplier: heightConstraintMultiplier
+ )
+ }
+ }
+
+ private func addConnectionView() {
let connectionController = UIHostingController(rootView: connectionView)
self.connectionController = connectionController
@@ -211,8 +249,7 @@ class TunnelViewController: UIViewController, RootContainment {
addChild(connectionController)
connectionController.didMove(toParent: self)
-
- view.addConstrainedSubviews([connectionViewProxy]) {
+ view.addConstrainedSubviews([activityIndicator, connectionViewProxy]) {
connectionViewProxy.pinEdgesToSuperview(.all())
}
}