diff --git a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift index 7824f6b..6060158 100644 --- a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift @@ -5,18 +5,19 @@ import Foundation import Mastodon import ServiceLayer -public final class AccountViewModel: CollectionItemViewModel, ObservableObject { - public let events: AnyPublisher, Never> +public final class AccountViewModel: ObservableObject { public let identityContext: IdentityContext public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote private let accountService: AccountService - private let eventsSubject = PassthroughSubject, Never>() + private let eventsSubject: PassthroughSubject, Never> - init(accountService: AccountService, identityContext: IdentityContext) { + init(accountService: AccountService, + identityContext: IdentityContext, + eventsSubject: PassthroughSubject, Never>) { self.accountService = accountService self.identityContext = identityContext - events = eventsSubject.eraseToAnyPublisher() + self.eventsSubject = eventsSubject } } diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemViewModel.swift deleted file mode 100644 index fd709ca..0000000 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemViewModel.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Combine -import Foundation - -public protocol CollectionItemViewModel { - var events: AnyPublisher, Never> { get } -} diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift index ad27a1e..5edb445 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift @@ -13,8 +13,8 @@ public class CollectionItemsViewModel: ObservableObject { @Published private var lastUpdate = CollectionUpdate.empty private let collectionService: CollectionService - private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]() - private let eventsSubject = PassthroughSubject() + private var viewModelCache = [CollectionItem: Any]() + private let eventsSubject = PassthroughSubject, Never>() private let loadingSubject = PassthroughSubject() private let expandAllSubject: CurrentValueSubject private var topVisibleIndexPath = IndexPath(item: 0, section: 0) @@ -83,7 +83,14 @@ extension CollectionItemsViewModel: CollectionViewModel { public var loading: AnyPublisher { loadingSubject.eraseToAnyPublisher() } - public var events: AnyPublisher { eventsSubject.eraseToAnyPublisher() } + public var events: AnyPublisher { + eventsSubject.flatMap { [weak self] eventPublisher -> AnyPublisher in + guard let self = self else { return Empty().eraseToAnyPublisher() } + + return eventPublisher.assignErrorsToAlertItem(to: \.alertItem, on: self).eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } public var canRefresh: Bool { collectionService.canRefresh } @@ -128,44 +135,38 @@ extension CollectionItemsViewModel: CollectionViewModel { switch item { case let .status(status, _): - eventsSubject.send( - .navigation(.collection(collectionService - .navigationService - .contextService(id: status.displayStatus.id)))) + send(event: .navigation(.collection(collectionService + .navigationService + .contextService(id: status.displayStatus.id)))) case let .loadMore(loadMore): lastSelectedLoadMore = loadMore (viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore() case let .account(account, _): - eventsSubject.send( - .navigation(.profile(collectionService - .navigationService - .profileService(account: account)))) + send(event: .navigation(.profile(collectionService + .navigationService + .profileService(account: account)))) case let .notification(notification, _): if let status = notification.status { - eventsSubject.send( - .navigation(.collection(collectionService - .navigationService - .contextService(id: status.displayStatus.id)))) + send(event: .navigation(.collection(collectionService + .navigationService + .contextService(id: status.displayStatus.id)))) } else { - eventsSubject.send( - .navigation(.profile(collectionService - .navigationService - .profileService(account: notification.account)))) + send(event: .navigation(.profile(collectionService + .navigationService + .profileService(account: notification.account)))) } case let .conversation(conversation): guard let status = conversation.lastStatus else { break } - eventsSubject.send( - .navigation(.collection(collectionService - .navigationService - .contextService(id: status.displayStatus.id)))) + send(event: .navigation(.collection(collectionService + .navigationService + .contextService(id: status.displayStatus.id)))) case let .tag(tag): - eventsSubject.send( - .navigation(.collection(collectionService - .navigationService - .timelineService(timeline: .tag(tag.name))))) + send(event: .navigation(.collection(collectionService + .navigationService + .timelineService(timeline: .tag(tag.name))))) case let .moreResults(moreResults): - eventsSubject.send(.navigation(.searchScope(moreResults.scope))) + send(event: .navigation(.searchScope(moreResults.scope))) } } @@ -191,9 +192,9 @@ extension CollectionItemsViewModel: CollectionViewModel { } // swiftlint:disable:next function_body_length cyclomatic_complexity - public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel { + public func viewModel(indexPath: IndexPath) -> Any { let item = lastUpdate.sections[indexPath.section].items[indexPath.item] - let cachedViewModel = viewModelCache[item]?.viewModel + let cachedViewModel = viewModelCache[item] switch item { case let .status(status, configuration): @@ -204,8 +205,9 @@ extension CollectionItemsViewModel: CollectionViewModel { } else { viewModel = .init( statusService: collectionService.navigationService.statusService(status: status), - identityContext: identityContext) - cache(viewModel: viewModel, forItem: item) + identityContext: identityContext, + eventsSubject: eventsSubject) + viewModelCache[item] = viewModel } viewModel.configuration = configuration @@ -217,9 +219,10 @@ extension CollectionItemsViewModel: CollectionViewModel { } let viewModel = LoadMoreViewModel( - loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore)) + loadMoreService: collectionService.navigationService.loadMoreService(loadMore: loadMore), + eventsSubject: eventsSubject) - cache(viewModel: viewModel, forItem: item) + viewModelCache[item] = viewModel return viewModel case let .account(account, configuration): @@ -230,31 +233,34 @@ extension CollectionItemsViewModel: CollectionViewModel { } else { viewModel = AccountViewModel( accountService: collectionService.navigationService.accountService(account: account), - identityContext: identityContext) - cache(viewModel: viewModel, forItem: item) + identityContext: identityContext, + eventsSubject: eventsSubject) + viewModelCache[item] = viewModel } viewModel.configuration = configuration return viewModel case let .notification(notification, statusConfiguration): - let viewModel: CollectionItemViewModel + let viewModel: Any if let cachedViewModel = cachedViewModel { viewModel = cachedViewModel } else if let status = notification.status, let statusConfiguration = statusConfiguration { let statusViewModel = StatusViewModel( statusService: collectionService.navigationService.statusService(status: status), - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: eventsSubject) statusViewModel.configuration = statusConfiguration viewModel = statusViewModel - cache(viewModel: viewModel, forItem: item) + viewModelCache[item] = viewModel } else { viewModel = NotificationViewModel( notificationService: collectionService.navigationService.notificationService( notification: notification), - identityContext: identityContext) - cache(viewModel: viewModel, forItem: item) + identityContext: identityContext, + eventsSubject: eventsSubject) + viewModelCache[item] = viewModel } return viewModel @@ -268,7 +274,7 @@ extension CollectionItemsViewModel: CollectionViewModel { conversation: conversation), identityContext: identityContext) - cache(viewModel: viewModel, forItem: item) + viewModelCache[item] = viewModel return viewModel case let .tag(tag): @@ -278,7 +284,7 @@ extension CollectionItemsViewModel: CollectionViewModel { let viewModel = TagViewModel(tag: tag, identityContext: identityContext) - cache(viewModel: viewModel, forItem: item) + viewModelCache[item] = viewModel return viewModel case let .moreResults(moreResults): @@ -288,7 +294,7 @@ extension CollectionItemsViewModel: CollectionViewModel { let viewModel = MoreResultsViewModel(moreResults: moreResults) - cache(viewModel: viewModel, forItem: item) + viewModelCache[item] = viewModel return viewModel } @@ -335,19 +341,12 @@ extension CollectionItemsViewModel: CollectionViewModel { private extension CollectionItemsViewModel { private static let lastReadIdDebounceInterval: TimeInterval = 0.5 - var lastUpdateWasContextParentOnly: Bool { - collectionService is ContextService && lastUpdate.sections.map(\.items).map(\.count) == [0, 1, 0] + func send(event: CollectionItemEvent) { + eventsSubject.send(Just(event).setFailureType(to: Error.self).eraseToAnyPublisher()) } - func cache(viewModel: CollectionItemViewModel, forItem item: CollectionItem) { - viewModelCache[item] = (viewModel, viewModel.events - .flatMap { [weak self] events -> AnyPublisher in - guard let self = self else { return Empty().eraseToAnyPublisher() } - - return events.assignErrorsToAlertItem(to: \.alertItem, on: self) - .eraseToAnyPublisher() - } - .sink { [weak self] in self?.eventsSubject.send($0) }) + var lastUpdateWasContextParentOnly: Bool { + collectionService is ContextService && lastUpdate.sections.map(\.items).map(\.count) == [0, 1, 0] } func process(sections: [CollectionSection]) { @@ -402,7 +401,7 @@ private extension CollectionItemsViewModel { if case let .remove(_, item, _) = removal, case let .loadMore(loadMore) = item, loadMore == lastSelectedLoadMore, - let direction = (viewModelCache[item]?.viewModel as? LoadMoreViewModel)?.direction, + let direction = (viewModelCache[item] as? LoadMoreViewModel)?.direction, direction == .up, let statusAfterLoadMore = items.first(where: { guard case let .status(status, _) = $0 else { return false } diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionViewModel.swift index 7aeee16..3931ab6 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionViewModel.swift @@ -19,7 +19,7 @@ public protocol CollectionViewModel { func viewedAtTop(indexPath: IndexPath) func select(indexPath: IndexPath) func canSelect(indexPath: IndexPath) -> Bool - func viewModel(indexPath: IndexPath) -> CollectionItemViewModel + func viewModel(indexPath: IndexPath) -> Any func toggleExpandAll() func applyAccountListEdit(viewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit) } diff --git a/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift index 6fa9e97..16f4c93 100644 --- a/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift @@ -5,32 +5,31 @@ import Foundation import Mastodon import ServiceLayer -public final class ConversationViewModel: CollectionItemViewModel, ObservableObject { +public final class ConversationViewModel: ObservableObject { public let accountViewModels: [AccountViewModel] public let statusViewModel: StatusViewModel? - public let events: AnyPublisher, Never> public let identityContext: IdentityContext private let conversationService: ConversationService - private let eventsSubject = PassthroughSubject, Never>() init(conversationService: ConversationService, identityContext: IdentityContext) { accountViewModels = conversationService.conversation.accounts.map { AccountViewModel( accountService: conversationService.navigationService.accountService(account: $0), - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: .init()) } if let status = conversationService.conversation.lastStatus { statusViewModel = StatusViewModel( statusService: conversationService.navigationService.statusService(status: status), - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: .init()) } else { statusViewModel = nil } self.conversationService = conversationService self.identityContext = identityContext - self.events = eventsSubject.eraseToAnyPublisher() } } diff --git a/ViewModels/Sources/ViewModels/View Models/LoadMoreViewModel.swift b/ViewModels/Sources/ViewModels/View Models/LoadMoreViewModel.swift index d654bc7..43f4c8b 100644 --- a/ViewModels/Sources/ViewModels/View Models/LoadMoreViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/LoadMoreViewModel.swift @@ -3,17 +3,17 @@ import Combine import ServiceLayer -public final class LoadMoreViewModel: ObservableObject, CollectionItemViewModel { +public final class LoadMoreViewModel: ObservableObject { public var direction = LoadMore.Direction.up @Published public private(set) var loading = false - public let events: AnyPublisher, Never> private let loadMoreService: LoadMoreService - private let eventsSubject = PassthroughSubject, Never>() + private let eventsSubject: PassthroughSubject, Never> - init(loadMoreService: LoadMoreService) { + init(loadMoreService: LoadMoreService, + eventsSubject: PassthroughSubject, Never>) { self.loadMoreService = loadMoreService - events = eventsSubject.eraseToAnyPublisher() + self.eventsSubject = eventsSubject } } diff --git a/ViewModels/Sources/ViewModels/View Models/MoreResultsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/MoreResultsViewModel.swift index dfe56c3..9fcd040 100644 --- a/ViewModels/Sources/ViewModels/View Models/MoreResultsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/MoreResultsViewModel.swift @@ -3,15 +3,11 @@ import Combine import ServiceLayer -public final class MoreResultsViewModel: ObservableObject, CollectionItemViewModel { - public var events: AnyPublisher, Never> - +public final class MoreResultsViewModel: ObservableObject { private let moreResults: MoreResults - private let eventsSubject = PassthroughSubject, Never>() init(moreResults: MoreResults) { self.moreResults = moreResults - events = eventsSubject.eraseToAnyPublisher() } } diff --git a/ViewModels/Sources/ViewModels/View Models/NotificationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/NotificationViewModel.swift index aac1799..d01baf2 100644 --- a/ViewModels/Sources/ViewModels/View Models/NotificationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/NotificationViewModel.swift @@ -5,32 +5,34 @@ import Foundation import Mastodon import ServiceLayer -public final class NotificationViewModel: CollectionItemViewModel, ObservableObject { +public final class NotificationViewModel: ObservableObject { public let accountViewModel: AccountViewModel public let statusViewModel: StatusViewModel? - public let events: AnyPublisher, Never> public let identityContext: IdentityContext private let notificationService: NotificationService - private let eventsSubject = PassthroughSubject, Never>() + private let eventsSubject: PassthroughSubject, Never> - init(notificationService: NotificationService, identityContext: IdentityContext) { + init(notificationService: NotificationService, + identityContext: IdentityContext, + eventsSubject: PassthroughSubject, Never>) { self.notificationService = notificationService self.identityContext = identityContext + self.eventsSubject = eventsSubject self.accountViewModel = AccountViewModel( accountService: notificationService.navigationService.accountService( account: notificationService.notification.account), - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: eventsSubject) if let status = notificationService.notification.status { statusViewModel = StatusViewModel( statusService: notificationService.navigationService.statusService(status: status), - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: eventsSubject) } else { statusViewModel = nil } - - self.events = eventsSubject.eraseToAnyPublisher() } } diff --git a/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift b/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift index 186bcfa..9733c3f 100644 --- a/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift @@ -13,6 +13,7 @@ final public class ProfileViewModel { private let profileService: ProfileService private let collectionViewModel: CurrentValueSubject + private let accountEventsSubject: PassthroughSubject, Never> private let imagePresentationsSubject = PassthroughSubject() private var cancellables = Set() @@ -25,8 +26,16 @@ final public class ProfileViewModel { collectionService: profileService.timelineService(profileCollection: .statuses), identityContext: identityContext)) + let accountEventsSubject = PassthroughSubject, Never>() + + self.accountEventsSubject = accountEventsSubject + profileService.accountServicePublisher - .map { AccountViewModel(accountService: $0, identityContext: identityContext) } + .map { + AccountViewModel(accountService: $0, + identityContext: identityContext, + eventsSubject: accountEventsSubject) + } .assignErrorsToAlertItem(to: \.alertItem, on: self) .assign(to: &$accountViewModel) @@ -102,10 +111,12 @@ extension ProfileViewModel: CollectionViewModel { } public var events: AnyPublisher { - $accountViewModel.compactMap { $0 } - .flatMap(\.events) - .flatMap { $0 } - .assignErrorsToAlertItem(to: \.alertItem, on: self) + accountEventsSubject + .flatMap { [weak self] eventPublisher -> AnyPublisher in + guard let self = self else { return Empty().eraseToAnyPublisher() } + + return eventPublisher.assignErrorsToAlertItem(to: \.alertItem, on: self).eraseToAnyPublisher() + } .merge(with: collectionViewModel.flatMap(\.events)) .eraseToAnyPublisher() } @@ -143,7 +154,7 @@ extension ProfileViewModel: CollectionViewModel { collectionViewModel.value.canSelect(indexPath: indexPath) } - public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel { + public func viewModel(indexPath: IndexPath) -> Any { collectionViewModel.value.viewModel(indexPath: indexPath) } diff --git a/ViewModels/Sources/ViewModels/View Models/ReportViewModel.swift b/ViewModels/Sources/ViewModels/View Models/ReportViewModel.swift index 62c76c9..5cbbe6c 100644 --- a/ViewModels/Sources/ViewModels/View Models/ReportViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/ReportViewModel.swift @@ -22,7 +22,9 @@ public final class ReportViewModel: ObservableObject { events = eventsSubject.eraseToAnyPublisher() if let statusService = statusService { - statusViewModel = StatusViewModel(statusService: statusService, identityContext: identityContext) + statusViewModel = StatusViewModel(statusService: statusService, + identityContext: identityContext, + eventsSubject: .init()) elements.statusIds.insert(statusService.status.displayStatus.id) } else { statusViewModel = nil diff --git a/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift b/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift index d627594..3d071b7 100644 --- a/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift @@ -5,7 +5,7 @@ import Foundation import Mastodon import ServiceLayer -public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderingViewModel, ObservableObject { +public final class StatusViewModel: AttachmentsRenderingViewModel, ObservableObject { public let content: NSAttributedString public let contentEmojis: [Emoji] public let displayName: String @@ -18,15 +18,17 @@ public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderin public let pollEmojis: [Emoji] @Published public var pollOptionSelections = Set() public var configuration = CollectionItem.StatusConfiguration.default - public let events: AnyPublisher, Never> public let identityContext: IdentityContext private let statusService: StatusService - private let eventsSubject = PassthroughSubject, Never>() + private let eventsSubject: PassthroughSubject, Never> - init(statusService: StatusService, identityContext: IdentityContext) { + init(statusService: StatusService, + identityContext: IdentityContext, + eventsSubject: PassthroughSubject, Never>) { self.statusService = statusService self.identityContext = identityContext + self.eventsSubject = eventsSubject content = statusService.status.displayStatus.content.attributed contentEmojis = statusService.status.displayStatus.emojis displayName = statusService.status.displayStatus.account.displayName.isEmpty @@ -42,7 +44,6 @@ public final class StatusViewModel: CollectionItemViewModel, AttachmentsRenderin attachmentViewModels = statusService.status.displayStatus.mediaAttachments .map { AttachmentViewModel(attachment: $0, identityContext: identityContext, status: statusService.status) } pollEmojis = statusService.status.displayStatus.poll?.emojis ?? [] - events = eventsSubject.eraseToAnyPublisher() } } @@ -221,7 +222,9 @@ public extension StatusViewModel { } func reply() { - let replyViewModel = Self(statusService: statusService, identityContext: identityContext) + let replyViewModel = Self(statusService: statusService, + identityContext: identityContext, + eventsSubject: .init()) replyViewModel.configuration = configuration.reply() @@ -292,7 +295,8 @@ public extension StatusViewModel { if let inReplyToStatusService = inReplyToStatusService { inReplyToViewModel = Self( statusService: inReplyToStatusService, - identityContext: identityContext) + identityContext: identityContext, + eventsSubject: .init()) inReplyToViewModel?.configuration = CollectionItem.StatusConfiguration.default.reply() } else { inReplyToViewModel = nil diff --git a/ViewModels/Sources/ViewModels/View Models/TagViewModel.swift b/ViewModels/Sources/ViewModels/View Models/TagViewModel.swift index 9ca6ac3..02c52ce 100644 --- a/ViewModels/Sources/ViewModels/View Models/TagViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/TagViewModel.swift @@ -4,16 +4,14 @@ import Combine import Foundation import Mastodon -public struct TagViewModel: CollectionItemViewModel { +public struct TagViewModel { public let identityContext: IdentityContext - public let events: AnyPublisher, Never> private let tag: Tag init(tag: Tag, identityContext: IdentityContext) { self.tag = tag self.identityContext = identityContext - events = Empty().eraseToAnyPublisher() } }