Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animate expansion of connection details view #7548

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */; };
440E5AB02CDBD67D00B09614 /* StatefulPreviewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */; };
440E5AB42CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */; };
4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA882D282687001B13C9 /* DetailsContainer.swift */; };
4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */; };
4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */; };
4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */; };
Expand Down Expand Up @@ -1021,7 +1020,6 @@
F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495752D02025200CFEC2A /* ChipContainerView.swift */; };
F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */; };
F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeature.swift */; };
F0B4957C2D03154200CFEC2A /* FeatureIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */; };
F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */; };
F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F02BF751E300817A42 /* RelayWithDistance.swift */; };
F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */; };
Expand Down Expand Up @@ -1067,6 +1065,7 @@
F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; };
F95C1C252D3E5E8E00EBE769 /* UIAlertController+InAppPurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */; };
F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
F998EFFA2D3656BA00D88D01 /* SKProduct+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */; };
Expand Down Expand Up @@ -1465,7 +1464,6 @@
44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPreviewWrapper.swift; sourceTree = "<group>"; };
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationSettingsWatchingObservableObject.swift; sourceTree = "<group>"; };
4419AA882D282687001B13C9 /* DetailsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsContainer.swift; sourceTree = "<group>"; };
4419AA8A2D2826E5001B13C9 /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = "<group>"; };
4419AA8D2D2828A4001B13C9 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2294,7 +2292,6 @@
F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = "<group>"; };
F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = "<group>"; };
F0B495792D02F41F00CFEC2A /* ChipFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeature.swift; sourceTree = "<group>"; };
F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsView.swift; sourceTree = "<group>"; };
F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = "<group>"; };
F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = "<group>"; };
F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2336,6 +2333,7 @@
F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; };
F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; };
F0FBD98E2C4A60CC00EE5323 /* KeyExchangingResultStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyExchangingResultStub.swift; sourceTree = "<group>"; };
F910A4002D3FF22E002FF3BB /* View+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Modifier.swift"; sourceTree = "<group>"; };
F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+InAppPurchase.swift"; sourceTree = "<group>"; };
F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -2733,15 +2731,14 @@
4419AA862D28264D001B13C9 /* ConnectionView */ = {
isa = PBXGroup;
children = (
44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */,
F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AA130972CFF364F00640DF9 /* FeatureIndicators */,
449E9A6E2D283C7400F8574A /* ButtonPanel.swift */,
F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */,
7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
4419AA882D282687001B13C9 /* DetailsContainer.swift */,
44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */,
4419AA8A2D2826E5001B13C9 /* DetailsView.swift */,
F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
4419AA8D2D2828A4001B13C9 /* HeaderView.swift */,
);
path = ConnectionView;
Expand Down Expand Up @@ -3199,6 +3196,7 @@
5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */,
7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */,
7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */,
F910A4002D3FF22E002FF3BB /* View+Modifier.swift */,
7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */,
7A8A18FA2CE4B66C000BCB5B /* View+TapAreaSize.swift */,
);
Expand Down Expand Up @@ -4134,15 +4132,6 @@
path = SelectLocation;
sourceTree = "<group>";
};
7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
isa = PBXGroup;
children = (
F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */,
F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
);
path = FeatureIndicators;
sourceTree = "<group>";
};
7AD63A422CDA661B00445268 /* Extensions */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5990,6 +5979,7 @@
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */,
F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */,
F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */,
7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */,
7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
Expand Down Expand Up @@ -6164,7 +6154,6 @@
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */,
5888AD83227B11080051EB06 /* LocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */,
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
Expand Down Expand Up @@ -6240,7 +6229,6 @@
586C0D782B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift in Sources */,
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
7A9CCCC02A96302800DD6A34 /* ProfileVoucherCoordinator.swift in Sources */,
F0B4957C2D03154200CFEC2A /* FeatureIndicatorsView.swift in Sources */,
7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
F0ADF1D52D01DCFD00299F09 /* ChipView.swift in Sources */,
Expand Down
26 changes: 26 additions & 0 deletions ios/MullvadVPN/Extensions/View+Modifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// View+Modifier.swift
// MullvadVPN
//
// Created by Steffen Ernst on 2025-01-21.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import SwiftUI

