From 920d5e13e26780c32a5c419331d467ebaafb25c0 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Fri, 5 Jun 2026 11:51:16 -0700 Subject: [PATCH 1/8] Update demo project --- .../PerformanceDemoViewController.swift | 6 +++--- Example/MagazineLayoutExample/RootMenuViewController.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Example/MagazineLayoutExample/PerformanceDemoViewController.swift b/Example/MagazineLayoutExample/PerformanceDemoViewController.swift index cd3b0ad..f255d52 100644 --- a/Example/MagazineLayoutExample/PerformanceDemoViewController.swift +++ b/Example/MagazineLayoutExample/PerformanceDemoViewController.swift @@ -26,7 +26,7 @@ final class PerformanceDemoViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Performance (10K Items)" + title = "Performance (100K Items)" view.backgroundColor = .systemBackground navigationItem.rightBarButtonItems = [ @@ -71,8 +71,8 @@ final class PerformanceDemoViewController: UIViewController { ] private func loadInitialData() { - // Create 10,000 items - items = (0..<10_000).map { index in + // Create 100,000 items + items = (0..<100_000).map { index in let color = colors[index % colors.count] let item = PerformanceItem(id: nextItemID, color: color) nextItemID += 1 diff --git a/Example/MagazineLayoutExample/RootMenuViewController.swift b/Example/MagazineLayoutExample/RootMenuViewController.swift index 7b5cf31..7a7a4b8 100644 --- a/Example/MagazineLayoutExample/RootMenuViewController.swift +++ b/Example/MagazineLayoutExample/RootMenuViewController.swift @@ -198,7 +198,7 @@ private enum DemoOption: String, CaseIterable { case .messageThread: return "Bottom-to-top layout with pagination" case .performance: - return "10,000 items with traditional data source" + return "100,000 items with traditional data source" } } From 6d014d71cdc464909de4d0dbfb0ee410b1a3f5c7 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Sat, 6 Jun 2026 15:56:56 -0700 Subject: [PATCH 2/8] Add signpost logging --- MagazineLayout.podspec | 4 +- MagazineLayout.xcodeproj/project.pbxproj | 8 +-- MagazineLayout/Public/MagazineLayout.swift | 57 +++++++++++++++++++ ...MagazineLayoutCollectionReusableView.swift | 7 +++ .../MagazineLayoutCollectionViewCell.swift | 7 +++ Package.swift | 4 +- 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/MagazineLayout.podspec b/MagazineLayout.podspec index 6e05b5b..204218a 100644 --- a/MagazineLayout.podspec +++ b/MagazineLayout.podspec @@ -8,8 +8,8 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/airbnb/MagazineLayout.git', :tag => "v#{ s.version.to_s }" } s.swift_version = '4.0' s.source_files = 'MagazineLayout/**/*.{swift,h}' - s.ios.deployment_target = '10.0' - s.tvos.deployment_target = '10.0' + s.ios.deployment_target = '12.0' + s.tvos.deployment_target = '12.0' s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' diff --git a/MagazineLayout.xcodeproj/project.pbxproj b/MagazineLayout.xcodeproj/project.pbxproj index 3730e5e..2f49eff 100644 --- a/MagazineLayout.xcodeproj/project.pbxproj +++ b/MagazineLayout.xcodeproj/project.pbxproj @@ -468,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -477,7 +477,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3"; - TVOS_DEPLOYMENT_TARGET = 10.0; + TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -530,7 +530,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos"; @@ -538,7 +538,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3"; - TVOS_DEPLOYMENT_TARGET = 10.0; + TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 60175dd..93fa6cd 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import os import UIKit /// A collection view layout that can display items in a grid and list arrangement. @@ -59,11 +60,23 @@ public final class MagazineLayout: UICollectionViewLayout { } override public var collectionViewContentSize: CGSize { + let signpostID = OSSignpostID(log: signpostLog) + os_signpost(.begin, log: signpostLog, name: SignpostName.collectionViewContentSize, signpostID: signpostID) + defer { + os_signpost(.end, log: signpostLog, name: SignpostName.collectionViewContentSize, signpostID: signpostID) + } + guard collectionView != nil else { return .zero } return layoutState.contentSize } override public func prepare() { + let prepareSignpostID = OSSignpostID(log: signpostLog) + os_signpost(.begin, log: signpostLog, name: SignpostName.prepare, signpostID: prepareSignpostID) + defer { + os_signpost(.end, log: signpostLog, name: SignpostName.prepare, signpostID: prepareSignpostID) + } + super.prepare() // Save the previous collection view width if necessary @@ -80,6 +93,9 @@ public final class MagazineLayout: UICollectionViewLayout { // Update layout metrics if necessary if prepareActions.contains(.updateLayoutMetrics) { + let signpostID = OSSignpostID(log: signpostLog) + os_signpost(.begin, log: signpostLog, name: SignpostName.prepareUpdateLayoutMetrics, signpostID: signpostID) + for sectionIndex in 0.. Date: Fri, 5 Jun 2026 16:24:11 -0700 Subject: [PATCH 3/8] Add new experimental perf improvements flag --- MagazineLayout/Public/MagazineLayout.swift | 3 +++ .../Public/MagazineLayoutInvalidationContext.swift | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 93fa6cd..151ef0b 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -43,6 +43,9 @@ public final class MagazineLayout: UICollectionViewLayout { // MARK: Public + /// A temporary flag to enable safely testing some optimizations. + public static var _enableExperimentalOptimizations = false + /// The vertical layout direction of items in the collection view. This property changes the behavior of /// scroll-position-preservation when performing batch updates or when the collection view's bounds changes. public var verticalLayoutDirection = MagazineLayoutVerticalLayoutDirection.topToBottom diff --git a/MagazineLayout/Public/MagazineLayoutInvalidationContext.swift b/MagazineLayout/Public/MagazineLayoutInvalidationContext.swift index 4119ca6..36ddc55 100644 --- a/MagazineLayout/Public/MagazineLayoutInvalidationContext.swift +++ b/MagazineLayout/Public/MagazineLayoutInvalidationContext.swift @@ -20,15 +20,12 @@ import UIKit /// Used to indicate that collection view properties and/or delegate layout metrics changed. public final class MagazineLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext { - /// A temporary flag to enable safely testing a change to how layout invalidation works. - public static var _invalidateLayoutMetricsDefaultValue = true - /// Indicates whether to recompute the positions and sizes of elements based on the current collection view and delegate layout /// metrics. /// - /// Defaults to `false`. Set to `true` when delegate-provided layout values (e.g. item size + /// Set to `true` when delegate-provided layout values (e.g. item size /// modes, header/footer visibility, section metrics) have changed and the layout needs to /// re-query the delegate. - public var invalidateLayoutMetrics = _invalidateLayoutMetricsDefaultValue + public var invalidateLayoutMetrics = !MagazineLayout._enableExperimentalOptimizations } From e7e65e3f73a688871be1db189ed488d4f81c6083 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Fri, 5 Jun 2026 16:33:55 -0700 Subject: [PATCH 4/8] Optimize delegate, collection view, and modelState access Fix retain cycle --- MagazineLayout/Public/MagazineLayout.swift | 57 +++++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 151ef0b..b11df51 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -70,7 +70,7 @@ public final class MagazineLayout: UICollectionViewLayout { } guard collectionView != nil else { return .zero } - return layoutState.contentSize + return updatedLayoutState().contentSize } override public func prepare() { @@ -82,6 +82,9 @@ public final class MagazineLayout: UICollectionViewLayout { super.prepare() + _currentCollectionView = collectionView + _delegateMagazineLayout = currentCollectionView.delegate as? UICollectionViewDelegateMagazineLayout + // Save the previous collection view width if necessary if prepareActions.contains(.cachePreviousWidth) { cachedCollectionViewWidth = currentCollectionView.bounds.width @@ -137,7 +140,7 @@ public final class MagazineLayout: UICollectionViewLayout { os_signpost(.begin, log: signpostLog, name: SignpostName.prepareRecreateSectionModels, signpostID: signpostID) layoutStateBeforeRecreateSectionModels = LayoutState( - modelState: layoutState.modelState.copy(), + modelState: modelState.copy(), bounds: currentCollectionView.bounds, contentInset: contentInset, scale: scale, @@ -165,7 +168,7 @@ public final class MagazineLayout: UICollectionViewLayout { } let layoutStateBeforeCollectionViewUpdates = LayoutState( - modelState: layoutState.modelState.copy(), + modelState: modelState.copy(), bounds: currentCollectionView.bounds, contentInset: contentInset, scale: scale, @@ -259,6 +262,7 @@ public final class MagazineLayout: UICollectionViewLayout { if let layoutStateBeforeCollectionViewUpdates{ let targetContentOffsetAnchor = layoutStateBeforeCollectionViewUpdates.targetContentOffsetAnchor + let layoutState = updatedLayoutState() let targetYOffset = layoutState.yOffset( for: targetContentOffsetAnchor, isPerformingBatchUpdates: true) @@ -280,7 +284,7 @@ public final class MagazineLayout: UICollectionViewLayout { if currentCollectionView.bounds.size != oldBounds.size { layoutStateBeforeAnimatedBoundsChange = LayoutState( - modelState: layoutState.modelState.copy(), + modelState: modelState.copy(), bounds: oldBounds, contentInset: contentInset, scale: scale, @@ -708,13 +712,15 @@ public final class MagazineLayout: UICollectionViewLayout { withOriginalAttributes: originalAttributes) as! MagazineLayoutInvalidationContext context.invalidateLayoutMetrics = false + let layoutState = updatedLayoutState() + switch preferredAttributes.representedElementCategory { case .cell: let targetContentOffsetAnchor = ( layoutStateBeforeRecreateSectionModels ?? layoutStateBeforeCollectionViewUpdates ?? layoutStateBeforeAnimatedBoundsChange ?? - self.layoutState + layoutState ).targetContentOffsetAnchor let targetYOffsetBefore = layoutState.yOffset( for: targetContentOffsetAnchor, @@ -867,7 +873,7 @@ public final class MagazineLayout: UICollectionViewLayout { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) } - let yOffset = layoutState.yOffset( + let yOffset = updatedLayoutState().yOffset( for: layoutStateBefore.targetContentOffsetAnchor, isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil) @@ -929,12 +935,26 @@ public final class MagazineLayout: UICollectionViewLayout { private var cachedCollectionViewWidth: CGFloat? private var previousContentInset: UIEdgeInsets? + // Unowned unsafe references to avoid weak reference overhead. + private unowned(unsafe) var _currentCollectionView: UICollectionView? + private unowned(unsafe) var _delegateMagazineLayout: UICollectionViewDelegateMagazineLayout? + + @inline(__always) private var currentCollectionView: UICollectionView { - guard let collectionView = collectionView else { - preconditionFailure("`collectionView` should not be `nil`") + if MagazineLayout._enableExperimentalOptimizations { + _currentCollectionView ?? collectionView! + } else { + collectionView! } + } - return collectionView + @inline(__always) + private var delegateMagazineLayout: UICollectionViewDelegateMagazineLayout? { + if MagazineLayout._enableExperimentalOptimizations { + _delegateMagazineLayout + } else { + currentCollectionView.delegate as? UICollectionViewDelegateMagazineLayout + } } // Used to provide the model state with the current visible bounds for the sole purpose of @@ -961,10 +981,6 @@ public final class MagazineLayout: UICollectionViewLayout { height: currentCollectionView.bounds.height - contentInset.top - contentInset.bottom + refreshControlHeight) } - private var delegateMagazineLayout: UICollectionViewDelegateMagazineLayout? { - return currentCollectionView.delegate as? UICollectionViewDelegateMagazineLayout - } - private var scale: CGFloat { collectionView?.traitCollection.nonZeroDisplayScale ?? 1 } @@ -973,7 +989,16 @@ public final class MagazineLayout: UICollectionViewLayout { currentCollectionView.adjustedContentInset } - private var layoutState: LayoutState { + private var modelState: ModelState { + if MagazineLayout._enableExperimentalOptimizations { + _layoutState.modelState + } else { + updatedLayoutState().modelState + } + } + + /// Relatively expensive compared to just grabbing the `_layoutState`; only use if you need updated metrics. + private func updatedLayoutState() -> LayoutState { _layoutState.bounds = currentCollectionView.bounds _layoutState.contentInset = contentInset _layoutState.scale = scale @@ -981,10 +1006,6 @@ public final class MagazineLayout: UICollectionViewLayout { return _layoutState } - private var modelState: ModelState { - layoutState.modelState - } - private func metricsForSection(atIndex sectionIndex: Int) -> MagazineLayoutSectionMetrics { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayoutSectionMetrics.defaultSectionMetrics( From 36314b5e38128c4b1548147f97637a5080259442 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Fri, 5 Jun 2026 16:36:37 -0700 Subject: [PATCH 5/8] Optimize IndexPath allocations Fix index paths --- MagazineLayout/Public/MagazineLayout.swift | 58 +++++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index b11df51..97668d4 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -97,12 +97,18 @@ public final class MagazineLayout: UICollectionViewLayout { hasPinnedHeaderOrFooter = false } + var reusableIndexPath = IndexPath(item: 0, section: 0) + // Update layout metrics if necessary if prepareActions.contains(.updateLayoutMetrics) { let signpostID = OSSignpostID(log: signpostLog) os_signpost(.begin, log: signpostLog, name: SignpostName.prepareUpdateLayoutMetrics, signpostID: signpostID) for sectionIndex in 0..]() + var reusableIndexPath = IndexPath(item: 0, section: 0) for updateItem in updateItems { let updateAction = updateItem.updateAction @@ -189,7 +207,12 @@ public final class MagazineLayout: UICollectionViewLayout { } if indexPath.item == NSNotFound { - let sectionModel = sectionModelForSection(atIndex: indexPath.section) + if Self._enableExperimentalOptimizations { + reusableIndexPath.section = indexPath.section + } + let sectionModel = sectionModelForSection( + atIndex: indexPath.section, + reusableIndexPath: &reusableIndexPath) updates.append(.sectionReload(sectionIndex: indexPath.section, newSection: sectionModel)) } else { let itemModel = itemModelForItem(at: indexPath) @@ -217,7 +240,12 @@ public final class MagazineLayout: UICollectionViewLayout { } if indexPath.item == NSNotFound { - let sectionModel = sectionModelForSection(atIndex: indexPath.section) + if Self._enableExperimentalOptimizations { + reusableIndexPath.section = indexPath.section + } + let sectionModel = sectionModelForSection( + atIndex: indexPath.section, + reusableIndexPath: &reusableIndexPath) updates.append(.sectionInsert(sectionIndex: indexPath.section, newSection: sectionModel)) } else { let itemModel = itemModelForItem(at: indexPath) @@ -1110,9 +1138,23 @@ public final class MagazineLayout: UICollectionViewLayout { } } - private func sectionModelForSection(atIndex sectionIndex: Int) -> SectionModel { - let itemModels = (0.. SectionModel + { + let numberOfItems = currentCollectionView.numberOfItems(inSection: sectionIndex) + var itemModels = [ItemModel]() + itemModels.reserveCapacity(numberOfItems) + + for itemIndex in 0.. Date: Sat, 6 Jun 2026 17:47:58 -0700 Subject: [PATCH 6/8] Optimize prepare when just updating widths --- MagazineLayout/Public/MagazineLayout.swift | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 97668d4..1525a9c 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -99,6 +99,19 @@ public final class MagazineLayout: UICollectionViewLayout { var reusableIndexPath = IndexPath(item: 0, section: 0) + // Update widths if necessary (e.g. after rotation or other bounds change) + if prepareActions.contains(.updateWidths) { + let signpostID = OSSignpostID(log: signpostLog) + os_signpost(.begin, log: signpostLog, name: SignpostName.prepareUpdateWidths, signpostID: signpostID) + + for sectionIndex in 0.. Date: Fri, 5 Jun 2026 20:15:22 -0700 Subject: [PATCH 7/8] Defer SectionModel calculateElementFramesIfNecessary --- MagazineLayout/LayoutCore/SectionModel.swift | 25 ++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/MagazineLayout/LayoutCore/SectionModel.swift b/MagazineLayout/LayoutCore/SectionModel.swift index 404ae81..8b780ed 100755 --- a/MagazineLayout/LayoutCore/SectionModel.swift +++ b/MagazineLayout/LayoutCore/SectionModel.swift @@ -38,7 +38,10 @@ struct SectionModel { numberOfRows = 0 updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0) - calculateElementFramesIfNecessary() + + if !MagazineLayout._enableExperimentalOptimizations { + calculateElementFramesIfNecessary() + } } // MARK: Internal @@ -257,15 +260,22 @@ struct SectionModel { } mutating func removeFooter() -> Bool { - guard let indexOfFooter = indexOfFooterRow() else { + guard footerModel != nil else { return false } - updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter) + // `indexOfFooterRow()` is `nil` if the section hasn't been laid out yet (deferred layout + // calculation). In that case the whole section is already invalidated from row 0, so there's no + // additional row to invalidate. + if let indexOfFooter = indexOfFooterRow() { + updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter) + } footerModel = nil return true } mutating func updateItemHeight(toPreferredHeight preferredHeight: CGFloat, atIndex index: Int) { + calculateElementFramesIfNecessary() + // Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes // and Swift retain / release calls. itemModels.withUnsafeMutableBufferPointer { directlyMutableItemModels in @@ -292,6 +302,8 @@ struct SectionModel { } mutating func updateHeaderHeight(toPreferredHeight preferredHeight: CGFloat) { + calculateElementFramesIfNecessary() + headerModel?.preferredHeight = preferredHeight if let indexOfHeaderRow = indexOfHeaderRow(), let headerModel = headerModel { @@ -312,6 +324,8 @@ struct SectionModel { } mutating func updateFooterHeight(toPreferredHeight preferredHeight: CGFloat) { + calculateElementFramesIfNecessary() + footerModel?.preferredHeight = preferredHeight if let indexOfFooterRow = indexOfFooterRow(), let footerModel = footerModel { @@ -393,7 +407,10 @@ struct SectionModel { } private func indexOfFooterRow() -> Int? { - guard footerModel != nil else { return nil } + // `numberOfRows` is 0 until the section's element frames have been calculated. With deferred + // layout calculation, the footer's row index isn't known yet in that state, so we return `nil` + // rather than a bogus `numberOfRows - 1` (which would be -1). + guard footerModel != nil, numberOfRows > 0 else { return nil } return numberOfRows - 1 } From 34e3b900434b6e157baf0f036a0389367b963f6c Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Mon, 8 Jun 2026 13:41:20 -0700 Subject: [PATCH 8/8] Separate file for signposting --- MagazineLayout.xcodeproj/project.pbxproj | 4 +++ .../LayoutCore/Types/Signposting.swift | 33 +++++++++++++++++++ MagazineLayout/Public/MagazineLayout.swift | 17 ---------- 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 MagazineLayout/LayoutCore/Types/Signposting.swift diff --git a/MagazineLayout.xcodeproj/project.pbxproj b/MagazineLayout.xcodeproj/project.pbxproj index 2f49eff..e5ce755 100644 --- a/MagazineLayout.xcodeproj/project.pbxproj +++ b/MagazineLayout.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 93A1C04E21ACED1100DED67D /* ElementLocationFramePairsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A1C00F21ACED0100DED67D /* ElementLocationFramePairsTests.swift */; }; 93A1C04F21ACED1100DED67D /* ModelStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A1C01021ACED0100DED67D /* ModelStateUpdateTests.swift */; }; 93A868552EE0D7870027691E /* IDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A868542EE0D7870027691E /* IDGenerator.swift */; }; + 93F8DAE62FD76026005325C0 /* Signposting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93F8DAE52FD76026005325C0 /* Signposting.swift */; }; FCAC642622085AF100973F4C /* MagazineLayoutHeaderVisibilityMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCAC642522085AF100973F4C /* MagazineLayoutHeaderVisibilityMode.swift */; }; FCAC642822085B0E00973F4C /* FooterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCAC642722085B0E00973F4C /* FooterModel.swift */; }; FD244FEF28B41F9900046C0D /* UITraitCollection+DisplayScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD244FEE28B41F9900046C0D /* UITraitCollection+DisplayScale.swift */; }; @@ -96,6 +97,7 @@ 93A1C05321ACEDFC00DED67D /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; 93A1C05421ACEDFC00DED67D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 93A868542EE0D7870027691E /* IDGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDGenerator.swift; sourceTree = ""; }; + 93F8DAE52FD76026005325C0 /* Signposting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposting.swift; sourceTree = ""; }; FCAC642522085AF100973F4C /* MagazineLayoutHeaderVisibilityMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagazineLayoutHeaderVisibilityMode.swift; sourceTree = ""; }; FCAC642722085B0E00973F4C /* FooterModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FooterModel.swift; sourceTree = ""; }; FD23F5F021AF4A1B00AA78D4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -221,6 +223,7 @@ 93A1C01F21ACED0100DED67D /* Types */ = { isa = PBXGroup; children = ( + 93F8DAE52FD76026005325C0 /* Signposting.swift */, 93A1C02221ACED0100DED67D /* MagazineLayoutSectionMetrics.swift */, 93A1C02621ACED0100DED67D /* MagazineLayoutItemWidthMode+WidthDivisor.swift */, 93A1C02321ACED0100DED67D /* CollectionViewUpdateItem.swift */, @@ -365,6 +368,7 @@ 93A1C03521ACED0100DED67D /* MagazineLayoutBackgroundVisibilityMode.swift in Sources */, 93A1C04421ACED0100DED67D /* SectionModel.swift in Sources */, 93A1C03E21ACED0100DED67D /* ElementLocation.swift in Sources */, + 93F8DAE62FD76026005325C0 /* Signposting.swift in Sources */, 93A1C04821ACED0100DED67D /* ModelState.swift in Sources */, 93A868552EE0D7870027691E /* IDGenerator.swift in Sources */, 9398462A2296864200E442DA /* RowOffsetTracker.swift in Sources */, diff --git a/MagazineLayout/LayoutCore/Types/Signposting.swift b/MagazineLayout/LayoutCore/Types/Signposting.swift new file mode 100644 index 0000000..ffcfa3a --- /dev/null +++ b/MagazineLayout/LayoutCore/Types/Signposting.swift @@ -0,0 +1,33 @@ +// Created by Bryan Keller on 6/8/26. +// Copyright © 2026 Airbnb Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import os + +// MARK: - Signposting + +let signpostLog = OSLog(subsystem: "com.airbnb.MagazineLayout", category: "MagazineLayout") + +enum SignpostName { + static let collectionViewContentSize: StaticString = "MagazineLayout.collectionViewContentSize" + static let prepare: StaticString = "MagazineLayout.prepare" + static let prepareUpdateWidths: StaticString = "MagazineLayout.prepare.prepareUpdateWidths" + static let prepareUpdateLayoutMetrics: StaticString = "MagazineLayout.prepare.prepareUpdateLayoutMetrics" + static let prepareRecreateSectionModels: StaticString = "MagazineLayout.prepare.recreateSectionModels" + static let layoutAttributesForElementsInRect: StaticString = "MagazineLayout.layoutAttributesForElementsInRect" + static let prepareForCollectionViewUpdates: StaticString = "MagazineLayout.prepareForCollectionViewUpdates" + static let invalidateLayout: StaticString = "MagazineLayout.invalidateLayout" + static let preferredLayoutAttributesFittingCell: StaticString = "MagazineLayout.preferredLayoutAttributesFitting.cell" + static let preferredLayoutAttributesFittingReusableView: StaticString = "MagazineLayout.preferredLayoutAttributesFitting.reusableView" +} diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 1525a9c..ed79328 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -1532,20 +1532,3 @@ private extension MagazineLayout { } } - -// MARK: - Signposting - -let signpostLog = OSLog(subsystem: "com.airbnb.MagazineLayout", category: "MagazineLayout") - -enum SignpostName { - static let collectionViewContentSize: StaticString = "MagazineLayout.collectionViewContentSize" - static let prepare: StaticString = "MagazineLayout.prepare" - static let prepareUpdateWidths: StaticString = "MagazineLayout.prepare.prepareUpdateWidths" - static let prepareUpdateLayoutMetrics: StaticString = "MagazineLayout.prepare.prepareUpdateLayoutMetrics" - static let prepareRecreateSectionModels: StaticString = "MagazineLayout.prepare.recreateSectionModels" - static let layoutAttributesForElementsInRect: StaticString = "MagazineLayout.layoutAttributesForElementsInRect" - static let prepareForCollectionViewUpdates: StaticString = "MagazineLayout.prepareForCollectionViewUpdates" - static let invalidateLayout: StaticString = "MagazineLayout.invalidateLayout" - static let preferredLayoutAttributesFittingCell: StaticString = "MagazineLayout.preferredLayoutAttributesFitting.cell" - static let preferredLayoutAttributesFittingReusableView: StaticString = "MagazineLayout.preferredLayoutAttributesFitting.reusableView" -}