diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 57979578f..c7e86949d 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 7dc4223a1..6650d323b 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 b8906ab02..1669f0124 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 5bca33bdb..84bde2e4a 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 000000000..d011ca897 --- /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 + ]) + } +}