From 6c96cad87a669f94a66aee9ff54bd6cc736d6eda Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Mon, 5 Oct 2020 00:50:59 -0700 Subject: [PATCH] Refactoring --- View Controllers/TableViewController.swift | 12 +- .../ViewModels/CollectionViewModel.swift | 6 +- .../Sources/ViewModels/ListViewModel.swift | 149 ++++++++---------- .../Sources/ViewModels/ProfileViewModel.swift | 12 +- 4 files changed, 77 insertions(+), 102 deletions(-) diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 3f6f0fa..1a9050d 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -16,9 +16,7 @@ class TableViewController: UITableViewController { private lazy var dataSource: UITableViewDiffableDataSource = { UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, identifier in - guard let self = self, - let cellViewModel = self.viewModel.viewModel(identifier: identifier) - else { return nil } + guard let cellViewModel = self?.viewModel.viewModel(indexPath: indexPath) else { return nil } let cell = tableView.dequeueReusableCell( withIdentifier: String(describing: identifier.kind.cellClass), @@ -113,17 +111,13 @@ class TableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - guard let identifier = dataSource.itemIdentifier(for: indexPath) else { return true } - - return viewModel.canSelect(identifier: identifier) + viewModel.canSelect(indexPath: indexPath) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - guard let identifier = dataSource.itemIdentifier(for: indexPath) else { return } - - viewModel.select(identifier: identifier) + viewModel.select(indexPath: indexPath) } override func viewDidLayoutSubviews() { diff --git a/ViewModels/Sources/ViewModels/CollectionViewModel.swift b/ViewModels/Sources/ViewModels/CollectionViewModel.swift index 79ffd7e..2ad616c 100644 --- a/ViewModels/Sources/ViewModels/CollectionViewModel.swift +++ b/ViewModels/Sources/ViewModels/CollectionViewModel.swift @@ -12,7 +12,7 @@ public protocol CollectionViewModel { var nextPageMaxID: String? { get } var maintainScrollPositionOfItem: CollectionItemIdentifier? { get } func request(maxID: String?, minID: String?) - func select(identifier: CollectionItemIdentifier) - func canSelect(identifier: CollectionItemIdentifier) -> Bool - func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel? + func select(indexPath: IndexPath) + func canSelect(indexPath: IndexPath) -> Bool + func viewModel(indexPath: IndexPath) -> CollectionItemViewModel } diff --git a/ViewModels/Sources/ViewModels/ListViewModel.swift b/ViewModels/Sources/ViewModels/ListViewModel.swift index 09c9dc9..8a72a5b 100644 --- a/ViewModels/Sources/ViewModels/ListViewModel.swift +++ b/ViewModels/Sources/ViewModels/ListViewModel.swift @@ -6,12 +6,11 @@ import Mastodon import ServiceLayer final public class ListViewModel: ObservableObject { - @Published public private(set) var identifiers = [[CollectionItemIdentifier]]() @Published public var alertItem: AlertItem? public private(set) var nextPageMaxID: String? public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier? - private var items = [CollectionItemIdentifier: CollectionItem]() + private let items = CurrentValueSubject<[[CollectionItem]], Never>([]) private let collectionService: CollectionService private var viewModelCache = [CollectionItem: (CollectionItemViewModel, AnyCancellable)]() private let navigationEventsSubject = PassthroughSubject() @@ -22,11 +21,11 @@ final public class ListViewModel: ObservableObject { self.collectionService = collectionService collectionService.sections - .handleEvents(receiveOutput: { [weak self] in self?.process(sections: $0) }) - .map { $0.map { $0.map(CollectionItemIdentifier.init(item:)) } } + .handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) }) .receive(on: DispatchQueue.main) .assignErrorsToAlertItem(to: \.alertItem, on: self) - .assign(to: &$identifiers) + .sink { _ in } + .store(in: &cancellables) collectionService.nextPageMaxIDs .sink { [weak self] in self?.nextPageMaxID = $0 } @@ -35,7 +34,9 @@ final public class ListViewModel: ObservableObject { } extension ListViewModel: CollectionViewModel { - public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> { $identifiers.eraseToAnyPublisher() } + public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> { + items.map { $0.map { $0.map(CollectionItemIdentifier.init(item:)) } }.eraseToAnyPublisher() + } public var title: AnyPublisher { Just(collectionService.title).eraseToAnyPublisher() } @@ -56,8 +57,8 @@ extension ListViewModel: CollectionViewModel { .store(in: &cancellables) } - public func select(identifier: CollectionItemIdentifier) { - guard let item = items[identifier] else { return } + public func select(indexPath: IndexPath) { + let item = items.value[indexPath.section][indexPath.item] switch item { case let .status(configuration): @@ -68,7 +69,7 @@ extension ListViewModel: CollectionViewModel { .navigationService .contextService(id: configuration.status.displayStatus.id)))) case .loadMore: - loadMoreViewModel(item: identifier)?.loadMore() + (viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore() case let .account(account): navigationEventsSubject.send( .profileNavigation( @@ -77,108 +78,88 @@ extension ListViewModel: CollectionViewModel { } } - public func canSelect(identifier: CollectionItemIdentifier) -> Bool { - if case .status = identifier.kind, identifier.id == collectionService.contextParentID { + public func canSelect(indexPath: IndexPath) -> Bool { + if case let .status(configuration) = items.value[indexPath.section][indexPath.item], + configuration.status.id == collectionService.contextParentID { return false } return true } - public func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel? { - switch identifier.kind { - case .status: - return statusViewModel(item: identifier) - case .loadMore: - return loadMoreViewModel(item: identifier) - case .account: - return accountViewModel(item: identifier) + public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel { + let item = items.value[indexPath.section][indexPath.item] + + switch item { + case let .status(configuration): + var viewModel: StatusViewModel + + if let cachedViewModel = viewModelCache[item]?.0 as? StatusViewModel { + viewModel = cachedViewModel + } else { + viewModel = StatusViewModel( + statusService: collectionService.navigationService.statusService(status: configuration.status)) + cache(viewModel: viewModel, forItem: item) + } + + viewModel.isContextParent = configuration.status.id == collectionService.contextParentID + viewModel.isPinned = configuration.pinned + viewModel.isReplyInContext = configuration.isReplyInContext + viewModel.hasReplyFollowing = configuration.hasReplyFollowing + + return viewModel + case let .loadMore(loadMore): + if let cachedViewModel = viewModelCache[item]?.0 as? LoadMoreViewModel { + return cachedViewModel + } + + let viewModel = LoadMoreViewModel( + loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore)) + + cache(viewModel: viewModel, forItem: item) + + return viewModel + case let .account(account): + if let cachedViewModel = viewModelCache[item]?.0 as? AccountViewModel { + return cachedViewModel + } + + let viewModel = AccountViewModel( + accountService: collectionService.navigationService.accountService(account: account)) + + cache(viewModel: viewModel, forItem: item) + + return viewModel } } } private extension ListViewModel { - func statusViewModel(item: CollectionItemIdentifier) -> StatusViewModel? { - guard let timelineItem = items[item], - case let .status(configuration) = timelineItem - else { return nil } - - var statusViewModel: StatusViewModel - - if let cachedViewModel = viewModelCache[timelineItem]?.0 as? StatusViewModel { - statusViewModel = cachedViewModel - } else { - statusViewModel = StatusViewModel( - statusService: collectionService.navigationService.statusService(status: configuration.status)) - cache(viewModel: statusViewModel, forItem: timelineItem) - } - - statusViewModel.isContextParent = configuration.status.id == collectionService.contextParentID - statusViewModel.isPinned = configuration.pinned - statusViewModel.isReplyInContext = configuration.isReplyInContext - statusViewModel.hasReplyFollowing = configuration.hasReplyFollowing - - return statusViewModel - } - - func loadMoreViewModel(item: CollectionItemIdentifier) -> LoadMoreViewModel? { - guard let timelineItem = items[item], - case let .loadMore(loadMore) = timelineItem - else { return nil } - - if let cachedViewModel = viewModelCache[timelineItem]?.0 as? LoadMoreViewModel { - return cachedViewModel - } - - let loadMoreViewModel = LoadMoreViewModel( - loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore)) - - cache(viewModel: loadMoreViewModel, forItem: timelineItem) - - return loadMoreViewModel - } - - func accountViewModel(item: CollectionItemIdentifier) -> AccountViewModel? { - guard let timelineItem = items[item], - case let .account(account) = timelineItem - else { return nil } - - var accountViewModel: AccountViewModel - - if let cachedViewModel = viewModelCache[timelineItem]?.0 as? AccountViewModel { - accountViewModel = cachedViewModel - } else { - accountViewModel = AccountViewModel( - accountService: collectionService.navigationService.accountService(account: account)) - cache(viewModel: accountViewModel, forItem: timelineItem) - } - - return accountViewModel - } - func cache(viewModel: CollectionItemViewModel, forItem item: CollectionItem) { viewModelCache[item] = (viewModel, viewModel.events.flatMap { $0.compactMap(NavigationEvent.init) } .assignErrorsToAlertItem(to: \.alertItem, on: self) .sink { [weak self] in self?.navigationEventsSubject.send($0) }) } - func process(sections: [[CollectionItem]]) { - determineIfScrollPositionShouldBeMaintained(newSections: sections) + func process(items: [[CollectionItem]]) { + determineIfScrollPositionShouldBeMaintained(newItems: items) + self.items.send(items) - let timelineItemKeys = Set(sections.reduce([], +)) + let itemsSet = Set(items.reduce([], +)) - items = Dictionary(uniqueKeysWithValues: timelineItemKeys.map { (.init(item: $0), $0) }) - viewModelCache = viewModelCache.filter { timelineItemKeys.contains($0.key) } + viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) } } - func determineIfScrollPositionShouldBeMaintained(newSections: [[CollectionItem]]) { + func determineIfScrollPositionShouldBeMaintained(newItems: [[CollectionItem]]) { maintainScrollPositionOfItem = nil // clear old value // Maintain scroll position of parent after initial load of context if let contextParentID = collectionService.contextParentID { let contextParentIdentifier = CollectionItemIdentifier(id: contextParentID, kind: .status, info: [:]) + let onlyContextParentID = [[], [contextParentIdentifier], []] - if identifiers == [[], [contextParentIdentifier], []] || identifiers.isEmpty { + if items.value.isEmpty + || items.value.map({ $0.map(CollectionItemIdentifier.init(item:)) }) == onlyContextParentID { maintainScrollPositionOfItem = contextParentIdentifier } } diff --git a/ViewModels/Sources/ViewModels/ProfileViewModel.swift b/ViewModels/Sources/ViewModels/ProfileViewModel.swift index ccc61dc..1c5868b 100644 --- a/ViewModels/Sources/ViewModels/ProfileViewModel.swift +++ b/ViewModels/Sources/ViewModels/ProfileViewModel.swift @@ -85,15 +85,15 @@ extension ProfileViewModel: CollectionViewModel { collectionViewModel.value.request(maxID: maxID, minID: minID) } - public func select(identifier: CollectionItemIdentifier) { - collectionViewModel.value.select(identifier: identifier) + public func select(indexPath: IndexPath) { + collectionViewModel.value.select(indexPath: indexPath) } - public func canSelect(identifier: CollectionItemIdentifier) -> Bool { - collectionViewModel.value.canSelect(identifier: identifier) + public func canSelect(indexPath: IndexPath) -> Bool { + collectionViewModel.value.canSelect(indexPath: indexPath) } - public func viewModel(identifier: CollectionItemIdentifier) -> CollectionItemViewModel? { - collectionViewModel.value.viewModel(identifier: identifier) + public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel { + collectionViewModel.value.viewModel(indexPath: indexPath) } }