Skip to content

Commit 337e7ed

Browse files
committed
Make animations of connection screen look much better
1 parent b704e4b commit 337e7ed

9 files changed

+168
-141
lines changed

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ButtonPanel.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension ConnectionView {
8484
}
8585

8686
#Preview {
87-
ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
87+
ConnectionViewComponentPreview(showIndicators: true) { _, vm, _ in
8888
ConnectionView.ButtonPanel(viewModel: vm, action: nil)
8989
}
9090
}

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift

+20-6
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import SwiftUI
1010

1111
struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
1212
@ObservedObject var viewModel: ViewModel
13+
let tunnelState: TunnelState
1314
@Binding var isExpanded: Bool
1415

1516
@State private var chipContainerHeight: CGFloat = .zero
16-
private let verticalPadding: CGFloat = 6
17+
private let verticalPadding: CGFloat = 8
1718

1819
var body: some View {
1920
GeometryReader { geo in
@@ -31,7 +32,9 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
3132
}
3233

3334
Button(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) {
34-
isExpanded.toggle()
35+
withAnimation {
36+
isExpanded.toggle()
37+
}
3538
}
3639
.font(.subheadline)
3740
.lineLimit(1)
@@ -41,17 +44,13 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
4144

4245
Spacer()
4346
}
44-
.animation(.default, value: isExpanded)
45-
.animation(.default, value: showMoreButton)
46-
.transition(.move(edge: .bottom).combined(with: .opacity))
4747
.sizeOfView { size in
4848
withAnimation {
4949
chipContainerHeight = size.height
5050
}
5151
}
5252
}
5353
.frame(height: chipContainerHeight)
54-
.padding(.vertical, -(verticalPadding - 1)) // Remove extra padding from chip views on top and bottom.
5554
}
5655