extension View {
/**
A view modifier that can be used to conditionally apply other view modifiers.
# Example #
```
.apply {
if #available(iOS 16.4, *) {
$0.scrollBounceBehavior(.basedOnSize)
} else {
$0
}
}
```
*/
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ extension ConnectionView {
}

#Preview {
ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
ConnectionViewComponentPreview(showIndicators: true) { _, vm, _ in
ConnectionView.ButtonPanel(viewModel: vm, action: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import SwiftUI

struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
@ObservedObject var viewModel: ViewModel
let tunnelState: TunnelState
@Binding var isExpanded: Bool

@State private var chipContainerHeight: CGFloat = .zero
private let verticalPadding: CGFloat = 6
private let verticalPadding: CGFloat = 8

var body: some View {
GeometryReader { geo in
Expand All @@ -31,19 +32,25 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
}

Button(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) {
isExpanded.toggle()
withAnimation {
isExpanded.toggle()
}
}
.font(.subheadline)
.lineLimit(1)
.foregroundStyle(UIColor.primaryTextColor.color)
.showIf(showMoreButton)
.transition(.move(edge: .bottom).combined(with: .opacity))

Spacer()
}
.sizeOfView { chipContainerHeight = $0.height }
.sizeOfView { size in
withAnimation {
chipContainerHeight = size.height
}
}
}
.frame(height: chipContainerHeight)
.padding(.vertical, -(verticalPadding - 1)) // Remove extra padding from chip views on top and bottom.
}

private func createChipViews(chips: [ChipModel], containerWidth: CGFloat) -> some View {
Expand Down Expand Up @@ -88,6 +95,31 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
StatefulPreviewWrapper(false) { isExpanded in
ChipContainerView(
viewModel: MockFeatureIndicatorsViewModel(),
tunnelState: .connected(
.init(
entry: nil,
exit: .init(
endpoint: .init(
ipv4Relay: .init(ip: .allHostsGroup, port: 1234),
ipv4Gateway: .allHostsGroup,
ipv6Gateway: .broadcast,
publicKey: Data()
),
hostname: "hostname",
location: .init(
country: "Sweden",
countryCode: "SE",
city: "Gothenburg",
cityCode: "gbg",
latitude: 1234,
longitude: 1234
)
),
retryAttempt: 0
),
isPostQuantum: false,
isDaita: false
),
isExpanded: isExpanded
)
.background(UIColor.secondaryColor.color)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import Foundation
import SwiftUI

struct ChipModel: Identifiable {
let id = UUID()
var id: String { name }
let name: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,57 +14,87 @@ struct ConnectionView: View {

@State private(set) var isExpanded = false

@State private(set) var scrollViewHeight: CGFloat = 0
var hasFeatureIndicators: Bool { !indicatorsViewModel.chips.isEmpty }
var action: ButtonPanel.Action?

var body: some View {
Spacer()
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
VStack {
Spacer()
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 0) {
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
.padding(.bottom, 8)
Divider()
.background(UIColor.secondaryTextColor.color)
.padding(.bottom, 16)
.showIf(isExpanded)

VStack(alignment: .leading, spacing: 0) {
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
.padding(.bottom, headerViewBottomPadding)
ScrollView {
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(LocalizedStringKey("Active features"))
.font(.footnote.weight(.semibold))
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
.showIf(isExpanded && hasFeatureIndicators)

DetailsContainer(
connectionViewModel: connectionViewModel,
indicatorsViewModel: indicatorsViewModel,
isExpanded: $isExpanded
)
.showIf(connectionViewModel.showsConnectionDetails)
ChipContainerView(
viewModel: indicatorsViewModel,
tunnelState: connectionViewModel.tunnelStatus.state,
isExpanded: $isExpanded
)
.padding(.bottom, isExpanded ? 16 : 0)
.showIf(hasFeatureIndicators)

ButtonPanel(viewModel: connectionViewModel, action: action)
.padding(.top, 16)
}
.padding(16)
.background(BlurView(style: .dark))
.cornerRadius(12)
.padding(EdgeInsets(top: 16, leading: 16, bottom: 24, trailing: 16))
.onReceive(connectionViewModel.combinedState) { _ in
if !connectionViewModel.showsConnectionDetails {
isExpanded = false
DetailsView(viewModel: connectionViewModel)
.padding(.bottom, 8)
.showIf(isExpanded)
}
Spacer()
}
.sizeOfView { size in
withAnimation {
scrollViewHeight = size.height
}
}
}
.frame(maxHeight: scrollViewHeight)
.apply {
if #available(iOS 16.4, *) {
$0.scrollBounceBehavior(.basedOnSize)
} else {
$0
}
}
}
.transformEffect(.identity)
.animation(.default, value: hasFeatureIndicators)
ButtonPanel(viewModel: connectionViewModel, action: action)
}
.padding(16)
.background(BlurView(style: .dark))
.cornerRadius(12)
.padding(EdgeInsets(top: 16, leading: 16, bottom: 24, trailing: 16))
.onChange(of: connectionViewModel.showsConnectionDetails) { showsConnectionDetails in
if !showsConnectionDetails {
withAnimation {
isExpanded = false
}
}
}
}
}
}

extension ConnectionView {
var headerViewBottomPadding: CGFloat {
let hasIndicators = !indicatorsViewModel.chips.isEmpty
let showConnectionDetails = connectionViewModel.showsConnectionDetails

return isExpanded
? showConnectionDetails ? 16 : 0
: hasIndicators && showConnectionDetails ? 16 : 0
}
}

#Preview("ConnectionView (Indicators)") {
ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, _ in
ConnectionViewComponentPreview(showIndicators: true) { indicatorModel, viewModel, _ in
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
}
}

#Preview("ConnectionView (No indicators)") {
ConnectionViewComponentPreview(showIndicators: false, isExpanded: true) { indicatorModel, viewModel, _ in
ConnectionViewComponentPreview(showIndicators: false) { indicatorModel, viewModel, _ in
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
}
}
Loading
Loading