How frequently is UICollectionView layoutSubviews being called during scrolling?

Sasha Terentev
Picsart Engineering
16 min readJun 13, 2023

--

It is my most favorite and intriguing question regarding iOS development.

As I often get wrong answers to it, unfortunately, even from ChatGPT, in this article I aim to provide a detailed explanation, debunk misconceptions, and explore various aspects of UIKit, CoreAnimation, and Foundation that contribute to the answer.

By understanding how gesture handling, UIScrollView, UIView layout, and other factors work, we can gain insights into the behavior of UICollectionView during scrolling. Let’s dive in!

The table of contents

1. Related Questions

2. Test Project

3. Answering the Main Question

4. Inconsistency in UICollectionView Before the Next Layout

5. Exploring the Rendering Pipeline and Theory

6. Investigating Call Stacks

1. Related Questions

First of all I want to list different variations of the main question from my arsenal:

And some other attendant questions:

Please don’t be afraid or puzzled by such a big list of questions as all of them are connected and some of them are even the same from the theory point of view. We are going to discover the details during the Rendering pipeline research.

2. Test project

To unravel the answers to our questions, we can create a test project with a basic UICollectionView implementation. By overriding certain default methods and adding logging statements, we can observe the behavior and find the answers we seek.

To make the logs more readable and informative at the beginning of each log line we are printing the time passed since the previous log:

var prevLogTime = CACurrentMediaTime()
var groupThreshold = 0.001 // 10 ms
func log(_ string: String) {
let newTime = CACurrentMediaTime()
let diff = newTime - prevLogTime
prevLogTime = newTime
if diff > groupThreshold {
print("\n\n")
}
print(String(format: "%.3f", diff * 1000) + " ms: " + string)
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
override func loadView() {
let layout = FlowLayout()
layout.sectionInset = .init(top: 1, left: 1, bottom: 1, right: 1)
layout.sectionInsetReference = .fromContentInset
layout.itemSize = .init(width: 100, height: 200)
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1

let view = View(frame: .zero, collectionViewLayout: layout)
view.register(UICollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "CellID")
view.dataSource = self
view.delegate = self
self.view = view
}

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

log("UICollectionViewController.viewWillLayoutSubviews()")
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

log("UICollectionViewController.viewDidLayoutSubviews()")
}

func numberOfSections(in collectionView: UICollectionView) -> Int {
1
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellID", for: indexPath)
cell.backgroundColor = .cyan
log("UICollectionViewController.cellForItem at: \(indexPath)")
return cell
}

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
log("UICollectionViewController.willDisplayCell at: \(indexPath)")
}

private class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
log("FlowLayout.layoutAttributesForElements(in:)")
return super.layoutAttributesForElements(in: rect)
}

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let result = super.shouldInvalidateLayout(forBoundsChange: newBounds)
log("FlowLayout.shouldInvalidateLayout(forBoundsChange:) -> \(result)")
return result
}
}

private class View: UICollectionView {
override var contentOffset: CGPoint {
willSet {
log("UICollectionView.contentOffset.willSet")
}

didSet {
log("UICollectionView.contentOffset.didSet")
}
}

override var bounds: CGRect {
willSet {
log("UICollectionView.bounds.willSet")
}

didSet {
log("UICollectionView.bounds.didSet")
}
}

override func setNeedsLayout() {
log("UICollectionView.setNeedsLayout()")
super.setNeedsLayout()
}

override func layoutSubviews() {
log("UICollectionView.layoutSubviews() start")
super.layoutSubviews()
log("UICollectionView.layoutSubviews() finish")
}
override func layoutSublayers(of layer: CALayer) {
log("UICollectionView.layoutSublayers() start")
super.layoutSublayers(of: layer)
log("UICollectionView.layoutSublayers() finish")
}
}
}

3. Answering the Main Question

During scrolling in the console, we can see log groups like this (the results from iPhone 13 pro):

