Skip to content

Commit 3332b19

Browse files
authored
Merge pull request #8293 from woocommerce/try/jitm-animate-take-2
[Just In Time Messages] Animate hiding of message in My Store screen
2 parents e75248d + 1f169d7 commit 3332b19

File tree

1 file changed

+115
-33
lines changed

1 file changed

+115
-33
lines changed

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ final class DashboardViewController: UIViewController {
5555
return view
5656
}()
5757

58+
/// Constraint to attach the content view's top to the bottom of the header
59+
/// When we hide the header, we disable this constraint so the content view can grow to fill the screen
60+
private var contentTopToHeaderConstraint: NSLayoutConstraint?
61+
62+
/// Stores an animator for showing/hiding the header view while there is an animation in progress
63+
/// so we can interrupt and reverse if needed
64+
private var headerAnimator: UIViewPropertyAnimator?
65+
5866
// Used to trick the navigation bar for large title (ref: issue 3 in p91TBi-45c-p2).
5967
private let hiddenScrollView = UIScrollView()
6068

@@ -98,6 +106,7 @@ final class DashboardViewController: UIViewController {
98106
private let viewModel: DashboardViewModel = .init()
99107

100108
private var subscriptions = Set<AnyCancellable>()
109+
private var navbarObserverSubscription: AnyCancellable?
101110

102111
// MARK: View Lifecycle
103112

@@ -119,7 +128,6 @@ final class DashboardViewController: UIViewController {
119128
configureBottomJetpackBenefitsBanner()
120129
observeSiteForUIUpdates()
121130
observeBottomJetpackBenefitsBannerVisibilityUpdates()
122-
observeNavigationBarHeightForHeaderExtrasVisibility()
123131
observeStatsVersionForDashboardUIUpdates()
124132
observeAnnouncements()
125133
observeShowWebViewSheet()
@@ -136,6 +144,17 @@ final class DashboardViewController: UIViewController {
136144
configureTitle()
137145
}
138146

147+
override func viewDidAppear(_ animated: Bool) {
148+
super.viewDidAppear(animated)
149+
updateHeaderVisibility(animated: false)
150+
observeNavigationBarHeightForHeaderVisibility()
151+
}
152+
153+
override func viewWillDisappear(_ animated: Bool) {
154+
stopObservingNavigationBarHeightForHeaderVisibility()
155+
super.viewWillDisappear(animated)
156+
}
157+
139158
override func viewDidLayoutSubviews() {
140159
super.viewDidLayoutSubviews()
141160
dashboardUI?.view.frame = containerView.bounds
@@ -144,17 +163,77 @@ final class DashboardViewController: UIViewController {
144163
override var shouldShowOfflineBanner: Bool {
145164
return true
146165
}
166+
}
147167

148-
/// Hide the announcement card when the navigation bar is compact
149-
///
150-
func updateAnnouncementCardVisibility() {
151-
announcementView?.isHidden = navigationBarIsShort
168+
// MARK: - Header animation
169+
private extension DashboardViewController {
170+
func showHeaderWithoutAnimation() {
171+
contentTopToHeaderConstraint?.isActive = true
172+
headerStackView.alpha = 1
173+
view.layoutIfNeeded()
152174
}
153175

154-
/// Hide the store name when the navigation bar is compact
155-
///
156-
func updateStoreNameLabelVisibility() {
157-
storeNameLabel.isHidden = !shouldShowStoreNameAsSubtitle || navigationBarIsShort
176+
func hideHeaderWithoutAnimation() {
177+
contentTopToHeaderConstraint?.isActive = false
178+
headerStackView.alpha = 0
179+
view.layoutIfNeeded()
180+
}
181+
182+
func updateHeaderVisibility(animated: Bool) {
183+
if navigationBarIsCollapsed() {
184+
hideHeader(animated: animated)
185+
} else {
186+
showHeader(animated: animated)
187+
}
188+
}
189+
190+
func showHeader(animated: Bool) {
191+
if animated {
192+
animateHeaderVisibility { [weak self] in
193+
self?.showHeaderWithoutAnimation()
194+
}
195+
} else {
196+
showHeaderWithoutAnimation()
197+
}
198+
}
199+
200+
func hideHeader(animated: Bool) {
201+
if animated {
202+
animateHeaderVisibility { [weak self] in
203+
self?.hideHeaderWithoutAnimation()
204+
}
205+
} else {
206+
hideHeaderWithoutAnimation()
207+
}
208+
}
209+
210+
func animateHeaderVisibility(animations: @escaping () -> Void) {
211+
if headerAnimator?.isRunning == true {
212+
headerAnimator?.stopAnimation(true)
213+
}
214+
headerAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
215+
withDuration: Constants.animationDurationSeconds,
216+
delay: 0,
217+
animations: animations,
218+
completion: { [weak self] position in
219+
self?.headerAnimator = nil
220+
})
221+
}
222+
223+
func navigationBarIsCollapsed() -> Bool {
224+
guard let frame = navigationController?.navigationBar.frame else {
225+
return false
226+
}
227+
228+
return frame.height <= collapsedNavigationBarHeight
229+
}
230+
231+
var collapsedNavigationBarHeight: CGFloat {
232+
if self.traitCollection.userInterfaceIdiom == .pad {
233+
return Constants.iPadCollapsedNavigationBarHeight
234+
} else {
235+
return Constants.iPhoneCollapsedNavigationBarHeight
236+
}
158237
}
159238
}
160239

@@ -208,8 +287,19 @@ private extension DashboardViewController {
208287

209288
func addViewBelowHeaderStackView(contentView: UIView) {
210289
contentView.translatesAutoresizingMaskIntoConstraints = false
290+
291+
// This constraint will pin the bottom of the header to the top of the content
292+
// We want this to be active when the header is visible
293+
contentTopToHeaderConstraint = contentView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor)
294+
contentTopToHeaderConstraint?.isActive = true
295+
296+
// This constraint has a lower priority and will pin the top of the content view to its superview
297+
// This way, it has a defined height when contentTopToHeaderConstraint is disabled
298+
let contentTopToContainerConstraint = contentView.topAnchor.constraint(equalTo: containerView.safeTopAnchor)
299+
contentTopToContainerConstraint.priority = .defaultLow
300+
211301
NSLayoutConstraint.activate([
212-
contentView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor),
302+
contentTopToContainerConstraint,
213303
contentView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
214304
contentView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
215305
])
@@ -377,8 +467,6 @@ private extension DashboardViewController {
377467
let indexAfterHeader = (headerStackView.arrangedSubviews.firstIndex(of: innerStackView) ?? -1) + 1
378468
headerStackView.insertArrangedSubview(uiView, at: indexAfterHeader)
379469

380-
updateAnnouncementCardVisibility()
381-
382470
hostingController.didMove(toParent: self)
383471
hostingController.view.layoutIfNeeded()
384472
}
@@ -402,11 +490,12 @@ private extension DashboardViewController {
402490
guard siteName.isNotEmpty else {
403491
shouldShowStoreNameAsSubtitle = false
404492
storeNameLabel.text = nil
493+
storeNameLabel.isHidden = true
405494
return
406495
}
407496
shouldShowStoreNameAsSubtitle = true
497+
storeNameLabel.isHidden = false
408498
storeNameLabel.text = siteName
409-
updateStoreNameLabelVisibility()
410499
}
411500
}
412501

@@ -585,31 +674,23 @@ private extension DashboardViewController {
585674
}.store(in: &subscriptions)
586675
}
587676

588-
func observeNavigationBarHeightForHeaderExtrasVisibility() {
589-
navigationController?.navigationBar.publisher(for: \.frame, options: [.initial, .new])
677+
func observeNavigationBarHeightForHeaderVisibility() {
678+
navbarObserverSubscription = navigationController?.navigationBar.publisher(for: \.frame, options: [.new])
679+
.map({ [weak self] rect in
680+
// This seems useless given that we're discarding the value later
681+
// and recalculating within updateHeaderVisibility, but this is an easy
682+
// way to avoid constant updates with the `removeDuplicates` that follows
683+
self?.navigationBarIsCollapsed() ?? false
684+
})
590685
.removeDuplicates()
591686
.sink(receiveValue: { [weak self] _ in
592-
guard let self else { return }
593-
self.updateStoreNameLabelVisibility()
594-
self.updateAnnouncementCardVisibility()
687+
self?.updateHeaderVisibility(animated: true)
595688
})
596-
.store(in: &subscriptions)
597689
}
598690

599-
/// Returns true if the navigation bar has a compact height as opposed to showing a large title
600-
///
601-
var navigationBarIsShort: Bool {
602-
guard let navigationBarHeight = navigationController?.navigationBar.frame.height else {
603-
return false
604-
}
605-
606-
let collapsedNavigationBarHeight: CGFloat
607-
if self.traitCollection.userInterfaceIdiom == .pad {
608-
collapsedNavigationBarHeight = Constants.iPadCollapsedNavigationBarHeight
609-
} else {
610-
collapsedNavigationBarHeight = Constants.iPhoneCollapsedNavigationBarHeight
611-
}
612-
return navigationBarHeight <= collapsedNavigationBarHeight
691+
func stopObservingNavigationBarHeightForHeaderVisibility() {
692+
navbarObserverSubscription?.cancel()
693+
navbarObserverSubscription = nil
613694
}
614695
}
615696

@@ -623,6 +704,7 @@ private extension DashboardViewController {
623704
}
624705

625706
enum Constants {
707+
static let animationDurationSeconds = CGFloat(0.3)
626708
static let bannerBottomMargin = CGFloat(8)
627709
static let horizontalMargin = CGFloat(16)
628710
static let storeNameTextColor: UIColor = .secondaryLabel

0 commit comments

Comments
 (0)