From d81b0a7d13a0a271fb6e4b0130c6ad8aacfa4a9a Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 7 Jan 2025 09:48:29 +0100 Subject: [PATCH] Fix multiple small issues in ConnectionView --- ios/MullvadVPN.xcodeproj/project.pbxproj | 22 ++---- .../Extensions/View+Conditionals.swift | 41 ++++++++++ .../Extensions/View+TapAreaSize.swift | 1 - .../ConnectionView/ButtonPanel.swift | 4 +- .../ChipView/ChipContainerView.swift | 14 ++-- .../ChipView/ChipFeature.swift | 4 + .../ChipView/ChipModel.swift | 0 .../ChipView/ChipView.swift | 2 +- .../ChipView/ChipViewModelProtocol.swift | 30 ++++---- .../ConnectionView/ConnectionView.swift | 60 ++++++++++----- .../ConnectionViewComponentPreview.swift | 19 +++-- .../ConnectionViewViewModel.swift | 9 ++- .../ConnectionView/DetailsContainer.swift | 46 +++++------ .../ConnectionView/DetailsView.swift | 4 +- .../ConnectionView/HeaderView.swift | 12 +-- .../Preview/ConnectionViewPreview.swift | 77 ------------------- .../FI_TunnelViewController.swift | 2 +- .../FeatureIndicatorsView.swift | 12 ++- .../Tunnel/TunnelViewController.swift | 2 +- .../TunnelViewControllerInteractor.swift | 4 +- ios/MullvadVPN/Views/MainButtonStyle.swift | 4 +- ios/MullvadVPN/Views/SplitMainButton.swift | 5 ++ 22 files changed, 172 insertions(+), 202 deletions(-) create mode 100644 ios/MullvadVPN/Extensions/View+Conditionals.swift rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/{ => ConnectionView}/ChipView/ChipContainerView.swift (87%) rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/{ => ConnectionView}/ChipView/ChipFeature.swift (93%) rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/{ => ConnectionView}/ChipView/ChipModel.swift (100%) rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/{ => ConnectionView}/ChipView/ChipView.swift (96%) rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/{ => ConnectionView}/ChipView/ChipViewModelProtocol.swift (67%) rename ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/{Preview => }/ConnectionViewComponentPreview.swift (86%) delete mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 9ba658df15a5..e1cda675d735 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -621,6 +621,7 @@ 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 */; }; + 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */; }; 7AA7046A2C8EFE2B0045699D /* StoredRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA704682C8EFE050045699D /* StoredRelays.swift */; }; 7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; }; 7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; }; @@ -661,7 +662,6 @@ 7AF36A9A2CA2964200E1D497 /* AnyIPAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */; }; 7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; }; 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */; }; - 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */; }; 7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */; }; 7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */; }; 7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */; }; @@ -2008,6 +2008,7 @@ 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitMainButton.swift; sourceTree = ""; }; 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = ""; }; + 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditionals.swift"; sourceTree = ""; }; 7AA704682C8EFE050045699D /* StoredRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredRelays.swift; sourceTree = ""; }; 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = ""; }; 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = ""; }; @@ -2041,7 +2042,6 @@ 7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddressTests.swift; sourceTree = ""; }; 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = ""; }; 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedRelaysStub+Stubs.swift"; sourceTree = ""; }; - 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewPreview.swift; sourceTree = ""; }; 7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilter.swift; sourceTree = ""; }; 7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewModel.swift; sourceTree = ""; }; 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = ""; }; @@ -2707,13 +2707,14 @@ 4419AA862D28264D001B13C9 /* ConnectionView */ = { isa = PBXGroup; children = ( - 449E9A6B2D2839FD00F8574A /* Preview */, + F0ADF1CF2D01B50B00299F09 /* ChipView */, + 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */, 7AA130982CFF365A00640DF9 /* ConnectionView.swift */, + 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */, 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */, 4419AA882D282687001B13C9 /* DetailsContainer.swift */, 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */, 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */, - 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */, ); path = ConnectionView; sourceTree = ""; @@ -2758,15 +2759,6 @@ path = MullvadSettings; sourceTree = ""; }; - 449E9A6B2D2839FD00F8574A /* Preview */ = { - isa = PBXGroup; - children = ( - 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */, - 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */, - ); - path = Preview; - sourceTree = ""; - }; 449EBA242B975B7C00DFA4EB /* Protocols */ = { isa = PBXGroup; children = ( @@ -3182,6 +3174,7 @@ 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */, 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */, 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */, + 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */, 7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */, 7A8A18FA2CE4B66C000BCB5B /* View+TapAreaSize.swift */, ); @@ -4120,7 +4113,6 @@ isa = PBXGroup; children = ( 4419AA862D28264D001B13C9 /* ConnectionView */, - F0ADF1CF2D01B50B00299F09 /* ChipView */, 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */, F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */, F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */, @@ -5958,6 +5950,7 @@ F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */, + 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */, 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */, 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */, F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */, @@ -5977,7 +5970,6 @@ 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */, - 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */, 58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */, 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */, diff --git a/ios/MullvadVPN/Extensions/View+Conditionals.swift b/ios/MullvadVPN/Extensions/View+Conditionals.swift new file mode 100644 index 000000000000..39e4405da8e6 --- /dev/null +++ b/ios/MullvadVPN/Extensions/View+Conditionals.swift @@ -0,0 +1,41 @@ +// +// View+Conditionals.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-01-07. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder func `if`( + _ conditional: Bool, + @ViewBuilder _ content: (Self) -> Content + ) -> some View { + if conditional { + content(self) + } else { + self + } + } + + @ViewBuilder func ifLet( + _ conditional: T?, + @ViewBuilder _ content: (Self, _ value: T) -> Content + ) -> some View { + if let value = conditional { + content(self, value) + } else { + self + } + } + + @ViewBuilder func showIf(_ conditional: Bool) -> some View { + if conditional { + self + } else { + EmptyView() + } + } +} diff --git a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift index bf81cd57be35..c9ab7ee14422 100644 --- a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift +++ b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift @@ -28,6 +28,5 @@ private struct TappablePadding: ViewModifier { height: max(actualViewSize.height, tappableViewSize.height) ) .contentShape(Rectangle()) - .frame(width: actualViewSize.width, height: actualViewSize.height) } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift index bfbe4370418d..515912004628 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift @@ -12,7 +12,7 @@ extension ConnectionView { internal struct ButtonPanel: View { typealias Action = (ConnectionViewViewModel.TunnelAction) -> Void - @StateObject var viewModel: ConnectionViewViewModel + @ObservedObject var viewModel: ConnectionViewViewModel var action: Action? var body: some View { @@ -32,10 +32,10 @@ extension ConnectionView { text: viewModel.localizedTitleForSelectLocationButton, image: .iconReload, style: .default, + accessibilityId: .selectLocationButton, primaryAction: { action?(.selectLocation) }, secondaryAction: { action?(.reconnect) } ) - .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString) case .disconnecting, .pendingReconnect, .disconnected: MainButton( text: viewModel.localizedTitleForSelectLocationButton, diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift similarity index 87% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift index 333761fa30ab..0d417542f35c 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift @@ -30,15 +30,13 @@ struct ChipContainerView: View where ViewModel: ChipViewModelProtocol createChipViews(chips: chipsToAdd, containerWidth: containerWidth) } - if showMoreButton { - Text(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) - .font(.subheadline) - .lineLimit(1) - .foregroundStyle(UIColor.primaryTextColor.color) - .onTapGesture { - isExpanded.toggle() - } + Button(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) { + isExpanded.toggle() } + .font(.subheadline) + .lineLimit(1) + .foregroundStyle(UIColor.primaryTextColor.color) + .showIf(showMoreButton) Spacer() } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift similarity index 93% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift index ea1d130dbede..a661a2cefc23 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift @@ -8,6 +8,10 @@ import MullvadSettings import SwiftUI +// Opting to use NSLocalizedString instead of LocalizedStringKey here in order +// to be able to fetch the string value at a later point (eg. in ChipViewModelProtocol, +// when calculating the text widths of the chips). + protocol ChipFeature { var isEnabled: Bool { get } var name: String { get } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipModel.swift similarity index 100% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipModel.swift diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift similarity index 96% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift index 1c6a5eb52267..57fc7cb042d3 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift @@ -11,7 +11,7 @@ import SwiftUI struct ChipView: View { let item: ChipModel var body: some View { - Text(LocalizedStringKey(item.name)) + Text(item.name) .font(.subheadline) .lineLimit(1) .foregroundStyle(UIColor.primaryTextColor.color) diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift similarity index 67% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift index fabec7f8c9e9..3a4c9da337da 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift @@ -13,12 +13,19 @@ protocol ChipViewModelProtocol: ObservableObject { } extension ChipViewModelProtocol { - func chipsToAdd(forContainerWidth containerWidth: CGFloat) -> (chips: [ChipModel], chipsWillOverflow: Bool) { + func chipsToAdd(forContainerWidth containerWidth: CGFloat) -> (chips: [ChipModel], isOverflowing: Bool) { var chipsToAdd = [ChipModel]() - var chipsWillOverflow = false + var isOverflowing = false - let moreTextWidth = "\(chips.count) more..." - .width(using: .preferredFont(forTextStyle: .subheadline)) + 4 // Some extra to be safe. + let moreTextWidth = String( + format: NSLocalizedString( + "CONNECTION_VIEW_CHIPS_MORE", + tableName: "ConnectionView", + value: "@d more...", + comment: "" + ), arguments: [chips.count] + ) + .width(using: .preferredFont(forTextStyle: .subheadline)) + 4 // Some extra to be safe. var totalChipsWidth: CGFloat = 0 for (index, chip) in chips.enumerated() { @@ -33,20 +40,15 @@ extension ChipViewModelProtocol { let chipWillFitWithMoreText = (totalChipsWidth + moreTextWidth) <= containerWidth let chipWillFit = totalChipsWidth <= containerWidth - if chipWillFitWithMoreText { - // If a chip can fit together with the "more" text, add it. - chipsToAdd.append(chip) - chipsWillOverflow = !isLastChip - } else if chipWillFit && isLastChip { - // If a chip can fit and it's the last one, add it. - chipsToAdd.append(chip) - chipsWillOverflow = false - } else { + guard (chipWillFit && isLastChip) || chipWillFitWithMoreText else { + isOverflowing = true break } + + chipsToAdd.append(chip) } - return (chipsToAdd, chipsWillOverflow) + return (chipsToAdd, isOverflowing) } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift index 929e0b17a3f2..6dfee3abed08 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift @@ -9,8 +9,8 @@ import SwiftUI struct ConnectionView: View { - @StateObject var connectionViewModel: ConnectionViewViewModel - @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel + @ObservedObject var connectionViewModel: ConnectionViewViewModel + @ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel @State private(set) var isExpanded = false @@ -19,53 +19,71 @@ struct ConnectionView: View { var body: some View { Spacer() + .accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString) + VStack(spacing: 22) { - if connectionViewModel.showsActivityIndicator { - CustomProgressView(style: .large) - } + CustomProgressView(style: .large) + .showIf(connectionViewModel.showsActivityIndicator) ZStack { BlurView(style: .dark) - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 0) { HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded) + .padding(.bottom, headerViewBottomPadding) - if connectionViewModel.showConnectionDetails { - DetailsContainer( - viewModel: connectionViewModel, - indicatorsViewModel: indicatorsViewModel, - isExpanded: $isExpanded - ) - } + DetailsContainer( + connectionViewModel: connectionViewModel, + indicatorsViewModel: indicatorsViewModel, + isExpanded: $isExpanded + ) + .showIf(connectionViewModel.showConnectionDetails) ButtonPanel(viewModel: connectionViewModel, action: action) + .padding(.top, 16) } .padding(16) } .cornerRadius(12) .padding(16) } - .padding(.bottom, 8) // Adding some spacing so as not to overlap with the map legal link. - .accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString) + .padding(.bottom, 8) // Some spacing to avoid overlap with the map legal link. .onChange(of: isExpanded) { _ in onContentUpdate?() } .onReceive(connectionViewModel.combinedState) { _, _ in - onContentUpdate?() - + // Only update expanded state when connections details should be hidden. + // This will contract the view on eg. disconnect, but leave it as-is on + // eg. connect. if !connectionViewModel.showConnectionDetails { -// withAnimation { isExpanded = false -// } + return } + + onContentUpdate?() } } } +extension ConnectionView { + var headerViewBottomPadding: CGFloat { + let hasIndicators = !indicatorsViewModel.chips.isEmpty + let showConnectionDetails = connectionViewModel.showConnectionDetails + + return isExpanded + ? 16 + : hasIndicators && showConnectionDetails ? 16 : 0 + } +} + #Preview("ConnectionView (Indicators)") { - ConnectionViewPreview(configuration: .normal).make() + ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, _ in + ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel) + } } #Preview("ConnectionView (No indicators)") { - ConnectionViewPreview(configuration: .normalNoIndicators).make() + ConnectionViewComponentPreview(showIndicators: false, isExpanded: true) { indicatorModel, viewModel, _ in + ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel) + } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift similarity index 86% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift index 89b3fb0aaf17..cc24537f13d7 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewComponentPreview.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift @@ -55,15 +55,14 @@ struct ConnectionViewComponentPreview: View { } var body: some View { - VStack { - content( - FeatureIndicatorsViewModel( - tunnelSettings: tunnelSettings, - ipOverrides: [] - ), - viewModel, - $isExpanded - ) - }.background(UIColor.secondaryColor.color) + content( + FeatureIndicatorsViewModel( + tunnelSettings: tunnelSettings, + ipOverrides: [] + ), + viewModel, + $isExpanded + ) + .background(UIColor.secondaryColor.color) } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift index 2f7af8a3b572..962eaa0d63fd 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift @@ -52,9 +52,9 @@ extension ConnectionViewViewModel { var showConnectionDetails: Bool { switch tunnelStatus.state { case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer, - .connected, .pendingReconnect, .waitingForConnectivity(.noNetwork): + .connected, .pendingReconnect: true - case .disconnecting, .disconnected, .error: + case .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error: false } } @@ -130,9 +130,10 @@ extension ConnectionViewViewModel { var localizedTitleForSelectLocationButton: LocalizedStringKey { switch tunnelStatus.state { - case .disconnecting, .pendingReconnect, .disconnected: + case .disconnecting, .pendingReconnect, .disconnected, .waitingForConnectivity(.noNetwork): LocalizedStringKey("Select location") - case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error: + case .connecting, .connected, .reconnecting, .waitingForConnectivity(.noConnection), + .negotiatingEphemeralPeer, .error: LocalizedStringKey("Switch location") } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift index 7da9aef5b8f6..6b2bb00399a1 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift @@ -1,5 +1,5 @@ // -// ConnectionDetailsContainer.swift +// DetailsContainer.swift // MullvadVPN // // Created by Andrew Bulhak on 2025-01-03. @@ -10,42 +10,38 @@ import SwiftUI extension ConnectionView { internal struct DetailsContainer: View { - @StateObject var viewModel: ConnectionViewViewModel - @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel + @ObservedObject var connectionViewModel: ConnectionViewViewModel + @ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel @Binding var isExpanded: Bool @State private var scrollViewHeight: CGFloat = 0 var body: some View { -// if isExpanded { - Divider() - .background(UIColor.secondaryTextColor.color) - .opacity(isExpanded ? 1.0 : 0.0) -// } + VStack(spacing: 16) { + Divider() + .background(UIColor.secondaryTextColor.color) + .showIf(isExpanded) - // This geometry reader is somewhat of a workaround. It's "smart" in that it takes up as much - // space as it can and thereby helps the view to understand the maximum allowed height when - // placed in a UIKit context. If ConnectionView would ever be placed as a subview of SwiftUI - // parent, this reader could probably be removed. - GeometryReader { _ in ScrollView { VStack(spacing: 16) { - if !indicatorsViewModel.chips.isEmpty { - FeatureIndicatorsView( - viewModel: indicatorsViewModel, - isExpanded: $isExpanded - ) - } + FeatureIndicatorsView( + viewModel: indicatorsViewModel, + isExpanded: $isExpanded + ) + .showIf(!indicatorsViewModel.chips.isEmpty) - if isExpanded { - DetailsView(viewModel: viewModel) - .transition(.move(edge: .bottom)) - } + DetailsView(viewModel: connectionViewModel) + .showIf(isExpanded) } .sizeOfView { scrollViewHeight = $0.height } } + .frame(maxHeight: scrollViewHeight) + .onTapGesture { + // If this callback is not set the child views will not reliably register tap events. + // This is a bug in iOS 16 and 17, but seemingly fixed in 18. Once we set the lowest + // supported version to iOS 18 we can probably remove it. + } } - .frame(maxHeight: scrollViewHeight) } } } @@ -53,7 +49,7 @@ extension ConnectionView { #Preview { ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, isExpanded in ConnectionView.DetailsContainer( - viewModel: viewModel, + connectionViewModel: viewModel, indicatorsViewModel: indicatorModel, isExpanded: isExpanded ) diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift index 25559757a1ce..ff07dc94b582 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift @@ -1,5 +1,5 @@ // -// ConnectionDetails.swift +// DetailsView.swift // MullvadVPN // // Created by Andrew Bulhak on 2025-01-03. @@ -10,7 +10,7 @@ import SwiftUI extension ConnectionView { internal struct DetailsView: View { - @StateObject var viewModel: ConnectionViewViewModel + @ObservedObject var viewModel: ConnectionViewViewModel @State private var columnWidth: CGFloat = 0 var body: some View { diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift index 5ec0eb9316cb..fa1112a80e1c 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift @@ -10,7 +10,7 @@ import SwiftUI extension ConnectionView { internal struct HeaderView: View { - @StateObject var viewModel: ConnectionViewViewModel + @ObservedObject var viewModel: ConnectionViewViewModel @Binding var isExpanded: Bool var body: some View { @@ -38,23 +38,19 @@ extension ConnectionView { } .accessibilityLabel(viewModel.localizedAccessibilityLabelForSecureLabel) - if viewModel.showConnectionDetails { + Group { Spacer() Image(.iconChevron) .renderingMode(.template) .rotationEffect(isExpanded ? .degrees(-90) : .degrees(90)) .foregroundStyle(.white) - .transaction { transaction in - transaction.animation = nil - } + .accessibilityIdentifier(AccessibilityIdentifier.relayStatusCollapseButton.asString) } + .showIf(viewModel.showConnectionDetails) } - .accessibilityIdentifier(AccessibilityIdentifier.relayStatusCollapseButton.asString) .contentShape(Rectangle()) .onTapGesture { -// withAnimation { isExpanded.toggle() -// } } } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift deleted file mode 100644 index 4f2fd49a924a..000000000000 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/Preview/ConnectionViewPreview.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ConnectionViewPreview.swift -// MullvadVPN -// -// Created by Jon Petersson on 2024-12-18. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import MullvadMockData -import MullvadSettings -import MullvadTypes -import PacketTunnelCore -import SwiftUI - -struct ConnectionViewPreview { - enum Configuration { - case normal, normalNoIndicators - } - - private let configuration: Configuration - - private let populatedTunnelSettings = LatestTunnelSettings( - wireGuardObfuscation: WireGuardObfuscationSettings(state: .udpOverTcp), - tunnelQuantumResistance: .on, - tunnelMultihopState: .on, - daita: DAITASettings(daitaState: .on) - ) - - private let viewModel = ConnectionViewViewModel( - tunnelStatus: TunnelStatus( - observedState: .connected(ObservedConnectionState( - selectedRelays: SelectedRelaysStub.selectedRelays, - relayConstraints: RelayConstraints(entryLocations: .any, exitLocations: .any, port: .any, filter: .any), - networkReachability: .reachable, - connectionAttemptCount: 0, - transportLayer: .udp, - remotePort: 80, - isPostQuantum: true, - isDaitaEnabled: true - )), - state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true) - ) - ) - - init(configuration: Configuration) { - self.configuration = configuration - } - - @ViewBuilder - func make() -> some View { - VStack { - switch configuration { - case .normal: - connectionView(with: populatedTunnelSettings, viewModel: viewModel) - case .normalNoIndicators: - connectionView(with: LatestTunnelSettings(), viewModel: viewModel) - } - } - .background(UIColor.secondaryColor.color) - } - - @ViewBuilder - private func connectionView( - with settings: LatestTunnelSettings, - viewModel: ConnectionViewViewModel, - isExpanded: Bool = false - ) -> some View { - ConnectionView( - connectionViewModel: viewModel, - indicatorsViewModel: FeatureIndicatorsViewModel( - tunnelSettings: settings, - ipOverrides: [] - ), - isExpanded: isExpanded - ) - } -} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift index 8d3f4c78d527..bb754a0c25fa 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift @@ -91,7 +91,7 @@ class FI_TunnelViewController: UIViewController, RootContainment { self?.view.setNeedsLayout() } - interactor.didGetOutGoingAddress = { [weak self] connectionInfo in + interactor.didGetOutgoingAddress = { [weak self] connectionInfo in self?.connectionViewViewModel.outgoingConnectionInfo = connectionInfo } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift index b1a369f99eb0..70e49a9c0493 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift @@ -13,13 +13,11 @@ struct FeatureIndicatorsView: View where ViewModel: ChipViewModelProt @Binding var isExpanded: Bool var body: some View { - VStack(alignment: .leading, spacing: 0) { - if isExpanded { - Text(LocalizedStringKey("Active features")) - .font(.footnote.weight(.semibold)) - .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) - .padding(.bottom, 8) - } + VStack(alignment: .leading, spacing: 8) { + Text(LocalizedStringKey("Active features")) + .font(.footnote.weight(.semibold)) + .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + .showIf(isExpanded) ChipContainerView(viewModel: viewModel, isExpanded: $isExpanded) } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 7cf879f3bf70..78bd6c27b0e9 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -65,7 +65,7 @@ class TunnelViewController: UIViewController, RootContainment { self?.updateViewModel(tunnelStatus: tunnelStatus) } - interactor.didGetOutGoingAddress = { [weak self] outgoingConnectionInfo in + interactor.didGetOutgoingAddress = { [weak self] outgoingConnectionInfo in self?.updateViewModel(outgoingConnectionInfo: outgoingConnectionInfo) } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift index ef902e637a44..e072ec283e3b 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift @@ -22,7 +22,7 @@ final class TunnelViewControllerInteractor { var didUpdateDeviceState: ((_ deviceState: DeviceState, _ previousDeviceState: DeviceState) -> Void)? var didUpdateTunnelSettings: ((LatestTunnelSettings) -> Void)? var didUpdateIpOverrides: (([IPOverride]) -> Void)? - var didGetOutGoingAddress: (@MainActor (OutgoingConnectionInfo) -> Void)? + var didGetOutgoingAddress: (@MainActor (OutgoingConnectionInfo) -> Void)? var tunnelStatus: TunnelStatus { tunnelManager.tunnelStatus @@ -64,7 +64,7 @@ final class TunnelViewControllerInteractor { .getOutgoingConnectionInfo() else { return } - await self?.didGetOutGoingAddress?(outgoingConnectionInfo) + await self?.didGetOutgoingAddress?(outgoingConnectionInfo) } } }, diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift index ceabd7761b10..e13758a15537 100644 --- a/ios/MullvadVPN/Views/MainButtonStyle.swift +++ b/ios/MullvadVPN/Views/MainButtonStyle.swift @@ -10,12 +10,10 @@ import SwiftUI struct MainButtonStyle: ButtonStyle { var style: Style - var disabled: Bool @Environment(\.isEnabled) private var isEnabled: Bool - init(_ style: Style, disabled: Bool = false) { + init(_ style: Style) { self.style = style - self.disabled = disabled } func makeBody(configuration: Configuration) -> some View { diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift index 6f36b839befe..72a879893547 100644 --- a/ios/MullvadVPN/Views/SplitMainButton.swift +++ b/ios/MullvadVPN/Views/SplitMainButton.swift @@ -12,6 +12,7 @@ struct SplitMainButton: View { var text: LocalizedStringKey var image: ImageResource var style: MainButtonStyle.Style + var accessibilityId: AccessibilityIdentifier? @State private var secondaryButtonWidth: CGFloat = 0 @@ -28,6 +29,10 @@ struct SplitMainButton: View { } .padding(.trailing, -secondaryButtonWidth) }) + .ifLet(accessibilityId) { view, value in + view.accessibilityIdentifier(value.asString) + } + Button(action: secondaryAction, label: { Image(image) .resizable()