7.174 ms: UICollectionView.contentOffset.willSet
0.074 ms: UICollectionView.bounds.willSet
0.021 ms: FlowLayout.shouldInvalidateLayout(forBoundsChange:) -> false
0.033 ms: UICollectionView.setNeedsLayout()
0.014 ms: UICollectionView.bounds.didSet
0.058 ms: UICollectionView.contentOffset.didSet
0.065 ms: UICollectionView.layoutSublayers() start
0.023 ms: UICollectionViewController.viewWillLayoutSubviews()
0.014 ms: UICollectionView.layoutSubviews() start
0.023 ms: FlowLayout.layoutAttributesForElements(in:)
0.263 ms: UICollectionView.layoutSubviews() finish
0.054 ms: UICollectionViewController.viewDidLayoutSubviews()
0.023 ms: UICollectionView.layoutSublayers() finish
0.261 ms: UICollectionViewController.cellForItem at: [0, 28]
0.167 ms: UICollectionView.layoutSublayers() start
0.027 ms: UICollectionViewController.viewWillLayoutSubviews()
0.014 ms: UICollectionView.layoutSubviews() start
0.021 ms: UICollectionView.layoutSubviews() finish
0.050 ms: UICollectionViewController.viewDidLayoutSubviews()
0.013 ms: UICollectionView.layoutSublayers() finish

or

6.350 ms: UICollectionView.contentOffset.willSet
0.141 ms: UICollectionView.bounds.willSet
0.034 ms: FlowLayout.shouldInvalidateLayout(forBoundsChange:) -> false
0.056 ms: UICollectionView.setNeedsLayout()
0.031 ms: UICollectionView.bounds.didSet
0.100 ms: UICollectionView.contentOffset.didSet
0.044 ms: UICollectionView.layoutSublayers() start
0.031 ms: UICollectionViewController.viewWillLayoutSubviews()
0.025 ms: UICollectionView.layoutSubviews() start
0.248 ms: UICollectionViewController.willDisplayCell at: [0, 27]
0.091 ms: UICollectionViewController.willDisplayCell at: [0, 28]
0.121 ms: UICollectionViewController.willDisplayCell at: [0, 29]
0.131 ms: UICollectionView.layoutSubviews() finish
0.128 ms: UICollectionViewController.viewDidLayoutSubviews()
0.030 ms: UICollectionView.layoutSublayers() finish
0.482 ms: UICollectionViewController.cellForItem at: [0, 35]
0.305 ms: UICollectionView.layoutSublayers() start
0.053 ms: UICollectionViewController.viewWillLayoutSubviews()
0.027 ms: UICollectionView.layoutSubviews() start
0.043 ms: UICollectionView.layoutSubviews() finish
0.114 ms: UICollectionViewController.viewDidLayoutSubviews()
0.028 ms: UICollectionView.layoutSublayers() finish

So roughly saying the answer is:

During scrolling, UICollectionView’s layout happens on every screen update.

Urgent remark

For one who wants to skip some parts or the rest of the article I want to highlight the next info:

The answer is fully correct in general for the layoutSubviews method (the theory for it will be introduced further), layoutSubviews happens on every collection geometry or data update.

Cell dequeing and dipslaying happen inside the layoutSubviews. But the frequency of cell updates depends on the UICollectionView implementation details and happened changes. The visible cells array may be changed not on every layoutSubviews if it is not necessary.

The calling frequenecy of UICollectionViewLayout methods depends on the internal implementation details and corresponding changes as well.

In other words we should not assert that UICollectionViewLayout attributes or the visible cells array are being changed on each layout update in general. But they are checked and actualized if needed on every layoutSubviews call.

So, an assertion we may stand on in general: the only moment when the UICollectionView (and every UIView in common) layout (the geometry of the subviews, visibleCells, etc) is consistent to all its properties (dataSource, layout, etc) is the moment right after the completion of the nearest layoutSubviews method call following the corresponding changes.

If the information is surprising to you let’s go further:)

4. Inconsistency in UICollectionView Before the Next Layout

Between an update signal and the next layoutSubviews call the collection view state may temporarily be inconsistent.

To illustrate and prove the statement that the layoutSubviews (and connected methods) is the best and the only reliable place for obtaining any information related to the layout state we can do next.

Let’s add some trivial data source changes to our test project:

var items = ["1", "2", "3"]

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
log("didSelectItemAt: \(indexPath)")

log("visiblePaths before handling: \(collectionView.indexPathsForVisibleItems)")

collectionView.deselectItem(at: indexPath, animated: true)

items = ["new1", "new2", "new3"]