5756
private func createChipViews(chips: [ChipModel], containerWidth: CGFloat) -> some View {
@@ -96,6 +95,21 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
9695
StatefulPreviewWrapper(false) { isExpanded in
9796
ChipContainerView(
9897
viewModel: MockFeatureIndicatorsViewModel(),
98+
tunnelState: .connected(.init(entry: nil,
99+
exit: .init(endpoint: .init(ipv4Relay: .init(ip: .allHostsGroup, port: 1234),
100+
ipv4Gateway: .allHostsGroup,
101+
ipv6Gateway: .broadcast,
102+
publicKey: Data()),
103+
hostname: "hostname",
104+
location: .init(country: "Sweden",
105+
countryCode: "SE",
106+
city: "Gothenburg",
107+
cityCode: "gbg",
108+
latitude: 1234,
109+
longitude: 1234)),
110+
retryAttempt: 0),
111+
isPostQuantum: false,
112+
isDaita: false),
99113
isExpanded: isExpanded
100114
)
101115
.background(UIColor.secondaryColor.color)

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionView.swift

+41-44
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,42 @@ struct ConnectionView: View {
1414

1515
@State private(set) var isExpanded = false
1616

17-
@State private(set) var showConnectionDetailsAnimated = false
18-
@State private(set) var isExpandedAnimated = false
1917
@State private(set) var scrollViewHeight: CGFloat = 0
20-
18+
var hasFeatureIndicators: Bool { !indicatorsViewModel.chips.isEmpty }
2119
var action: ButtonPanel.Action?
2220

2321
var body: some View {
24-
Spacer()
25-
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
26-
27-
VStack(alignment: .leading, spacing: 16) {
28-
VStack(alignment: .leading, spacing: 16) {
29-
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
30-
31-
if showConnectionDetailsAnimated {
22+
VStack {
23+
Spacer()
24+
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
25+
VStack(spacing: 16) {
26+
VStack(alignment: .leading, spacing: 0) {
27+
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
28+
.padding(.bottom, 8)
3229
Divider()
3330
.background(UIColor.secondaryTextColor.color)
34-
.showIf(isExpandedAnimated)
31+
.padding(.bottom, 16)
32+
.showIf(isExpanded)
3533

3634
ScrollView {
37-
VStack(alignment: .leading, spacing: 0) {
38-
Text(LocalizedStringKey("Active features"))
39-
.font(.footnote.weight(.semibold))
40-
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
41-
.padding(.bottom, isExpandedAnimated ? 8 : 0)
42-
.showIf(!indicatorsViewModel.chips.isEmpty && isExpandedAnimated)
35+
HStack {
36+
VStack(alignment: .leading, spacing: 0) {
37+
Text(LocalizedStringKey("Active features"))
38+
.font(.footnote.weight(.semibold))
39+
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
40+
.showIf(isExpanded && hasFeatureIndicators)
4341

44-
ChipContainerView(viewModel: indicatorsViewModel, isExpanded: $isExpanded)
42+
ChipContainerView(viewModel: indicatorsViewModel,
43+
tunnelState: connectionViewModel.tunnelStatus.state,
44+
isExpanded: $isExpanded)
45+
.padding(.bottom, isExpanded ? 16 : 0)
46+
.showIf(hasFeatureIndicators)
4547

46-
DetailsView(viewModel: connectionViewModel)
47-
.padding(.top, indicatorsViewModel.chips.isEmpty ? 0 : 16)
48-
.showIf(isExpandedAnimated)
48+
DetailsView(viewModel: connectionViewModel)
49+
.padding(.bottom, 8)
50+
.showIf(isExpanded)
51+
}
52+
Spacer()
4953
}
5054
.sizeOfView { size in
5155
withAnimation {
@@ -61,41 +65,34 @@ struct ConnectionView: View {
6165
$0
6266
}
6367
}
64-
.showIf(isExpandedAnimated || !indicatorsViewModel.chips.isEmpty)
6568
}
69+
.transformEffect(.identity)
70+
.animation(.default, value: hasFeatureIndicators)
71+
ButtonPanel(viewModel: connectionViewModel, action: action)
6672
}
67-
.transformEffect(.identity)
68-
69-
ButtonPanel(viewModel: connectionViewModel, action: action)
70-
}
71-
.padding()
72-
.background(BlurView(style: .dark))
73-
.cornerRadius(12)
74-
.padding()
75-
.onChange(of: isExpanded) { newValue in
76-
withAnimation {
77-
isExpandedAnimated = newValue
78-
}
79-
}
80-
.onChange(of: connectionViewModel.showsConnectionDetails) { newValue in
81-
if !newValue {
82-
isExpanded = false
83-
}
84-
withAnimation {
85-
showConnectionDetailsAnimated = newValue
73+
.padding()
74+
.background(BlurView(style: .dark))
75+
.cornerRadius(12)
76+
.padding()
77+
.onChange(of: connectionViewModel.showsConnectionDetails) { newValue in
78+
if !newValue {
79+
withAnimation {
80+
isExpanded = false
81+
}
82+
}
8683
}
8784
}
8885
}
8986
}
9087

9188
#Preview("ConnectionView (Indicators)") {
92-
ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, _ in
89+
ConnectionViewComponentPreview(showIndicators: true) { indicatorModel, viewModel, _ in
9390
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
9491
}
9592
}
9693

9794
#Preview("ConnectionView (No indicators)") {
98-
ConnectionViewComponentPreview(showIndicators: false, isExpanded: true) { indicatorModel, viewModel, _ in
95+
ConnectionViewComponentPreview(showIndicators: false) { indicatorModel, viewModel, _ in
9996
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
10097
}
10198
}

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionViewComponentPreview.swift

+25-22
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ import SwiftUI
1515

1616
struct ConnectionViewComponentPreview<Content: View>: View {
1717
let showIndicators: Bool
18+
let connectedTunnelStatus = TunnelStatus(
19+
observedState: .connected(ObservedConnectionState(
20+
selectedRelays: SelectedRelaysStub.selectedRelays,
21+
relayConstraints: RelayConstraints(entryLocations: .any, exitLocations: .any, port: .any, filter: .any),
22+
networkReachability: .reachable,
23+
connectionAttemptCount: 0,
24+
transportLayer: .udp,
25+
remotePort: 80,
26+
isPostQuantum: true,
27+
isDaitaEnabled: true
28+
)),
29+
state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true)
30+
)
1831

1932
private var tunnelSettings: LatestTunnelSettings {
2033
LatestTunnelSettings(
@@ -25,44 +38,34 @@ struct ConnectionViewComponentPreview<Content: View>: View {
2538
)
2639
}
2740

28-
private let viewModel = ConnectionViewViewModel(
29-
tunnelStatus: TunnelStatus(
30-
observedState: .connected(ObservedConnectionState(
31-
selectedRelays: SelectedRelaysStub.selectedRelays,
32-
relayConstraints: RelayConstraints(entryLocations: .any, exitLocations: .any, port: .any, filter: .any),
33-
networkReachability: .reachable,
34-
connectionAttemptCount: 0,
35-
transportLayer: .udp,
36-
remotePort: 80,
37-
isPostQuantum: true,
38-
isDaitaEnabled: true
39-
)),
40-
state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true)
41-
),
42-
relayConstraints: RelayConstraints(),
43-
relayCache: RelayCache(cacheDirectory: ApplicationConfiguration.containerURL),
44-
customListRepository: CustomListRepository()
45-
)
41+
private let viewModel: ConnectionViewViewModel
4642

4743
var content: (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
4844

49-
@State var isExpanded: Bool
45+
@State var isExpanded: Bool = false
5046

5147
init(
5248
showIndicators: Bool,
53-
isExpanded: Bool,
5449
content: @escaping (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
5550
) {
5651
self.showIndicators = showIndicators
57-
self._isExpanded = State(wrappedValue: isExpanded)
5852
self.content = content
53+
viewModel = ConnectionViewViewModel(
54+
tunnelStatus: connectedTunnelStatus,
55+
relayConstraints: RelayConstraints(),
56+
relayCache: RelayCache(cacheDirectory: ApplicationConfiguration.containerURL),
57+
customListRepository: CustomListRepository()
58+
)
59+
viewModel.outgoingConnectionInfo = OutgoingConnectionInfo(ipv4: .init(ip: .allHostsGroup, exitIP: true),
60+
ipv6: IPV6ConnectionData(ip: .broadcast, exitIP: true))
5961
}
6062

6163
var body: some View {
6264
content(
6365
FeatureIndicatorsViewModel(
6466
tunnelSettings: tunnelSettings,
65-
ipOverrides: []
67+
ipOverrides: [],
68+
tunnelState: connectedTunnelStatus.state
6669
),
6770
viewModel,
6871
$isExpanded

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ConnectionViewViewModel.swift

-11
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ class ConnectionViewViewModel: ObservableObject {
3434
@Published var relayConstraints: RelayConstraints
3535
let destinationDescriber: DestinationDescribing
3636

37-
var combinedState: Publishers.CombineLatest<
38-
Published<TunnelStatus>.Publisher,
39-
Published<Bool>.Publisher
40-
> {
41-
$tunnelStatus.combineLatest($showsActivityIndicator)
42-
}
43-
4437
var tunnelIsConnected: Bool {
4538
if case .connected = tunnelStatus.state {
4639
true
@@ -72,10 +65,6 @@ class ConnectionViewViewModel: ObservableObject {
7265

7366
func update(tunnelStatus: TunnelStatus) {
7467
self.tunnelStatus = tunnelStatus
75-
76-
if !tunnelIsConnected {
77-
outgoingConnectionInfo = nil
78-
}
7968
}
8069
}
8170

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/DetailsView.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ extension ConnectionView {
4848
}
4949
}
5050
}
51+
.animation(.default, value: viewModel.inAddress)
52+
.animation(.default, value: viewModel.tunnelIsConnected)
5153
}
5254

5355
@ViewBuilder
@@ -72,7 +74,7 @@ extension ConnectionView {
7274
}
7375

7476
#Preview {
75-
ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
77+
ConnectionViewComponentPreview(showIndicators: true) { _, vm, _ in
7678
ConnectionView.DetailsView(viewModel: vm)
7779
}
7880
}

ios/MullvadVPN/View controllers/Tunnel/ConnectionView/FeatureIndicatorsViewModel.swift

+22-12
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,34 @@ import SwiftUI
1212
class FeatureIndicatorsViewModel: ChipViewModelProtocol {
1313
@Published var tunnelSettings: LatestTunnelSettings
1414
@Published var ipOverrides: [IPOverride]
15+
@Published var tunnelState: TunnelState
1516

16-
init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride]) {
17+
init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride], tunnelState: TunnelState) {
1718
self.tunnelSettings = tunnelSettings
1819
self.ipOverrides = ipOverrides
20+
self.tunnelState = tunnelState
1921
}
2022

2123
var chips: [ChipModel] {
22-
let features: [ChipFeature] = [
23-
DaitaFeature(settings: tunnelSettings),
24-
QuantumResistanceFeature(settings: tunnelSettings),
25-
MultihopFeature(settings: tunnelSettings),
26-
ObfuscationFeature(settings: tunnelSettings),
27-
DNSFeature(settings: tunnelSettings),
28-
IPOverrideFeature(overrides: ipOverrides),
29-
]
24+
// Here can be a check if a feature indicator should show in other connection states
25+
// e.g. Access local network in blocked state
26+
switch tunnelState {
27+
case .connecting, .reconnecting, .negotiatingEphemeralPeer,
28+
.connected, .pendingReconnect:
29+
let features: [ChipFeature] = [
30+
DaitaFeature(settings: tunnelSettings),
31+
QuantumResistanceFeature(settings: tunnelSettings),
32+
MultihopFeature(settings: tunnelSettings),
33+
ObfuscationFeature(settings: tunnelSettings),
34+
DNSFeature(settings: tunnelSettings),
35+
IPOverrideFeature(overrides: ipOverrides),
36+
]
3037

31-
return features
32-
.filter { $0.isEnabled }
33-
.map { ChipModel(name: $0.name) }
38+
return features
39+
.filter { $0.isEnabled }
40+
.map { ChipModel(name: $0.name) }
41+
default:
42+
return []
43+
}
3444
}
3545
}

0 commit comments

Comments
 (0)