forked from zelo72/mastodon-ios
175 lines
5.6 KiB
Swift
175 lines
5.6 KiB
Swift
//
|
|
// HomeTimelineNavigationBarTitleViewModel.swift
|
|
// Mastodon
|
|
//
|
|
// Created by sxiaojian on 2021/3/15.
|
|
//
|
|
|
|
import Combine
|
|
import Foundation
|
|
import UIKit
|
|
|
|
final class HomeTimelineNavigationBarTitleViewModel {
|
|
|
|
static let offlineCounterLimit = 3
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
private(set) var publishingProgressSubscription: AnyCancellable?
|
|
|
|
// input
|
|
let context: AppContext
|
|
var networkErrorCount = CurrentValueSubject<Int, Never>(0)
|
|
var networkErrorPublisher = PassthroughSubject<Void, Never>()
|
|
|
|
// output
|
|
let state = CurrentValueSubject<State, Never>(.logoImage)
|
|
let hasNewPosts = CurrentValueSubject<Bool, Never>(false)
|
|
let isOffline = CurrentValueSubject<Bool, Never>(false)
|
|
let isPublishingPost = CurrentValueSubject<Bool, Never>(false)
|
|
let isPublished = CurrentValueSubject<Bool, Never>(false)
|
|
let publishingProgress = PassthroughSubject<Float, Never>()
|
|
|
|
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 .logoImage
|
|
}
|
|
.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 logoImage
|
|
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<Error>) {
|
|
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)
|
|
}
|
|
|
|
}
|
|
|