From d78aeb9f6d70dd5e5785cba4bd5e2d4bb0847d6d Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Mon, 17 Nov 2025 14:22:01 -0800 Subject: [PATCH 1/3] Fix target content offset anchor calculation --- MagazineLayout/LayoutCore/LayoutState.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MagazineLayout/LayoutCore/LayoutState.swift b/MagazineLayout/LayoutCore/LayoutState.swift index 69b88f5..58c9d6c 100644 --- a/MagazineLayout/LayoutCore/LayoutState.swift +++ b/MagazineLayout/LayoutCore/LayoutState.swift @@ -64,6 +64,8 @@ struct LayoutState { var targetContentOffsetAnchor: TargetContentOffsetAnchor { var visibleItemLocationFramePairs = [ElementLocationFramePair]() for itemLocationFramePair in modelState.itemLocationFramePairs(forItemsIn: bounds) { + // Only consider fully-visible items + guard bounds.contains(itemLocationFramePair.frame) else { continue } visibleItemLocationFramePairs.append(itemLocationFramePair) } visibleItemLocationFramePairs.sort { $0.elementLocation < $1.elementLocation } @@ -74,7 +76,7 @@ struct LayoutState { modelState.isItemHeightSettled(indexPath: $0.elementLocation.indexPath) } ?? visibleItemLocationFramePairs.first // fallback to the first item if we can't find one with a settled height - let lastVisibleItemLocationFramePair = visibleItemLocationFramePairs.reversed().first { + let lastVisibleItemLocationFramePair = visibleItemLocationFramePairs.last { // When scrolling down, only calculate a target content offset based on visible, already-sized // cells. Otherwise, scrolling will be jumpy. modelState.isItemHeightSettled(indexPath: $0.elementLocation.indexPath) From 68668392b3bb1ecd8166f08fb32282bf6a3d0524 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Mon, 17 Nov 2025 14:23:04 -0800 Subject: [PATCH 2/3] Example project tweaks --- .../GridDemoViewController.swift | 18 +----------------- .../ListDemoViewController.swift | 18 +----------------- .../MessageThreadDemoViewController.swift | 4 +--- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/Example/MagazineLayoutExample/GridDemoViewController.swift b/Example/MagazineLayoutExample/GridDemoViewController.swift index ddf3454..51cbc37 100644 --- a/Example/MagazineLayoutExample/GridDemoViewController.swift +++ b/Example/MagazineLayoutExample/GridDemoViewController.swift @@ -291,23 +291,7 @@ extension GridDemoViewController: UICollectionViewDelegateMagazineLayout { private struct GridItem: Hashable { - // MARK: Lifecycle - - init( - id: UUID = UUID(), - text: String, - color: UIColor, - widthMode: MagazineLayoutItemWidthMode) - { - self.id = id - self.text = text - self.color = color - self.widthMode = widthMode - } - - // MARK: Internal - - let id: UUID + let id = UUID() let text: String let color: UIColor let widthMode: MagazineLayoutItemWidthMode diff --git a/Example/MagazineLayoutExample/ListDemoViewController.swift b/Example/MagazineLayoutExample/ListDemoViewController.swift index 2e33247..62c6552 100644 --- a/Example/MagazineLayoutExample/ListDemoViewController.swift +++ b/Example/MagazineLayoutExample/ListDemoViewController.swift @@ -383,23 +383,7 @@ private struct ListSection: Hashable { private struct ListItem: Hashable { - // MARK: Lifecycle - - init( - id: UUID = UUID(), - title: String, - subtitle: String, - color: UIColor) - { - self.id = id - self.title = title - self.subtitle = subtitle - self.color = color - } - - // MARK: Internal - - let id: UUID + let id = UUID() let title: String let subtitle: String let color: UIColor diff --git a/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift b/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift index bd0c8d2..d4bd105 100644 --- a/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift +++ b/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift @@ -302,12 +302,10 @@ private struct Message: Hashable { // MARK: Lifecycle init( - id: UUID = UUID(), text: String, isSent: Bool, timestamp: Date = Date()) { - self.id = id self.text = text self.isSent = isSent self.timestamp = timestamp @@ -315,7 +313,7 @@ private struct Message: Hashable { // MARK: Internal - let id: UUID + let id = UUID() let text: String let isSent: Bool let timestamp: Date From dcde31868f41e4cff781266dbf98f6384b5fe4b2 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Mon, 17 Nov 2025 16:11:39 -0800 Subject: [PATCH 3/3] Fix tests --- Tests/LayoutStateTargetContentOffsetTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/LayoutStateTargetContentOffsetTests.swift b/Tests/LayoutStateTargetContentOffsetTests.swift index e2cc1bf..7367ce9 100644 --- a/Tests/LayoutStateTargetContentOffsetTests.swift +++ b/Tests/LayoutStateTargetContentOffsetTests.swift @@ -40,8 +40,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, verticalLayoutDirection: .topToBottom) - let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 5, section: 0))! - XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, distanceFromTop: -160)) + let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 6, section: 0))! + XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, distanceFromTop: -25)) } func testAnchor_TopToBottom_ScrolledToBottom() throws { @@ -61,8 +61,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { contentInset: measurementLayoutState.contentInset, scale: measurementLayoutState.scale, verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection) - let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 7, section: 0))! - XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, distanceFromTop: -80)) + let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 9, section: 0))! + XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, distanceFromTop: 25)) } // MARK: Bottom-to-Top Anchor Tests @@ -87,8 +87,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, verticalLayoutDirection: .bottomToTop) - let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 12, section: 0))! - XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem(id: id, distanceFromBottom: 190)) + let id = layoutState.modelState.idForItemModel(at: IndexPath(item: 10, section: 0))! + XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem(id: id, distanceFromBottom: -10)) } func testAnchor_BottomToTop_ScrolledToBottom() throws {