log("will call reloadData")
collectionView.reloadData()
log("did call reloadData. Item at index: \(indexPath.item) data source item: [\(items[indexPath.item])]")
log("visiblePaths after reloadData: \(collectionView.indexPathsForVisibleItems)")
log("visibleCells after reloadData: \(collectionView.visibleCells)")

collectionView.layoutIfNeeded()
log("visiblePaths after layoutIfNeeded: \(collectionView.indexPathsForVisibleItems)")
}

And if we run the new code and select any cell we can see the next logs in the console:

1308.096 ms: didSelectItemAt: [0, 2]
0.535 ms: visiblePaths before handling: [[0, 1], [0, 0], [0, 2]]
0.130 ms: will call reloadData
0.082 ms: UICollectionView.setNeedsLayout()
0.037 ms: UICollectionView.setNeedsLayout()
0.029 ms: UICollectionView.setNeedsLayout()
0.007 ms: UICollectionView.setNeedsLayout()
0.047 ms: did call reloadData. Item at index: 2 data source item: [new3]
0.007 ms: visiblePaths after reloadData: []
0.012 ms: visibleCells after reloadData: []
0.010 ms: UICollectionView.layoutSublayers() start
0.023 ms: UICollectionViewController.viewWillLayoutSubviews()
0.008 ms: UICollectionView.layoutSubviews() start
0.043 ms: FlowLayout.layoutAttributesForElements(in:)
0.149 ms: UICollectionViewController.cellForItem at: [0, 0]
0.055 ms: UICollectionViewController.willDisplayCell at: [0, 0]
0.022 ms: UICollectionViewController.cellForItem at: [0, 1]
0.018 ms: UICollectionViewController.willDisplayCell at: [0, 1]
0.017 ms: UICollectionViewController.cellForItem at: [0, 2]
0.014 ms: UICollectionViewController.willDisplayCell at: [0, 2]
0.029 ms: UICollectionView.layoutSubviews() finish
0.009 ms: UICollectionViewController.viewDidLayoutSubviews()
0.004 ms: UICollectionView.layoutSublayers() finish
0.063 ms: visiblePaths after layoutIfNeeded: [[0, 1], [0, 0], [0, 2]]

The main lines we should highlight:

0.047 ms: did call reloadData. Item at index: 2 data source item: [new3]
0.007 ms: visiblePaths after reloadData: []
0.012 ms: visibleCells after reloadData: []
...
0.063 ms: visiblePaths after layoutIfNeeded: [[0, 1], [0, 0], [0, 2]]

It means:

The visibleCells and visiblePaths don’t match the data source state until the next layout phase.

It is crucial to understand that attempting to access collection cell information during this interval can yield unexpected values.

Such situations are very possible in some cases as in general we may have a lot of executable code in between an invalidation signal and the nearest collection layout (normally during the CATransaction.commit phase, it will be described further in the theory part).

Also, I want to note that forced collectionView.layoutIfNeeded() the call is not the best solution for the problem as it leads to unnecessary layout operations and potentially impacts UI performance.

To discover some more insights from the test project we can add breakpoints to the overridden methods and investigate the received call stacks. If you wish to do this first of all, you may skip the theory part and switch to the “Investigating Call Stacks” part and then return to the theory to clarify the meaning of the discoveries.

5. Exploring the Rendering Pipeline and Theory

To shed light on the console output and understand what’s happening behind the scenes, let’s refresh our understanding of the iOS screen update cycle.

And I want to start with the Run loop.

Run loop

Our app, as all responsive UI apps, has at least one event loop. In case of iOS apps it is the run loop executing on the main thread. Hardly saying this run loop is an infinite loop which:

  • keeps our app executing — avoids it from finishing (returning from the main function) immediately;
  • on each iteration handles incoming events.

Event examples:

Here the process of user event handling begins for us. We can skip the part of user event handler searching (you can read about: first responder view search and event propagation in the opposite way via the responder chain). In abstract, we may describe the run loop like this:

while (!appShouldTerminate) {
...
if (dispatchBlocks.count > 0) {
doBlocks()
}
...
if (hasPanEvent) {
handlePan() // change content offset -> change bounds -> setNeedsLayout
}
...
CATransaction.flush() // commit all unclosed transactions
}

In the code above you may have noticed CATransaction.

