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