summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2023-04-20 11:34:37 +0200
committerJon Petersson <jon.petersson@kvadrat.se>2023-04-25 13:39:39 +0200
commitb7199a6598a10ce747730eb01abe3161146df8f2 (patch)
tree239d4f3c7f30af75e1ad5a67982f20214d7980d8
parent43412a54f7812194e6358920dc95a9246541fa36 (diff)
downloadmullvadvpn-b7199a6598a10ce747730eb01abe3161146df8f2.tar.xz
mullvadvpn-b7199a6598a10ce747730eb01abe3161146df8f2.zip
Improve content blocker settings interface
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj19
-rw-r--r--ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift2
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json16
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf71
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift10
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift130
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift49
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCell.swift54
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift143
-rwxr-xr-xios/convert-assets.rb4
11 files changed, 454 insertions, 49 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 3180a3d59e..ad4cec56cd 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -359,6 +359,7 @@
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; };
+ 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; };
@@ -945,6 +946,7 @@
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
+ 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentBlockersHeaderView.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; };
@@ -1276,17 +1278,19 @@
583FE01829C19709006E85F9 /* Settings */ = {
isa = PBXGroup;
children = (
- 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */,
- 58CCA01122424D11004F3011 /* SettingsViewController.swift */,
- 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */,
- 58677711290976FB006F721F /* SettingsInteractor.swift */,
- 582BB1AE229566420055B6EF /* SettingsCell.swift */,
- 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
- 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
+ 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */,
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */,
+ 582BB1AE229566420055B6EF /* SettingsCell.swift */,
+ 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */,
+ 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */,
58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */,
58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */,
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
+ 58677711290976FB006F721F /* SettingsInteractor.swift */,
+ 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */,
+ 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
+ 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
+ 58CCA01122424D11004F3011 /* SettingsViewController.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -2715,6 +2719,7 @@
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */,
586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */,
+ 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */,
5878F50229CDB989003D4BE2 /* ChangeLogCoordinator.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
diff --git a/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
index 307978927b..0034494437 100644
--- a/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
+++ b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
@@ -31,7 +31,7 @@ extension CustomDateComponentsFormatting {
let dateComponents = calendar.dateComponents([.year, .day], from: start, to: end)
let years = dateComponents.year ?? 0
- var days = dateComponents.day ?? 0
+ let days = dateComponents.day ?? 0
if years >= 2 {
formatter.allowedUnits = [.year]
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json
new file mode 100644
index 0000000000..87bd86d2ec
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "IconInfo.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf
new file mode 100644
index 0000000000..07f05b149b
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf
@@ -0,0 +1,71 @@
+%PDF-1.5
+%
+4 0 obj
+<< /Length 5 0 R
+ /Filter /FlateDecode
+>>
+stream
+xUMn0>
+`=B̢2"a焁np<w"d
+fnâzL?A1H]بKQ8FXBGj=2q0rM𹂝b gK(5ly!%F.-W
+V8Pwf2q͖qhVkz"׏_~g 床mDrucY8-W:IMxAXUZݝe- }~V {+/ױzM+ٰze+j S27@>{~b
+endstream
+endobj
+5 0 obj
+ 296
+endobj
+3 0 obj
+<<
+ /ExtGState <<
+ /a0 << /CA 1 /ca 1 >>
+ >>
+>>
+endobj
+2 0 obj
+<< /Type /Page % 1
+ /Parent 1 0 R
+ /MediaBox [ 0 0 18 18 ]
+ /Contents 4 0 R
+ /Group <<
+ /Type /Group
+ /S /Transparency
+ /I true
+ /CS /DeviceRGB
+ >>
+ /Resources 3 0 R
+>>
+endobj
+1 0 obj
+<< /Type /Pages
+ /Kids [ 2 0 R ]
+ /Count 1
+>>
+endobj
+6 0 obj
+<< /Producer (cairo 1.16.0 (https://cairographics.org))
+ /CreationDate (20200729113941+00'00)
+>>
+endobj
+7 0 obj
+<< /Type /Catalog
+ /Pages 1 0 R
+>>
+endobj
+xref
+0 8
+0000000000 65535 f
+0000000698 00000 n
+0000000482 00000 n
+0000000410 00000 n
+0000000015 00000 n
+0000000388 00000 n
+0000000763 00000 n
+0000000877 00000 n
+trailer
+<< /Size 8
+ /Root 7 0 R
+ /Info 6 0 R
+>>
+startxref
+929
+%%EOF
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
index 8f78abd844..38fc56090f 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
@@ -12,6 +12,7 @@ protocol PreferencesCellEventHandler {
func addDNSEntry()
func didChangeDNSEntry(with identifier: UUID, inputString: String) -> Bool
func didChangeState(for item: PreferencesDataSource.Item, isOn: Bool)
+ func didPressInfoButton(for item: PreferencesDataSource.Item)
}
final class PreferencesCellFactory: CellFactoryProtocol {
@@ -45,6 +46,7 @@ final class PreferencesCellFactory: CellFactoryProtocol {
comment: ""
)
cell.accessibilityHint = nil
+ cell.applySubCellStyling()
cell.setOn(viewModel.blockAdvertising, animated: false)
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(
@@ -63,6 +65,7 @@ final class PreferencesCellFactory: CellFactoryProtocol {
comment: ""
)
cell.accessibilityHint = nil
+ cell.applySubCellStyling()
cell.setOn(viewModel.blockTracking, animated: false)
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(
@@ -81,7 +84,12 @@ final class PreferencesCellFactory: CellFactoryProtocol {
comment: ""
)
cell.accessibilityHint = nil
+ cell.applySubCellStyling()
+ cell.setInfoButtonIsVisible(true)
cell.setOn(viewModel.blockMalware, animated: false)
+ cell.infoButtonHandler = { [weak self] in
+ self?.delegate?.didPressInfoButton(for: .blockMalware)
+ }
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(for: .blockMalware, isOn: isOn)
}
@@ -96,6 +104,7 @@ final class PreferencesCellFactory: CellFactoryProtocol {
comment: ""
)
cell.accessibilityHint = nil
+ cell.applySubCellStyling()
cell.setOn(viewModel.blockAdultContent, animated: false)
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(
@@ -114,6 +123,7 @@ final class PreferencesCellFactory: CellFactoryProtocol {
comment: ""
)
cell.accessibilityHint = nil
+ cell.applySubCellStyling()
cell.setOn(viewModel.blockGambling, animated: false)
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
index b750f5cb68..c57eb887f9 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
@@ -12,6 +12,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
PreferencesDataSource.Section,
PreferencesDataSource.Item
>, UITableViewDelegate {
+ typealias InfoButtonHandler = (PreferencesDataSource.Item) -> Void
+
enum CellReuseIdentifiers: String, CaseIterable {
case settingSwitch
case dnsServer
@@ -30,11 +32,14 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
private enum HeaderFooterReuseIdentifiers: String, CaseIterable {
+ case contentBlockerHeader
case customDNSFooter
case spacer
var reusableViewClass: AnyClass {
switch self {
+ case .contentBlockerHeader:
+ return SettingsContentBlockersHeaderView.self
case .customDNSFooter:
return SettingsStaticTextFooterView.self
case .spacer:
@@ -43,8 +48,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
}
- enum Section: String, Hashable {
- case mullvadDNS
+ enum Section: String, Hashable, CaseIterable {
+ case contentBlockers
case customDNS
}
@@ -58,6 +63,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
case addDNSServer
case dnsServer(_ uniqueID: UUID)
+ static var contentBlockers: [Item] {
+ return [.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling]
+ }
+
var accessibilityIdentifier: String {
switch self {
case .blockAdvertising:
@@ -101,7 +110,9 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
private var isEditing = false
- private(set) var viewModel = PreferencesViewModel()
+ private(set) var viewModel = PreferencesViewModel() { didSet {
+ preferencesCellFactory.viewModel = viewModel
+ }}
private(set) var viewModelBeforeEditing = PreferencesViewModel()
private let preferencesCellFactory: PreferencesCellFactory
private weak var tableView: UITableView?
@@ -142,7 +153,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
updateSnapshot()
reloadCustomDNSFooter()
- updateCellFactory(with: viewModel)
viewModel.customDNSDomains.forEach { entry in
self.reload(item: .dnsServer(entry.identifier))
}
@@ -160,7 +170,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
viewModel = mergedViewModel
}
- updateCellFactory(with: viewModel)
updateSnapshot()
reloadCustomDNSFooter()
}
@@ -225,7 +234,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
let removedEntry = viewModel.customDNSDomains.remove(at: sourceIndex)
viewModel.customDNSDomains.insert(removedEntry, at: destinationIndex)
- updateCellFactory(with: viewModel)
updateSnapshot()
}
@@ -236,16 +244,29 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- return tableView.dequeueReusableHeaderFooterView(
- withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue
- )
+ let sectionIdentifier = snapshot().sectionIdentifiers[section]
+
+ switch sectionIdentifier {
+ case .contentBlockers:
+ let view = tableView
+ .dequeueReusableHeaderFooterView(
+ withIdentifier: HeaderFooterReuseIdentifiers.contentBlockerHeader.rawValue
+ ) as! SettingsContentBlockersHeaderView
+ configureContentBlockersHeader(view)
+ return view
+
+ case .customDNS:
+ return tableView.dequeueReusableHeaderFooterView(
+ withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue
+ )
+ }
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
- case .mullvadDNS:
+ case .contentBlockers:
return nil
case .customDNS:
@@ -260,14 +281,22 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
- return UIMetrics.sectionSpacing
+ let sectionIdentifier = snapshot().sectionIdentifiers[section]
+
+ switch sectionIdentifier {
+ case .contentBlockers:
+ return UITableView.automaticDimension
+
+ case .customDNS:
+ return UIMetrics.sectionSpacing
+ }
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
- case .mullvadDNS:
+ case .contentBlockers:
return 0
case .customDNS:
@@ -347,25 +376,30 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
private func updateSnapshot(animated: Bool = false, completion: (() -> Void)? = nil) {
- var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
+ var newSnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
- snapshot.appendSections([.mullvadDNS, .customDNS])
- snapshot.appendItems(
- [.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling],
- toSection: .mullvadDNS
- )
- snapshot.appendItems([.useCustomDNS], toSection: .customDNS)
+ newSnapshot.appendSections(Section.allCases)
+
+ let oldSnapshot = snapshot()
+ if oldSnapshot.indexOfSection(.contentBlockers) != nil {
+ newSnapshot.appendItems(
+ oldSnapshot.itemIdentifiers(inSection: .contentBlockers),
+ toSection: .contentBlockers
+ )
+ }
+
+ newSnapshot.appendItems([.useCustomDNS], toSection: .customDNS)
let dnsServerItems = viewModel.customDNSDomains.map { entry in
return Item.dnsServer(entry.identifier)
}
- snapshot.appendItems(dnsServerItems, toSection: .customDNS)
+ newSnapshot.appendItems(dnsServerItems, toSection: .customDNS)
if isEditing, viewModel.customDNSDomains.count < DNSSettings.maxAllowedCustomDNSDomains {
- snapshot.appendItems([.addDNSServer], toSection: .customDNS)
+ newSnapshot.appendItems([.addDNSServer], toSection: .customDNS)
}
- apply(snapshot, completion: completion)
+ apply(newSnapshot, completion: completion)
}
private func reload(item: Item) {
@@ -376,10 +410,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
}
- func updateCellFactory(with viewModel: PreferencesViewModel) {
- preferencesCellFactory.viewModel = viewModel
- }
-
private func setBlockAdvertising(_ isEnabled: Bool) {
let oldViewModel = viewModel
@@ -478,7 +508,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
let newDNSEntry = DNSServerEntry(address: "")
viewModel.customDNSDomains.append(newDNSEntry)
- updateCellFactory(with: viewModel)
updateSnapshot(animated: true) { [weak self] in
if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition {
self?.reloadCustomDNSFooter()
@@ -516,7 +545,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
viewModel.customDNSDomains.remove(at: entryIndex)
- updateCellFactory(with: viewModel)
updateSnapshot(animated: true) { [weak self] in
if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition {
self?.reloadCustomDNSFooter()
@@ -525,14 +553,46 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
private func reloadCustomDNSFooter() {
- updateCellFactory(with: viewModel)
reload(item: .useCustomDNS)
let sectionIndex = snapshot().indexOfSection(.customDNS)!
- if let reusableView = tableView?
- .footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView
- {
- configureFooterView(reusableView)
+
+ UIView.performWithoutAnimation {
+ tableView?.performBatchUpdates({
+ if let reusableView = tableView?.footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView {
+ configureFooterView(reusableView)
+ }
+ })
+ }
+ }
+
+ private func configureContentBlockersHeader(_ reusableView: SettingsContentBlockersHeaderView) {
+ reusableView.titleLabel.text = NSLocalizedString(
+ "BLOCK_ADS_CELL_LABEL",
+ tableName: "Preferences",
+ value: "DNS content blockers",
+ comment: ""
+ )
+
+ reusableView.infoButtonHandler = { [weak self] in
+ if let self = self {
+ self.delegate?.preferencesDataSource(self, didPressInfoButton: nil)
+ }
+ }
+
+ reusableView.didCollapseHandler = { [weak self] headerView in
+ guard let self = self else { return }
+
+ var snapshot = self.snapshot()
+
+ if headerView.isExpanded {
+ snapshot.deleteItems(Item.contentBlockers)
+ } else {
+ snapshot.appendItems(Item.contentBlockers, toSection: .contentBlockers)
+ }
+
+ headerView.isExpanded.toggle()
+ self.apply(snapshot, animatingDifferences: true)
}
}
@@ -580,4 +640,8 @@ extension PreferencesDataSource: PreferencesCellEventHandler {
) -> Bool {
return handleDNSEntryChange(with: identifier, inputString: inputString)
}
+
+ func didPressInfoButton(for item: Item) {
+ delegate?.preferencesDataSource(self, didPressInfoButton: item)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
index 84f18d93e8..b7242814fe 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
@@ -13,4 +13,9 @@ protocol PreferencesDataSourceDelegate: AnyObject {
_ dataSource: PreferencesDataSource,
didChangeViewModel viewModel: PreferencesViewModel
)
+
+ func preferencesDataSource(
+ _ dataSource: PreferencesDataSource,
+ didPressInfoButton item: PreferencesDataSource.Item?
+ )
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
index e18ea20e7f..93ccdaa854 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
@@ -11,6 +11,7 @@ import UIKit
class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate {
private let interactor: PreferencesInteractor
private var dataSource: PreferencesDataSource?
+ private let alertPresenter = AlertPresenter()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
@@ -47,8 +48,10 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
interactor.dnsSettingsDidChange = { [weak self] newDNSSettings in
self?.dataSource?.update(from: newDNSSettings)
}
-
dataSource?.update(from: interactor.dnsSettings)
+
+ tableView.tableHeaderView =
+ UIView(frame: .init(origin: .zero, size: .init(width: 0, height: UIMetrics.sectionSpacing)))
}
override func setEditing(_ editing: Bool, animated: Bool) {
@@ -62,6 +65,23 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
super.setEditing(editing, animated: animated)
}
+ private func showContentBlockerInfo(with message: String) {
+ let alertController = UIAlertController(
+ title: nil,
+ message: message,
+ preferredStyle: .alert
+ )
+ alertController.addAction(
+ UIAlertAction(title: NSLocalizedString(
+ "PREFERENCES_CONTENT_BLOCKERS_OK_ACTION",
+ tableName: "ContentBlockers",
+ value: "Got it!",
+ comment: ""
+ ), style: .cancel)
+ )
+ alertPresenter.enqueue(alertController, presentingController: self)
+ }
+
// MARK: - PreferencesDataSourceDelegate
func preferencesDataSource(
@@ -72,4 +92,31 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
interactor.setDNSSettings(dnsSettings)
}
+
+ func preferencesDataSource(
+ _ dataSource: PreferencesDataSource,
+ didPressInfoButton item: PreferencesDataSource.Item?
+ ) {
+ let message: String
+
+ switch item {
+ case .blockMalware:
+ message = NSLocalizedString(
+ "PREFERENCES_CONTENT_BLOCKERS_MALWARE",
+ tableName: "ContentBlockers",
+ value: "Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.",
+ comment: ""
+ )
+
+ default:
+ message = NSLocalizedString(
+ "PREFERENCES_CONTENT_BLOCKERS_GENERAL",
+ tableName: "ContentBlockers",
+ value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.",
+ comment: ""
+ )
+ }
+
+ showContentBlockerInfo(with: message)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
index 8157e300cc..b7320d9ef3 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
@@ -29,9 +29,12 @@ enum SettingsDisclosureType {
}
class SettingsCell: UITableViewCell {
+ typealias InfoButtonHandler = () -> Void
+
let titleLabel = UILabel()
let detailTitleLabel = UILabel()
let disclosureImageView = UIImageView(image: nil)
+ var infoButtonHandler: InfoButtonHandler?
var disclosureType: SettingsDisclosureType = .none {
didSet {
@@ -52,6 +55,15 @@ class SettingsCell: UITableViewCell {
}
}
+ private let buttonWidth: CGFloat = 24
+ private let infoButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.accessibilityIdentifier = "InfoButton"
+ button.tintColor = .white
+ button.setImage(UIImage(named: "IconInfo"), for: .normal)
+ return button
+ }()
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
@@ -65,6 +77,13 @@ class SettingsCell: UITableViewCell {
backgroundColor = .clear
contentView.backgroundColor = .clear
+ infoButton.isHidden = true
+ infoButton.addTarget(
+ self,
+ action: #selector(handleInfoButton(_:)),
+ for: .touchUpInside
+ )
+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = UIFont.systemFont(ofSize: 17)
titleLabel.textColor = UIColor.Cell.titleTextColor
@@ -81,21 +100,30 @@ class SettingsCell: UITableViewCell {
setLayoutMargins()
- contentView.addConstrainedSubviews([titleLabel, detailTitleLabel]) {
+ let buttonAreaWidth = UIMetrics.contentLayoutMargins.left + UIMetrics.contentLayoutMargins.right + buttonWidth
+
+ contentView.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) {
switch style {
case .subtitle:
- titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.bottom))
+ titleLabel.pinEdgesToSuperviewMargins(.init([.top(0), .leading(0)]))
detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.top))
detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1)
+ infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: contentView.trailingAnchor)
default:
titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing))
detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.leading))
- detailTitleLabel.leadingAnchor.constraint(
- greaterThanOrEqualToSystemSpacingAfter: titleLabel.trailingAnchor,
- multiplier: 1
- )
+ detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor)
}
+
+ infoButton.pinEdgesToSuperview(.init([.top(0)]))
+ infoButton.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor)
+ infoButton.leadingAnchor.constraint(
+ equalTo: titleLabel.trailingAnchor,
+ constant: -UIMetrics.interButtonSpacing
+ )
+ infoButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor)
+ infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth)
}
}
@@ -106,9 +134,23 @@ class SettingsCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
+ setInfoButtonIsVisible(false)
setLayoutMargins()
}
+ func applySubCellStyling() {
+ contentView.layoutMargins.left += UIMetrics.cellIndentationWidth
+ backgroundView?.backgroundColor = UIColor.SubCell.backgroundColor
+ }
+
+ func setInfoButtonIsVisible(_ visible: Bool) {
+ infoButton.isHidden = !visible
+ }
+
+ @objc private func handleInfoButton(_ sender: UIControl) {
+ infoButtonHandler?()
+ }
+
private func setLayoutMargins() {
// Set layout margins for standard acceessories added into the cell (reorder control, etc..)
layoutMargins = UIMetrics.settingsCellLayoutMargins
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift
new file mode 100644
index 0000000000..6e21173d49
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift
@@ -0,0 +1,143 @@
+//
+// SettingsContentBlockersHeaderView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-04-06.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class SettingsContentBlockersHeaderView: UITableViewHeaderFooterView {
+ typealias InfoButtonHandler = () -> Void
+ typealias CollapseHandler = (SettingsContentBlockersHeaderView) -> Void
+
+ let titleLabel: UILabel = {
+ let titleLabel = UILabel()
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
+ titleLabel.font = .systemFont(ofSize: 17)
+ titleLabel.textColor = UIColor.Cell.titleTextColor
+ titleLabel.numberOfLines = 0
+ return titleLabel
+ }()
+
+ let infoButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.accessibilityIdentifier = "InfoButton"
+ button.tintColor = .white
+ button.setImage(UIImage(named: "IconInfo"), for: .normal)
+ return button
+ }()
+
+ let collapseButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.accessibilityIdentifier = "CollapseButton"
+ button.isAccessibilityElement = false
+ button.tintColor = .white
+ return button
+ }()
+
+ var isExpanded = false {
+ didSet {
+ updateCollapseImage()
+ updateAccessibilityCustomActions()
+ }
+ }
+
+ var didCollapseHandler: CollapseHandler?
+ var infoButtonHandler: InfoButtonHandler?
+
+ private let chevronDown = UIImage(named: "IconChevronDown")
+ private let chevronUp = UIImage(named: "IconChevronUp")
+ private let buttonWidth: CGFloat = 24
+
+ override init(reuseIdentifier: String?) {
+ super.init(reuseIdentifier: reuseIdentifier)
+
+ infoButton.addTarget(
+ self,
+ action: #selector(handleInfoButton(_:)),
+ for: .touchUpInside
+ )
+
+ collapseButton.addTarget(
+ self,
+ action: #selector(handleCollapseButton(_:)),
+ for: .touchUpInside
+ )
+
+ contentView.layoutMargins = UIMetrics.settingsCellLayoutMargins
+ contentView.backgroundColor = UIColor.Cell.backgroundColor
+
+ let buttonAreaWidth = UIMetrics.contentLayoutMargins.left + UIMetrics.contentLayoutMargins.right + buttonWidth
+
+ contentView.addConstrainedSubviews([titleLabel, infoButton, collapseButton]) {
+ titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing).excluding(.bottom))
+ titleLabel.bottomAnchor.constraint(
+ equalTo: contentView.bottomAnchor,
+ constant: -contentView.layoutMargins.bottom
+ ).withPriority(.defaultHigh)
+
+ infoButton.pinEdgesToSuperview(.init([.top(0), .bottom(0)]))
+ infoButton.leadingAnchor.constraint(
+ equalTo: titleLabel.trailingAnchor,
+ constant: -UIMetrics.interButtonSpacing
+ )
+ infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth)
+
+ collapseButton.pinEdgesToSuperview(.all().excluding(.leading))
+ collapseButton.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor)
+ collapseButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth)
+ }
+
+ updateCollapseImage()
+ updateAccessibilityCustomActions()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func handleInfoButton(_ sender: UIControl) {
+ infoButtonHandler?()
+ }
+
+ @objc private func handleCollapseButton(_ sender: UIControl) {
+ didCollapseHandler?(self)
+ }
+
+ @objc private func toggleCollapseAccessibilityAction() -> Bool {
+ didCollapseHandler?(self)
+ return true
+ }
+
+ private func updateCollapseImage() {
+ let image = isExpanded ? chevronUp : chevronDown
+
+ collapseButton.setImage(image, for: .normal)
+ }
+
+ private func updateAccessibilityCustomActions() {
+ let actionName = isExpanded
+ ? NSLocalizedString(
+ "CONTENT_BLOCKERS_COLLAPSE_ACCESSIBILITY_ACTION",
+ tableName: "Settings",
+ value: "Collapse content blockers",
+ comment: ""
+ )
+ : NSLocalizedString(
+ "CONTENT_BLOCKERS_EXPAND_ACCESSIBILITY_ACTION",
+ tableName: "Settings",
+ value: "Expand content blockers",
+ comment: ""
+ )
+
+ accessibilityCustomActions = [
+ UIAccessibilityCustomAction(
+ name: actionName,
+ target: self,
+ selector: #selector(toggleCollapseAccessibilityAction)
+ ),
+ ]
+ }
+}
diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb
index 41fd7a7919..9b2954faea 100755
--- a/ios/convert-assets.rb
+++ b/ios/convert-assets.rb
@@ -27,6 +27,7 @@ GRAPHICAL_ASSETS = [
"icon-chevron.svg",
"icon-extLink.svg",
"icon-fail.svg",
+ "icon-info.svg",
"icon-reload.svg",
"icon-settings.svg",
"icon-spinner.svg",
@@ -45,7 +46,8 @@ GRAPHICAL_ASSETS = [
# graphical assets to resize.
RESIZE_ASSETS = {
- "icon-tick.svg" => ["icon-tick-sml.svg", 16, 16],
+ "icon-info.svg" => ["icon-info.svg", 18, 18],
+ "icon-tick.svg" => ["icon-tick-sml.svg", 16, 16]
}
# Additional assets generated from SVG -> vector PDF