From 06cb140bd19da43e08f19e29411619ac8f71bb59 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Tue, 10 Jun 2025 16:22:09 -0700 Subject: [PATCH 1/5] Adjust content offset when bottom inset changes --- MagazineLayout/Public/MagazineLayout.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 14132f4..2d1b816 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -787,6 +787,19 @@ public final class MagazineLayout: UICollectionViewLayout { return } + // If our layout direction is `bottomToTop`, allow changes to the bottom content inset to + // automatically adjust the content offset. `UICollectionView` behaves this way by default when + // the top content inset changes, so this adds the same behavior to the bottom. + let bottomContentInset = contentInset.bottom + if + case .bottomToTop = verticalLayoutDirection, + let previousBottomContentInset, + previousBottomContentInset != bottomContentInset + { + context.contentOffsetAdjustment.y = bottomContentInset - previousBottomContentInset + } + previousBottomContentInset = bottomContentInset + let shouldInvalidateLayoutMetrics = !context.invalidateEverything && !context.invalidateDataSourceCounts @@ -899,6 +912,7 @@ public final class MagazineLayout: UICollectionViewLayout { private var isPerformingAnimatedBoundsChange = false private var targetContentOffsetAnchor: TargetContentOffsetAnchor? private var stagedContentOffsetAdjustment: CGPoint? + private var previousBottomContentInset: CGFloat? // Used to provide the model state with the current visible bounds for the sole purpose of // supporting pinned headers and footers. From 5b3285b14eab900041710fab22a7abd64413b396 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 20 Jun 2025 13:00:55 -0700 Subject: [PATCH 2/5] Add offset adjustment behavior to top and bottom --- MagazineLayout/Public/MagazineLayout.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 2d1b816..ab80400 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -787,18 +787,21 @@ public final class MagazineLayout: UICollectionViewLayout { return } - // If our layout direction is `bottomToTop`, allow changes to the bottom content inset to - // automatically adjust the content offset. `UICollectionView` behaves this way by default when - // the top content inset changes, so this adds the same behavior to the bottom. - let bottomContentInset = contentInset.bottom + // If our layout direction is `bottomToTop`, allow changes to the top and bottom content insets + // to automatically adjust the content offset. `UICollectionView` behaves this way by default + // when the top content inset changes, so this adds the same behavior to the bottom. if case .bottomToTop = verticalLayoutDirection, - let previousBottomContentInset, - previousBottomContentInset != bottomContentInset + let previousContentInset { - context.contentOffsetAdjustment.y = bottomContentInset - previousBottomContentInset + if previousContentInset.top != contentInset.top { + context.contentOffsetAdjustment.y += contentInset.top - previousContentInset.top + } + if previousContentInset.bottom != contentInset.bottom { + context.contentOffsetAdjustment.y += contentInset.bottom - previousContentInset.bottom + } } - previousBottomContentInset = bottomContentInset + previousContentInset = contentInset let shouldInvalidateLayoutMetrics = !context.invalidateEverything && !context.invalidateDataSourceCounts @@ -912,7 +915,7 @@ public final class MagazineLayout: UICollectionViewLayout { private var isPerformingAnimatedBoundsChange = false private var targetContentOffsetAnchor: TargetContentOffsetAnchor? private var stagedContentOffsetAdjustment: CGPoint? - private var previousBottomContentInset: CGFloat? + private var previousContentInset: UIEdgeInsets? // Used to provide the model state with the current visible bounds for the sole purpose of // supporting pinned headers and footers. From 841472d8b7d2241f536c9805dee9cc95a941d2a3 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 20 Jun 2025 13:01:16 -0700 Subject: [PATCH 3/5] Add unit tests --- MagazineLayout.xcodeproj/project.pbxproj | 4 + ...tentInsetAdjustingContentOffsetTests.swift | 84 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 Tests/ContentInsetAdjustingContentOffsetTests.swift diff --git a/MagazineLayout.xcodeproj/project.pbxproj b/MagazineLayout.xcodeproj/project.pbxproj index 2b03d6d..0b6f69c 100644 --- a/MagazineLayout.xcodeproj/project.pbxproj +++ b/MagazineLayout.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 60432D952E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */; }; 9332FB0822969B5600483D99 /* RowOffsetTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */; }; 93424B012256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93424B002256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift */; }; 93540AB0282E25D90008BD6F /* ScreenPixelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93540AAF282E25D90008BD6F /* ScreenPixelAlignment.swift */; }; @@ -58,6 +59,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentInsetAdjustingContentOffsetTests.swift; sourceTree = ""; }; 9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowOffsetTrackerTests.swift; sourceTree = ""; }; 93424B002256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagazineLayoutFooterVisibilityMode.swift; sourceTree = ""; }; 93540AAF282E25D90008BD6F /* ScreenPixelAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenPixelAlignment.swift; sourceTree = ""; }; @@ -156,6 +158,7 @@ 9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */, 93A1C00E21ACED0100DED67D /* TestingSupport.swift */, 93540AB1282E26340008BD6F /* ScreenPixelAlignmentTests.swift */, + 60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */, ); path = Tests; sourceTree = ""; @@ -382,6 +385,7 @@ files = ( FDE08E162B2CC47800C9D24D /* TargetContentOffsetAnchorTests.swift in Sources */, 93A1C04B21ACED1100DED67D /* ModelStateInitiallSetUpTests.swift in Sources */, + 60432D952E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift in Sources */, 9332FB0822969B5600483D99 /* RowOffsetTrackerTests.swift in Sources */, 93A1C04C21ACED1100DED67D /* ModelStateLayoutTests.swift in Sources */, 93A1C04921ACED1100DED67D /* ModelStateEmptySectionLayoutTests.swift in Sources */, diff --git a/Tests/ContentInsetAdjustingContentOffsetTests.swift b/Tests/ContentInsetAdjustingContentOffsetTests.swift new file mode 100644 index 0000000..71da29f --- /dev/null +++ b/Tests/ContentInsetAdjustingContentOffsetTests.swift @@ -0,0 +1,84 @@ +// Created by Bryn Bodayle on 6/20/25. +// Copyright © 2025 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 XCTest +@testable import MagazineLayout + +final class ContentInsetAdjustingContentOffsetTests: XCTestCase { + + func testContentOffsetIsNotAdjustedForTopInsetChangeWithToTopBottomLayout() { + let layout = MagazineLayout(verticalLayoutDirection: .topToBottom) + let collectionView = StubCollectionView( + frame: .zero, + collectionViewLayout: layout) + let context = MagazineLayoutInvalidationContext() + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .zero) + + collectionView.stubAdjustedContentInset = .init(top: 50, left: 0, bottom: 50, right: 0) + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .zero) + } + + func testContentOffsetIsAdjustedForTopInsetChangeWithBottomToTopLayout() { + let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop) + let collectionView = StubCollectionView( + frame: .zero, + collectionViewLayout: layout) + let context = MagazineLayoutInvalidationContext() + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .zero) + + collectionView.stubAdjustedContentInset = .init(top: 50, left: 0, bottom: 0, right: 0) + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 50)) + } + + func testContentOffsetIsAdjustedForBottomInsetChangeWithBottomToTopLayout() { + let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop) + let collectionView = StubCollectionView( + frame: .zero, + collectionViewLayout: layout) + let context = MagazineLayoutInvalidationContext() + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .zero) + + collectionView.stubAdjustedContentInset = .init(top: 0, left: 0, bottom: 75, right: 0) + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 75)) + } + + func testContentOffsetIsAdjustedForTopAndBottomInsetChangesWithBottomToTopLayout() { + let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop) + let collectionView = StubCollectionView( + frame: .zero, + collectionViewLayout: layout) + let context = MagazineLayoutInvalidationContext() + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .zero) + + collectionView.stubAdjustedContentInset = .init(top: 100, left: 0, bottom: 100, right: 0) + layout.invalidateLayout(with: context) + XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 200)) + } +} + +private class StubCollectionView: UICollectionView { + + var stubAdjustedContentInset: UIEdgeInsets = .zero + override var adjustedContentInset: UIEdgeInsets { + stubAdjustedContentInset + } +} From 352b66fd6d3ff1b39b6fbfb40a8cbd1948ed1077 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 20 Jun 2025 13:01:21 -0700 Subject: [PATCH 4/5] Remove duplicate header --- Tests/ScreenPixelAlignmentTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/ScreenPixelAlignmentTests.swift b/Tests/ScreenPixelAlignmentTests.swift index 6c11974..10061df 100644 --- a/Tests/ScreenPixelAlignmentTests.swift +++ b/Tests/ScreenPixelAlignmentTests.swift @@ -1,9 +1,6 @@ // Created by Bryan Keller on 5/12/22. // Copyright © 2022 Airbnb Inc. All rights reserved. -// Created by Bryan Keller on 3/31/20. -// Copyright © 2020 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 From b898fd280b489d01c13e92a4c473b50881793464 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 20 Jun 2025 13:02:37 -0700 Subject: [PATCH 5/5] Update comment --- MagazineLayout/Public/MagazineLayout.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index ab80400..4d5bc45 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -789,7 +789,7 @@ public final class MagazineLayout: UICollectionViewLayout { // If our layout direction is `bottomToTop`, allow changes to the top and bottom content insets // to automatically adjust the content offset. `UICollectionView` behaves this way by default - // when the top content inset changes, so this adds the same behavior to the bottom. + // when the top content inset changes, so this adds the same behavior. if case .bottomToTop = verticalLayoutDirection, let previousContentInset