summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/PreferencesCellFactory.swift202
-rw-r--r--ios/MullvadVPN/PreferencesDataSource.swift449
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift12
-rw-r--r--ios/MullvadVPN/SettingsAddDNSEntryCell.swift4
5 files changed, 352 insertions, 319 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index f6755cfec3..0872f8f484 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -342,6 +342,7 @@
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
7AD8490D29BA1EC500878E53 /* SettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */; };
7AD8490F29BA26B000878E53 /* CellFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */; };
+ 7AD8491129BA316500878E53 /* PreferencesCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* StringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* StringFormatter.swift */; };
@@ -907,6 +908,7 @@
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = "<group>"; };
7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = "<group>"; };
+ 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E158B35F285381C60002F069 /* StringFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringFormatter.swift; sourceTree = "<group>"; };
@@ -1445,6 +1447,7 @@
587EB671271451E300123C75 /* PreferencesViewModel.swift */,
5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
+ 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */,
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
5867771529097C5B006F721F /* ProductState.swift */,
@@ -2355,6 +2358,7 @@
58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */,
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */,
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */,
+ 7AD8491129BA316500878E53 /* PreferencesCellFactory.swift in Sources */,
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
diff --git a/ios/MullvadVPN/PreferencesCellFactory.swift b/ios/MullvadVPN/PreferencesCellFactory.swift
new file mode 100644
index 0000000000..8f160d3fdc
--- /dev/null
+++ b/ios/MullvadVPN/PreferencesCellFactory.swift
@@ -0,0 +1,202 @@
+//
+// PreferencesCellFactory.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-03-09.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+protocol PreferencesCellEventHandler {
+ func addDNSEntry()
+ func didChangeDNSEntry(with identifier: UUID, inputString: String) -> Bool
+ func didChangeState(for item: PreferencesDataSource.Item, isOn: Bool)
+}
+
+final class PreferencesCellFactory: CellFactoryProtocol {
+ let tableView: UITableView
+ var viewModel: PreferencesViewModel
+ var delegate: PreferencesCellEventHandler?
+ var isEditing = false
+
+ init(tableView: UITableView, viewModel: PreferencesViewModel) {
+ self.tableView = tableView
+ self.viewModel = viewModel
+ }
+
+ func makeCell(for item: PreferencesDataSource.Item, indexPath: IndexPath) -> UITableViewCell {
+ let cell: UITableViewCell
+
+ switch item {
+ case .addDNSServer:
+ cell = tableView.dequeueReusableCell(
+ withIdentifier: PreferencesDataSource.CellReuseIdentifiers.addDNSServer.rawValue,
+ for: indexPath
+ )
+ case .dnsServer:
+ cell = tableView.dequeueReusableCell(
+ withIdentifier: PreferencesDataSource.CellReuseIdentifiers.dnsServer.rawValue,
+ for: indexPath
+ )
+ default:
+ cell = tableView.dequeueReusableCell(
+ withIdentifier: PreferencesDataSource.CellReuseIdentifiers.settingSwitch.rawValue,
+ for: indexPath
+ )
+ }
+
+ configureCell(cell, item: item, indexPath: indexPath)
+
+ return cell
+ }
+
+ func configureCell(
+ _ cell: UITableViewCell,
+ item: PreferencesDataSource.Item,
+ indexPath: IndexPath
+ ) {
+ switch item {
+ case .blockAdvertising:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "BLOCK_ADS_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Block ads",
+ comment: ""
+ )
+ cell.accessibilityHint = nil
+ cell.setOn(viewModel.blockAdvertising, animated: false)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(
+ for: .blockAdvertising,
+ isOn: isOn
+ )
+ }
+
+ case .blockTracking:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "BLOCK_TRACKERS_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Block trackers",
+ comment: ""
+ )
+ cell.accessibilityHint = nil
+ cell.setOn(viewModel.blockTracking, animated: false)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(
+ for: .blockTracking,
+ isOn: isOn
+ )
+ }
+
+ case .blockMalware:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "BLOCK_MALWARE_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Block malware",
+ comment: ""
+ )
+ cell.accessibilityHint = nil
+ cell.setOn(viewModel.blockMalware, animated: false)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(for: .blockMalware, isOn: isOn)
+ }
+
+ case .blockAdultContent:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "BLOCK_ADULT_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Block adult content",
+ comment: ""
+ )
+ cell.accessibilityHint = nil
+ cell.setOn(viewModel.blockAdultContent, animated: false)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(
+ for: .blockAdultContent,
+ isOn: isOn
+ )
+ }
+
+ case .blockGambling:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "BLOCK_GAMBLING_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Block gambling",
+ comment: ""
+ )
+ cell.accessibilityHint = nil
+ cell.setOn(viewModel.blockGambling, animated: false)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(
+ for: .blockGambling,
+ isOn: isOn
+ )
+ }
+
+ case .useCustomDNS:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "CUSTOM_DNS_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Use custom DNS",
+ comment: ""
+ )
+ cell.setEnabled(viewModel.customDNSPrecondition == .satisfied)
+ cell.setOn(viewModel.effectiveEnableCustomDNS, animated: false)
+ cell.accessibilityHint = viewModel.customDNSPrecondition
+ .localizedDescription(isEditing: isEditing)
+ cell.action = { [weak self] isOn in
+ self?.delegate?.didChangeState(for: .useCustomDNS, isOn: isOn)
+ }
+
+ case .addDNSServer:
+ guard let cell = cell as? SettingsAddDNSEntryCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "ADD_CUSTOM_DNS_SERVER_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Add a server",
+ comment: ""
+ )
+ cell.action = { [weak self] in
+ self?.delegate?.addDNSEntry()
+ }
+
+ case let .dnsServer(entryIdentifier):
+ guard let cell = cell as? SettingsDNSTextCell else { return }
+
+ let dnsServerEntry = viewModel.dnsEntry(entryIdentifier: entryIdentifier)!
+
+ cell.textField.text = dnsServerEntry.address
+ cell.isValidInput = dsnEntryIsValid(identifier: entryIdentifier, cell: cell)
+
+ cell.onTextChange = { [weak self] cell in
+ cell.isValidInput = self?
+ .dsnEntryIsValid(identifier: entryIdentifier, cell: cell) ?? false
+ }
+
+ cell.onReturnKey = { cell in
+ cell.endEditing(false)
+ }
+ }
+ }
+
+ private func dsnEntryIsValid(identifier: UUID, cell: SettingsDNSTextCell) -> Bool {
+ return delegate?.didChangeDNSEntry(
+ with: identifier,
+ inputString: cell.textField.text ?? ""
+ ) ?? false
+ }
+}
diff --git a/ios/MullvadVPN/PreferencesDataSource.swift b/ios/MullvadVPN/PreferencesDataSource.swift
index 80200a88de..650f02c14e 100644
--- a/ios/MullvadVPN/PreferencesDataSource.swift
+++ b/ios/MullvadVPN/PreferencesDataSource.swift
@@ -8,8 +8,11 @@
import UIKit
-class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
- private enum CellReuseIdentifiers: String, CaseIterable {
+final class PreferencesDataSource: UITableViewDiffableDataSource<
+ PreferencesDataSource.Section,
+ PreferencesDataSource.Item
+>, UITableViewDelegate {
+ enum CellReuseIdentifiers: String, CaseIterable {
case settingSwitch
case dnsServer
case addDNSServer
@@ -40,12 +43,12 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- private enum Section: String, Hashable {
+ enum Section: String, Hashable {
case mullvadDNS
case customDNS
}
- private enum Item: Hashable {
+ enum Item: Hashable {
case blockAdvertising
case blockTracking
case blockMalware
@@ -86,35 +89,38 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
private var isEditing = false
- private var snapshot = DataSourceSnapshot<Section, Item>()
private(set) var viewModel = PreferencesViewModel()
private(set) var viewModelBeforeEditing = PreferencesViewModel()
+ private let preferencesCellFactory: PreferencesCellFactory
+ private weak var tableView: UITableView?
weak var delegate: PreferencesDataSourceDelegate?
- weak var tableView: UITableView? {
- didSet {
- tableView?.dataSource = self
- tableView?.delegate = self
+ init(tableView: UITableView) {
+ self.tableView = tableView
- registerClasses()
+ let preferencesCellFactory = PreferencesCellFactory(
+ tableView: tableView,
+ viewModel: viewModel
+ )
+ self.preferencesCellFactory = preferencesCellFactory
+
+ super.init(tableView: tableView) { tableView, indexPath, itemIdentifier in
+ preferencesCellFactory.makeCell(for: itemIdentifier, indexPath: indexPath)
}
- }
- override init() {
- super.init()
+ tableView.delegate = self
+ preferencesCellFactory.delegate = self
- updateSnapshot()
+ registerClasses()
}
func setEditing(_ editing: Bool, animated: Bool) {
guard isEditing != editing else { return }
- let oldSnapshot = snapshot
- let oldDNSDomains = viewModel.customDNSDomains
-
isEditing = editing
+ preferencesCellFactory.isEditing = isEditing
if editing {
viewModelBeforeEditing = viewModel
@@ -123,28 +129,11 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
updateSnapshot()
+ reloadCustomDNSFooter()
- // Reconfigure cells for items with corresponding DNS entries that were changed during
- // sanitization.
- let itemsToReload: [Item] = oldDNSDomains.filter { oldDNSEntry in
- guard let newDNSEntry = viewModel.dnsEntry(entryIdentifier: oldDNSEntry.identifier)
- else { return false }
-
- return newDNSEntry.address != oldDNSEntry.address
- }.map { dnsEntry in
- return .dnsServer(dnsEntry.identifier)
- }
-
- snapshot.reconfigureItems(itemsToReload)
-
- if animated {
- let diffResult = oldSnapshot.difference(snapshot)
- if let tableView = tableView {
- diffResult.apply(to: tableView, animateDifferences: animated)
- reloadCustomDNSFooter()
- }
- } else {
- tableView?.reloadData()
+ updateCellFactory(with: viewModel)
+ viewModel.customDNSDomains.forEach { entry in
+ self.reload(item: .dnsServer(entry.identifier))
}
if !editing, viewModelBeforeEditing != viewModel {
@@ -158,38 +147,19 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
if viewModel != mergedViewModel {
viewModel = mergedViewModel
+
updateSnapshot()
- tableView?.reloadData()
+ reloadCustomDNSFooter()
}
}
// MARK: - UITableViewDataSource
- func numberOfSections(in tableView: UITableView) -> Int {
- return snapshot.numberOfSections()
- }
-
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- guard let sectionIdentifier = snapshot.section(at: section) else { return 0 }
-
- return snapshot.numberOfItems(in: sectionIdentifier) ?? 0
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let item = snapshot.itemForIndexPath(indexPath)!
- let cell = dequeueCellForItem(item, in: tableView, at: indexPath)
-
- let section = snapshot.section(at: indexPath.section)!
- cell.accessibilityIdentifier = "\(section.rawValue).\(item.accessibilityIdentifier)"
-
- return cell
- }
-
- func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
+ override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Disable swipe to delete when not editing the table view
guard isEditing else { return false }
- let item = snapshot.itemForIndexPath(indexPath)
+ let item = itemIdentifier(for: indexPath)
switch item {
case .dnsServer, .addDNSServer:
@@ -199,12 +169,12 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- func tableView(
+ override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath
) {
- let item = snapshot.itemForIndexPath(indexPath)
+ let item = itemIdentifier(for: indexPath)
if case .addDNSServer = item, editingStyle == .insert {
addDNSServerEntry()
@@ -215,8 +185,8 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
- let item = snapshot.itemForIndexPath(indexPath)
+ override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
+ let item = itemIdentifier(for: indexPath)
switch item {
case .dnsServer:
@@ -226,13 +196,13 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- func tableView(
+ override func tableView(
_ tableView: UITableView,
moveRowAt sourceIndexPath: IndexPath,
to destinationIndexPath: IndexPath
) {
- let sourceItem = snapshot.itemForIndexPath(sourceIndexPath)!
- let destinationItem = snapshot.itemForIndexPath(destinationIndexPath)!
+ let sourceItem = itemIdentifier(for: sourceIndexPath)!
+ let destinationItem = itemIdentifier(for: destinationIndexPath)!
guard case let .dnsServer(sourceIdentifier) = sourceItem,
case let .dnsServer(targetIdentifier) = destinationItem,
@@ -243,6 +213,7 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
let removedEntry = viewModel.customDNSDomains.remove(at: sourceIndex)
viewModel.customDNSDomains.insert(removedEntry, at: destinationIndex)
+ updateCellFactory(with: viewModel)
updateSnapshot()
}
@@ -259,7 +230,7 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
- let sectionIdentifier = snapshot.section(at: section)!
+ let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
case .mullvadDNS:
@@ -281,7 +252,7 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
- let sectionIdentifier = snapshot.section(at: section)!
+ let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
case .mullvadDNS:
@@ -301,7 +272,7 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
_ tableView: UITableView,
editingStyleForRowAt indexPath: IndexPath
) -> UITableViewCell.EditingStyle {
- let item = snapshot.itemForIndexPath(indexPath)
+ let item = itemIdentifier(for: indexPath)
switch item {
case .dnsServer:
@@ -318,17 +289,17 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath,
toProposedIndexPath proposedDestinationIndexPath: IndexPath
) -> IndexPath {
- guard let sectionIdentifier = snapshot.section(at: sourceIndexPath.section),
- case .customDNS = sectionIdentifier else { return sourceIndexPath }
+ let sectionIdentifier = snapshot().sectionIdentifiers[sourceIndexPath.section]
+ guard case .customDNS = sectionIdentifier else { return sourceIndexPath }
- let items = snapshot.items(in: sectionIdentifier)
+ let items = snapshot().itemIdentifiers(inSection: sectionIdentifier)
let indexPathForFirstRow = items.first(where: Item.isDNSServerItem).flatMap { item in
- return snapshot.indexPathForItem(item)
+ return indexPath(for: item)
}
let indexPathForLastRow = items.last(where: Item.isDNSServerItem).flatMap { item in
- return snapshot.indexPathForItem(item)
+ return indexPath(for: item)
}
guard let indexPathForFirstRow = indexPathForFirstRow,
@@ -363,203 +334,40 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- private func updateSnapshot() {
- var newSnapshot = DataSourceSnapshot<Section, Item>()
- newSnapshot.appendSections([.mullvadDNS, .customDNS])
- newSnapshot.appendItems(
+ private func updateSnapshot(animated: Bool = false, completion: (() -> Void)? = nil) {
+ var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
+
+ snapshot.appendSections([.mullvadDNS, .customDNS])
+ snapshot.appendItems(
[.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling],
- in: .mullvadDNS
+ toSection: .mullvadDNS
)
- newSnapshot.appendItems([.useCustomDNS], in: .customDNS)
+ snapshot.appendItems([.useCustomDNS], toSection: .customDNS)
let dnsServerItems = viewModel.customDNSDomains.map { entry in
return Item.dnsServer(entry.identifier)
}
- newSnapshot.appendItems(dnsServerItems, in: .customDNS)
+ snapshot.appendItems(dnsServerItems, toSection: .customDNS)
if isEditing, viewModel.customDNSDomains.count < DNSSettings.maxAllowedCustomDNSDomains {
- newSnapshot.appendItems([.addDNSServer], in: .customDNS)
+ snapshot.appendItems([.addDNSServer], toSection: .customDNS)
}
- snapshot = newSnapshot
+ apply(snapshot, completion: completion)
}
- private func dequeueCellForItem(
- _ item: Item,
- in tableView: UITableView,
- at indexPath: IndexPath
- ) -> UITableViewCell {
- switch item {
- case .blockAdvertising:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "BLOCK_ADS_CELL_LABEL",
- tableName: "Preferences",
- value: "Block ads",
- comment: ""
- )
- cell.accessibilityHint = nil
- cell.setOn(viewModel.blockAdvertising, animated: false)
- cell.action = { [weak self] isOn in
- self?.setBlockAdvertising(isOn)
- }
-
- return cell
-
- case .blockTracking:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "BLOCK_TRACKERS_CELL_LABEL",
- tableName: "Preferences",
- value: "Block trackers",
- comment: ""
- )
- cell.accessibilityHint = nil
- cell.setOn(viewModel.blockTracking, animated: false)
- cell.action = { [weak self] isOn in
- self?.setBlockTracking(isOn)
- }
-
- return cell
-
- case .blockMalware:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "BLOCK_MALWARE_CELL_LABEL",
- tableName: "Preferences",
- value: "Block malware",
- comment: ""
- )
- cell.accessibilityHint = nil
- cell.setOn(viewModel.blockMalware, animated: false)
- cell.action = { [weak self] isOn in
- self?.setBlockMalware(isOn)
- }
-
- return cell
-
- case .blockAdultContent:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "BLOCK_ADULT_CELL_LABEL",
- tableName: "Preferences",
- value: "Block adult content",
- comment: ""
- )
- cell.accessibilityHint = nil
- cell.setOn(viewModel.blockAdultContent, animated: false)
- cell.action = { [weak self] isOn in
- self?.setBlockAdultContent(isOn)
- }
-
- return cell
-
- case .blockGambling:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "BLOCK_GAMBLING_CELL_LABEL",
- tableName: "Preferences",
- value: "Block gambling",
- comment: ""
- )
- cell.accessibilityHint = nil
- cell.setOn(viewModel.blockGambling, animated: false)
- cell.action = { [weak self] isOn in
- self?.setBlockGambling(isOn)
- }
-
- return cell
-
- case .useCustomDNS:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.settingSwitch.rawValue,
- for: indexPath
- ) as! SettingsSwitchCell
-
- cell.titleLabel.text = NSLocalizedString(
- "CUSTOM_DNS_CELL_LABEL",
- tableName: "Preferences",
- value: "Use custom DNS",
- comment: ""
- )
- cell.setEnabled(viewModel.customDNSPrecondition == .satisfied)
- cell.setOn(viewModel.effectiveEnableCustomDNS, animated: false)
- cell.action = { [weak self] isOn in
- self?.setEnableCustomDNS(isOn)
- }
-
- cell.accessibilityHint = viewModel.customDNSPrecondition
- .localizedDescription(isEditing: isEditing)
-
- return cell
-
- case .addDNSServer:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.addDNSServer.rawValue,
- for: indexPath
- ) as! SettingsAddDNSEntryCell
- cell.titleLabel.text = NSLocalizedString(
- "ADD_CUSTOM_DNS_SERVER_CELL_LABEL",
- tableName: "Preferences",
- value: "Add a server",
- comment: ""
- )
-
- cell.actionHandler = { [weak self] cell in
- self?.addDNSServerEntry()
- }
-
- return cell
-
- case let .dnsServer(entryIdentifier):
- let dnsServerEntry = viewModel.dnsEntry(entryIdentifier: entryIdentifier)!
-
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.dnsServer.rawValue,
- for: indexPath
- ) as! SettingsDNSTextCell
- cell.textField.text = dnsServerEntry.address
- cell.isValidInput = viewModel.validateDNSDomainUserInput(dnsServerEntry.address)
-
- cell.onTextChange = { [weak self] cell in
- guard let self = self,
- let indexPath = self.tableView?.indexPath(for: cell) else { return }
-
- if case let .dnsServer(entryIdentifier) = self.snapshot
- .itemForIndexPath(indexPath)
- {
- self.handleDNSEntryChange(entryIdentifier: entryIdentifier, cell: cell)
- }
- }
-
- cell.onReturnKey = { cell in
- cell.endEditing(false)
- }
-
- return cell
+ private func reload(item: Item) {
+ if let indexPath = indexPath(for: item),
+ let cell = tableView?.cellForRow(at: indexPath)
+ {
+ preferencesCellFactory.configureCell(cell, item: item, indexPath: indexPath)
}
}
+ func updateCellFactory(with viewModel: PreferencesViewModel) {
+ preferencesCellFactory.viewModel = viewModel
+ }
+
private func setBlockAdvertising(_ isEnabled: Bool) {
let oldViewModel = viewModel
@@ -640,16 +448,16 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
}
}
- private func handleDNSEntryChange(entryIdentifier: UUID, cell: SettingsDNSTextCell) {
- let string = cell.textField.text ?? ""
+ private func handleDNSEntryChange(with identifier: UUID, inputString: String) -> Bool {
let oldViewModel = viewModel
- viewModel.updateDNSEntry(entryIdentifier: entryIdentifier, newAddress: string)
- cell.isValidInput = viewModel.validateDNSDomainUserInput(string)
+ viewModel.updateDNSEntry(entryIdentifier: identifier, newAddress: inputString)
if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition {
reloadCustomDNSFooter()
}
+
+ return viewModel.validateDNSDomainUserInput(inputString)
}
private func addDNSServerEntry() {
@@ -658,42 +466,35 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
let newDNSEntry = DNSServerEntry(address: "")
viewModel.customDNSDomains.append(newDNSEntry)
- let oldSnapshot = snapshot
- updateSnapshot()
-
- let diffResult = oldSnapshot.difference(snapshot)
- if let tableView = tableView {
- diffResult.apply(to: tableView, animateDifferences: true) { completed in
- if oldViewModel.customDNSPrecondition != self.viewModel.customDNSPrecondition {
- self.reloadCustomDNSFooter()
- }
+ updateCellFactory(with: viewModel)
+ updateSnapshot(animated: true) { [weak self] in
+ if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition {
+ self?.reloadCustomDNSFooter()
+ }
- if completed {
- // Focus on the new entry text field.
- let lastDNSEntry = self.snapshot.items(in: .customDNS).last { item in
- if case let .dnsServer(entryIdentifier) = item {
- return entryIdentifier == newDNSEntry.identifier
- } else {
- return false
- }
+ // Focus on the new entry text field.
+ let lastDNSEntry = self?.snapshot().itemIdentifiers(inSection: .customDNS)
+ .last { item in
+ if case let .dnsServer(entryIdentifier) = item {
+ return entryIdentifier == newDNSEntry.identifier
+ } else {
+ return false
}
+ }
- if let lastDNSEntry = lastDNSEntry,
- let indexPath = self.snapshot.indexPathForItem(lastDNSEntry)
- {
- let cell = self.tableView?.cellForRow(at: indexPath) as? SettingsDNSTextCell
+ if let lastDNSEntry = lastDNSEntry,
+ let indexPath = self?.indexPath(for: lastDNSEntry)
+ {
+ let cell = self?.tableView?.cellForRow(at: indexPath) as? SettingsDNSTextCell
- self.tableView?.scrollToRow(at: indexPath, at: .bottom, animated: true)
- cell?.textField.becomeFirstResponder()
- }
- }
+ self?.tableView?.scrollToRow(at: indexPath, at: .bottom, animated: true)
+ cell?.textField.becomeFirstResponder()
}
}
}
private func deleteDNSServerEntry(entryIdentifier: UUID) {
let oldViewModel = viewModel
- let oldSnapshot = snapshot
let entryIndex = viewModel.customDNSDomains.firstIndex { entry in
return entry.identifier == entryIdentifier
@@ -702,36 +503,24 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
guard let entryIndex = entryIndex else { return }
viewModel.customDNSDomains.remove(at: entryIndex)
- updateSnapshot()
- let diffResult = oldSnapshot.difference(snapshot)
-
- if let tableView = tableView {
- diffResult.apply(to: tableView, animateDifferences: true) { completed in
- if oldViewModel.customDNSPrecondition != self.viewModel.customDNSPrecondition {
- self.reloadCustomDNSFooter()
- }
+ updateCellFactory(with: viewModel)
+ updateSnapshot(animated: true) { [weak self] in
+ if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition {
+ self?.reloadCustomDNSFooter()
}
}
}
private func reloadCustomDNSFooter() {
- let sectionIndex = snapshot.indexOfSection(.customDNS)!
- let indexPath = snapshot.indexPathForItem(.useCustomDNS)!
-
- // Reload footer view
- tableView?.performBatchUpdates {
- if let reusableView = tableView?
- .footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView
- {
- configureFooterView(reusableView)
- }
- }
+ updateCellFactory(with: viewModel)
+ reload(item: .useCustomDNS)
- // Reload "Use custom DNS" row
- if let cell = tableView?.cellForRow(at: indexPath) as? SettingsSwitchCell {
- cell.setEnabled(viewModel.customDNSPrecondition == .satisfied)
- cell.setOn(viewModel.effectiveEnableCustomDNS, animated: true)
+ let sectionIndex = snapshot().indexOfSection(.customDNS)!
+ if let reusableView = tableView?
+ .footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView
+ {
+ configureFooterView(reusableView)
}
}
@@ -742,3 +531,41 @@ class PreferencesDataSource: NSObject, UITableViewDataSource, UITableViewDelegat
.attributedLocalizedDescription(isEditing: isEditing, preferredFont: font)
}
}
+
+extension PreferencesDataSource: PreferencesCellEventHandler {
+ func didChangeState(for item: Item, isOn: Bool) {
+ switch item {
+ case .blockAdvertising:
+ setBlockAdvertising(isOn)
+
+ case .blockTracking:
+ setBlockTracking(isOn)
+
+ case .blockMalware:
+ setBlockMalware(isOn)
+
+ case .blockAdultContent:
+ setBlockAdultContent(isOn)
+
+ case .blockGambling:
+ setBlockGambling(isOn)
+
+ case .useCustomDNS:
+ setEnableCustomDNS(isOn)
+
+ default:
+ break
+ }
+ }
+
+ func addDNSEntry() {
+ addDNSServerEntry()
+ }
+
+ func didChangeDNSEntry(
+ with identifier: UUID,
+ inputString: String
+ ) -> Bool {
+ return handleDNSEntryChange(with: identifier, inputString: inputString)
+ }
+}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
index ea85b62b0f..ec00d19641 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -10,7 +10,7 @@ import UIKit
class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate {
private let interactor: PreferencesInteractor
- private let dataSource = PreferencesDataSource()
+ private var dataSource: PreferencesDataSource?
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
@@ -33,8 +33,8 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
- dataSource.tableView = tableView
- dataSource.delegate = self
+ dataSource = PreferencesDataSource(tableView: tableView)
+ dataSource?.delegate = self
navigationItem.title = NSLocalizedString(
"NAVIGATION_TITLE",
@@ -45,14 +45,14 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
navigationItem.rightBarButtonItem = editButtonItem
interactor.dnsSettingsDidChange = { [weak self] newDNSSettings in
- self?.dataSource.update(from: newDNSSettings)
+ self?.dataSource?.update(from: newDNSSettings)
}
- dataSource.update(from: interactor.dnsSettings)
+ dataSource?.update(from: interactor.dnsSettings)
}
override func setEditing(_ editing: Bool, animated: Bool) {
- dataSource.setEditing(editing, animated: animated)
+ dataSource?.setEditing(editing, animated: animated)
navigationItem.setHidesBackButton(editing, animated: animated)
diff --git a/ios/MullvadVPN/SettingsAddDNSEntryCell.swift b/ios/MullvadVPN/SettingsAddDNSEntryCell.swift
index 3aaadd90f7..01872824f5 100644
--- a/ios/MullvadVPN/SettingsAddDNSEntryCell.swift
+++ b/ios/MullvadVPN/SettingsAddDNSEntryCell.swift
@@ -9,7 +9,7 @@
import UIKit
class SettingsAddDNSEntryCell: SettingsCell {
- var actionHandler: ((SettingsAddDNSEntryCell) -> Void)?
+ var action: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
@@ -29,7 +29,7 @@ class SettingsAddDNSEntryCell: SettingsCell {
@objc func handleTap(_ sender: UIGestureRecognizer) {
if case .ended = sender.state {
- actionHandler?(self)
+ action?()
}
}
}