CATransaction

This object batches all the updates that need to appear on the screen. There are two types of transactions. Implicit transactions are created automatically when the layer tree is modified by a thread without an active transaction and are committed automatically when the thread’s runloop next iterates. Explicit ones are created by CATransaction.begin() method. So all the layout (and displaying) changes happen inside the current transaction.

The pipeline

CATransaction has different phases:

  • prepare;
  • commit;
  • decode;
  • draw;
  • render;
  • display.

Here is an image from WWDC 2014 — Session 419:

It’s essential to recognize that UIKit operates asynchronously, with layout changes occurring inside UIView.layoutSubviews method during the CATransaction.commit phase.

All transaction phases are asynchronous relating to the previous ones

In the context of our topic it means that UIKit is asynchronous from the next point of view:

  • we change UIView (more precisely, CALayer properties) and then later perform corresponding updates.

It is the case for layout changes as well. We are marking our view as setNeedsLayout while some event handling and later we are actualizing the layout during the CATransaction.commit -> UIView.layoutSubviews phase.

All visible cell changes happen in CATransaction.commit -> UICollectionView.layoutSubviews the method as well.

This asynchronous nature allows us to batch different property changes and reduce the number of heavy update operations. That’s why calling layoutIfNeeded() should generally be avoided.

Drawing updates (draw(_:)) has the same lifecycle: setNeedsDisplay(), CALayer.displayIfNeeded().

Some service operations such as uncompressed image decoding also take place in the commit phase.

All phases after the commit are being executed out of our app process

The moment right after the commit phase is the last and the only moment in our app when the layout is consistent with the view state.

Further update processing happens on the Render Server. Offscreen rendering, CAAnimation performing, etc happen in this separate process.

Regarding UICollectionView we need to distinguish the difference between UIView.animate { collectionView.contentOffset = somePoint } and collectionView.setContentOffset(somePoint, animated:true).

In the first case, CoreAnimation adds a base CAAnimation and UIKit layouts the view to the final state immediately. Consequently, in case of a significant difference between the initial offset and the final offset the cells except for the final viewport ones will be missed during the animation (it will look like empty space).

In the case of collectionView.setContentOffset(somePoint, animated:true) UICollectionView: UIScrollView creates a timer (CADisplayLink) which actualizes the scroll view layout each time the screen should be updated.

The whole update preparation duration

One more interesting thing about the core animation is asynchronous work. As the screen update preparation is separated between different processes the full duration of it is 2 frame times (in the case of 60 FPS it is 1/30 seconds). As the Apple engineers said on WWDC 2014 — Session 419 it is a purposed optimization. When the Render server is processing the previous update our app is already preparing a new one:

Run Loop + CATransaction

We should keep in mind that the runloop iteration count is hardly strict. Also, the runloop flushes existing transactions at the end of each iteration. So if our continuous frequent updates don’t have any limiting mechanism and we change any view from the window hierarchy too often, for instance writing something like this:

func update() {
DispatchQueue.main.async {
someView.setNeedsLayout()
self.update()
}
}

we send thousands of transactions (screen update trials) to the render server per second!

In the case of UIScrollView the update, the count is limited by either CADisplayLink or user event firing frequency.

Window hierarchy

One more thing I want to highlight. In general, the views which are out of the window hierarchy are not being laid out until they are added to the hierarchy. So layout actualisation (layoutSubview call) may never follow setNeedsLayout call for these views. Or minutes or even hours may pass before the nearest layout, so different new update signals may appear during such periods.

6. Investigating Call Stacks

To validate the theory presented earlier, we can examine the call stacks of the overridden methods in our test project. By adding breakpoints and observing the call stacks, we can gain practical insights into the behavior of UICollectionView during scrolling. This hands-on investigation helps solidify the concepts discussed in the previous sections.

User gesture caused scroll animation

setNeedsLayout in case of user scrolling (pan gesture) with the FPS frequency:

#0 0x0000000100565480 in ViewController.View.setNeedsLayout() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:179
#1 0x00000001005654e4 in @objc ViewController.View.setNeedsLayout() ()
#2 0x0000000193b52b18 in -[UICollectionView _setNeedsVisibleCellsUpdate:withLayoutAttributes:] ()
#3 0x0000000193a09c64 in -[UICollectionView setBounds:] ()
#4 0x0000000100565368 in ViewController.View.bounds.setter ()
#5 0x000000010056529c in @objc ViewController.View.bounds.setter ()
#6 0x0000000193a08f14 in -[UIScrollView setContentOffset:] ()
#7 0x0000000193a72eb0 in -[UICollectionView setContentOffset:] ()
#8 0x000000010056505c in ViewController.View.contentOffset.setter ()
#9 0x0000000100564fbc in @objc ViewController.View.contentOffset.setter ()
#10 0x0000000193a72620 in -[UIScrollView _updatePanGesture] ()
#11 0x0000000193a73254 in -[UIScrollView handlePan:] ()
#12 0x0000000193a718b4 in -[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] ()
#13 0x0000000193ddc2a8 in _UIGestureRecognizerSendTargetActions ()
#14 0x0000000193b58674 in _UIGestureRecognizerSendActions ()
#15 0x0000000193a3aa4c in -[UIGestureRecognizer _updateGestureForActiveEvents] ()
#16 0x0000000193ae58ac in _UIGestureEnvironmentUpdate ()
#17 0x000000019437500c in -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] ()
#18 0x0000000193aaa520 in -[UIGestureEnvironment _updateForEvent:window:] ()
#19 0x0000000193aaed94 in -[UIWindow sendEvent:] ()
#20 0x0000000193aae054 in -[UIApplication sendEvent:] ()
#21 0x0000000193aac340 in __dispatchPreprocessedEventFromEventQueue ()
#22 0x0000000193af5018 in __processEventQueue ()
#23 0x0000000194767e48 in updateCycleEntry ()
#24 0x00000001940044b0 in _UIUpdateSequenceRun ()
#25 0x0000000194668c8c in schedulerStepScheduledMainSection ()
#26 0x00000001946681e8 in runloopSourceCallback ()
#27 0x0000000191a1a128 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#28 0x0000000191a267b4 in __CFRunLoopDoSource0 ()
#29 0x00000001919ab5e8 in __CFRunLoopDoSources0 ()
#30 0x00000001919c10d4 in __CFRunLoopRun ()
#31 0x00000001919c63ec in CFRunLoopRunSpecific ()
#32 0x00000001cce8b35c in GSEventRunModal ()
#33 0x0000000193d536e8 in -[UIApplication _run] ()
#34 0x0000000193d5334c in UIApplicationMain ()
#35 0x000000019a3db0b0 in UIApplicationMain(_:_:_:_:) ()
#36 0x0000000100566544 in static UIApplicationDelegate.main() ()
#37 0x00000001005664bc in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#38 0x00000001005665c0 in main ()
#39 0x00000001b0ec6dec in start ()

in this case, the system controls the scroll view animation frame rate by the frequency of gesture event handling.

Automatic scroll animation

setNeedsLayout in case of deceleration animation (or collectionView.setContentOffset(somePoint, animated:true) using) with the FPS frequency:

#0 0x00000001027f5480 in ViewController.View.setNeedsLayout() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:179
#1 0x00000001027f54e4 in @objc ViewController.View.setNeedsLayout() ()
#2 0x0000000193b52b18 in -[UICollectionView _setNeedsVisibleCellsUpdate:withLayoutAttributes:] ()
#3 0x0000000193a09c64 in -[UICollectionView setBounds:] ()
#4 0x00000001027f5368 in ViewController.View.bounds.setter ()
#5 0x00000001027f529c in @objc ViewController.View.bounds.setter ()
#6 0x0000000193a08f14 in -[UIScrollView setContentOffset:] ()
#7 0x0000000193a72eb0 in -[UICollectionView setContentOffset:] ()
#8 0x00000001027f505c in ViewController.View.contentOffset.setter ()
#9 0x00000001027f4fbc in @objc ViewController.View.contentOffset.setter ()
#10 0x0000000193a71220 in -[UIScrollView _smoothScrollSyncWithUpdateTime:] ()
#11 0x0000000193a70960 in -[UIScrollView _smoothScrollWithUpdateTime:] ()
#12 0x0000000193d3dc90 in -[UIScrollView _smoothScrollDisplayLink:] ()
#13 0x0000000192e9446c in CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long) ()
#14 0x0000000192fb1f28 in CA::Display::DisplayLink::dispatch_deferred_display_links(unsigned int) ()
#15 0x00000001940044b0 in _UIUpdateSequenceRun ()
#16 0x0000000194668c8c in schedulerStepScheduledMainSection ()
#17 0x00000001946681e8 in runloopSourceCallback ()
#18 0x0000000191a1a128 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#19 0x0000000191a267b4 in __CFRunLoopDoSource0 ()
#20 0x00000001919ab5e8 in __CFRunLoopDoSources0 ()
#21 0x00000001919c10d4 in __CFRunLoopRun ()
#22 0x00000001919c63ec in CFRunLoopRunSpecific ()
#23 0x00000001cce8b35c in GSEventRunModal ()
#24 0x0000000193d536e8 in -[UIApplication _run] ()
#25 0x0000000193d5334c in UIApplicationMain ()
#26 0x000000019a3db0b0 in UIApplicationMain(_:_:_:_:) ()
#27 0x00000001027f6544 in static UIApplicationDelegate.main() ()
#28 0x00000001027f64bc in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#29 0x00000001027f65c0 in main ()
#30 0x00000001b0ec6dec in start ()

you may see in that case the CADisplayLink timer usage to generate content offset update signals with a frequency equal to the frame rate.

One important thing here. As we mentioned in the theory part if we use the next code:

UIView.animate(withDuration: .25) {
collectionView.contentOffset = somePoint
collectionView.layoutIfNeeded()
}

no timer is created. CoreAnimation adds a base CAAnimation and UIKit layouts the view to the final state immediately. Consequently, in case of a significant difference between the initial offset and the final offset the cells except for the final view port ones will be missed during the animation (it will look like empty space).

Cell updates

As I've mentioned before the frequency of UICollectionView specific methods depends on its implementation details only. But these calls happen in general in the UIKit related methods.

collectionView(_:cellForItemAt:):

#0 0x00000001027f4328 in ViewController.collectionView(_:cellForItemAt:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:133
#1 0x00000001027f45a4 in @objc ViewController.collectionView(_:cellForItemAt:) ()
#2 0x0000000193affa44 in -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] ()
#3 0x00000001940b00cc in -[UICollectionView _createVisibleViewsForSingleCategoryAttributes:limitCreation:fadeForBoundsChange:] ()
#4 0x00000001940b03a4 in -[UICollectionView _createVisibleViewsForAttributes:fadeForBoundsChange:notifyLayoutForVisibleCellsPass:] ()
#5 0x00000001939df6dc in -[UICollectionView _updateVisibleCellsNow:] ()
#6 0x00000001939defa0 in -[UICollectionView layoutSubviews] ()
#7 0x00000001027f5574 in ViewController.View.layoutSubviews() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:184
#8 0x00000001027f55d8 in @objc ViewController.View.layoutSubviews() ()
#9 0x00000001939babe0 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#10 0x00000001027f5678 in ViewController.View.layoutSublayers(of:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:190
#11 0x00000001027f56ec in @objc ViewController.View.layoutSublayers(of:) ()
#12 0x0000000192e75074 in CA::Layer::layout_if_needed(CA::Transaction*) ()
#13 0x0000000192e885f0 in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#14 0x0000000192e99a1c in CA::Context::commit_transaction(CA::Transaction*, double, double*) ()
#15 0x0000000192ec8ff4 in CA::Transaction::commit() ()
#16 0x0000000192eb2f3c in CA::Transaction::flush_as_runloop_observer(bool) ()
#17 0x0000000193eb3c04 in _UIApplicationFlushCATransaction ()
#18 0x00000001940044b0 in _UIUpdateSequenceRun ()
#19 0x0000000194668c8c in schedulerStepScheduledMainSection ()
#20 0x00000001946681e8 in runloopSourceCallback ()
#21 0x0000000191a1a128 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#22 0x0000000191a267b4 in __CFRunLoopDoSource0 ()
#23 0x00000001919ab5e8 in __CFRunLoopDoSources0 ()
#24 0x00000001919c10d4 in __CFRunLoopRun ()
#25 0x00000001919c63ec in CFRunLoopRunSpecific ()
#26 0x00000001cce8b35c in GSEventRunModal ()
#27 0x0000000193d536e8 in -[UIApplication _run] ()
#28 0x0000000193d5334c in UIApplicationMain ()
#29 0x000000019a3db0b0 in UIApplicationMain(_:_:_:_:) ()
#30 0x00000001027f6544 in static UIApplicationDelegate.main() ()
#31 0x00000001027f64bc in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#32 0x00000001027f65c0 in main ()
#33 0x00000001b0ec6dec in start ()

