// // HomeTimelineNavigationBarTitleViewModel.swift // Mastodon // // Created by sxiaojian on 2021/3/15. // import Combine import Foundation import UIKit import MastodonCore final class HomeTimelineNavigationBarTitleViewModel { static let offlineCounterLimit = 3 var disposeBag = Set() private(set) var publishingProgressSubscription: AnyCancellable? // input let context: AppContext var networkErrorCount = CurrentValueSubject(0) var networkErrorPublisher = PassthroughSubject() // output let state = CurrentValueSubject(.logo) let hasNewPosts = CurrentValueSubject(false) let isOffline = CurrentValueSubject(false) let isPublishingPost = CurrentValueSubject(false) let isPublished = CurrentValueSubject(false) let publishingProgress = PassthroughSubject() init(context: AppContext) { self.context = context networkErrorPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } self.networkErrorCount.value += self.networkErrorCount.value + 1 } .store(in: &disposeBag) networkErrorCount .receive(on: DispatchQueue.main) .map { count in return count >= HomeTimelineNavigationBarTitleViewModel.offlineCounterLimit } .assign(to: \.value, on: isOffline) .store(in: &disposeBag) // context.statusPublishService.latestPublishingComposeViewModel // .receive(on: DispatchQueue.main) // .sink { [weak self] composeViewModel in // guard let self = self else { return } // guard let composeViewModel = composeViewModel, // let state = composeViewModel.publishStateMachine.currentState else { // self.isPublishingPost.value = false // self.isPublished.value = false // return // } // // self.isPublishingPost.value = state is ComposeViewModel.PublishState.Publishing || state is ComposeViewModel.PublishState.Fail // self.isPublished.value = state is ComposeViewModel.PublishState.Finish // } // .store(in: &disposeBag) Publishers.CombineLatest4( hasNewPosts.eraseToAnyPublisher(), isOffline.eraseToAnyPublisher(), isPublishingPost.eraseToAnyPublisher(), isPublished.eraseToAnyPublisher() ) .map { hasNewPosts, isOffline, isPublishingPost, isPublished -> State in guard !isPublished else { return .publishedButton } guard !isPublishingPost else { return .publishingPostLabel } guard !isOffline else { return .offlineButton } guard !hasNewPosts else { return .newPostButton } return .logo } .receive(on: DispatchQueue.main) .assign(to: \.value, on: state) .store(in: &disposeBag) state .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self = self else { return } switch state { case .publishingPostLabel: self.setupPublishingProgress() default: self.suspendPublishingProgress() } } .store(in: &disposeBag) } } extension HomeTimelineNavigationBarTitleViewModel { // state order by priority from low to high enum State: String { case logo case newPostButton case offlineButton case publishingPostLabel case publishedButton } } // MARK: - New post state extension HomeTimelineNavigationBarTitleViewModel { func newPostsIncoming() { hasNewPosts.value = true } private func resetNewPostState() { hasNewPosts.value = false } } // MARK: - Offline state extension HomeTimelineNavigationBarTitleViewModel { func resetOfflineCounterListener() { networkErrorCount.value = 0 } func receiveLoadingStateCompletion(_ completion: Subscribers.Completion) { switch completion { case .failure: networkErrorPublisher.send() case .finished: resetOfflineCounterListener() } } func handleScrollViewDidScroll(_ scrollView: UIScrollView) { guard hasNewPosts.value else { return } let contentOffsetY = scrollView.contentOffset.y let isScrollToTop = contentOffsetY < -scrollView.contentInset.top guard isScrollToTop else { return } resetNewPostState() } } // MARK: Publish post state extension HomeTimelineNavigationBarTitleViewModel { func setupPublishingProgress() { let progressUpdatePublisher = Timer.publish(every: 0.016, on: .main, in: .common) // ~ 60FPS .autoconnect() .share() .eraseToAnyPublisher() publishingProgressSubscription = progressUpdatePublisher .map { _ in Float(0) } .scan(0.0) { progress, _ -> Float in return 0.95 * progress + 0.05 // progress + 0.05 * (1.0 - progress). ~ 1 sec to 0.95 (under 60FPS) } .subscribe(publishingProgress) } func suspendPublishingProgress() { publishingProgressSubscription = nil publishingProgress.send(0) } }