forked from organicmaps/organicmaps-tmp
[ios] implement recently deleted
feature UI (screen and view model tests)
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
parent
f3d1cc63c2
commit
5881612fe4
10 changed files with 808 additions and 35 deletions
|
@ -1,6 +1,7 @@
|
|||
enum BMCSection {
|
||||
case categories
|
||||
case actions
|
||||
case recentlyDeleted
|
||||
case notifications
|
||||
}
|
||||
|
||||
|
@ -10,6 +11,7 @@ enum BMCAction: BMCModel {
|
|||
case create
|
||||
case exportAll
|
||||
case `import`
|
||||
case recentlyDeleted(Int)
|
||||
}
|
||||
|
||||
extension BMCAction {
|
||||
|
@ -21,6 +23,8 @@ extension BMCAction {
|
|||
return L("bookmarks_export")
|
||||
case .import:
|
||||
return L("bookmarks_import")
|
||||
case .recentlyDeleted(let count):
|
||||
return L("bookmarks_recently_deleted") + " (\(count))"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +36,8 @@ extension BMCAction {
|
|||
return UIImage(named: "ic24PxShare")!
|
||||
case .import:
|
||||
return UIImage(named: "ic24PxImport")!
|
||||
case .recentlyDeleted:
|
||||
return UIImage(named: "ic_route_manager_trash_open")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,12 +40,8 @@ final class BMCViewController: MWMViewController {
|
|||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
viewModel.reloadData()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
viewModel.addToObserverList()
|
||||
viewModel.reloadData()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
|
@ -157,13 +153,18 @@ final class BMCViewController: MWMViewController {
|
|||
let deleteAction = UIAlertAction(title: delete, style: .destructive, handler: { [viewModel] _ in
|
||||
viewModel!.deleteCategory(at: index)
|
||||
})
|
||||
deleteAction.isEnabled = (viewModel.numberOfRows(section: .categories) > 1)
|
||||
deleteAction.isEnabled = (viewModel.canDeleteCategory())
|
||||
actionSheet.addAction(deleteAction)
|
||||
let cancel = L("cancel")
|
||||
actionSheet.addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
|
||||
|
||||
present(actionSheet, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func openRecentlyDeleted() {
|
||||
let recentlyDeletedController = RecentlyDeletedCategoriesViewController(viewModel: RecentlyDeletedCategoriesViewModel(bookmarksManager: BookmarksManager.shared()))
|
||||
MapViewController.topViewController().navigationController?.pushViewController(recentlyDeletedController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: BMCView {
|
||||
|
@ -201,7 +202,7 @@ extension BMCViewController: UITableViewDataSource {
|
|||
func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch viewModel.sectionType(section: section) {
|
||||
case .categories: fallthrough
|
||||
case .actions: fallthrough
|
||||
case .actions, .recentlyDeleted: fallthrough
|
||||
case .notifications: return viewModel.numberOfRows(section: section)
|
||||
}
|
||||
}
|
||||
|
@ -217,6 +218,8 @@ extension BMCViewController: UITableViewDataSource {
|
|||
delegate: self)
|
||||
case .actions:
|
||||
return dequeCell(BMCActionsCell.self).config(model: viewModel.action(at: indexPath.row))
|
||||
case .recentlyDeleted:
|
||||
return dequeCell(BMCActionsCell.self).config(model: viewModel.recentlyDeletedCategories())
|
||||
case .notifications:
|
||||
return dequeCell(BMCNotificationsCell.self)
|
||||
}
|
||||
|
@ -229,7 +232,7 @@ extension BMCViewController: UITableViewDelegate {
|
|||
return false
|
||||
}
|
||||
|
||||
return viewModel.numberOfRows(section: .categories) > 1
|
||||
return viewModel.canDeleteCategory()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
|
@ -248,7 +251,7 @@ extension BMCViewController: UITableViewDelegate {
|
|||
switch viewModel.sectionType(section: section) {
|
||||
case .notifications: fallthrough
|
||||
case .categories: return 48
|
||||
case .actions: return 24
|
||||
case .actions, .recentlyDeleted: return 24
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,7 +263,7 @@ extension BMCViewController: UITableViewDelegate {
|
|||
categoriesHeader.title = L("bookmark_lists")
|
||||
categoriesHeader.delegate = self
|
||||
return categoriesHeader
|
||||
case .actions: return actionsHeader
|
||||
case .actions, .recentlyDeleted: return actionsHeader
|
||||
case .notifications: return notificationsHeader
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +278,10 @@ extension BMCViewController: UITableViewDelegate {
|
|||
case .create: createNewCategory()
|
||||
case .exportAll: shareAllCategories(anchor: tableView.cellForRow(at: indexPath))
|
||||
case .import: showImportDialog()
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
case .recentlyDeleted: openRecentlyDeleted()
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
|
|
@ -27,74 +27,89 @@ final class BMCDefaultViewModel: NSObject {
|
|||
reloadData()
|
||||
}
|
||||
|
||||
private func setCategories() {
|
||||
categories = manager.sortedUserCategories()
|
||||
private func getCategories() -> [BookmarkGroup] {
|
||||
manager.sortedUserCategories()
|
||||
}
|
||||
|
||||
private func setActions() {
|
||||
actions = [.create]
|
||||
private func getActions() -> [BMCAction] {
|
||||
var actions: [BMCAction] = [.create]
|
||||
actions.append(.import)
|
||||
if !manager.areAllCategoriesEmpty() {
|
||||
actions.append(.exportAll)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
private func setNotifications() {
|
||||
notifications = [.load]
|
||||
private func getNotifications() -> [BMCNotification] {
|
||||
[.load]
|
||||
}
|
||||
|
||||
func reloadData() {
|
||||
sections = []
|
||||
sections.removeAll()
|
||||
|
||||
if manager.areBookmarksLoaded() {
|
||||
sections.append(.categories)
|
||||
setCategories()
|
||||
categories = getCategories()
|
||||
|
||||
sections.append(.actions)
|
||||
setActions()
|
||||
actions = getActions()
|
||||
|
||||
if manager.recentlyDeletedCategoriesCount() != .zero {
|
||||
sections.append(.recentlyDeleted)
|
||||
}
|
||||
} else {
|
||||
sections.append(.notifications)
|
||||
setNotifications()
|
||||
notifications = getNotifications()
|
||||
}
|
||||
|
||||
view?.update(sections: [])
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCDefaultViewModel {
|
||||
func numberOfSections() -> Int {
|
||||
return sections.count
|
||||
sections.count
|
||||
}
|
||||
|
||||
func sectionType(section: Int) -> BMCSection {
|
||||
return sections[section]
|
||||
sections[section]
|
||||
}
|
||||
|
||||
func sectionIndex(section: BMCSection) -> Int {
|
||||
return sections.firstIndex(of: section)!
|
||||
sections.firstIndex(of: section)!
|
||||
}
|
||||
|
||||
func numberOfRows(section: Int) -> Int {
|
||||
return numberOfRows(section: sectionType(section: section))
|
||||
numberOfRows(section: sectionType(section: section))
|
||||
}
|
||||
|
||||
func numberOfRows(section: BMCSection) -> Int {
|
||||
switch section {
|
||||
case .categories: return categories.count
|
||||
case .actions: return actions.count
|
||||
case .recentlyDeleted: return 1
|
||||
case .notifications: return notifications.count
|
||||
}
|
||||
}
|
||||
|
||||
func category(at index: Int) -> BookmarkGroup {
|
||||
return categories[index]
|
||||
categories[index]
|
||||
}
|
||||
|
||||
func canDeleteCategory() -> Bool {
|
||||
categories.count > 1
|
||||
}
|
||||
|
||||
func action(at index: Int) -> BMCAction {
|
||||
return actions[index]
|
||||
actions[index]
|
||||
}
|
||||
|
||||
func recentlyDeletedCategories() -> BMCAction {
|
||||
.recentlyDeleted(Int(manager.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
func notification(at index: Int) -> BMCNotification {
|
||||
return notifications[index]
|
||||
notifications[index]
|
||||
}
|
||||
|
||||
func areAllCategoriesHidden() -> Bool {
|
||||
|
@ -130,7 +145,7 @@ extension BMCDefaultViewModel {
|
|||
}
|
||||
|
||||
func checkCategory(name: String) -> Bool {
|
||||
return manager.checkCategoryName(name)
|
||||
manager.checkCategoryName(name)
|
||||
}
|
||||
|
||||
func shareCategoryFile(at index: Int, fileType: KmlFileType, handler: @escaping SharingResultCompletionHandler) {
|
||||
|
@ -164,12 +179,11 @@ extension BMCDefaultViewModel {
|
|||
}
|
||||
|
||||
func areNotificationsEnabled() -> Bool {
|
||||
return manager.areNotificationsEnabled()
|
||||
manager.areNotificationsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCDefaultViewModel: BookmarksObserver {
|
||||
|
||||
func onBookmarksLoadFinished() {
|
||||
reloadData()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
final class RecentlyDeletedCategoriesViewController: MWMViewController {
|
||||
|
||||
private enum LocalizedStrings {
|
||||
static let clear = L("clear")
|
||||
static let delete = L("delete")
|
||||
static let deleteAll = L("delete_all")
|
||||
static let recover = L("recover")
|
||||
static let recoverAll = L("recover_all")
|
||||
static let recentlyDeleted = L("bookmarks_recently_deleted")
|
||||
static let searchInTheList = L("search_in_the_list")
|
||||
}
|
||||
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
|
||||
private lazy var clearButton = UIBarButtonItem(title: LocalizedStrings.clear, style: .done, target: self, action: #selector(clearButtonDidTap))
|
||||
private lazy var recoverButton = UIBarButtonItem(title: LocalizedStrings.recover, style: .done, target: self, action: #selector(recoverButtonDidTap))
|
||||
private lazy var deleteButton = UIBarButtonItem(title: LocalizedStrings.delete, style: .done, target: self, action: #selector(deleteButtonDidTap))
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
private let viewModel: RecentlyDeletedCategoriesViewModel
|
||||
|
||||
init(viewModel: RecentlyDeletedCategoriesViewModel = RecentlyDeletedCategoriesViewModel(bookmarksManager: BookmarksManager.shared())) {
|
||||
self.viewModel = viewModel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewModel.stateDidChange = { [weak self] state in
|
||||
self?.updateState(state)
|
||||
}
|
||||
viewModel.filteredDataSourceDidChange = { [weak self] dataSource in
|
||||
guard let self else { return }
|
||||
if dataSource.isEmpty {
|
||||
self.tableView.reloadData()
|
||||
} else {
|
||||
let indexes = IndexSet(integersIn: 0...dataSource.count - 1)
|
||||
self.tableView.update { self.tableView.reloadSections(indexes, with: .automatic) }
|
||||
}
|
||||
}
|
||||
viewModel.onCategoriesIsEmpty = { [weak self] in
|
||||
self?.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupView()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController?.setToolbarHidden(true, animated: true)
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
setupNavigationBar()
|
||||
setupToolBar()
|
||||
setupSearchBar()
|
||||
setupTableView()
|
||||
layout()
|
||||
updateState(viewModel.state)
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
title = LocalizedStrings.recentlyDeleted
|
||||
}
|
||||
|
||||
private func setupToolBar() {
|
||||
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
toolbarItems = [flexibleSpace, recoverButton, flexibleSpace, deleteButton, flexibleSpace]
|
||||
navigationController?.isToolbarHidden = false
|
||||
}
|
||||
|
||||
private func setupSearchBar() {
|
||||
searchController.searchBar.placeholder = LocalizedStrings.searchInTheList
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchBar.delegate = self
|
||||
searchController.searchBar.applyTheme()
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = true
|
||||
}
|
||||
|
||||
private func setupTableView() {
|
||||
tableView.styleName = "TableView:PressBackground";
|
||||
tableView.allowsMultipleSelectionDuringEditing = true
|
||||
tableView.register(cell: RecentlyDeletedTableViewCell.self)
|
||||
tableView.setEditing(true, animated: false)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
}
|
||||
|
||||
private func layout() {
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func updateState(_ state: RecentlyDeletedCategoriesViewModel.State) {
|
||||
switch state {
|
||||
case .searching:
|
||||
navigationController?.setToolbarHidden(true, animated: false)
|
||||
searchController.searchBar.isUserInteractionEnabled = true
|
||||
case .nothingSelected:
|
||||
navigationController?.setToolbarHidden(false, animated: false)
|
||||
recoverButton.title = LocalizedStrings.recoverAll
|
||||
deleteButton.title = LocalizedStrings.deleteAll
|
||||
searchController.searchBar.isUserInteractionEnabled = true
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: true)}
|
||||
case .someSelected:
|
||||
navigationController?.setToolbarHidden(false, animated: false)
|
||||
recoverButton.title = LocalizedStrings.recover
|
||||
deleteButton.title = LocalizedStrings.delete
|
||||
searchController.searchBar.isUserInteractionEnabled = false
|
||||
navigationItem.rightBarButtonItem = clearButton
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@objc private func clearButtonDidTap() {
|
||||
viewModel.cancelSelecting()
|
||||
}
|
||||
|
||||
@objc private func recoverButtonDidTap() {
|
||||
viewModel.recoverSelectedCategories()
|
||||
}
|
||||
|
||||
@objc private func deleteButtonDidTap() {
|
||||
viewModel.deleteSelectedCategories()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension RecentlyDeletedCategoriesViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
viewModel.filteredDataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.filteredDataSource[section].content.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(cell: RecentlyDeletedTableViewCell.self, indexPath: indexPath)
|
||||
let category = viewModel.filteredDataSource[indexPath.section].content[indexPath.row]
|
||||
cell.configureWith(RecentlyDeletedTableViewCell.ViewModel(category))
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension RecentlyDeletedCategoriesViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard tableView.isEditing else {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
return
|
||||
}
|
||||
viewModel.selectCategory(at: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
guard tableView.isEditing else { return }
|
||||
guard let selectedIndexPaths = tableView.indexPathsForSelectedRows, !selectedIndexPaths.isEmpty else {
|
||||
viewModel.deselectAllCategories()
|
||||
return
|
||||
}
|
||||
viewModel.deselectCategory(at: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: LocalizedStrings.delete) { [weak self] (_, _, completion) in
|
||||
self?.viewModel.deleteCategory(at: indexPath)
|
||||
completion(true)
|
||||
}
|
||||
let recoverAction = UIContextualAction(style: .normal, title: LocalizedStrings.recover) { [weak self] (_, _, completion) in
|
||||
self?.viewModel.recoverCategory(at: indexPath)
|
||||
completion(true)
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction, recoverAction])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
extension RecentlyDeletedCategoriesViewController: UISearchBarDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(true, animated: true)
|
||||
viewModel.startSearching()
|
||||
}
|
||||
|
||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(false, animated: true)
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.text = nil
|
||||
searchBar.resignFirstResponder()
|
||||
viewModel.cancelSearching()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
viewModel.search(searchText)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
final class RecentlyDeletedCategoriesViewModel: NSObject {
|
||||
|
||||
typealias BookmarksManager = RecentlyDeletedCategoriesManager & BookmarksObservable
|
||||
|
||||
enum Section: CaseIterable {
|
||||
struct Model: Equatable {
|
||||
var content: [RecentlyDeletedCategory]
|
||||
}
|
||||
|
||||
case main
|
||||
}
|
||||
|
||||
enum State {
|
||||
case searching
|
||||
case nothingSelected
|
||||
case someSelected
|
||||
}
|
||||
|
||||
private var recentlyDeletedCategoriesManager: BookmarksManager
|
||||
private var dataSource: [Section.Model] = [] {
|
||||
didSet {
|
||||
if dataSource.isEmpty {
|
||||
onCategoriesIsEmpty?()
|
||||
}
|
||||
}
|
||||
}
|
||||
private(set) var state: State = .nothingSelected
|
||||
private(set) var filteredDataSource: [Section.Model] = []
|
||||
private(set) var selectedIndexPaths: [IndexPath] = []
|
||||
private(set) var searchText = String()
|
||||
|
||||
var stateDidChange: ((State) -> Void)?
|
||||
var filteredDataSourceDidChange: (([Section.Model]) -> Void)?
|
||||
var onCategoriesIsEmpty: (() -> Void)?
|
||||
|
||||
init(bookmarksManager: BookmarksManager) {
|
||||
self.recentlyDeletedCategoriesManager = bookmarksManager
|
||||
super.init()
|
||||
subscribeOnBookmarksManagerNotifications()
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
|
||||
deinit {
|
||||
unsubscribeFromBookmarksManagerNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func subscribeOnBookmarksManagerNotifications() {
|
||||
recentlyDeletedCategoriesManager.add(self)
|
||||
}
|
||||
|
||||
private func unsubscribeFromBookmarksManagerNotifications() {
|
||||
recentlyDeletedCategoriesManager.remove(self)
|
||||
}
|
||||
|
||||
private func updateState(to newState: State) {
|
||||
guard state != newState else { return }
|
||||
state = newState
|
||||
stateDidChange?(state)
|
||||
}
|
||||
|
||||
private func updateFilteredDataSource(_ dataSource: [Section.Model]) {
|
||||
filteredDataSource = dataSource.filtered(using: searchText)
|
||||
filteredDataSourceDidChange?(filteredDataSource)
|
||||
}
|
||||
|
||||
private func updateSelectionAtIndexPath(_ indexPath: IndexPath, isSelected: Bool) {
|
||||
if isSelected {
|
||||
updateState(to: .someSelected)
|
||||
} else {
|
||||
let allDeselected = dataSource.allSatisfy { $0.content.isEmpty }
|
||||
updateState(to: allDeselected ? .nothingSelected : .someSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeCategories(at indexPaths: [IndexPath], completion: ([URL]) -> Void) {
|
||||
var fileToRemoveURLs: [URL]
|
||||
if indexPaths.isEmpty {
|
||||
// Remove all without selection.
|
||||
fileToRemoveURLs = dataSource.flatMap { $0.content.map { $0.fileURL } }
|
||||
dataSource.removeAll()
|
||||
} else {
|
||||
fileToRemoveURLs = [URL]()
|
||||
indexPaths.forEach { [weak self] indexPath in
|
||||
guard let self else { return }
|
||||
let fileToRemoveURL = self.filteredDataSource[indexPath.section].content[indexPath.row].fileURL
|
||||
self.dataSource[indexPath.section].content.removeAll { $0.fileURL == fileToRemoveURL }
|
||||
fileToRemoveURLs.append(fileToRemoveURL)
|
||||
}
|
||||
}
|
||||
updateFilteredDataSource(dataSource)
|
||||
updateState(to: .nothingSelected)
|
||||
completion(fileToRemoveURLs)
|
||||
}
|
||||
|
||||
private func removeSelectedCategories(completion: ([URL]) -> Void) {
|
||||
let removeAll = selectedIndexPaths.isEmpty || selectedIndexPaths.count == dataSource.flatMap({ $0.content }).count
|
||||
removeCategories(at: removeAll ? [] : selectedIndexPaths, completion: completion)
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
extension RecentlyDeletedCategoriesViewModel {
|
||||
func fetchRecentlyDeletedCategories() {
|
||||
let categories = recentlyDeletedCategoriesManager.getRecentlyDeletedCategories()
|
||||
guard !categories.isEmpty else { return }
|
||||
dataSource = [Section.Model(content: categories)]
|
||||
updateFilteredDataSource(dataSource)
|
||||
}
|
||||
|
||||
func deleteCategory(at indexPath: IndexPath) {
|
||||
removeCategories(at: [indexPath]) { recentlyDeletedCategoriesManager.deleteRecentlyDeletedCategory(at: $0) }
|
||||
}
|
||||
|
||||
func deleteSelectedCategories() {
|
||||
removeSelectedCategories { recentlyDeletedCategoriesManager.deleteRecentlyDeletedCategory(at: $0) }
|
||||
}
|
||||
|
||||
func recoverCategory(at indexPath: IndexPath) {
|
||||
removeCategories(at: [indexPath]) { recentlyDeletedCategoriesManager.recoverRecentlyDeletedCategories(at: $0) }
|
||||
}
|
||||
|
||||
func recoverSelectedCategories() {
|
||||
removeSelectedCategories { recentlyDeletedCategoriesManager.recoverRecentlyDeletedCategories(at: $0) }
|
||||
}
|
||||
|
||||
func startSelecting() {
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func selectCategory(at indexPath: IndexPath) {
|
||||
selectedIndexPaths.append(indexPath)
|
||||
updateState(to: .someSelected)
|
||||
}
|
||||
|
||||
func deselectCategory(at indexPath: IndexPath) {
|
||||
selectedIndexPaths.removeAll { $0 == indexPath }
|
||||
if selectedIndexPaths.isEmpty {
|
||||
updateState(to: state == .searching ? .searching : .nothingSelected)
|
||||
}
|
||||
}
|
||||
|
||||
func selectAllCategories() {
|
||||
selectedIndexPaths = dataSource.enumerated().flatMap { sectionIndex, section in
|
||||
section.content.indices.map { IndexPath(row: $0, section: sectionIndex) }
|
||||
}
|
||||
updateState(to: .someSelected)
|
||||
}
|
||||
|
||||
func deselectAllCategories() {
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: state == .searching ? .searching : .nothingSelected)
|
||||
}
|
||||
|
||||
func cancelSelecting() {
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func startSearching() {
|
||||
updateState(to: .searching)
|
||||
}
|
||||
|
||||
func cancelSearching() {
|
||||
searchText.removeAll()
|
||||
selectedIndexPaths.removeAll()
|
||||
updateFilteredDataSource(dataSource)
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func search(_ searchText: String) {
|
||||
updateState(to: .searching)
|
||||
guard !searchText.isEmpty else {
|
||||
cancelSearching()
|
||||
return
|
||||
}
|
||||
self.searchText = searchText
|
||||
updateFilteredDataSource(dataSource)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
|
||||
extension RecentlyDeletedCategoriesViewModel: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
|
||||
func onRecentlyDeletedBookmarksCategoriesChanged() {
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension Array where Element == RecentlyDeletedCategoriesViewModel.Section.Model {
|
||||
func filtered(using searchText: String) -> [Element] {
|
||||
let filteredArray = map { section in
|
||||
let filteredContent = section.content.filter {
|
||||
guard !searchText.isEmpty else { return true }
|
||||
return $0.title.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
return RecentlyDeletedCategoriesViewModel.Section.Model(content: filteredContent)
|
||||
}
|
||||
return filteredArray
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
final class RecentlyDeletedTableViewCell: UITableViewCell {
|
||||
|
||||
struct ViewModel: Equatable, Hashable {
|
||||
let fileName: String
|
||||
let fileURL: URL
|
||||
let deletionDate: Date
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configureWith(_ viewModel: ViewModel) {
|
||||
textLabel?.text = viewModel.fileName
|
||||
detailTextLabel?.text = Self.dateFormatter.string(from: viewModel.deletionDate)
|
||||
}
|
||||
}
|
||||
|
||||
extension RecentlyDeletedTableViewCell.ViewModel {
|
||||
init(_ category: RecentlyDeletedCategory) {
|
||||
self.fileName = category.title
|
||||
self.fileURL = category.fileURL
|
||||
self.deletionDate = category.deletionDate
|
||||
}
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_route_manager_trash_open.pdf"
|
||||
"filename" : "ic_route_manager_trash_open.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -493,6 +493,11 @@
|
|||
EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; };
|
||||
EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; };
|
||||
EDC3573B2B7B5029001AE9CA /* CALayer+SetCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */; };
|
||||
EDC4E34B2C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3472C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift */; };
|
||||
EDC4E34C2C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3482C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift */; };
|
||||
EDC4E34D2C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3492C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift */; };
|
||||
EDC4E3612C5E2576009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3412C5D1BD3009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift */; };
|
||||
EDC4E3692C5E6F5B009286A2 /* MockRecentlyDeletedCategoriesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3402C5D1BD3009286A2 /* MockRecentlyDeletedCategoriesManager.swift */; };
|
||||
EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243D52B6CF3980057369B /* AboutController.swift */; };
|
||||
EDE243E52B6D3F400057369B /* OSMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E42B6D3F400057369B /* OSMView.swift */; };
|
||||
EDE243E72B6D55610057369B /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E02B6D3EA00057369B /* InfoView.swift */; };
|
||||
|
@ -1415,6 +1420,11 @@
|
|||
EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = "<group>"; };
|
||||
EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = "<group>"; };
|
||||
EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+SetCorner.swift"; sourceTree = "<group>"; };
|
||||
EDC4E3402C5D1BD3009286A2 /* MockRecentlyDeletedCategoriesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockRecentlyDeletedCategoriesManager.swift; sourceTree = "<group>"; };
|
||||
EDC4E3412C5D1BD3009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedCategoriesViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EDC4E3472C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedCategoriesViewController.swift; sourceTree = "<group>"; };
|
||||
EDC4E3482C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedCategoriesViewModel.swift; sourceTree = "<group>"; };
|
||||
EDC4E3492C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedTableViewCell.swift; sourceTree = "<group>"; };
|
||||
EDE243D52B6CF3980057369B /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
|
||||
EDE243E02B6D3EA00057369B /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = "<group>"; };
|
||||
EDE243E42B6D3F400057369B /* OSMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2070,6 +2080,7 @@
|
|||
3404F4A02028A6C00090E401 /* Categories */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E34A2C5D1BEF009286A2 /* RecentlyDeleted */,
|
||||
33046837219C605E0041F3A8 /* Category settings */,
|
||||
343D7B6D202AF4CA007D56A8 /* Actions */,
|
||||
3404F48F202898CC0090E401 /* BMCModels.swift */,
|
||||
|
@ -3076,6 +3087,7 @@
|
|||
ED1ADA312BC6B19E0029209F /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E3442C5D1BD3009286A2 /* Bookmarks */,
|
||||
4B4153B82BF970B800EE4B02 /* Classes */,
|
||||
4B4153B62BF9709100EE4B02 /* Core */,
|
||||
);
|
||||
|
@ -3129,6 +3141,41 @@
|
|||
path = ColorPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDC4E3422C5D1BD3009286A2 /* RecentlyDeletedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E3402C5D1BD3009286A2 /* MockRecentlyDeletedCategoriesManager.swift */,
|
||||
EDC4E3412C5D1BD3009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift */,
|
||||
);
|
||||
path = RecentlyDeletedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDC4E3432C5D1BD3009286A2 /* Categories */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E3422C5D1BD3009286A2 /* RecentlyDeletedTests */,
|
||||
);
|
||||
path = Categories;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDC4E3442C5D1BD3009286A2 /* Bookmarks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E3432C5D1BD3009286A2 /* Categories */,
|
||||
);
|
||||
path = Bookmarks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDC4E34A2C5D1BEF009286A2 /* RecentlyDeleted */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC4E3472C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift */,
|
||||
EDC4E3482C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift */,
|
||||
EDC4E3492C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift */,
|
||||
);
|
||||
path = RecentlyDeleted;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDE8EAE32C2DB74A002777F5 /* OpenInAppActionSheet */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4351,6 +4398,7 @@
|
|||
3463BA671DE81DB90082417F /* MWMTrafficButtonViewController.mm in Sources */,
|
||||
ED79A5D52BDF8D6100952D1F /* SynchronizationError.swift in Sources */,
|
||||
993DF10323F6BDB100AC231A /* MainTheme.swift in Sources */,
|
||||
EDC4E34D2C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift in Sources */,
|
||||
34AB66051FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm in Sources */,
|
||||
993DF12A23F6BDB100AC231A /* Style.swift in Sources */,
|
||||
34ABA6171C2D185C00FE1BEC /* MWMAuthorizationOSMLoginViewController.mm in Sources */,
|
||||
|
@ -4443,6 +4491,7 @@
|
|||
342CC5F21C2D7730005F3FE5 /* MWMAuthorizationLoginViewController.mm in Sources */,
|
||||
340475591E081A4600C92850 /* WebViewController.m in Sources */,
|
||||
3404F4992028A20D0090E401 /* BMCCategoryCell.swift in Sources */,
|
||||
EDC4E34C2C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift in Sources */,
|
||||
F62607FD207B790300176C5A /* SpinnerAlert.swift in Sources */,
|
||||
3444DFD21F17620C00E73099 /* MWMMapWidgetsHelper.mm in Sources */,
|
||||
3472B5E1200F86C800DC6CD5 /* MWMEditorHelper.mm in Sources */,
|
||||
|
@ -4492,6 +4541,7 @@
|
|||
3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */,
|
||||
34E7761F1F14DB48003040B3 /* PlacePageArea.swift in Sources */,
|
||||
ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */,
|
||||
EDC4E34B2C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift in Sources */,
|
||||
ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */,
|
||||
4728F69322CF89A400E00028 /* GradientView.swift in Sources */,
|
||||
F6381BF61CD12045004CA943 /* LocaleTranslator.mm in Sources */,
|
||||
|
@ -4677,9 +4727,11 @@
|
|||
ED1ADA332BC6B1B40029209F /* CarPlayServiceTests.swift in Sources */,
|
||||
EDF838C32C00B9D6007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDF838BE2C00B9D0007E4E67 /* LocalDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDC4E3692C5E6F5B009286A2 /* MockRecentlyDeletedCategoriesManager.swift in Sources */,
|
||||
EDF838BF2C00B9D0007E4E67 /* SynchronizationStateManagerTests.swift in Sources */,
|
||||
4B83AE4B2C2E642100B0C3BC /* TTSTesterTest.m in Sources */,
|
||||
EDF838C22C00B9D6007E4E67 /* MetadataItemStubs.swift in Sources */,
|
||||
EDC4E3612C5E2576009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift in Sources */,
|
||||
4B4153B52BF9695500EE4B02 /* MWMTextToSpeechTests.mm in Sources */,
|
||||
EDF838C42C00B9D6007E4E67 /* FileManagerMock.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
class MockRecentlyDeletedCategoriesManager: NSObject, RecentlyDeletedCategoriesManager, BookmarksObservable {
|
||||
|
||||
var categories = [RecentlyDeletedCategory]()
|
||||
|
||||
func recentlyDeletedCategoriesCount() -> UInt64 {
|
||||
UInt64(categories.count)
|
||||
}
|
||||
|
||||
func getRecentlyDeletedCategories() -> [RecentlyDeletedCategory] {
|
||||
categories
|
||||
}
|
||||
|
||||
func deleteFile(at urls: [URL]) {
|
||||
categories.removeAll { urls.contains($0.fileURL) }
|
||||
}
|
||||
|
||||
func deleteAllRecentlyDeletedCategories() {
|
||||
categories.removeAll()
|
||||
}
|
||||
|
||||
func recoverRecentlyDeletedCategories(at urls: [URL]) {
|
||||
categories.removeAll { urls.contains($0.fileURL) }
|
||||
}
|
||||
|
||||
func deleteRecentlyDeletedCategory(at urls: [URL]) {
|
||||
categories.removeAll { urls.contains($0.fileURL) }
|
||||
}
|
||||
|
||||
func add(_ observer: any BookmarksObserver) {}
|
||||
|
||||
func remove(_ observer: any BookmarksObserver) {}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
final class RecentlyDeletedCategoriesViewModelTests: XCTestCase {
|
||||
var viewModel: RecentlyDeletedCategoriesViewModel!
|
||||
var bookmarksManagerMock: MockRecentlyDeletedCategoriesManager!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
bookmarksManagerMock = MockRecentlyDeletedCategoriesManager()
|
||||
setupBookmarksManagerStubs()
|
||||
|
||||
viewModel = RecentlyDeletedCategoriesViewModel(bookmarksManager: bookmarksManagerMock)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
viewModel = nil
|
||||
bookmarksManagerMock = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
private func setupBookmarksManagerStubs() {
|
||||
bookmarksManagerMock.categories = [
|
||||
RecentlyDeletedCategory(title: "test1", fileURL: URL(string: "test1")!, deletionDate: Date()),
|
||||
RecentlyDeletedCategory(title: "test2", fileURL: URL(string: "test2")!, deletionDate: Date()),
|
||||
RecentlyDeletedCategory(title: "lol", fileURL: URL(string: "lol")!, deletionDate: Date()),
|
||||
RecentlyDeletedCategory(title: "te1", fileURL: URL(string: "te1")!, deletionDate: Date()),
|
||||
]
|
||||
}
|
||||
|
||||
func testInitializationFetchesCategories() {
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
// MARK: - Selection Tests
|
||||
func testMultipleSelectionAndDeselection() {
|
||||
viewModel.selectAllCategories()
|
||||
let initialSelectedCount = viewModel.selectedIndexPaths.count
|
||||
XCTAssertEqual(initialSelectedCount, viewModel.filteredDataSource.flatMap { $0.content }.count)
|
||||
|
||||
viewModel.deselectAllCategories()
|
||||
XCTAssertTrue(viewModel.selectedIndexPaths.isEmpty)
|
||||
}
|
||||
|
||||
func testSelectAndDeselectSpecificCategory() {
|
||||
let specificIndexPath = IndexPath(row: 0, section: 0)
|
||||
viewModel.selectCategory(at: specificIndexPath)
|
||||
XCTAssertTrue(viewModel.selectedIndexPaths.contains(specificIndexPath))
|
||||
|
||||
viewModel.deselectCategory(at: specificIndexPath)
|
||||
XCTAssertFalse(viewModel.selectedIndexPaths.contains(specificIndexPath))
|
||||
}
|
||||
|
||||
func testSelectAndDeselectSpecificCategories() {
|
||||
let indexPath1 = IndexPath(row: 0, section: 0)
|
||||
let indexPath2 = IndexPath(row: 1, section: 0)
|
||||
let indexPath3 = IndexPath(row: 2, section: 0)
|
||||
viewModel.selectCategory(at: indexPath1)
|
||||
viewModel.selectCategory(at: indexPath2)
|
||||
viewModel.selectCategory(at: indexPath3)
|
||||
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath1))
|
||||
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath2))
|
||||
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath3))
|
||||
|
||||
viewModel.deselectCategory(at: indexPath1)
|
||||
XCTAssertFalse(viewModel.selectedIndexPaths.contains(indexPath1))
|
||||
XCTAssertEqual(viewModel.state, .someSelected)
|
||||
|
||||
viewModel.deselectCategory(at: indexPath2)
|
||||
viewModel.deselectCategory(at: indexPath3)
|
||||
XCTAssertEqual(viewModel.selectedIndexPaths.count, .zero)
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
}
|
||||
|
||||
func testStateChangesOnSelection() {
|
||||
let indexPath = IndexPath(row: 1, section: 0)
|
||||
viewModel.selectCategory(at: indexPath)
|
||||
XCTAssertEqual(viewModel.state, .someSelected)
|
||||
|
||||
viewModel.deselectCategory(at: indexPath)
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
}
|
||||
|
||||
func testStateChangesOnDone() {
|
||||
let indexPath = IndexPath(row: 1, section: 0)
|
||||
viewModel.selectCategory(at: indexPath)
|
||||
XCTAssertEqual(viewModel.state, .someSelected)
|
||||
|
||||
viewModel.cancelSelecting()
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
// MARK: - Searching Tests
|
||||
func testSearchWithEmptyString() {
|
||||
viewModel.search("")
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 4)
|
||||
}
|
||||
|
||||
func testSearchWithNoResults() {
|
||||
viewModel.search("xyz") // Assuming "xyz" matches no category names
|
||||
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.isEmpty })
|
||||
}
|
||||
|
||||
func testCancelSearchRestoresDataSource() {
|
||||
let searchText = "test"
|
||||
viewModel.search(searchText)
|
||||
XCTAssertEqual(viewModel.state, .searching)
|
||||
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 2)
|
||||
|
||||
viewModel.cancelSearching()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 4)
|
||||
}
|
||||
|
||||
// MARK: - Deletion Tests
|
||||
func testDeleteCategory() {
|
||||
let initialCount = bookmarksManagerMock.categories.count
|
||||
viewModel.deleteCategory(at: IndexPath(row: 0, section: 0))
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, initialCount - 1)
|
||||
}
|
||||
|
||||
func testDeleteAllWhenNoOneIsSelected() {
|
||||
viewModel.deleteSelectedCategories()
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, .zero)
|
||||
}
|
||||
|
||||
func testDeleteAllWhenNoSoneAreSelected() {
|
||||
viewModel.selectCategory(at: IndexPath(row: 0, section: 0))
|
||||
viewModel.selectCategory(at: IndexPath(row: 1, section: 0))
|
||||
viewModel.deleteSelectedCategories()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, 2)
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 2)
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
// MARK: - Recovery Tests
|
||||
func testRecoverCategory() {
|
||||
viewModel.recoverCategory(at: IndexPath(row: 0, section: 0))
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, 3)
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
}
|
||||
|
||||
func testRecoverAll() {
|
||||
viewModel.recoverSelectedCategories()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, 0)
|
||||
}
|
||||
|
||||
func testRecoverAllWhenSomeAreSelected() {
|
||||
viewModel.selectCategory(at: IndexPath(row: 0, section: 0))
|
||||
viewModel.selectCategory(at: IndexPath(row: 1, section: 0))
|
||||
viewModel.recoverSelectedCategories()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
XCTAssertEqual(bookmarksManagerMock.categories.count, 2)
|
||||
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
func testSearchFiltersCategories() {
|
||||
var searchText = "test"
|
||||
viewModel.search(searchText)
|
||||
XCTAssertEqual(viewModel.state, .searching)
|
||||
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
|
||||
|
||||
searchText = "te"
|
||||
viewModel.search(searchText)
|
||||
XCTAssertEqual(viewModel.state, .searching)
|
||||
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
|
||||
}
|
||||
|
||||
func testDeleteAllCategories() {
|
||||
viewModel.deleteSelectedCategories()
|
||||
XCTAssertTrue(bookmarksManagerMock.categories.isEmpty)
|
||||
}
|
||||
|
||||
func testRecoverAllCategories() {
|
||||
viewModel.recoverSelectedCategories()
|
||||
XCTAssertTrue(bookmarksManagerMock.categories.isEmpty)
|
||||
}
|
||||
|
||||
func testDeleteAndRecoverAllCategoriesWhenEmpty() {
|
||||
bookmarksManagerMock.categories = []
|
||||
viewModel.fetchRecentlyDeletedCategories()
|
||||
viewModel.deleteSelectedCategories()
|
||||
viewModel.recoverSelectedCategories()
|
||||
XCTAssertTrue(viewModel.filteredDataSource.isEmpty)
|
||||
}
|
||||
|
||||
func testMultipleStateTransitions() {
|
||||
viewModel.startSelecting()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
|
||||
viewModel.startSearching()
|
||||
XCTAssertEqual(viewModel.state, .searching)
|
||||
|
||||
viewModel.cancelSearching()
|
||||
viewModel.cancelSelecting()
|
||||
XCTAssertEqual(viewModel.state, .nothingSelected)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue