diff options
| author | mojganii <mojgan.jelodar@mullvad.net> | 2025-07-01 13:46:30 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-07-17 14:05:24 +0200 |
| commit | 6e92f0fe096048770e534d30ca04087d4bc714a4 (patch) | |
| tree | c1ab9ce0daea35bfceff8b721f56032b30d99b37 | |
| parent | ae02a1f9b790d5d8966b55f918227a0983789035 (diff) | |
| download | mullvadvpn-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
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) + } +} |
