diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesCellFactory.swift | 202 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesDataSource.swift | 449 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesViewController.swift | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsAddDNSEntryCell.swift | 4 |
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?() } } } |
