1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
//
// ConnectionView.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-12-03.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import SwiftUI
struct ConnectionView: View {
@ObservedObject var connectionViewModel: ConnectionViewViewModel
@ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel
@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 {
VStack {
Spacer()
.accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 0) {
HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
.padding(.bottom, 4)
Divider()
.background(UIColor.secondaryTextColor.color)
.padding(.top, 4)
.padding(.bottom, 8)
.showIf(isExpanded)
ScrollView {
VStack(alignment: .leading, spacing: 2) {
if let titleForCountryAndCity = connectionViewModel.titleForCountryAndCity {
Text(titleForCountryAndCity)
.lineLimit(isExpanded ? 2 : 1)
.font(.title3.weight(.semibold))
.foregroundStyle(UIColor.primaryTextColor.color)
}
if let titleForServer = connectionViewModel.titleForServer {
Text(titleForServer)
.lineLimit(isExpanded ? 3 : 1)
.font(.body)
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
.accessibilityIdentifier(
AccessibilityIdentifier.connectionPanelServerLabel.asString
)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
}
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(LocalizedStringKey("Active features"))
.font(.footnote.weight(.semibold))
.foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
.padding(.top, 8)
.showIf(isExpanded && hasFeatureIndicators)
ChipContainerView(
viewModel: indicatorsViewModel,
tunnelState: connectionViewModel.tunnelStatus.state,
isExpanded: $isExpanded
)
.padding(.bottom, isExpanded ? 16 : 0)
.showIf(hasFeatureIndicators)
DetailsView(viewModel: connectionViewModel)
.padding(.bottom, 8)
.padding(.top, !hasFeatureIndicators ? 8 : 0)
.showIf(isExpanded)
}
Spacer()
}
}.frame(maxWidth: .infinity, alignment: .leading)
.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
}
}
}
}
}
}
#Preview("ConnectionView (Indicators)") {
ConnectionViewComponentPreview(showIndicators: true) { indicatorModel, viewModel, _ in
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
}
}
#Preview("ConnectionView (No indicators)") {
ConnectionViewComponentPreview(showIndicators: false) { indicatorModel, viewModel, _ in
ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
}
}
|