diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2025-01-20 13:58:15 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2025-01-20 13:58:15 +0100 |
| commit | 7cc4ba8b2e3359150835e765a097070f3792606d (patch) | |
| tree | cf1730ef72f05ac7c16ea6e83125d7e3ec931130 | |
| parent | 5b3b433809f9fcadba1cc12d616a3edc67592356 (diff) | |
| parent | 1cfba069c0e066a34c98b2c80bc0a91174d3f7ef (diff) | |
| download | mullvadvpn-7cc4ba8b2e3359150835e765a097070f3792606d.tar.xz mullvadvpn-7cc4ba8b2e3359150835e765a097070f3792606d.zip | |
Merge branch 'position-location-marker-on-the-map-correctly-ios-993'
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()) } } |
