Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Example/MagazineLayoutExample/RootMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
4 changes: 2 additions & 2 deletions MagazineLayout.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 8 additions & 4 deletions MagazineLayout.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
93F8DAE52FD76026005325C0 /* Signposting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposting.swift; sourceTree = "<group>"; };
FCAC642522085AF100973F4C /* MagazineLayoutHeaderVisibilityMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagazineLayoutHeaderVisibilityMode.swift; sourceTree = "<group>"; };
FCAC642722085B0E00973F4C /* FooterModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FooterModel.swift; sourceTree = "<group>"; };
FD23F5F021AF4A1B00AA78D4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -221,6 +223,7 @@
93A1C01F21ACED0100DED67D /* Types */ = {
isa = PBXGroup;
children = (
93F8DAE52FD76026005325C0 /* Signposting.swift */,
93A1C02221ACED0100DED67D /* MagazineLayoutSectionMetrics.swift */,
93A1C02621ACED0100DED67D /* MagazineLayoutItemWidthMode+WidthDivisor.swift */,
93A1C02321ACED0100DED67D /* CollectionViewUpdateItem.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -468,7 +472,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;
Expand All @@ -477,7 +481,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 = "";
};
Expand Down Expand Up @@ -530,15 +534,15 @@
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";
SWIFT_COMPILATION_MODE = wholemodule;
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 = "";
Expand Down
25 changes: 21 additions & 4 deletions MagazineLayout/LayoutCore/SectionModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ struct SectionModel {
numberOfRows = 0

updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0)
calculateElementFramesIfNecessary()

if !MagazineLayout._enableExperimentalOptimizations {
calculateElementFramesIfNecessary()
}
}

// MARK: Internal
Expand Down Expand Up @@ -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
Expand All @@ -292,6 +302,8 @@ struct SectionModel {
}

mutating func updateHeaderHeight(toPreferredHeight preferredHeight: CGFloat) {
calculateElementFramesIfNecessary()

headerModel?.preferredHeight = preferredHeight

if let indexOfHeaderRow = indexOfHeaderRow(), let headerModel = headerModel {
Expand All @@ -312,6 +324,8 @@ struct SectionModel {
}

mutating func updateFooterHeight(toPreferredHeight preferredHeight: CGFloat) {
calculateElementFramesIfNecessary()

footerModel?.preferredHeight = preferredHeight

if let indexOfFooterRow = indexOfFooterRow(), let footerModel = footerModel {
Expand Down Expand Up @@ -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
}

Expand Down
33 changes: 33 additions & 0 deletions MagazineLayout/LayoutCore/Types/Signposting.swift
Original file line number Diff line number Diff line change
@@ -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"
}
Loading
Loading