From 0b046e46730173a7b4b079b375fe76dded1bc2d9 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 15 Mar 2021 20:03:40 +0800 Subject: [PATCH 1/8] feature: add navigationBar state --- Localization/app.json | 10 +- Mastodon.xcodeproj/project.pbxproj | 8 ++ Mastodon/Generated/Strings.swift | 10 ++ .../Resources/en.lproj/Localizable.strings | 4 + .../HomeTimelineNavigationBarState.swift | 115 ++++++++++++++++++ .../HomeTimelineNavigationBarView.swift | 70 +++++++++++ .../HomeTimelineViewController.swift | 8 +- ...omeTimelineViewModel+LoadLatestState.swift | 7 +- ...omeTimelineViewModel+LoadMiddleState.swift | 3 + ...omeTimelineViewModel+LoadOldestState.swift | 2 + .../HomeTimeline/HomeTimelineViewModel.swift | 7 ++ 11 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift create mode 100644 Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift diff --git a/Localization/app.json b/Localization/app.json index 4c79a97b..6170c000 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -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" } } -} \ No newline at end of file +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9859676c..8b75de62 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = ""; }; 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = ""; }; + 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarState.swift; sourceTree = ""; }; + 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = ""; }; 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 399d1a5e..166a6122 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -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 diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index c79e3d0c..2a72b92c 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift new file mode 100644 index 00000000..41be16f8 --- /dev/null +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -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() + var errorCountDownDispose: AnyCancellable? + var networkErrorCountSubject = PassthroughSubject() + + var titleViewBeforePublishing: UIView? // used for restore titleView after published + + var newTopContent = CurrentValueSubject(false) + var newBottomContent = CurrentValueSubject(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) { + switch completion { + case .failure: + networkErrorCountSubject.send(false) + case .finished: + reCountdown() + } + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift new file mode 100644 index 00000000..b8906ab0 --- /dev/null +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift @@ -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 + } +} + diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 9db551f6..fe273241 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index c085471c..0df4334a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -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) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift index 5a212f35..7b7f3a70 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift @@ -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) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index c6eb988b..5bca33bd 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -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) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 44457839..263e76a9 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -29,9 +29,16 @@ final class HomeTimelineViewModel: NSObject { let isFetchingLatestTimeline = CurrentValueSubject(false) let viewDidAppear = PassthroughSubject() + 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 From 21362b56c3e9e34cfa779552076c0fef714505bf Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 15 Mar 2021 20:23:27 +0800 Subject: [PATCH 2/8] chore: add gesture to scroll manually --- Mastodon.xcodeproj/project.pbxproj | 4 ++ Mastodon/Extension/UIScrollView.swift | 32 +++++++++ .../HomeTimelineNavigationBarState.swift | 66 +++++++++++++------ 3 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 Mastodon/Extension/UIScrollView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 8b75de62..57979578 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 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 */; }; + 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.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 */; }; @@ -322,6 +323,7 @@ 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = ""; }; 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarState.swift; sourceTree = ""; }; 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = ""; }; + 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; @@ -1145,6 +1147,7 @@ 2D206B9125F60EA700143C56 /* UIControl.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, + 2D84350425FF858100EECE90 /* UIScrollView.swift */, ); path = Extension; sourceTree = ""; @@ -1646,6 +1649,7 @@ 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, + 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, diff --git a/Mastodon/Extension/UIScrollView.swift b/Mastodon/Extension/UIScrollView.swift new file mode 100644 index 00000000..8999d255 --- /dev/null +++ b/Mastodon/Extension/UIScrollView.swift @@ -0,0 +1,32 @@ +// +// UIScrollView.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/15. +// + +import UIKit + +extension UIScrollView { + public enum ScrollDirection { + case top + case bottom + case left + case right + } + + public func scroll(to direction: ScrollDirection, animated: Bool) { + let offset: CGPoint + switch direction { + case .top: + offset = CGPoint(x: contentOffset.x, y: -adjustedContentInset.top) + case .bottom: + offset = CGPoint(x: contentOffset.x, y: max(-adjustedContentInset.top, contentSize.height - frame.height + adjustedContentInset.bottom)) + case .left: + offset = CGPoint(x: -adjustedContentInset.left, y: contentOffset.y) + case .right: + offset = CGPoint(x: max(-adjustedContentInset.left, contentSize.width - frame.width + adjustedContentInset.right), y: contentOffset.y) + } + setContentOffset(offset, animated: animated) + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift index 41be16f8..7dc4223a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -17,8 +17,8 @@ final class HomeTimelineNavigationBarState { var titleViewBeforePublishing: UIView? // used for restore titleView after published - var newTopContent = CurrentValueSubject(false) - var newBottomContent = CurrentValueSubject(false) + var newTopContent = CurrentValueSubject(false) + var newBottomContent = CurrentValueSubject(false) var hasContentBeforeFetching: Bool = true weak var viewController: HomeTimelineViewController? @@ -26,6 +26,7 @@ final class HomeTimelineNavigationBarState { init() { reCountdown() subscribeNewContent() + addGesture() } } @@ -56,15 +57,54 @@ extension HomeTimelineNavigationBarState { } } +extension HomeTimelineNavigationBarState { + 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.adjustedContentInset.top, scrollView.contentSize.height - scrollView.frame.height + scrollView.adjustedContentInset.bottom) + if isBottom { + newBottomContent.value = false + showMastodonLogoInNavigationBar() + } + } + + func addGesture() { + let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer + tapGesture.addTarget(self, action: #selector(newPostsNewDidPressed)) + HomeTimelineNavigationBarView.newPostsView.addGestureRecognizer(tapGesture) + } + + @objc func newPostsNewDidPressed() { + if newTopContent.value == true { + scrollToDirection(direction: .top) + } + if newBottomContent.value == true { + scrollToDirection(direction: .bottom) + } + } + + func scrollToDirection(direction: UIScrollView.ScrollDirection) { + viewController?.tableView.scroll(to: direction, animated: true) + } +} + extension HomeTimelineNavigationBarState { func subscribeNewContent() { newTopContent .receive(on: DispatchQueue.main) .sink { [weak self] newContent in guard let self = self else { return } - if self.hasContentBeforeFetching && newContent { + if self.hasContentBeforeFetching, newContent { self.showNewPostsInNavigationBar() } + if newContent { + self.newBottomContent.value = false + } } .store(in: &disposeBag) newBottomContent @@ -74,10 +114,13 @@ extension HomeTimelineNavigationBarState { if newContent { self.showNewPostsInNavigationBar() } + if (newContent) { + self.newTopContent.value = false + } } .store(in: &disposeBag) - } + func reCountdown() { errorCountDownDispose = networkErrorCountSubject .scan(0) { value, _ in value + 1 } @@ -89,21 +132,6 @@ extension HomeTimelineNavigationBarState { }) } - 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) { switch completion { case .failure: From 50a30cd18e5af23a6912059e3907ce6c1249cd16 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 10:12:03 +0800 Subject: [PATCH 3/8] chore: export colors from zeplin --- Mastodon/Generated/Assets.swift | 4 +++ .../backgroundLight.colorset/Contents.json | 20 ++++++++++++++ .../buttonDefault.colorset/Contents.json | 20 ++++++++++++++ .../buttonDisabled.colorset/Contents.json | 20 ++++++++++++++ .../buttonInactive.colorset/Contents.json | 20 ++++++++++++++ .../lightAlertYellow.colorset/Contents.json | 14 +++++----- .../lightDangerRed.colorset/Contents.json | 16 ++++++------ .../lightDarkGray.colorset/Contents.json | 16 ++++++------ .../lightSecondaryText.colorset/Contents.json | 26 +++++++++---------- .../lightSuccessGreen.colorset/Contents.json | 20 +++++++------- .../Colors/lightWhite.colorset/Contents.json | 18 ++++++------- .../Resources/en.lproj/Localizable.strings | 2 +- 12 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/backgroundLight.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/buttonDefault.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/buttonDisabled.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/buttonInactive.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index f0222a9e..cc94eaf6 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -67,6 +67,10 @@ internal enum Asset { internal static let invalid = ColorAsset(name: "Colors/TextField/invalid") internal static let valid = ColorAsset(name: "Colors/TextField/valid") } + internal static let backgroundLight = ColorAsset(name: "Colors/backgroundLight") + internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault") + internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled") + internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive") internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow") internal static let lightBackground = ColorAsset(name: "Colors/lightBackground") internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/backgroundLight.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/backgroundLight.colorset/Contents.json new file mode 100644 index 00000000..0e4687fb --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/backgroundLight.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.910", + "green" : "0.882", + "red" : "0.851" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/buttonDefault.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/buttonDefault.colorset/Contents.json new file mode 100644 index 00000000..2e1ce5f3 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/buttonDefault.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.851", + "green" : "0.565", + "red" : "0.169" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/buttonDisabled.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/buttonDisabled.colorset/Contents.json new file mode 100644 index 00000000..78cde95f --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/buttonDisabled.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.784", + "green" : "0.682", + "red" : "0.608" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/buttonInactive.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/buttonInactive.colorset/Contents.json new file mode 100644 index 00000000..69dc6385 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/buttonInactive.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.549", + "green" : "0.510", + "red" : "0.431" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightAlertYellow.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightAlertYellow.colorset/Contents.json index 29b7bba3..0e29336a 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightAlertYellow.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightAlertYellow.colorset/Contents.json @@ -1,20 +1,20 @@ { "colors" : [ { + "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "alpha" : "1.000", + "red" : "0.792", "blue" : "0.016", "green" : "0.561", - "red" : "0.792" + "alpha" : "1.000" } - }, - "idiom" : "universal" + } } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightDangerRed.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightDangerRed.colorset/Contents.json index dabccc33..8ea3105e 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightDangerRed.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightDangerRed.colorset/Contents.json @@ -1,20 +1,20 @@ { + "info" : { + "version" : 1, + "author" : "xcode" + }, "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", + "red" : "0.875", "blue" : "0.353", - "green" : "0.251", - "red" : "0.875" + "green" : "0.251" } }, "idiom" : "universal" } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} + ] +} \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightDarkGray.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightDarkGray.colorset/Contents.json index 8d54c84c..e6461f1d 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightDarkGray.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightDarkGray.colorset/Contents.json @@ -1,6 +1,11 @@ { + "info" : { + "version" : 1, + "author" : "xcode" + }, "colors" : [ { + "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { @@ -9,12 +14,7 @@ "green" : "0.137", "red" : "0.122" } - }, - "idiom" : "universal" + } } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} + ] +} \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightSecondaryText.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightSecondaryText.colorset/Contents.json index ba375b79..ac36bf1f 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightSecondaryText.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightSecondaryText.colorset/Contents.json @@ -1,20 +1,20 @@ { + "info" : { + "version" : 1, + "author" : "xcode" + }, "colors" : [ { + "idiom" : "universal", "color" : { - "color-space" : "srgb", "components" : { + "blue" : "0.263", + "green" : "0.235", "alpha" : "0.600", - "blue" : "67", - "green" : "60", - "red" : "60" - } - }, - "idiom" : "universal" + "red" : "0.235" + }, + "color-space" : "srgb" + } } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} + ] +} \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightSuccessGreen.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightSuccessGreen.colorset/Contents.json index 8716dcb7..8ef654ce 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightSuccessGreen.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightSuccessGreen.colorset/Contents.json @@ -1,20 +1,20 @@ { + "info" : { + "version" : 1, + "author" : "xcode" + }, "colors" : [ { + "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.604", "green" : "0.741", - "red" : "0.475" + "red" : "0.475", + "blue" : "0.604" } - }, - "idiom" : "universal" + } } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} + ] +} \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Colors/lightWhite.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/lightWhite.colorset/Contents.json index a5291a59..5147016b 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/lightWhite.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/lightWhite.colorset/Contents.json @@ -1,20 +1,20 @@ { "colors" : [ { + "idiom" : "universal", "color" : { - "color-space" : "srgb", "components" : { + "red" : "0.996", "alpha" : "1.000", "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" + "green" : "1.000" + }, + "color-space" : "srgb" + } } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 2a72b92c..5ed3c68a 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -96,4 +96,4 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.Title" = "Some ground rules."; "Scene.Welcome.Slogan" = "Social networking -back in your hands."; +back in your hands."; \ No newline at end of file From b60fe36b25356774cb0faa7df11bf8dc1fff8696 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 11:45:30 +0800 Subject: [PATCH 4/8] chore: add publishing state in navigationBar --- Mastodon.xcodeproj/project.pbxproj | 4 ++ .../HomeTimelineNavigationBarState.swift | 50 ++++++++++++++--- .../HomeTimelineNavigationBarView.swift | 13 +++++ ...omeTimelineViewModel+LoadOldestState.swift | 1 + .../Content/NavigationBarProgressView.swift | 56 +++++++++++++++++++ 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 Mastodon/Scene/Share/View/Content/NavigationBarProgressView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 57979578..c7e86949 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; }; 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; }; 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; }; + 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; @@ -300,6 +301,7 @@ 2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = ""; }; 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = ""; }; + 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = ""; }; 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = ""; }; 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = ""; }; 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = ""; }; @@ -593,6 +595,7 @@ children = ( 2D152A8B25C295CC009AA50C /* StatusView.swift */, 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */, + 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */, ); path = Content; sourceTree = ""; @@ -1605,6 +1608,7 @@ 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */, DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */, + 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift index 7dc4223a..6650d323 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -9,20 +9,25 @@ import Combine import Foundation import UIKit + final class HomeTimelineNavigationBarState { static let errorCountMax: Int = 3 var disposeBag = Set() var errorCountDownDispose: AnyCancellable? + var timerDispose: AnyCancellable? var networkErrorCountSubject = PassthroughSubject() - var titleViewBeforePublishing: UIView? // used for restore titleView after published - var newTopContent = CurrentValueSubject(false) var newBottomContent = CurrentValueSubject(false) var hasContentBeforeFetching: Bool = true weak var viewController: HomeTimelineViewController? + let timestampUpdatePublisher = Timer.publish(every: NavigationBarProgressView.progressAnimationDuration, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + init() { reCountdown() subscribeNewContent() @@ -40,15 +45,42 @@ extension HomeTimelineNavigationBarState { } func showPublishingNewPostInNavigationBar() { - titleViewBeforePublishing = viewController?.navigationItem.titleView + let progressView = HomeTimelineNavigationBarView.progressView + if let navigationBar = viewController?.navigationBar(), progressView.superview == nil { + navigationBar.addSubview(progressView) + NSLayoutConstraint.activate([ + progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor), + progressView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor), + progressView.heightAnchor.constraint(equalToConstant: 3) + ]) + } + progressView.layoutIfNeeded() + progressView.progress = 0 + viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishingLabel + + var times: Int = 0 + timerDispose = timestampUpdatePublisher + .map { _ in + times += 1 + return Double(times) + } + .scan(0) { value,count in + value + 1 / pow(Double(2), count) + } + .receive(on: DispatchQueue.main) + .sink { value in + print(value) + progressView.progress = CGFloat(value) + } } func showPublishedInNavigationBar() { + timerDispose = nil + HomeTimelineNavigationBarView.progressView.removeFromSuperview() 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 - } + self.showMastodonLogoInNavigationBar() } } @@ -60,7 +92,10 @@ extension HomeTimelineNavigationBarState { extension HomeTimelineNavigationBarState { func handleScrollViewDidScroll(_ scrollView: UIScrollView) { let contentOffsetY = scrollView.contentOffset.y - print(contentOffsetY) + let isShowingNewPostsNew = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.newPostsView + if !isShowingNewPostsNew { + return + } let isTop = contentOffsetY < -scrollView.contentInset.top if isTop { newTopContent.value = false @@ -138,6 +173,7 @@ extension HomeTimelineNavigationBarState { networkErrorCountSubject.send(false) case .finished: reCountdown() + showPublishingNewPostInNavigationBar() } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift index b8906ab0..1669f012 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift @@ -36,6 +36,19 @@ final class HomeTimelineNavigationBarView { return view }() + static var progressView: NavigationBarProgressView = { + let view = NavigationBarProgressView() + return view + }() + + static var publishingLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .black + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) + label.text = L10n.Scene.HomeTimeline.NavigationBarState.publishing + return label + }() static func addLabelToView(label: UILabel,view:UIView) { view.addSubview(label) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 5bca33bd..84bde2e4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -58,6 +58,7 @@ extension HomeTimelineViewModel.LoadOldestState { .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): os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) diff --git a/Mastodon/Scene/Share/View/Content/NavigationBarProgressView.swift b/Mastodon/Scene/Share/View/Content/NavigationBarProgressView.swift new file mode 100644 index 00000000..d011ca89 --- /dev/null +++ b/Mastodon/Scene/Share/View/Content/NavigationBarProgressView.swift @@ -0,0 +1,56 @@ +// +// NavigationBarProgressView.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/16. +// + +import UIKit + +class NavigationBarProgressView: UIView { + + static let progressAnimationDuration: TimeInterval = 0.3 + + let sliderView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.buttonDefault.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + var sliderTrailingAnchor: NSLayoutConstraint! + + var progress: CGFloat = 0 { + willSet(value) { + sliderTrailingAnchor.constant = (1 - progress) * bounds.width + UIView.animate(withDuration: NavigationBarProgressView.progressAnimationDuration) { + self.setNeedsLayout() + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } +} + +extension NavigationBarProgressView { + func _init() { + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = .clear + addSubview(sliderView) + sliderTrailingAnchor = trailingAnchor.constraint(equalTo: sliderView.trailingAnchor) + NSLayoutConstraint.activate([ + sliderView.topAnchor.constraint(equalTo: topAnchor), + sliderView.leadingAnchor.constraint(equalTo: leadingAnchor), + sliderView.bottomAnchor.constraint(equalTo: bottomAnchor), + sliderTrailingAnchor + ]) + } +} From 1abe55074524d2d9ae695ebe2009a624d99e2f9e Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 14:41:32 +0800 Subject: [PATCH 5/8] chore: remove navigationBar newPostsView when loadmore --- .../HomeTimelineNavigationBarState.swift | 12 +++++++----- .../HomeTimeline/HomeTimelineNavigationBarView.swift | 8 +++----- .../HomeTimelineViewModel+LoadOldestState.swift | 2 -- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift index 6650d323..3ae74a26 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -9,7 +9,6 @@ import Combine import Foundation import UIKit - final class HomeTimelineNavigationBarState { static let errorCountMax: Int = 3 var disposeBag = Set() @@ -46,7 +45,7 @@ extension HomeTimelineNavigationBarState { func showPublishingNewPostInNavigationBar() { let progressView = HomeTimelineNavigationBarView.progressView - if let navigationBar = viewController?.navigationBar(), progressView.superview == nil { + if let navigationBar = viewController?.navigationBar(), progressView.superview == nil { navigationBar.addSubview(progressView) NSLayoutConstraint.activate([ progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor), @@ -65,7 +64,7 @@ extension HomeTimelineNavigationBarState { times += 1 return Double(times) } - .scan(0) { value,count in + .scan(0) { value, count in value + 1 / pow(Double(2), count) } .receive(on: DispatchQueue.main) @@ -149,7 +148,7 @@ extension HomeTimelineNavigationBarState { if newContent { self.showNewPostsInNavigationBar() } - if (newContent) { + if newContent { self.newTopContent.value = false } } @@ -173,7 +172,10 @@ extension HomeTimelineNavigationBarState { networkErrorCountSubject.send(false) case .finished: reCountdown() - showPublishingNewPostInNavigationBar() + let isShowingOfflineView = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.offlineView + if isShowingOfflineView { + showMastodonLogoInNavigationBar() + } } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift index 1669f012..d371ffe5 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift @@ -8,7 +8,6 @@ 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 @@ -50,7 +49,7 @@ final class HomeTimelineNavigationBarView { return label }() - static func addLabelToView(label: UILabel,view:UIView) { + static func addLabelToView(label: UILabel, view: UIView) { view.addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), @@ -60,11 +59,11 @@ final class HomeTimelineNavigationBarView { ]) label.sizeToFit() view.layoutIfNeeded() - view.layer.cornerRadius = view.frame.height/2 + view.layer.cornerRadius = view.frame.height / 2 view.clipsToBounds = true } - static func backgroundViewWithColor(color:UIColor) -> UIView { + static func backgroundViewWithColor(color: UIColor) -> UIView { let view = UIView() view.backgroundColor = color view.translatesAutoresizingMaskIntoConstraints = false @@ -80,4 +79,3 @@ final class HomeTimelineNavigationBarView { return label } } - diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 84bde2e4..b18a66c0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -71,10 +71,8 @@ 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) From 27307ed4dd117f501295df28c64675967d6387f3 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 15:05:01 +0800 Subject: [PATCH 6/8] chore: remove newBottomContent logic --- .../HomeTimelineNavigationBarState.swift | 37 +++---------------- .../HomeTimelineNavigationBarView.swift | 9 ++--- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift index 3ae74a26..11692eaa 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -17,7 +17,6 @@ final class HomeTimelineNavigationBarState { var networkErrorCountSubject = PassthroughSubject() var newTopContent = CurrentValueSubject(false) - var newBottomContent = CurrentValueSubject(false) var hasContentBeforeFetching: Bool = true weak var viewController: HomeTimelineViewController? @@ -36,10 +35,12 @@ final class HomeTimelineNavigationBarState { extension HomeTimelineNavigationBarState { func showOfflineInNavigationBar() { + HomeTimelineNavigationBarView.progressView.removeFromSuperview() viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.offlineView } func showNewPostsInNavigationBar() { + HomeTimelineNavigationBarView.progressView.removeFromSuperview() viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.newPostsView } @@ -84,6 +85,7 @@ extension HomeTimelineNavigationBarState { } func showMastodonLogoInNavigationBar() { + HomeTimelineNavigationBarView.progressView.removeFromSuperview() viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView } } @@ -100,30 +102,18 @@ extension HomeTimelineNavigationBarState { newTopContent.value = false showMastodonLogoInNavigationBar() } - let isBottom = contentOffsetY > max(-scrollView.adjustedContentInset.top, scrollView.contentSize.height - scrollView.frame.height + scrollView.adjustedContentInset.bottom) - if isBottom { - newBottomContent.value = false - showMastodonLogoInNavigationBar() - } } func addGesture() { let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer - tapGesture.addTarget(self, action: #selector(newPostsNewDidPressed)) + tapGesture.addTarget(self, action: #selector(HomeTimelineNavigationBarState.newPostsNewDidPressed(_:))) HomeTimelineNavigationBarView.newPostsView.addGestureRecognizer(tapGesture) } - @objc func newPostsNewDidPressed() { + @objc func newPostsNewDidPressed(_ sender: UITapGestureRecognizer) { if newTopContent.value == true { - scrollToDirection(direction: .top) + viewController?.tableView.scroll(to: .top, animated: true) } - if newBottomContent.value == true { - scrollToDirection(direction: .bottom) - } - } - - func scrollToDirection(direction: UIScrollView.ScrollDirection) { - viewController?.tableView.scroll(to: direction, animated: true) } } @@ -136,21 +126,6 @@ extension HomeTimelineNavigationBarState { if self.hasContentBeforeFetching, newContent { self.showNewPostsInNavigationBar() } - if newContent { - self.newBottomContent.value = false - } - } - .store(in: &disposeBag) - newBottomContent - .receive(on: DispatchQueue.main) - .sink { [weak self] newContent in - guard let self = self else { return } - if newContent { - self.showNewPostsInNavigationBar() - } - if newContent { - self.newTopContent.value = false - } } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift index d371ffe5..c19d45e4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift @@ -55,18 +55,17 @@ final class HomeTimelineNavigationBarView { 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) + view.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 1), + view.heightAnchor.constraint(equalToConstant: 24), ]) - 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 + view.layer.cornerRadius = 12 + view.clipsToBounds = true return view } From f7b4b5993ad6be4147de7c59e0e35215aa0d56fb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 15:37:21 +0800 Subject: [PATCH 7/8] fix: tableView can't scrolling to the top --- .../Scene/HomeTimeline/HomeTimelineNavigationBarState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift index 11692eaa..2d1da216 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarState.swift @@ -112,7 +112,7 @@ extension HomeTimelineNavigationBarState { @objc func newPostsNewDidPressed(_ sender: UITapGestureRecognizer) { if newTopContent.value == true { - viewController?.tableView.scroll(to: .top, animated: true) + viewController?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) } } } From 12e2c5f0d5e7bd6962375ffa423eac3bb8b91d5f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 16 Mar 2021 15:54:03 +0800 Subject: [PATCH 8/8] chore: remove newPostsView when load gap toots --- .../HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift index 7b7f3a70..bb1211d2 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift @@ -83,10 +83,8 @@ 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)