feature: add navigationBar state

This commit is contained in:
sunxiaojian 2021-03-15 20:03:40 +08:00
parent c15a4a1e89
commit 0b046e4673
11 changed files with 236 additions and 8 deletions

View File

@ -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"
}
}
}
}

View File

@ -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 */,

View File

@ -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

View File

@ -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";

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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