UIScrollView + LargeTitle (iOS 11) - scroll to top and reveal the large title

4.5k Views Asked by At

I use the following code to scroll to top of the UICollectionView:

scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)

However, on iOS 11 and 12 the scrollView only scrolls to the top, without revealing the large title of the UINavigationBar (when prefersLargeTitle has ben set to true.)

Here is how it looks like: enter image description here

The result I want to achieve:

enter image description here

4

There are 4 best solutions below

2
On BEST ANSWER

It works as it is designed, you are scrolling to position y = 0, assign your controller to be UIScrollView delegate and print out scroll offset:

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    print(scrollView.contentOffset)
}

You will see when Large title is displayed and you move your scroll view a but and it jumps back to the Large title it will not print (0.0, 0.0) but (0.0, -64.0) or (0.0, -116.0) - this is the same value as scrollView.adjustedContentInset, so if you want to scroll up and display large title you should do:

scrollView.scrollRectToVisible(CGRect(x: 0, y: -64, width: 1, height: 1), animated: true)
2
On

You don't want to use any 'magic values' (as -64 in the currently accepted answer). These may change (also, -64 isn't correct anyway).

A better solution is to observe the SafeAreaInsets changes and save the biggest top inset. Then use this value in the setContentOffset method. Like this:

class CollectioViewController: UIViewController {
    var biggestTopSafeAreaInset: CGFloat = 0
            
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        self.biggestTopSafeAreaInset = max(ui.safeAreaInsets.top, biggestTopSafeAreaInset)
    }
    
    func scrollToTop(animated: Bool) {
        ui.scrollView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
    }
}
0
On

It seems that using a negative content offset is the way to go.

I really like the idea of Demosthese to keep track of the biggest top inset. However, there is a problem with this approach. Sometime large titles cannot be displayed, for example, when an iPhone is in landscape mode.

If this method is used after a device has been rotated to landscape then the offset of the table will be too much because the large title is not displayed in the navigation bar.

An improvements to this technique is to consider biggestTopSafeAreaInset only when the navigation bar can display a large title. Now the problem is to understand when a navigation bar can display a large title. I did some test on different devices and it seems that large titles are not displayed when the vertical size class is compact.

So, Demosthese solution can be improved in this way:

class TableViewController: UITableViewController {
    var biggestTopSafeAreaInset: CGFloat = 0
            
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        self.biggestTopSafeAreaInset = max(view.safeAreaInsets.top, biggestTopSafeAreaInset)
    }
    
    func scrollToTop(animated: Bool) {
        if traitCollection.verticalSizeClass == .compact {
            tableView.setContentOffset(CGPoint(x: 0, y: -view.safeAreaInsets.top), animated: animated)
        } else {
            tableView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
        }
    }
}

There is still a case that could cause the large title to not be displayed after the scroll.

If the user:

  1. Open the app with the device rotated in landscape mode.
  2. Scroll the view.
  3. Rotate the device in portrait.

At this point biggestTopSafeAreaInset has not yet had a chance to find the greatest value and if the scrollToTop method is called the large title will be not displayed. Fortunately, this is a case that shouldn't happen often.

0
On

Quite late here but I have my version of the story.

Since iOS 11 there is the adjustedContentInset on the scroll view. That however reflects only the current state of the UI thus if the large navigation title is not revealed, it won't be taken into account.

So my solution is to make couple of extra calls to make the system consider the large title size and calculate it to the adjustedContentInset:

extension UIScrollView {
    func scrollToTop(animated: Bool = true) {
        if animated {
            // 1
            let currentOffset = contentOffset
            // 2
            setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
            // 3
            let newAdjustedContentInset = adjustedContentInset
            // 4
            setContentOffset(currentOffset, animated: false)
            // 5
            setContentOffset(CGPoint(x: 0, y: -newAdjustedContentInset.top), animated: true)
        } else {
            // 1
            setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
            // 2
            setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top), animated: false)
        }
    }
}

Here is what's happening:

When animated:

  1. Get the current offset to be able to apply it again (important for achieving the animation)
  2. Scroll without animating to the currently calculated adjustedContentInset plus some more because the large title was not considered when calculating the adjustedContentInset
  3. Now the system takes into account the large title so get the current adjustedContentInset that will include its size so store it to a constant that will be used in the last step
  4. Scroll back to the original offset without animating so no visual changes will be noticed
  5. Scroll to the previously calculated adjustedContentInset this time animating to achieve the desired animated scrolling

When !animated:

  1. Scroll without animation to the adjustedContentInset plus some more. At this stage the system will consider the large title so...
  2. Scroll to the current adjustedContentInset as it was calculated with the large title in it

Kind of a hack but does work.