summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@mullvad.net>2025-07-01 13:46:30 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-07-17 14:05:24 +0200
commit6e92f0fe096048770e534d30ca04087d4bc714a4 (patch)
treec1ab9ce0daea35bfceff8b721f56032b30d99b37
parentae02a1f9b790d5d8966b55f918227a0983789035 (diff)
downloadmullvadvpn-6e92f0fe096048770e534d30ca04087d4bc714a4.tar.xz
mullvadvpn-6e92f0fe096048770e534d30ca04087d4bc714a4.zip
Fix dynamic sizing and layout issues across multiple views
- Fix font scaling and overlapping in device management - Adjust font size for account number - Scale info button dynamically - Resolve dynamic size issues in: - Settings - IP overrides - API access - Filter view and filter chips - Select location - Edit API access method - MullvadList - Fix line breaks in settings view - Unify padding across pages
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj18
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarView.swift8
-rw-r--r--ios/MullvadVPN/Coordinators/AccountCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift4
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift1
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift53
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift4
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift35
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideStatusView.swift15
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift48
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/Views/SwitchRowView.swift1
-rw-r--r--ios/MullvadVPN/Extensions/UIFont+Weight.swift7
-rw-r--r--ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift3
-rw-r--r--ios/MullvadVPN/UI appearance/Font+Mullvad.swift36
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift3
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift31
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift38
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift14
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift13
-rw-r--r--ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift10
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceListView.swift48
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementView.swift105
-rw-r--r--ios/MullvadVPN/View controllers/RelayFilter/ChipFlowLayout.swift77
-rw-r--r--ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/RelayFilter/RelayFilterViewController.swift3
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/DAITAInfoView.swift6
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift74
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift7
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderFooterView.swift118
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift13
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift11
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDNSInfoCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift6
-rw-r--r--ios/MullvadVPN/Views/List/MullvadList.swift90
-rw-r--r--ios/MullvadVPN/Views/List/MullvadListActionItemView.swift22
-rw-r--r--ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift17
-rw-r--r--ios/MullvadVPN/Views/ScaledSegmentedControl.swift61
42 files changed, 587 insertions, 431 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 33c22cf023..a4bf13fb88 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -515,7 +515,6 @@
7A45CFC62C05FF6A00D80B21 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */; };
7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; };
7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; };
- 7A5110D72DE734DE00686850 /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */; };
7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */; };
7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */; };
@@ -935,7 +934,6 @@
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */; };
A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; };
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; };
- A9EE855F2DF0893E00F2D769 /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
A9EE85612DF1BE2900F2D769 /* TermsOfServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
@@ -1026,6 +1024,7 @@
F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; };
F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; };
+ F0A7AAA22E1D31E200D433E8 /* ScaledSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7AAA12E1D31E200D433E8 /* ScaledSegmentedControl.swift */; };
F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; };
F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
F0A89CB32D9D6C2100580C27 /* MullvadDeviceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */; };
@@ -1123,12 +1122,12 @@
F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */; };
F9394EF32DC21D8C009595EA /* MullvadList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EF22DC21D8C009595EA /* MullvadList.swift */; };
+ F97C38CA2DE49869006DCB08 /* MultihopSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38C92DE49869006DCB08 /* MultihopSettingsCoordinator.swift */; };
+ F97C38D92DE5930F006DCB08 /* CustomDNSCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38D82DE59307006DCB08 /* CustomDNSCoordinator.swift */; };
F97C38DF2DEEDB0F006DCB08 /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
F97C38E32DEEDC28006DCB08 /* MullvadListActionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38E22DEEDC28006DCB08 /* MullvadListActionItemView.swift */; };
F97C38E52DEEDFD6006DCB08 /* Image+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38E42DEEDFD2006DCB08 /* Image+Assets.swift */; };
F97C38E82DF025D9006DCB08 /* MullvadAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38E72DF025D9006DCB08 /* MullvadAlert.swift */; };
- F97C38CA2DE49869006DCB08 /* MultihopSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38C92DE49869006DCB08 /* MultihopSettingsCoordinator.swift */; };
- F97C38D92DE5930F006DCB08 /* CustomDNSCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38D82DE59307006DCB08 /* CustomDNSCoordinator.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 */; };
F9E3BCF72DD35B78009986C3 /* ListAccessViewModelBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */; };
@@ -2473,6 +2472,7 @@
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = "<group>"; };
F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = "<group>"; };
F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = "<group>"; };
+ F0A7AAA12E1D31E200D433E8 /* ScaledSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledSegmentedControl.swift; sourceTree = "<group>"; };
F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = "<group>"; };
F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadDeviceProxy.swift; sourceTree = "<group>"; };
F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+UnsafePointer.swift"; sourceTree = "<group>"; };
@@ -2553,11 +2553,11 @@
F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Mullvad.swift"; sourceTree = "<group>"; };
F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadListNavigationItemView.swift; sourceTree = "<group>"; };
F9394EF22DC21D8C009595EA /* MullvadList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadList.swift; sourceTree = "<group>"; };
+ F97C38C92DE49869006DCB08 /* MultihopSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopSettingsCoordinator.swift; sourceTree = "<group>"; };
+ F97C38D82DE59307006DCB08 /* CustomDNSCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSCoordinator.swift; sourceTree = "<group>"; };
F97C38E22DEEDC28006DCB08 /* MullvadListActionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadListActionItemView.swift; sourceTree = "<group>"; };
F97C38E42DEEDFD2006DCB08 /* Image+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Assets.swift"; sourceTree = "<group>"; };
F97C38E72DF025D9006DCB08 /* MullvadAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAlert.swift; sourceTree = "<group>"; };
- F97C38C92DE49869006DCB08 /* MultihopSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopSettingsCoordinator.swift; sourceTree = "<group>"; };
- F97C38D82DE59307006DCB08 /* CustomDNSCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSCoordinator.swift; sourceTree = "<group>"; };
F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; };
F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessViewModelBridge.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -3334,8 +3334,6 @@
583FE01F29C197ED006E85F9 /* Views */ = {
isa = PBXGroup;
children = (
- F91CCBFB2DFAF5E1007F1925 /* MullvadProgressViewStyle.swift */,
- F97C38E72DF025D9006DCB08 /* MullvadAlert.swift */,
7A5869962B32EA4500640D27 /* AppButton.swift */,
7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */,
7A9FA1412A2E3306000B728D /* CheckboxView.swift */,
@@ -3354,8 +3352,11 @@
F9394EF12DC21D7B009595EA /* List */,
7AA1309A2D0048D800640DF9 /* MainButton.swift */,
7A0EAE992D01B41500D3EB8B /* MainButtonStyle.swift */,
+ F97C38E72DF025D9006DCB08 /* MullvadAlert.swift */,
F91B94A62DC9EB5E00132C28 /* MullvadInfoHeaderView.swift */,
+ F91CCBFB2DFAF5E1007F1925 /* MullvadProgressViewStyle.swift */,
7A8A190F2CEE3918000BCB5B /* RowSeparator.swift */,
+ F0A7AAA12E1D31E200D433E8 /* ScaledSegmentedControl.swift */,
58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */,
7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */,
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */,
@@ -6665,6 +6666,7 @@
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
586C0D812B03CA8400E7CDD7 /* CurrentValueSubject+UIActionBindings.swift in Sources */,
+ F0A7AAA22E1D31E200D433E8 /* ScaledSegmentedControl.swift in Sources */,
581DFAEA2B176C51005D6D1C /* PersistentProxyConfiguration+ViewModel.swift in Sources */,
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */,
F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */,
diff --git a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
index 9188a9cd50..67c0f326b8 100644
--- a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
+++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
@@ -24,7 +24,7 @@ class HeaderBarView: UIView {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
- stackView.spacing = 16.0
+ stackView.spacing = 8.0
return stackView
}()
@@ -33,7 +33,8 @@ class HeaderBarView: UIView {
label.font = .mullvadMiniSemiBold
label.adjustsFontForContentSizeCategory = true
label.textColor = UIColor(white: 1.0, alpha: 0.8)
- label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ label.setContentHuggingPriority(.defaultHigh, for: .horizontal) // Resist growing
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setAccessibilityIdentifier(.headerDeviceNameLabel)
return label
}()
@@ -43,7 +44,8 @@ class HeaderBarView: UIView {
label.font = .mullvadMiniSemiBold
label.adjustsFontForContentSizeCategory = true
label.textColor = UIColor(white: 1.0, alpha: 0.8)
- label.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal) // Allow growing
+ label.setContentCompressionResistancePriority(.required, for: .horizontal)
return label
}()
diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
index 4d83a4716f..228ba731f5 100644
--- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
@@ -136,7 +136,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked
currentDeviceId: currentDeviceId,
devicesProxy: interactor.deviceProxy
),
- style: .normal,
+ style: .deviceManagement,
onError: { title, error in
let errorDescription = if case let .network(urlError) = error as? REST.Error {
urlError.localizedDescription
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift
index d6cf5e7ac4..8ffe9dd369 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift
@@ -25,7 +25,8 @@ class AddLocationsViewController: UIViewController {
let tableView = UITableView()
tableView.separatorColor = .secondaryColor
tableView.separatorInset = .zero
- tableView.rowHeight = UIMetrics.TableView.rowHeight
+ tableView.estimatedRowHeight = UIMetrics.TableView.rowHeight
+ tableView.rowHeight = UITableView.automaticDimension
tableView.indicatorStyle = .white
tableView.setAccessibilityIdentifier(.editCustomListEditLocationsTableView)
return tableView
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift
index 767924700b..16d60ad373 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift
@@ -79,10 +79,6 @@ extension CustomListDataSourceConfiguration: UITableViewDelegate {
}
}
- func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- UIMetrics.SettingsCell.customListsCellHeight
- }
-
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let snapshot = dataSource.snapshot()
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
index 6d845b426c..1b929f0d04 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
@@ -87,7 +87,6 @@ class ListCustomListViewController: UIViewController {
tableView.separatorColor = .secondaryColor
tableView.separatorInset = .zero
tableView.separatorStyle = .singleLine
- tableView.rowHeight = UIMetrics.SettingsCell.customListsCellHeight
tableView.registerReusableViews(from: CellReuseIdentifier.self)
tableView.setAccessibilityIdentifier(.listCustomListsTableView)
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
index 97afabdce1..fe9345dc71 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
@@ -54,13 +54,10 @@ class EditAccessMethodViewController: UIViewController {
isModalInPresentation = true
let title = createTitle()
- let headerView = createHeaderView()
- view.addConstrainedSubviews([title, headerView, tableView]) {
+ view.addConstrainedSubviews([title, tableView]) {
title.pinEdgesToSuperviewMargins(PinnableEdges([.leading(7), .trailing(7), .top(0)]))
- headerView.pinEdgesToSuperviewMargins(PinnableEdges([.leading(8), .trailing(8)]))
tableView.pinEdgesToSuperview(.all().excluding(.top))
- headerView.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4)
- tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20)
+ tableView.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4)
}
configureDataSource()
@@ -83,21 +80,6 @@ class EditAccessMethodViewController: UIViewController {
label.textColor = UIColor.NavigationBar.titleColor
return label
}
-
- private func createHeaderView() -> UIView {
- var headerView: InfoHeaderView?
-
- if let headerConfig = subject.value.infoHeaderConfig {
- headerView = InfoHeaderView(config: headerConfig)
-
- headerView?.onAbout = { [weak self] in
- guard let self, let infoModalConfig = subject.value.infoModalConfig else { return }
- delegate?.controllerShouldShowMethodInfo(self, config: infoModalConfig)
- }
- }
-
- return headerView ?? UIView()
- }
}
// MARK: - UITableViewDelegate
@@ -118,11 +100,34 @@ extension EditAccessMethodViewController: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return UIMetrics.SettingsCell.apiAccessCellHeight
+ UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- return nil
+ guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return nil }
+ switch sectionIdentifier {
+ case .enableMethod:
+ var headerView: InfoHeaderView?
+
+ if let headerConfig = subject.value.infoHeaderConfig {
+ headerView = InfoHeaderView(config: headerConfig)
+
+ headerView?.onAbout = { [weak self] in
+ guard let self, let infoModalConfig = subject.value.infoModalConfig else { return }
+ delegate?.controllerShouldShowMethodInfo(self, config: infoModalConfig)
+ }
+ }
+ headerView?.directionalLayoutMargins = NSDirectionalEdgeInsets(
+ top: 4,
+ leading: 0,
+ bottom: 16,
+ trailing: 0
+ )
+
+ return headerView ?? UIView()
+ default:
+ return nil
+ }
}
// Header height shenanigans to avoid extra spacing in testing sections when testing is NOT ongoing.
@@ -130,11 +135,11 @@ extension EditAccessMethodViewController: UITableViewDelegate {
guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 }
switch sectionIdentifier {
- case .methodSettings, .deleteMethod, .testMethod:
+ case .methodSettings, .deleteMethod, .testMethod, .enableMethod:
return UITableView.automaticDimension
case .testingStatus:
return subject.value.testingStatus == .initial ? 0 : UITableView.automaticDimension
- case .enableMethod, .cancelTest:
+ default:
return 0
}
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift
index ce3efe478b..3a3f285c49 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift
@@ -108,10 +108,10 @@ class MethodSettingsViewController: UITableViewController {
switch itemIdentifier {
case .name, .protocol, .proxyConfiguration, .cancelTest:
- return UIMetrics.SettingsCell.apiAccessCellHeight
+ return UITableView.automaticDimension
case .validationError:
return contentValidationErrors.isEmpty
- ? UIMetrics.SettingsCell.apiAccessCellHeight
+ ? 44.0
: UITableView.automaticDimension
case .testingStatus:
return UITableView.automaticDimension
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift
index b5f42d4144..cdc7389bef 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift
@@ -40,14 +40,23 @@ struct ListAccessMethodView<ViewModel>: View where ViewModel: ListAccessViewMode
value: "About API access…",
comment: ""
)
- MullvadInfoHeaderView(
- bodyText: text,
- link: about,
- onTapLink: viewModel.showAbout
- )
- .padding(.horizontal, 16)
- .padding(.bottom, 16)
- MullvadList(viewModel.items) { item in
+
+ MullvadList(viewModel.items, header: {
+ MullvadInfoHeaderView(
+ bodyText: text,
+ link: about,
+ onTapLink: viewModel.showAbout
+ )
+ .padding(.bottom, 16)
+ }, footer: {
+ MainButton(
+ text: LocalizedStringKey("Add"),
+ style: .default
+ ) {
+ viewModel.addNewMethod()
+ }
+ .padding(.top, 24)
+ }, content: { item in
let accessibilityId: AccessibilityIdentifier? = switch item.id {
case AccessMethodRepository.directId:
AccessibilityIdentifier.accessMethodDirectCell
@@ -86,7 +95,7 @@ struct ListAccessMethodView<ViewModel>: View where ViewModel: ListAccessViewMode
viewModel.methodSelected(item)
}
)
- }
+ })
.accessibilityIdentifier(
AccessibilityIdentifier.apiAccessListView.asString
)
@@ -97,15 +106,7 @@ struct ListAccessMethodView<ViewModel>: View where ViewModel: ListAccessViewMode
$0
}
}
- .padding(.bottom, 24)
- MainButton(
- text: LocalizedStringKey("Add"),
- style: .default
- ) {
- viewModel.addNewMethod()
- }
.accessibilityIdentifier(AccessibilityIdentifier.addAccessMethodButton.asString)
- .padding(.horizontal)
Spacer()
}
.background(Color.mullvadBackground)
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift
index ea4c6cd147..963ea59256 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift
@@ -109,7 +109,7 @@ class ListItemPickerViewController<DataSource: ListItemDataSourceProtocol>: UITa
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return UIMetrics.SettingsCell.apiAccessCellHeight
+ UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideStatusView.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideStatusView.swift
index 1618f9079f..49bf33e184 100644
--- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideStatusView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideStatusView.swift
@@ -14,11 +14,19 @@ class IPOverrideStatusView: UIView {
label.font = .mullvadTinySemiBold
label.adjustsFontForContentSizeCategory = true
label.textColor = .white
+ label.numberOfLines = 0
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()
private lazy var statusIcon: UIImageView = {
- return UIImageView()
+ let imageView = UIImageView()
+ imageView.contentMode = .scaleAspectFit
+ imageView.contentMode = .center
+ imageView.setContentHuggingPriority(.required, for: .horizontal)
+ imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
+ return imageView
}()
private lazy var descriptionLabel: UILabel = {
@@ -27,14 +35,17 @@ class IPOverrideStatusView: UIView {
label.adjustsFontForContentSizeCategory = true
label.textColor = .white.withAlphaComponent(0.6)
label.numberOfLines = 0
+ label.setContentHuggingPriority(.required, for: .vertical)
+ label.setContentCompressionResistancePriority(.required, for: .vertical)
return label
}()
init() {
super.init(frame: .zero)
- let titleContainerView = UIStackView(arrangedSubviews: [titleLabel, statusIcon, UIView()])
+ let titleContainerView = UIStackView(arrangedSubviews: [titleLabel, statusIcon])
titleContainerView.spacing = 6
+ titleContainerView.distribution = .fill
let contentContainterView = UIStackView(arrangedSubviews: [
titleContainerView,
diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift
index 5ccbf82744..ad1f252a18 100644
--- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift
@@ -20,7 +20,7 @@ class IPOverrideViewController: UIViewController {
private lazy var containerView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
- view.spacing = 20
+ view.spacing = 16
return view
}()
@@ -51,19 +51,12 @@ class IPOverrideViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
-
view.backgroundColor = .secondaryColor
- view.directionalLayoutMargins = UIMetrics.contentHeadingLayoutMargins
-
configureNavigation()
addHeaderView()
addImportButtons()
addStatusLabel()
-
- view.addConstrainedSubviews([containerView, clearButton]) {
- containerView.pinEdgesToSuperviewMargins(.all().excluding(.bottom))
- clearButton.pinEdgesToSuperviewMargins(PinnableEdges([.leading(0), .trailing(0), .bottom(16)]))
- }
+ addScrollView()
interactor.statusPublisher.sink { [weak self] status in
self?.statusView.setStatus(status)
@@ -80,6 +73,43 @@ class IPOverrideViewController: UIViewController {
)
}
+ private func addScrollView() {
+ let scrollView = UIScrollView()
+ let contentView = UIView()
+ contentView.directionalLayoutMargins = UIMetrics.contentHeadingLayoutMargins
+
+ view.addConstrainedSubviews([scrollView]) {
+ scrollView.pinEdgesToSuperview()
+ }
+
+ scrollView.addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
+ contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
+ contentView.heightAnchor.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.heightAnchor)
+ }
+
+ let spacer = UIView()
+
+ let contentStackView = UIStackView(arrangedSubviews: [containerView, spacer, clearButton])
+ contentStackView.axis = .vertical
+ contentStackView.distribution = .fill
+ contentStackView.spacing = 8
+
+ contentView.addConstrainedSubviews([contentStackView]) {
+ contentStackView.pinEdgesToSuperviewMargins()
+ }
+
+ // Hugging & resistance priorities
+ spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
+ spacer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+ containerView.setContentHuggingPriority(.required, for: .vertical)
+ containerView.setContentCompressionResistancePriority(.required, for: .vertical)
+
+ clearButton.setContentHuggingPriority(.required, for: .vertical)
+ clearButton.setContentCompressionResistancePriority(.required, for: .vertical)
+ }
+
private func addHeaderView() {
let body = NSLocalizedString(
"IP_OVERRIDE_HEADER_BODY",
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift
index 9178b65f2c..e0b950a4f2 100644
--- a/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift
@@ -73,7 +73,8 @@ class SettingsFieldValidationErrorContentView: UIView, UIContentView {
let label = UILabel()
label.text = error.errorDescription
label.numberOfLines = 0
- label.font = .systemFont(ofSize: 13)
+ label.adjustsFontForContentSizeCategory = true
+ label.font = .mullvadMini
label.textColor = .white.withAlphaComponent(0.6)
let stackView = UIStackView(arrangedSubviews: [icon, label])
diff --git a/ios/MullvadVPN/Coordinators/Settings/Views/SwitchRowView.swift b/ios/MullvadVPN/Coordinators/Settings/Views/SwitchRowView.swift
index e4540c0ccc..ed867ab582 100644
--- a/ios/MullvadVPN/Coordinators/Settings/Views/SwitchRowView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/Views/SwitchRowView.swift
@@ -28,7 +28,6 @@ struct SwitchRowView: View {
))
.disabled(disabled)
.font(.mullvadSmall)
- .frame(height: UIMetrics.SettingsRowView.height)
.padding(UIMetrics.SettingsRowView.layoutMargins)
.background(Color(.primaryColor))
.foregroundColor(Color(.primaryTextColor))
diff --git a/ios/MullvadVPN/Extensions/UIFont+Weight.swift b/ios/MullvadVPN/Extensions/UIFont+Weight.swift
index 146cbd128b..7b69712610 100644
--- a/ios/MullvadVPN/Extensions/UIFont+Weight.swift
+++ b/ios/MullvadVPN/Extensions/UIFont+Weight.swift
@@ -10,12 +10,7 @@ import UIKit
extension UIFont {
static func preferredFont(forTextStyle style: TextStyle, weight: Weight) -> UIFont {
- let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
- .addingAttributes([
- .traits: [UIFontDescriptor.TraitKey.weight: weight],
- ])
-
- return UIFont(descriptor: descriptor, size: 0)
+ return .preferredFont(forTextStyle: style).withWeight(weight)
}
func withWeight(_ weight: UIFont.Weight) -> UIFont {
diff --git a/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift b/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift
index 2247f826c2..aea21715a0 100644
--- a/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift
+++ b/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift
@@ -44,6 +44,7 @@ extension UIListContentConfiguration {
configuration.textProperties.color = .TableSection.headerTextColor
configuration.textProperties.font = .mullvadTiny
configuration.textProperties.adjustsFontForContentSizeCategory = true
+ configuration.textProperties.numberOfLines = 0
applyMargins(to: &configuration, tableStyle: tableStyle)
@@ -56,7 +57,7 @@ extension UIListContentConfiguration {
configuration.textProperties.color = .TableSection.footerTextColor
configuration.textProperties.font = .mullvadMini
configuration.textProperties.adjustsFontForContentSizeCategory = true
-
+ configuration.textProperties.numberOfLines = 0
applyMargins(to: &configuration, tableStyle: tableStyle)
return configuration
diff --git a/ios/MullvadVPN/UI appearance/Font+Mullvad.swift b/ios/MullvadVPN/UI appearance/Font+Mullvad.swift
index 9a77d6d690..387495fe01 100644
--- a/ios/MullvadVPN/UI appearance/Font+Mullvad.swift
+++ b/ios/MullvadVPN/UI appearance/Font+Mullvad.swift
@@ -1,25 +1,25 @@
import SwiftUI
extension Font {
- static let mullvadBig: Font = .largeTitle.bold()
- static let mullvadLarge: Font = .title.bold()
- static let mullvadMedium: Font = .title3.weight(.semibold)
- static let mullvadSmall: Font = .body
- static let mullvadSmallSemiBold: Font = mullvadSmall.weight(.semibold)
- static let mullvadTiny: Font = .subheadline
- static let mullvadTinySemiBold: Font = .mullvadTiny.weight(.semibold)
- static let mullvadMini: Font = .footnote
- static let mullvadMiniSemiBold: Font = mullvadMini.weight(.semibold)
+ static var mullvadBig: Font { .largeTitle.bold() }
+ static var mullvadLarge: Font { .title.bold() }
+ static var mullvadMedium: Font { .title3.weight(.semibold) }
+ static var mullvadSmall: Font { .body }
+ static var mullvadSmallSemiBold: Font { mullvadSmall.weight(.semibold) }
+ static var mullvadTiny: Font { .subheadline }
+ static var mullvadTinySemiBold: Font { .mullvadTiny.weight(.semibold) }
+ static var mullvadMini: Font { .footnote }
+ static var mullvadMiniSemiBold: Font { mullvadMini.weight(.semibold) }
}
extension UIFont {
- static let mullvadBig: UIFont = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
- static let mullvadLarge: UIFont = .preferredFont(forTextStyle: .title1, weight: .bold)
- static let mullvadMedium: UIFont = .preferredFont(forTextStyle: .title3, weight: .semibold)
- static let mullvadSmall: UIFont = .preferredFont(forTextStyle: .body)
- static let mullvadSmallSemiBold: UIFont = .preferredFont(forTextStyle: .body, weight: .semibold)
- static let mullvadTiny: UIFont = .preferredFont(forTextStyle: .subheadline)
- static let mullvadTinySemiBold: UIFont = .preferredFont(forTextStyle: .subheadline, weight: .semibold)
- static let mullvadMini: UIFont = .preferredFont(forTextStyle: .footnote)
- static let mullvadMiniSemiBold: UIFont = .preferredFont(forTextStyle: .footnote, weight: .semibold)
+ static var mullvadBig: UIFont { .preferredFont(forTextStyle: .largeTitle, weight: .bold) }
+ static var mullvadLarge: UIFont { .preferredFont(forTextStyle: .title1, weight: .bold) }
+ static var mullvadMedium: UIFont { .preferredFont(forTextStyle: .title3, weight: .semibold) }
+ static var mullvadSmall: UIFont { .preferredFont(forTextStyle: .body) }
+ static var mullvadSmallSemiBold: UIFont { .preferredFont(forTextStyle: .body, weight: .semibold) }
+ static var mullvadTiny: UIFont { .preferredFont(forTextStyle: .subheadline) }
+ static var mullvadTinySemiBold: UIFont { .preferredFont(forTextStyle: .subheadline, weight: .semibold) }
+ static var mullvadMini: UIFont { .preferredFont(forTextStyle: .footnote) }
+ static var mullvadMiniSemiBold: UIFont { .preferredFont(forTextStyle: .footnote, weight: .semibold) }
}
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 47e83662dc..a9562e1143 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -94,8 +94,6 @@ enum UIMetrics {
bottom: 8,
trailing: 16
)
- static let apiAccessCellHeight: CGFloat = 44
- static let customListsCellHeight: CGFloat = 44
static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4
static let apiAccessPickerListContentInsetTop: CGFloat = 16
static let verticalDividerHeight: CGFloat = 22
@@ -108,7 +106,6 @@ enum UIMetrics {
}
enum SettingsRowView {
- static let height: CGFloat = 44
static let cornerRadius: CGFloat = 10
static let layoutMargins = EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
static let footerLayoutMargins = EdgeInsets(top: 5, leading: 16, bottom: 5, trailing: 16)
diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
index 1513108013..0c8a87bd2c 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
@@ -130,17 +130,36 @@ class AccountContentView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
-
- directionalLayoutMargins = UIMetrics.contentLayoutMargins
setAccessibilityIdentifier(.accountView)
+ addScrollView()
+ }
+
+ private func addScrollView() {
+ let scrollView = UIScrollView()
+ let contentView = UIView()
- addConstrainedSubviews([contentStackView, buttonStackView]) {
+ addConstrainedSubviews([scrollView]) {
+ scrollView.pinEdgesToSuperviewMargins()
+ }
+
+ scrollView.addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
+ contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
+ contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.frameLayoutGuide.heightAnchor)
+ }
+
+ let spacer = UIView()
+
+ contentView.addConstrainedSubviews([contentStackView, spacer, buttonStackView]) {
contentStackView.pinEdgesToSuperviewMargins(.all().excluding(.bottom))
- buttonStackView.topAnchor.constraint(
- greaterThanOrEqualTo: contentStackView.bottomAnchor,
+ spacer.pinEdgesToSuperviewMargins(.all().excluding(.top).excluding(.bottom))
+ buttonStackView.pinEdgesToSuperviewMargins(.all().excluding(.top))
+
+ spacer.bottomAnchor.constraint(equalTo: buttonStackView.topAnchor)
+ spacer.topAnchor.constraint(
+ equalTo: contentStackView.bottomAnchor,
constant: UIMetrics.TableView.sectionSpacing
)
- buttonStackView.pinEdgesToSuperviewMargins(.all().excluding(.top))
}
}
diff --git a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
index 399358c82a..07cac860f1 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
@@ -38,14 +38,17 @@ class AccountDeviceRow: UIView {
label.font = .mullvadSmall
label.adjustsFontForContentSizeCategory = true
label.textColor = .white
+ label.numberOfLines = 0
return label
}()
- private let deviceManagementButton: UIButton = {
- let button = IncreasedHitButton(type: .system)
- button.isExclusiveTouch = true
- button.adjustsImageSizeForAccessibilityContentSizeCategory = true
- button.setAccessibilityIdentifier(.deviceManagementButton)
+ private let deviceManagementButton: UILabel = {
+ let button = UILabel()
+ button.adjustsFontForContentSizeCategory = true
+ button.isUserInteractionEnabled = true
+ button.numberOfLines = 0
+ button.textAlignment = .center
+
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.mullvadSmallSemiBold,
.foregroundColor: UIColor.primaryTextColor,
@@ -57,11 +60,10 @@ class AccountDeviceRow: UIView {
value: "Manage devices",
comment: ""
)
- let attributeString = NSMutableAttributedString(
+ button.attributedText = NSMutableAttributedString(
string: title,
attributes: attributes
)
- button.setAttributedTitle(attributeString, for: .normal)
return button
}()
@@ -73,22 +75,28 @@ class AccountDeviceRow: UIView {
contentContainerView.alignment = .leading
contentContainerView.spacing = 8
+ contentContainerView.setContentCompressionResistancePriority(.required, for: .horizontal)
+ contentContainerView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
+ deviceManagementButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ deviceManagementButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
addConstrainedSubviews(
[contentContainerView, deviceManagementButton]
) {
- contentContainerView.pinEdgesToSuperview()
- deviceManagementButton.centerYAnchor.constraint(equalTo: deviceLabel.centerYAnchor)
- deviceManagementButton.pinEdgeToSuperview(.trailing(0))
+ contentContainerView.pinEdgesToSuperview(PinnableEdges([.leading(0), .bottom(0), .top(0)]))
+ deviceManagementButton.topAnchor.constraint(equalTo: deviceLabel.topAnchor)
+ deviceManagementButton.pinEdgesToSuperview(PinnableEdges([.trailing(0), .bottom(0)]))
+ deviceManagementButton.leadingAnchor.constraint(equalTo: contentContainerView.trailingAnchor, constant: 16)
}
isAccessibilityElement = true
accessibilityLabel = titleLabel.text
- deviceManagementButton.addTarget(
- self,
- action: #selector(didTapDeviceManagementButton),
- for: .touchUpInside
- )
+ deviceManagementButton.addGestureRecognizer(UITapGestureRecognizer(
+ target: self,
+ action: #selector(didTapDeviceManagementButton)
+ ))
}
required init?(coder: NSCoder) {
diff --git a/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift b/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift
index 00ebb6b16e..7133739466 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift
@@ -53,6 +53,7 @@ class AccountExpiryRow: UIView {
comment: ""
)
textLabel.font = .mullvadTiny
+ textLabel.numberOfLines = 0
textLabel.adjustsFontForContentSizeCategory = true
textLabel.textColor = UIColor(white: 1.0, alpha: 0.6)
return textLabel
@@ -64,6 +65,7 @@ class AccountExpiryRow: UIView {
valueLabel.font = .mullvadSmall
valueLabel.adjustsFontForContentSizeCategory = true
valueLabel.textColor = .white
+ valueLabel.numberOfLines = 0
valueLabel.setAccessibilityIdentifier(.accountPagePaidUntilLabel)
return valueLabel
}()
diff --git a/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
index 6dbc787f58..c9d13e4058 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
@@ -40,9 +40,10 @@ class AccountNumberRow: UIView {
private let accountNumberLabel: UILabel = {
let textLabel = UILabel()
- textLabel.font = .mullvadMiniSemiBold
+ textLabel.font = .mullvadSmall
textLabel.adjustsFontForContentSizeCategory = true
textLabel.textColor = .white
+ textLabel.numberOfLines = 0
return textLabel
}()
@@ -73,7 +74,7 @@ class AccountNumberRow: UIView {
accountNumberLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: UIMetrics.padding8)
accountNumberLabel.leadingAnchor.constraint(equalTo: leadingAnchor)
- accountNumberLabel.trailingAnchor.constraint(equalTo: showHideButton.leadingAnchor)
+ accountNumberLabel.trailingAnchor.constraint(greaterThanOrEqualTo: showHideButton.leadingAnchor)
accountNumberLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
showHideButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor)
@@ -104,6 +105,15 @@ class AccountNumberRow: UIView {
isAccessibilityElement = true
accessibilityLabel = titleLabel.text
+ showHideButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+ showHideButton.setContentHuggingPriority(.required, for: .horizontal)
+
+ copyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+ copyButton.setContentHuggingPriority(.required, for: .horizontal)
+
+ accountNumberLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ accountNumberLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
showCheckmark(false)
updateView()
}
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 7d6c1302fa..831839ac50 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -34,7 +34,6 @@ class AccountViewController: UIViewController, @unchecked Sendable {
private let contentView: AccountContentView = {
let contentView = AccountContentView()
- contentView.translatesAutoresizingMaskIntoConstraints = false
return contentView
}()
@@ -116,16 +115,8 @@ class AccountViewController: UIViewController, @unchecked Sendable {
// MARK: - Private
private func configUI() {
- let scrollView = UIScrollView()
-
- view.addConstrainedSubviews([scrollView]) {
- scrollView.pinEdgesToSuperview()
- }
-
- scrollView.addConstrainedSubviews([contentView]) {
- contentView.pinEdgesToSuperview(.all().excluding(.bottom))
- contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.bottomAnchor)
- contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
+ view.addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
}
}
diff --git a/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift b/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
index 87c85913bb..64dc2dedd2 100644
--- a/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
+++ b/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
@@ -28,18 +28,20 @@ class RestorePurchasesView: UIView {
label.attributedText = makeAttributedString()
label.adjustsFontForContentSizeCategory = true
label.isUserInteractionEnabled = true
+ label.numberOfLines = 0
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapRestoreButton)))
return label
}()
private lazy var infoButton: UIButton = {
- let button = IncreasedHitButton(type: .custom)
+ let button = UIButton(type: .system)
+ button.adjustsImageSizeForAccessibilityContentSizeCategory = true
+ button.tintColor = .white
button.isExclusiveTouch = true
- button.setBackgroundImage(UIImage.Buttons.info, for: .normal)
+ button.setImage(UIImage.Buttons.info, for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside)
- button.heightAnchor.constraint(equalToConstant: UIMetrics.Button.accountInfoSize).isActive = true
- button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1).isActive = true
+ button.largeContentImageInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
return button
}()
diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceListView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceListView.swift
index d699648eb3..47d9c5fc87 100644
--- a/ios/MullvadVPN/View controllers/DeviceList/DeviceListView.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceListView.swift
@@ -4,6 +4,7 @@ struct DeviceListView: View {
@Binding var devices: [Device]?
@Binding var loading: Bool
var onRemoveDevice: ((Device) -> Void)?
+ let header: (() -> AnyView)?
struct Device: Identifiable, Hashable {
let id: String
@@ -20,15 +21,34 @@ struct DeviceListView: View {
}
var body: some View {
- if loading {
- ProgressView()
- .progressViewStyle(MullvadProgressViewStyle())
- .padding(.top, 24)
- Text("Fetching devices...")
- .padding(.top, 16)
- .foregroundColor(.mullvadTextPrimary.opacity(0.6))
- } else if let devices {
- MullvadList(devices) { device in
+ let headerContent: () -> some View = {
+ VStack {
+ if let header {
+ header()
+ }
+
+ if loading {
+ Spacer()
+
+ VStack(spacing: 16) {
+ ProgressView()
+ .progressViewStyle(MullvadProgressViewStyle())
+
+ Text("Fetching devices...")
+ .foregroundColor(.mullvadTextPrimary.opacity(0.6))
+ }
+
+ Spacer()
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ MullvadList(
+ devices ?? [],
+ header: headerContent,
+ footer: { EmptyView() },
+ content: { device in
MullvadListActionItemView(
item: .init(
id: device.id,
@@ -54,8 +74,8 @@ struct DeviceListView: View {
}
)
}
- .accessibilityIdentifier(.deviceListView)
- }
+ )
+ .accessibilityIdentifier(.deviceListView)
}
}
@@ -78,7 +98,8 @@ struct DeviceListView: View {
),
]),
loading: .constant(false),
- onRemoveDevice: nil
+ onRemoveDevice: nil,
+ header: nil
)
}
@@ -86,7 +107,8 @@ struct DeviceListView: View {
DeviceListView(
devices: .constant([]),
loading: .constant(true),
- onRemoveDevice: nil
+ onRemoveDevice: nil,
+ header: nil
)
.background(Color.mullvadBackground)
}
diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementView.swift
index 9094067a57..4a2b2a6996 100644
--- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementView.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementView.swift
@@ -4,11 +4,11 @@ import SwiftUI
struct DeviceManagementView: View {
enum Style {
case tooManyDevices((Bool) -> Void)
- case normal
+ case deviceManagement
var actionButtonTitle: LocalizedStringKey {
switch self {
- case .normal:
+ case .deviceManagement:
return "Remove"
case .tooManyDevices:
return "Yes, log out device"
@@ -19,7 +19,7 @@ struct DeviceManagementView: View {
switch self {
case .tooManyDevices:
.danger
- case .normal:
+ case .deviceManagement:
.default
}
}
@@ -35,7 +35,7 @@ struct DeviceManagementView: View {
LocalizedStringKey(
"Are you sure you want to log \(attributedDeviceName) out?"
)
- case .normal:
+ case .deviceManagement:
LocalizedStringKey("""
Remove \(attributedDeviceName)?
The device will be removed from the list and logged out.
@@ -49,7 +49,7 @@ struct DeviceManagementView: View {
let style: Style
let onError: (String, Error) -> Void
- @State private var loggedInDevices: [DeviceListView.Device]? = nil
+ @State private var loggedInDevices: [DeviceListView.Device]?
@State private var loading = true
var canLoginNewDevice: Bool {
@@ -61,17 +61,24 @@ struct DeviceManagementView: View {
var bodyText: LocalizedStringKey {
switch style {
- case .normal:
+ case .deviceManagement:
"""
View and manage all your logged in devices. \
You can have up to 5 devices on one account at a time. \
Each device gets a name when logged in to help you tell them apart easily.
"""
case .tooManyDevices:
- """
- Please log out of at least one by removing it from the list below. \
- You can find the corresponding device name under the device’s Account settings.
- """
+ if canLoginNewDevice {
+ """
+ You can now continue logging in on this device.
+ """
+
+ } else {
+ """
+ Please log out of at least one by removing it from the list below. \
+ You can find the corresponding device name under the device’s Account settings.
+ """
+ }
}
}
@@ -101,52 +108,6 @@ struct DeviceManagementView: View {
@State var deviceManagementAlert: MullvadAlert?
var body: some View {
VStack {
- if case .tooManyDevices = style {
- VStack(alignment: .leading, spacing: 8) {
- if canLoginNewDevice {
- HStack {
- Spacer()
- Image.mullvadIconSuccess
- Spacer()
- }
- Text("Super!")
- .font(.mullvadBig)
- .foregroundStyle(Color.mullvadTextPrimary)
- } else {
- HStack {
- Spacer()
- Image.mullvadIconFail
- Spacer()
- }
- Text("Too many devices")
- .font(.mullvadBig)
- .foregroundStyle(Color.mullvadTextPrimary)
- }
- }
- .padding(
- EdgeInsets(
- top: UIMetrics.contentLayoutMargins.top,
- leading: UIMetrics.contentLayoutMargins.leading,
- bottom: 0,
- trailing: UIMetrics.contentLayoutMargins.trailing
- )
- )
- }
- HStack {
- Text(bodyText)
- .foregroundColor(.mullvadTextPrimary)
- .opacity(0.6)
- .font(.mullvadTinySemiBold)
- Spacer()
- }
- .padding(
- EdgeInsets(
- top: 8,
- leading: UIMetrics.contentLayoutMargins.leading,
- bottom: 16,
- trailing: UIMetrics.contentLayoutMargins.trailing
- )
- )
DeviceListView(
devices: $loggedInDevices,
loading: $loading,
@@ -190,6 +151,36 @@ struct DeviceManagementView: View {
),
dismissButtonTitle: "Cancel"
)
+ }, header: {
+ AnyView(VStack(alignment: .leading, spacing: 8) {
+ if case .tooManyDevices = style {
+ if canLoginNewDevice {
+ HStack {
+ Spacer()
+ Image.mullvadIconSuccess
+ Spacer()
+ }
+ Text("Super!")
+ .font(.mullvadBig)
+ .foregroundStyle(Color.mullvadTextPrimary)
+ } else {
+ HStack {
+ Spacer()
+ Image.mullvadIconFail
+ Spacer()
+ }
+ Text("Too many devices")
+ .font(.mullvadBig)
+ .foregroundStyle(Color.mullvadTextPrimary)
+ }
+ }
+ Text(bodyText)
+ .foregroundColor(.mullvadTextPrimary)
+ .opacity(0.6)
+ .font(.mullvadTinySemiBold)
+ .padding(.bottom, 16.0)
+ }
+ )
}
)
Spacer()
@@ -254,7 +245,7 @@ struct DeviceManagementView: View {
NavigationView {
DeviceManagementView(
deviceManaging: MockDeviceManaging(),
- style: .normal,
+ style: .deviceManagement,
onError: { _, _ in }
)
.navigationTitle("Manage Devices")
diff --git a/ios/MullvadVPN/View controllers/RelayFilter/ChipFlowLayout.swift b/ios/MullvadVPN/View controllers/RelayFilter/ChipFlowLayout.swift
index e217cb7e5d..7bcd500a7a 100644
--- a/ios/MullvadVPN/View controllers/RelayFilter/ChipFlowLayout.swift
+++ b/ios/MullvadVPN/View controllers/RelayFilter/ChipFlowLayout.swift
@@ -8,38 +8,61 @@
import UIKit
-class ChipFlowLayout: UICollectionViewCompositionalLayout {
- init() {
- super.init { _, _ -> NSCollectionLayoutSection? in
- // Create an item with flexible size
- let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(50), heightDimension: .estimated(20))
- let item = NSCollectionLayoutItem(layoutSize: itemSize)
- item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
- leading: .fixed(0),
- top: .fixed(0),
- trailing: .fixed(0),
- bottom: .fixed(0)
- )
+class ChipFlowLayout: UICollectionViewFlowLayout {
+ override init() {
+ super.init()
+ estimatedItemSize = UICollectionViewFlowLayout.automaticSize
+ scrollDirection = .vertical
+ minimumInteritemSpacing = UIMetrics.FilterView.interChipViewSpacing
+ minimumLineSpacing = UIMetrics.FilterView.interChipViewSpacing
+ sectionInset = .zero
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
+ guard let originalAttributes = super.layoutAttributesForElements(in: rect) else {
+ return nil
+ }
+
+ let attributes = originalAttributes.compactMap { $0.copy() as? UICollectionViewLayoutAttributes }
- // Create a group that fills the available width and wraps items with proper spacing
- let groupSize = NSCollectionLayoutSize(
- widthDimension: .fractionalWidth(1.0),
- heightDimension: .estimated(20)
- )
- let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
- group.interItemSpacing = .fixed(UIMetrics.FilterView.interChipViewSpacing)
- group.contentInsets = .zero
+ // Detect RTL
+ let languageCode = Locale.current.languageCode ?? "en"
+ let isRTL = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
- // Create a section with zero inter-group spacing and no content insets
- let section = NSCollectionLayoutSection(group: group)
- section.interGroupSpacing = UIMetrics.FilterView.interChipViewSpacing
- section.contentInsets = .zero
+ var currentLineY: CGFloat = -1
+ var currentLineAttributes: [UICollectionViewLayoutAttributes] = []
- return section
+ for attribute in attributes where attribute.representedElementCategory == .cell {
+ if abs(attribute.frame.origin.y - currentLineY) > 1 {
+ // Align previous line before starting new
+ align(attributes: currentLineAttributes, isRTL: isRTL)
+ currentLineY = attribute.frame.origin.y
+ currentLineAttributes = [attribute]
+ } else {
+ currentLineAttributes.append(attribute)
+ }
}
+
+ // Align last line
+ align(attributes: currentLineAttributes, isRTL: isRTL)
+
+ return attributes
}
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
+ private func align(attributes: [UICollectionViewLayoutAttributes], isRTL: Bool) {
+ guard !attributes.isEmpty else { return }
+
+ var currentX = isRTL ? collectionViewContentSize.width - sectionInset.right : sectionInset.left
+
+ for attr in isRTL ? attributes.reversed() : attributes {
+ var frame = attr.frame
+ frame.origin.x = currentX - (isRTL ? frame.width : 0)
+ attr.frame = frame
+ currentX += (isRTL ? -1 : 1) * (frame.width + minimumInteritemSpacing)
+ }
}
}
diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
index 27110e97a2..5fb0fb2a0e 100644
--- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
+++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
@@ -110,7 +110,7 @@ class RelayFilterView: UIView {
contentContainer.distribution = .fill
collectionViewHeightConstraint = chipsView.collectionView.heightAnchor
- .constraint(equalToConstant: 8)
+ .constraint(greaterThanOrEqualToConstant: 8)
collectionViewHeightConstraint.isActive = true
dummyView.addConstrainedSubviews([titleLabel]) {
diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterViewController.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterViewController.swift
index 5ff7700741..d63214f9f8 100644
--- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterViewController.swift
+++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterViewController.swift
@@ -30,7 +30,8 @@ class RelayFilterViewController: UIViewController {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
- label.font = .preferredFont(forTextStyle: .body)
+ label.font = .mullvadSmall
+ label.adjustsFontForContentSizeCategory = true
label.textColor = .secondaryTextColor
label.textAlignment = .center
return label
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/DAITAInfoView.swift b/ios/MullvadVPN/View controllers/SelectLocation/DAITAInfoView.swift
index bdfea369b4..35e9a4e785 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/DAITAInfoView.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/DAITAInfoView.swift
@@ -28,11 +28,12 @@ class DAITAInfoView: UIView {
comment: ""
),
attributes: [
- .font: UIFont.systemFont(ofSize: 15),
+ .font: UIFont.mullvadSmall,
.foregroundColor: UIColor.white,
.paragraphStyle: infoTextParagraphStyle,
]
)
+ label.adjustsFontForContentSizeCategory = true
return label
}()
@@ -60,11 +61,10 @@ class DAITAInfoView: UIView {
settingsButton.addTarget(self, action: #selector(didPressButton), for: .touchUpInside)
addConstrainedSubviews([infoLabel, settingsButton]) {
- infoLabel.pinEdgesToSuperviewMargins(.init([.leading(24), .trailing(24)]))
+ infoLabel.pinEdgesToSuperviewMargins(.init([.leading(24), .trailing(24), .top(8)]))
settingsButton.pinEdgesToSuperviewMargins(.init([.leading(0), .trailing(0)]))
settingsButton.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 32)
- settingsButton.bottomAnchor.constraint(equalTo: centerYAnchor)
}
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
index e497625583..5e018cd656 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
@@ -251,71 +251,57 @@ extension LocationDataSource {
extension LocationDataSource: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ guard let headerView = tableView
+ .dequeueReusableHeaderFooterView(withIdentifier: LocationSectionHeaderFooterView
+ .reuseIdentifier
+ ) as? LocationSectionHeaderFooterView else { return nil }
+
switch sections[section] {
case .allLocations:
- LocationSectionHeaderFooterView(
- configuration: LocationSectionHeaderFooterView.Configuration(
- name: LocationSection.allLocations.header,
- style: .header
- )
- )
+ headerView.configure(configuration: LocationSectionHeaderFooterView.Configuration(
+ name: LocationSection.allLocations.header,
+ style: .header
+ ))
case .customLists:
- LocationSectionHeaderFooterView(configuration: LocationSectionHeaderFooterView.Configuration(
+ headerView.configure(configuration: LocationSectionHeaderFooterView.Configuration(
name: LocationSection.customLists.header,
style: .header,
- primaryAction: UIAction(
- handler: { [weak self] _ in
- self?.didTapEditCustomLists?()
- }
- )
+ primaryAction: UIAction { [weak self] _ in
+ self?.didTapEditCustomLists?()
+ }
))
}
+
+ return headerView
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
+ guard let footerView = tableView
+ .dequeueReusableHeaderFooterView(withIdentifier: LocationSectionHeaderFooterView
+ .reuseIdentifier
+ ) as? LocationSectionHeaderFooterView else { return nil }
+
switch sections[section] {
case .allLocations:
- return LocationSectionHeaderFooterView(configuration: LocationSectionHeaderFooterView.Configuration(
+ guard dataSources[section].nodes.isEmpty else {
+ return nil
+ }
+ footerView.configure(configuration: LocationSectionHeaderFooterView.Configuration(
name: LocationSection.allLocations.footer,
- style: .footer,
- directionalEdgeInsets: NSDirectionalEdgeInsets(top: 24, leading: 16, bottom: 0, trailing: 16)
+ style: .footer
))
case .customLists:
guard dataSources[section].nodes.isEmpty else {
return nil
}
-
- let text = NSMutableAttributedString(string: NSLocalizedString(
- "CUSTOM_LIST_FOOTER",
- tableName: "SelectLocation",
- value: "To create a custom list, tap on ",
- comment: ""
+ footerView.configure(configuration: LocationSectionHeaderFooterView.Configuration(
+ name: LocationSection.customLists.footer,
+ style: .footer,
+ directionalEdgeInsets: NSDirectionalEdgeInsets(top: 11, leading: 16, bottom: 24, trailing: 8)
))
-
- text.append(NSAttributedString(string: "\""))
-
- if let image = UIImage(systemName: "ellipsis") {
- text.append(NSAttributedString(attachment: NSTextAttachment(image: image)))
- } else {
- text.append(NSAttributedString(string: "..."))
- }
-
- text.append(NSAttributedString(string: "\""))
-
- var contentConfiguration = UIListContentConfiguration.mullvadGroupedFooter(tableStyle: tableView.style)
- contentConfiguration.attributedText = text
-
- return UIListContentView(configuration: contentConfiguration)
}
- }
- func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
- switch sections[section] {
- case .allLocations:
- dataSources[section].nodes.isEmpty ? 80 : .zero
- case .customLists:
- dataSources[section].nodes.isEmpty ? UITableView.automaticDimension : 24
- }
+ return footerView
}
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift
index 2992dbf1e9..f92d86e8b3 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift
@@ -31,7 +31,12 @@ enum LocationSection: String, Hashable, CaseIterable, CellIdentifierProtocol, Se
var footer: String {
switch self {
case .customLists:
- return ""
+ return NSLocalizedString(
+ "CUSTOM_LIST_FOOTER",
+ tableName: "SelectLocation",
+ value: "To create a custom list, tap on \"...\" ",
+ comment: ""
+ )
case .allLocations:
return NSLocalizedString(
"FOOTER_SELECT_LOCATION_ALL_LOCATIONS",
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderFooterView.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderFooterView.swift
index dcce16c689..c49e04efff 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderFooterView.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderFooterView.swift
@@ -1,5 +1,5 @@
//
-// LocationSectionHeaderView.swift
+// LocationSectionHeaderFooterView.swift
// MullvadVPN
//
// Created by Mojgan on 2024-01-25.
@@ -9,116 +9,86 @@
import Foundation
import UIKit
-class LocationSectionHeaderFooterView: UIView, UIContentView {
- var configuration: UIContentConfiguration {
- get {
- actualConfiguration
- } set {
- guard let newConfiguration = newValue as? Configuration,
- actualConfiguration != newConfiguration else { return }
- actualConfiguration = newConfiguration
- apply(configuration: newConfiguration)
- }
- }
+class LocationSectionHeaderFooterView: UITableViewHeaderFooterView {
+ static let reuseIdentifier = "LocationSectionHeaderFooterView"
- private var actualConfiguration: Configuration
+ private let label = UILabel()
+ private let button = UIButton(type: .system)
- private let containerView: UIStackView = {
- let containerView = UIStackView()
- containerView.axis = .horizontal
- containerView.spacing = 8
- containerView.isLayoutMarginsRelativeArrangement = true
- return containerView
- }()
-
- private let nameLabel: UILabel = {
- let label = UILabel()
- label.numberOfLines = 0
- label.lineBreakMode = .byWordWrapping
- label.textColor = .primaryTextColor
- label.font = .systemFont(ofSize: 16, weight: .semibold)
- return label
- }()
+ override init(reuseIdentifier: String?) {
+ super.init(reuseIdentifier: reuseIdentifier)
- private let actionButton: UIButton = {
- let button = UIButton(type: .system)
+ // Configure button
button.setImage(UIImage(systemName: "ellipsis"), for: .normal)
button.tintColor = UIColor(white: 1, alpha: 0.6)
- return button
- }()
- init(configuration: Configuration) {
- self.actualConfiguration = configuration
- super.init(frame: .zero)
- apply(configuration: configuration)
- addSubviews()
+ contentView.addConstrainedSubviews([label, button]) {
+ label.pinEdgesToSuperviewMargins(.all().excluding(.trailing))
+ button.pinEdgesToSuperviewMargins(.all().excluding(.leading))
+ button.leadingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: 8)
+ }
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
- private func addSubviews() {
- containerView.addArrangedSubview(nameLabel)
- containerView.addArrangedSubview(actionButton)
- addConstrainedSubviews([containerView]) {
- containerView.pinEdgesToSuperviewMargins()
- actionButton.widthAnchor.constraint(equalTo: actionButton.heightAnchor)
- }
- }
+ func configure(configuration: Configuration) {
+ var contentConfig = UIListContentConfiguration.groupedHeader()
+ contentConfig.text = configuration.name
+ contentConfig.textProperties.alignment = configuration.style.textAlignment
+ contentConfig.textProperties.color = configuration.style.textColor
+ contentConfig.textProperties.font = configuration.style.font
+ contentConfig.textProperties.adjustsFontForContentSizeCategory = true
- private func apply(configuration: Configuration) {
- let isActionHidden = configuration.primaryAction == nil
- backgroundColor = configuration.style.backgroundColor
- nameLabel.textColor = configuration.style.textColor
- nameLabel.text = configuration.name
- nameLabel.font = configuration.style.font
- nameLabel.adjustsFontForContentSizeCategory = true
- nameLabel.textAlignment = configuration.style.textAlignment
- actionButton.isHidden = isActionHidden
- actionButton.accessibilityIdentifier = nil
- actualConfiguration.primaryAction.flatMap { action in
- actionButton.setAccessibilityIdentifier(.openCustomListsMenuButton)
- actionButton.addAction(action, for: .touchUpInside)
+ contentView.backgroundColor = configuration.style.backgroundColor
+ directionalLayoutMargins = configuration.directionalEdgeInsets
+
+ // Apply the font and color directly to the label:
+ label.text = configuration.name
+ label.font = contentConfig.textProperties.font
+ label.textColor = contentConfig.textProperties.color
+ label.adjustsFontForContentSizeCategory = true
+ label.numberOfLines = 0
+ label.lineBreakMode = .byWordWrapping
+ label.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+ if let buttonAction = configuration.primaryAction {
+ button.isHidden = false
+ button.removeTarget(nil, action: nil, for: .allEvents)
+ button.addAction(buttonAction, for: .touchUpInside)
+ } else {
+ button.isHidden = true
}
- directionalLayoutMargins = actualConfiguration.directionalEdgeInsets
}
}
extension LocationSectionHeaderFooterView {
- struct Style: Equatable {
+ struct Style: Equatable, @unchecked Sendable {
let font: UIFont
let textColor: UIColor
- let textAlignment: NSTextAlignment
+ let textAlignment: UIListContentConfiguration.TextProperties.TextAlignment
let backgroundColor: UIColor
static let header = Style(
- font: .preferredFont(forTextStyle: .body, weight: .semibold),
+ font: .mullvadSmallSemiBold,
textColor: .primaryTextColor,
textAlignment: .natural,
backgroundColor: .primaryColor
)
static let footer = Style(
- font: .preferredFont(forTextStyle: .body, weight: .regular),
+ font: .mullvadTiny,
textColor: .secondaryTextColor,
textAlignment: .center,
backgroundColor: .clear
)
}
- struct Configuration: UIContentConfiguration, Equatable {
+ struct Configuration {
let name: String
let style: Style
- var directionalEdgeInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 8)
+ var directionalEdgeInsets = NSDirectionalEdgeInsets(top: 11, leading: 16, bottom: 11, trailing: 8)
var primaryAction: UIAction?
-
- func makeContentView() -> UIView & UIContentView {
- LocationSectionHeaderFooterView(configuration: self)
- }
-
- func updated(for state: UIConfigurationState) -> Configuration {
- self
- }
}
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
index 8f1336ff21..c3d8ed2110 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
@@ -171,11 +171,20 @@ final class LocationViewController: UIViewController {
tableView.backgroundColor = view.backgroundColor
tableView.separatorColor = .secondaryColor
tableView.separatorInset = .zero
- tableView.rowHeight = UIMetrics.TableView.rowHeight
- tableView.sectionHeaderHeight = UIMetrics.TableView.rowHeight
+ tableView.estimatedRowHeight = UIMetrics.TableView.rowHeight
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.estimatedSectionHeaderHeight = UIMetrics.TableView.rowHeight
+ tableView.sectionHeaderHeight = UITableView.automaticDimension
+ tableView.estimatedSectionFooterHeight = UIMetrics.TableView.rowHeight
+ tableView.sectionFooterHeight = UITableView.automaticDimension
tableView.indicatorStyle = .white
tableView.keyboardDismissMode = .onDrag
tableView.setAccessibilityIdentifier(.selectLocationTableView)
+
+ tableView.register(
+ LocationSectionHeaderFooterView.self,
+ forHeaderFooterViewReuseIdentifier: LocationSectionHeaderFooterView.reuseIdentifier
+ )
}
private func setUpTopContent() {
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
index e7db58800e..b2e8ea61a0 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
@@ -46,7 +46,7 @@ final class LocationViewControllerWrapper: UIViewController {
private var entryLocationViewController: LocationViewController?
private let exitLocationViewController: LocationViewController
- private let segmentedControl = UISegmentedControl()
+ private let segmentedControl = ScaledSegmentedControl()
private let locationViewContainer = UIView()
private var settings: LatestTunnelSettings
private var relaySelectorWrapper: RelaySelectorWrapper
@@ -204,10 +204,6 @@ final class LocationViewControllerWrapper: UIViewController {
private func setUpSegmentedControl() {
segmentedControl.backgroundColor = .SegmentedControl.backgroundColor
segmentedControl.selectedSegmentTintColor = .SegmentedControl.selectedColor
- segmentedControl.setTitleTextAttributes([
- .foregroundColor: UIColor.white,
- .font: UIFont.systemFont(ofSize: 17, weight: .medium),
- ], for: .normal)
segmentedControl.insertSegment(
withTitle: MultihopContext.entry.description,
@@ -226,10 +222,11 @@ final class LocationViewControllerWrapper: UIViewController {
private func addSubviews() {
view.addConstrainedSubviews([segmentedControl, locationViewContainer]) {
- segmentedControl.heightAnchor.constraint(equalToConstant: 44)
+ segmentedControl.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
segmentedControl.pinEdgesToSuperviewMargins(PinnableEdges([.top(0), .leading(8), .trailing(8)]))
locationViewContainer.pinEdgesToSuperview(.all().excluding(.top))
+ locationViewContainer.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 8)
if settings.tunnelMultihopState.isEnabled {
locationViewContainer.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 4)
@@ -324,7 +321,7 @@ private extension WireGuardObfuscationState {
/// This flag affects whether the "Setting: Obfuscation" pill is shown when selecting a location
var affectsRelaySelection: Bool {
switch self {
- case .shadowsocks:
+ case .shadowsocks, .quic:
true
default: false
}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
index b2cb09788f..fe701f7818 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
@@ -62,6 +62,7 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
let titleLabel: UILabel = {
let label = UILabel()
label.font = .mullvadSmallSemiBold
+ label.numberOfLines = 0
label.adjustsFontForContentSizeCategory = true
label.textColor = UIColor.Cell.titleTextColor
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
@@ -72,6 +73,7 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
let detailTitleLabel: UILabel = {
let label = UILabel()
label.font = .mullvadTiny
+ label.numberOfLines = 0
label.adjustsFontForContentSizeCategory = true
label.textColor = UIColor.Cell.detailTextColor
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDNSInfoCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDNSInfoCell.swift
index a8b85961f8..70ba6039d1 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsDNSInfoCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsDNSInfoCell.swift
@@ -21,6 +21,8 @@ class SettingsDNSInfoCell: UITableViewCell {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.textColor = UIColor.Cell.titleTextColor
titleLabel.numberOfLines = 0
+ titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
+ titleLabel.setContentHuggingPriority(.defaultLow, for: .vertical)
contentView.addConstrainedSubviews([titleLabel]) {
titleLabel.pinEdgesToSuperviewMargins()
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
index 356ae9bf5c..f76100bd29 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
@@ -423,7 +423,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
configureQuantumResistanceHeader(view)
return view
default:
- return nil
+ return UIView()
}
}
@@ -443,7 +443,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
case .localNetworkSharing: return .leastNonzeroMagnitude
#endif
default:
- return tableView.estimatedRowHeight
+ return UITableView.automaticDimension
}
}
@@ -451,8 +451,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
let sectionIdentifier = snapshot().sectionIdentifiers[section]
return switch sectionIdentifier {
- // 0 due to there already being a separator between .dnsSettings and .ipOverrides.
- case .dnsSettings: 0
case .ipOverrides, .quantumResistance: UITableView.automaticDimension
#if DEBUG
case .localNetworkSharing: UITableView.automaticDimension
diff --git a/ios/MullvadVPN/Views/List/MullvadList.swift b/ios/MullvadVPN/Views/List/MullvadList.swift
index c951292fb5..20195f92cf 100644
--- a/ios/MullvadVPN/Views/List/MullvadList.swift
+++ b/ios/MullvadVPN/Views/List/MullvadList.swift
@@ -7,62 +7,70 @@ import SwiftUI
/// * transparent background
struct MullvadList<Content: View, Data: RandomAccessCollection<ID>, ID: Hashable>: View {
let data: Data
+ let id: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Content
- let id: KeyPath<Data.Element, ID>?
+ let header: (() -> AnyView)?
+ let footer: (() -> AnyView)?
- @State var itemHeight: CGFloat = 0
- var maxListHeight: CGFloat {
- let height = itemHeight * CGFloat(data.count)
- return height > 0 ? height : .infinity
- }
-
- init(_ data: Data, id: KeyPath<Data.Element, ID>, @ViewBuilder content: @escaping (Data.Element) -> Content) {
+ init(
+ _ data: Data,
+ id: KeyPath<Data.Element, ID>,
+ header: (() -> some View)? = nil,
+ footer: (() -> some View)? = nil,
+ @ViewBuilder content: @escaping (Data.Element) -> Content
+ ) {
self.data = data
self.id = id
+ self.header = header.map { builder in { AnyView(builder()) } }
+ self.footer = footer.map { builder in { AnyView(builder()) } }
self.content = content
}
- init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) {
- self.data = data
- self.content = content
- self.id = nil
+ init(
+ _ data: Data,
+ header: (() -> some View)? = nil,
+ footer: (() -> some View)? = nil,
+ @ViewBuilder content: @escaping (Data.Element) -> Content
+ ) where Data.Element == ID {
+ self.init(data, id: \.self, header: header, footer: footer, content: content)
}
var body: some View {
- List(data, id: id ?? \.self) { item in
- content(item)
- .sizeOfView { size in
- if itemHeight < size.height {
- itemHeight = size.height
- }
- }
- .listRowInsets(.init())
- .listSectionSeparator(.hidden, edges: .bottom)
- .listRowSeparatorTint(.MullvadList.separator)
- .listRowBackground(Color.clear)
- .apply {
- if #available(iOS 16.0, *) {
- $0.alignmentGuide(.listRowSeparatorLeading) { _ in
- 0
- }
- } else {
- $0
- }
- }
- .frame(height: itemHeight)
+ List {
+ if let headerView = header?() {
+ headerView
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ }
+
+ ForEach(data, id: id) { item in
+ content(item)
+ .listRowInsets(.init())
+ .listSectionSeparator(.hidden, edges: .bottom)
+ .listRowSeparatorTint(.MullvadList.separator)
+ .listRowBackground(Color.clear)
+ }
+
+ if let footerView = footer?() {
+ footerView
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ }
}
.listStyle(.plain)
- .frame(maxHeight: maxListHeight)
}
}
#Preview {
- MullvadList([1, 2, 3]) { item in
- VStack {
- ForEach(0 ..< item, id: \.self) {
- Text("\($0)")
- }
+ MullvadList(
+ [1, 2, 3],
+ header: {
+ Text("Header")
+ }, footer: {
+ Text("Footer")
+ },
+ content: { item in
+ Text("Item \(item)").padding()
}
- .padding()
- }
+ )
}
diff --git a/ios/MullvadVPN/Views/List/MullvadListActionItemView.swift b/ios/MullvadVPN/Views/List/MullvadListActionItemView.swift
index 195ebea091..83d371a115 100644
--- a/ios/MullvadVPN/Views/List/MullvadListActionItemView.swift
+++ b/ios/MullvadVPN/Views/List/MullvadListActionItemView.swift
@@ -47,6 +47,7 @@ struct MullvadListActionItemView<Icon: View>: View {
Text(detail)
.foregroundStyle(Color(.Cell.detailTextColor.withAlphaComponent(0.6)))
.font(.mullvadMiniSemiBold)
+ .fixedSize(horizontal: false, vertical: true)
}
}
Spacer()
@@ -61,16 +62,16 @@ struct MullvadListActionItemView<Icon: View>: View {
} label: {
icon
}
+ .padding(.leading, 8)
.accessibilityIdentifier(accessibilityIdentifier)
}
}
.padding(EdgeInsets(
- top: 12,
+ top: 8,
leading: UIMetrics.contentLayoutMargins.leading,
- bottom: 12,
+ bottom: 8,
trailing: UIMetrics.contentLayoutMargins.trailing
))
- .frame(minHeight: UIMetrics.TableView.rowHeight, maxHeight: .infinity)
.background(Color.MullvadList.background)
}
}
@@ -98,13 +99,16 @@ struct MullvadListActionItemView<Icon: View>: View {
accessibilityIdentifier: nil,
pressed: nil
),
- ]
- ) { item in
- MullvadListActionItemView(item: item) {
- if item.pressed != nil {
- Image.mullvadIconClose
+ ],
+ header: { EmptyView() },
+ footer: { EmptyView() },
+ content: { item in
+ MullvadListActionItemView(item: item) {
+ if item.pressed != nil {
+ Image.mullvadIconClose
+ }
}
}
- }
+ )
}
}
diff --git a/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift b/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift
index 619212184d..ed1be91a5c 100644
--- a/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift
+++ b/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift
@@ -46,10 +46,12 @@ struct MullvadListNavigationItemView: View {
Text(title)
.foregroundStyle(Color(.Cell.titleTextColor))
.font(.mullvadSmallSemiBold)
+ .fixedSize(horizontal: false, vertical: true)
if let detail {
Text(detail)
.foregroundStyle(Color(.Cell.detailTextColor.withAlphaComponent(0.6)))
.font(.mullvadMiniSemiBold)
+ .fixedSize(horizontal: false, vertical: true)
}
}
Spacer()
@@ -57,12 +59,12 @@ struct MullvadListNavigationItemView: View {
Text(state)
.foregroundStyle(Color(.Cell.titleTextColor.withAlphaComponent(0.6)))
.font(.mullvadTiny)
+ .fixedSize(horizontal: false, vertical: true)
}
Image(.iconChevron)
}
.padding(.horizontal, 16)
- .padding(.vertical, 12)
- .frame(minHeight: UIMetrics.TableView.rowHeight, maxHeight: .infinity)
+ .padding(.vertical, 11)
.background(
isPressed ? Color.MullvadButton.primaryPressed : Color.MullvadButton
.primary
@@ -114,9 +116,12 @@ private struct MullvadListButtonStyle: ButtonStyle {
accessibilityIdentifier: nil,
didSelect: { print("selected") }
),
- ]
- ) { item in
- MullvadListNavigationItemView(item: item)
- }
+ ],
+ header: { EmptyView() },
+ footer: { EmptyView() },
+ content: { item in
+ MullvadListNavigationItemView(item: item)
+ }
+ )
}
}
diff --git a/ios/MullvadVPN/Views/ScaledSegmentedControl.swift b/ios/MullvadVPN/Views/ScaledSegmentedControl.swift
new file mode 100644
index 0000000000..ab477a3be5
--- /dev/null
+++ b/ios/MullvadVPN/Views/ScaledSegmentedControl.swift
@@ -0,0 +1,61 @@
+//
+// ScaledSegmentedControl.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-07-02.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import UIKit
+
+final class ScaledSegmentedControl: UISegmentedControl {
+ private let textStyle: UIFont.TextStyle
+ private let fontWeight: UIFont.Weight
+
+ init(textStyle: UIFont.TextStyle = .body, weight: UIFont.Weight = .regular) {
+ self.textStyle = textStyle
+ self.fontWeight = weight
+ super.init(frame: .zero)
+ applyTextAttributes()
+ subscribeToDynamicType()
+ }
+
+ required init?(coder: NSCoder) {
+ self.textStyle = .body
+ self.fontWeight = .regular
+ super.init(coder: coder)
+ applyTextAttributes()
+ subscribeToDynamicType()
+ }
+
+ override func insertSegment(withTitle title: String?, at segment: Int, animated: Bool) {
+ super.insertSegment(withTitle: title, at: segment, animated: animated)
+ applyTextAttributes()
+ }
+
+ private func applyTextAttributes() {
+ let font = UIFont.preferredFont(forTextStyle: textStyle).withWeight(fontWeight)
+ let attributes: [NSAttributedString.Key: Any] = [
+ .font: font,
+ .foregroundColor: UIColor.primaryTextColor,
+ ]
+ setTitleTextAttributes(attributes, for: .normal)
+ setTitleTextAttributes(attributes, for: .selected)
+ }
+
+ private func subscribeToDynamicType() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(contentSizeChanged),
+ name: UIContentSizeCategory.didChangeNotification,
+ object: nil
+ )
+ }
+
+ @objc private func contentSizeChanged() {
+ applyTextAttributes()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+}