forked from zelo72/mastodon-ios
feature: add navigationBar state
This commit is contained in:
parent
c15a4a1e89
commit
0b046e4673
|
@ -169,10 +169,16 @@
|
|||
}
|
||||
},
|
||||
"home_timeline": {
|
||||
"title": "Home"
|
||||
"title": "Home",
|
||||
"navigation_bar_state": {
|
||||
"offline": "Offline",
|
||||
"new_posts": "See new posts",
|
||||
"published": "Published!",
|
||||
"Publishing": "Publishing post..."
|
||||
},
|
||||
},
|
||||
"public_timeline": {
|
||||
"title": "Public"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */; };
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */; };
|
||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
|
||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
|
||||
|
@ -318,6 +320,8 @@
|
|||
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
||||
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarState.swift; sourceTree = "<group>"; };
|
||||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = "<group>"; };
|
||||
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
|
||||
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
||||
|
@ -612,6 +616,8 @@
|
|||
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
|
||||
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
|
||||
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
|
||||
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */,
|
||||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */,
|
||||
);
|
||||
path = HomeTimeline;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1603,7 +1609,9 @@
|
|||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
|
|
|
@ -162,6 +162,16 @@ internal enum L10n {
|
|||
internal enum HomeTimeline {
|
||||
/// Home
|
||||
internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title")
|
||||
internal enum NavigationBarState {
|
||||
/// See new posts
|
||||
internal static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts")
|
||||
/// Offline
|
||||
internal static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline")
|
||||
/// Published!
|
||||
internal static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published")
|
||||
/// Publishing post...
|
||||
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
||||
}
|
||||
}
|
||||
internal enum PublicTimeline {
|
||||
/// Public
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
|
||||
tap the link to confirm your account.";
|
||||
"Scene.ConfirmEmail.Title" = "One last thing.";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
|
||||
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
|
||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
||||
"Scene.HomeTimeline.Title" = "Home";
|
||||
"Scene.PublicTimeline.Title" = "Public";
|
||||
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// HomeTimelineNavigationBarState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/3/15.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class HomeTimelineNavigationBarState {
|
||||
static let errorCountMax: Int = 3
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var errorCountDownDispose: AnyCancellable?
|
||||
var networkErrorCountSubject = PassthroughSubject<Bool, Never>()
|
||||
|
||||
var titleViewBeforePublishing: UIView? // used for restore titleView after published
|
||||
|
||||
var newTopContent = CurrentValueSubject<Bool,Never>(false)
|
||||
var newBottomContent = CurrentValueSubject<Bool,Never>(false)
|
||||
var hasContentBeforeFetching: Bool = true
|
||||
|
||||
weak var viewController: HomeTimelineViewController?
|
||||
|
||||
init() {
|
||||
reCountdown()
|
||||
subscribeNewContent()
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineNavigationBarState {
|
||||
func showOfflineInNavigationBar() {
|
||||
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.offlineView
|
||||
}
|
||||
|
||||
func showNewPostsInNavigationBar() {
|
||||
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.newPostsView
|
||||
}
|
||||
|
||||
func showPublishingNewPostInNavigationBar() {
|
||||
titleViewBeforePublishing = viewController?.navigationItem.titleView
|
||||
}
|
||||
|
||||
func showPublishedInNavigationBar() {
|
||||
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishedView
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
|
||||
if let titleView = self.titleViewBeforePublishing, let navigationItem = self.viewController?.navigationItem {
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showMastodonLogoInNavigationBar() {
|
||||
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineNavigationBarState {
|
||||
func subscribeNewContent() {
|
||||
newTopContent
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] newContent in
|
||||
guard let self = self else { return }
|
||||
if self.hasContentBeforeFetching && newContent {
|
||||
self.showNewPostsInNavigationBar()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
newBottomContent
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] newContent in
|
||||
guard let self = self else { return }
|
||||
if newContent {
|
||||
self.showNewPostsInNavigationBar()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
func reCountdown() {
|
||||
errorCountDownDispose = networkErrorCountSubject
|
||||
.scan(0) { value, _ in value + 1 }
|
||||
.sink(receiveValue: { [weak self] errorCount in
|
||||
guard let self = self else { return }
|
||||
if errorCount >= HomeTimelineNavigationBarState.errorCountMax {
|
||||
self.showOfflineInNavigationBar()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let contentOffsetY = scrollView.contentOffset.y
|
||||
print(contentOffsetY)
|
||||
let isTop = contentOffsetY < -scrollView.contentInset.top
|
||||
if isTop {
|
||||
newTopContent.value = false
|
||||
showMastodonLogoInNavigationBar()
|
||||
}
|
||||
let isBottom = contentOffsetY > max(-scrollView.contentInset.top, scrollView.contentSize.height - scrollView.frame.height + scrollView.contentInset.bottom)
|
||||
if isBottom {
|
||||
newBottomContent.value = false
|
||||
showMastodonLogoInNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
func receiveCompletion(completion: Subscribers.Completion<Error>) {
|
||||
switch completion {
|
||||
case .failure:
|
||||
networkErrorCountSubject.send(false)
|
||||
case .finished:
|
||||
reCountdown()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// HomeTimelineNavigationBarView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/3/15.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HomeTimelineNavigationBarView {
|
||||
|
||||
static let mastodonLogoTitleView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
static let offlineView: UIView = {
|
||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightDangerRed.color)
|
||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
|
||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||
return view
|
||||
}()
|
||||
|
||||
static let newPostsView: UIView = {
|
||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.highlight.color)
|
||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts)
|
||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||
return view
|
||||
}()
|
||||
|
||||
static var publishedView: UIView = {
|
||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightSuccessGreen.color)
|
||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.published)
|
||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||
return view
|
||||
}()
|
||||
|
||||
|
||||
static func addLabelToView(label: UILabel,view:UIView) {
|
||||
view.addSubview(label)
|
||||
NSLayoutConstraint.activate([
|
||||
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||
view.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16),
|
||||
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 1),
|
||||
view.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 1)
|
||||
])
|
||||
label.sizeToFit()
|
||||
view.layoutIfNeeded()
|
||||
view.layer.cornerRadius = view.frame.height/2
|
||||
view.clipsToBounds = true
|
||||
}
|
||||
|
||||
static func backgroundViewWithColor(color:UIColor) -> UIView {
|
||||
let view = UIView()
|
||||
view.backgroundColor = color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}
|
||||
|
||||
static func contentLabel(text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.textColor = .white
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
|
||||
label.text = text
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
|
@ -64,11 +64,7 @@ extension HomeTimelineViewController {
|
|||
|
||||
title = L10n.Scene.HomeTimeline.title
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
navigationItem.titleView = {
|
||||
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
return imageView
|
||||
}()
|
||||
navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
|
||||
navigationItem.leftBarButtonItem = settingBarButtonItem
|
||||
#if DEBUG
|
||||
// long press to trigger debug menu
|
||||
|
@ -101,6 +97,7 @@ extension HomeTimelineViewController {
|
|||
])
|
||||
|
||||
viewModel.tableView = tableView
|
||||
viewModel.viewController = self
|
||||
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
|
@ -208,6 +205,7 @@ extension HomeTimelineViewController {
|
|||
extension HomeTimelineViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
handleScrollViewDidScroll(scrollView)
|
||||
self.viewModel.homeTimelineNavigationBarState.handleScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
|||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
viewModel.homeTimelineNavigationBarState.hasContentBeforeFetching = !latestTootIDs.isEmpty
|
||||
let end = CACurrentMediaTime()
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
|
||||
|
||||
|
@ -80,6 +81,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
|||
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
|
@ -97,9 +99,12 @@ extension HomeTimelineViewModel.LoadLatestState {
|
|||
let toots = response.value
|
||||
let newToots = toots.filter { !latestTootIDs.contains($0.id) }
|
||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
|
||||
|
||||
|
||||
if newToots.isEmpty {
|
||||
viewModel.isFetchingLatestTimeline.value = false
|
||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = false
|
||||
} else {
|
||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = true
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
|
|
|
@ -68,6 +68,7 @@ extension HomeTimelineViewModel.LoadMiddleState {
|
|||
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
|
@ -82,8 +83,10 @@ extension HomeTimelineViewModel.LoadMiddleState {
|
|||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", ((#file as NSString).lastPathComponent), #line, #function, toots.count, newToots.count)
|
||||
if newToots.isEmpty {
|
||||
stateMachine.enter(Fail.self)
|
||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = false
|
||||
} else {
|
||||
stateMachine.enter(Success.self)
|
||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = true
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
|
|
|
@ -70,8 +70,10 @@ extension HomeTimelineViewModel.LoadOldestState {
|
|||
// enter no more state when no new toots
|
||||
if toots.isEmpty || (toots.count == 1 && toots[0].id == maxID) {
|
||||
stateMachine.enter(NoMore.self)
|
||||
viewModel.homeTimelineNavigationBarState.newBottomContent.value = false
|
||||
} else {
|
||||
stateMachine.enter(Idle.self)
|
||||
viewModel.homeTimelineNavigationBarState.newBottomContent.value = true
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
|
|
|
@ -29,9 +29,16 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
let homeTimelineNavigationBarState = HomeTimelineNavigationBarState()
|
||||
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
weak var tableView: UITableView?
|
||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
weak var viewController: HomeTimelineViewController? {
|
||||
willSet(value) {
|
||||
self.homeTimelineNavigationBarState.viewController = value
|
||||
}
|
||||
}
|
||||
|
||||
// output
|
||||
// top loader
|
||||
|
|
Loading…
Reference in New Issue