collectionView(_:willDisplay:forItemAt:):

#0 0x0000000100564648 in ViewController.collectionView(_:willDisplay:forItemAt:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:140
#1 0x00000001005647cc in @objc ViewController.collectionView(_:willDisplay:forItemAt:) ()
#2 0x0000000193b00094 in -[UICollectionView _notifyWillDisplayCellIfNeeded:forIndexPath:] ()
#3 0x0000000193affd7c in -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] ()
#4 0x00000001940b00cc in -[UICollectionView _createVisibleViewsForSingleCategoryAttributes:limitCreation:fadeForBoundsChange:] ()
#5 0x00000001940b03a4 in -[UICollectionView _createVisibleViewsForAttributes:fadeForBoundsChange:notifyLayoutForVisibleCellsPass:] ()
#6 0x00000001939df6dc in -[UICollectionView _updateVisibleCellsNow:] ()
#7 0x00000001939defa0 in -[UICollectionView layoutSubviews] ()
#8 0x0000000100565574 in ViewController.View.layoutSubviews() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:184
#9 0x00000001005655d8 in @objc ViewController.View.layoutSubviews() ()
#10 0x00000001939babe0 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#11 0x0000000100565678 in ViewController.View.layoutSublayers(of:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:190
#12 0x00000001005656ec in @objc ViewController.View.layoutSublayers(of:) ()
#13 0x0000000192e75074 in CA::Layer::layout_if_needed(CA::Transaction*) ()
#14 0x0000000192e885f0 in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#15 0x0000000192e99a1c in CA::Context::commit_transaction(CA::Transaction*, double, double*) ()
#16 0x0000000192ec8ff4 in CA::Transaction::commit() ()
#17 0x0000000192eb2f3c in CA::Transaction::flush_as_runloop_observer(bool) ()
#18 0x0000000193eb3c04 in _UIApplicationFlushCATransaction ()
#19 0x00000001940044b0 in _UIUpdateSequenceRun ()
#20 0x0000000194668c8c in schedulerStepScheduledMainSection ()
#21 0x00000001946681e8 in runloopSourceCallback ()
#22 0x0000000191a1a128 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#23 0x0000000191a267b4 in __CFRunLoopDoSource0 ()
#24 0x00000001919ab648 in __CFRunLoopDoSources0 ()
#25 0x00000001919c10d4 in __CFRunLoopRun ()
#26 0x00000001919c63ec in CFRunLoopRunSpecific ()
#27 0x00000001cce8b35c in GSEventRunModal ()
#28 0x0000000193d536e8 in -[UIApplication _run] ()
#29 0x0000000193d5334c in UIApplicationMain ()
#30 0x000000019a3db0b0 in UIApplicationMain(_:_:_:_:) ()
#31 0x0000000100566544 in static UIApplicationDelegate.main() ()
#32 0x00000001005664bc in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#33 0x00000001005665c0 in main ()
#34 0x00000001b0ec6dec in start ()

UICollectionViewFlowLayout.layoutAttributesForElements(in:):

#0 0x0000000100ba0714 in ViewController.FlowLayout.layoutAttributesForElements(in:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:146
#1 0x0000000100ba0860 in @objc ViewController.FlowLayout.layoutAttributesForElements(in:) ()
#2 0x0000000116725f64 in __45-[UICollectionViewData validateLayoutInRect:]_block_invoke ()
#3 0x000000011672535c in -[UICollectionViewData validateLayoutInRect:] ()
#4 0x00000001166ef0e0 in -[UICollectionView layoutSubviews] ()
#5 0x0000000100ba1404 in ViewController.View.layoutSubviews() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:184
#6 0x0000000100ba1468 in @objc ViewController.View.layoutSubviews() ()
#7 0x0000000117396c00 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#8 0x0000000100ba1508 in ViewController.View.layoutSublayers(of:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:190
#9 0x0000000100ba157c in @objc ViewController.View.layoutSublayers(of:) ()
#10 0x0000000101c54528 in CA::Layer::layout_if_needed(CA::Transaction*) ()
#11 0x0000000101c5f288 in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#12 0x0000000101b89130 in CA::Context::commit_transaction(CA::Transaction*, double, double*) ()
#13 0x0000000101bb40f4 in CA::Transaction::commit() ()
#14 0x0000000101bb5518 in CA::Transaction::flush_as_runloop_observer(bool) ()
#15 0x0000000103574c10 in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()
#16 0x000000010356f57c in __CFRunLoopDoObservers ()
#17 0x000000010356fa20 in __CFRunLoopRun ()
#18 0x000000010356f254 in CFRunLoopRunSpecific ()
#19 0x000000010758fc9c in GSEventRunModal ()
#20 0x0000000116ef2ff0 in -[UIApplication _run] ()
#21 0x0000000116ef6f3c in UIApplicationMain ()
#22 0x0000000102210454 in UIApplicationMain(_:_:_:_:) ()
#23 0x0000000100ba23d4 in static UIApplicationDelegate.main() ()
#24 0x0000000100ba234c in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#25 0x0000000100ba2450 in main ()
#26 0x0000000100e95514 in start_sim ()
#27 0x0000000100c69f28 in start ()

In the case of our test project, this method is being called on each screen update. And even twice for some cell preparation purposes:

#0 0x0000000100ba0714 in ViewController.FlowLayout.layoutAttributesForElements(in:) at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/ViewController.swift:146
#1 0x0000000100ba0860 in @objc ViewController.FlowLayout.layoutAttributesForElements(in:) ()
#2 0x0000000116725f64 in __45-[UICollectionViewData validateLayoutInRect:]_block_invoke ()
#3 0x000000011672535c in -[UICollectionViewData validateLayoutInRect:] ()
#4 0x0000000116727ce4 in -[UICollectionViewData layoutAttributesForCellsInRect:validateLayout:] ()
#5 0x00000001166e584c in -[UICollectionView _computeMainPrefetchCandidatesForVisibleBounds:futureVisibleBounds:prefetchVector:] ()
#6 0x00000001166e5774 in -[UICollectionView _computeMainPrefetchCandidatesForVelocity:] ()
#7 0x00000001166e77f4 in -[UICollectionView _updatePrefetchedCells:] ()
#8 0x00000001166e7af0 in -[UICollectionView _updateCycleIdleUntil:] ()
#9 0x0000000116b61750 in ___UIUpdateCycleNotifyIdle_block_invoke ()
#10 0x0000000102b70528 in _dispatch_call_block_and_release ()
#11 0x0000000102b71d50 in _dispatch_client_callout ()
#12 0x0000000102b82808 in _dispatch_main_queue_drain ()
#13 0x0000000102b822d4 in _dispatch_main_queue_callback_4CF ()
#14 0x0000000103575784 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#15 0x000000010356fde4 in __CFRunLoopRun ()
#16 0x000000010356f254 in CFRunLoopRunSpecific ()
#17 0x000000010758fc9c in GSEventRunModal ()
#18 0x0000000116ef2ff0 in -[UIApplication _run] ()
#19 0x0000000116ef6f3c in UIApplicationMain ()
#20 0x0000000102210454 in UIApplicationMain(_:_:_:_:) ()
#21 0x0000000100ba23d4 in static UIApplicationDelegate.main() ()
#22 0x0000000100ba234c in static AppDelegate.$main() at /Users/aleksandrterentev/projects/TestCollectionView/TestCollectionView/AppDelegate.swift:10
#23 0x0000000100ba2450 in main ()
#24 0x0000000100e95514 in start_sim ()
#25 0x0000000100c69f28 in start ()

I’ve checked the behavior in the case of the compositional layout and it was a little surprising for me. This method was called on each update of the nested section scroll view during horizontal scrolling but was rarely being called for the main collection view layout updates during vertical scrolling. That’s why I insist that the call frequency of such methods depends only on the internal collection view implementation details and may be changed.

The rest of the methods you can check by yourself :)

References

  • Documentation
  • iOS Core Animation: Advanced Techniques. Nick Lockwood
  • WWDC 2014 — Session 419

--

--