Skip to content

Commit a682bda

Browse files
Adjust content offset when adjusted content insets change (#136)
* Adjust content offset when bottom inset changes * Add offset adjustment behavior to top and bottom * Add unit tests * Remove duplicate header * Update comment --------- Co-authored-by: Bryn Bodayle <[email protected]>
1 parent 28b7b44 commit a682bda

File tree

4 files changed

+105
-3
lines changed

4 files changed

+105
-3
lines changed

MagazineLayout.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
60432D952E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */; };
1011
9332FB0822969B5600483D99 /* RowOffsetTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */; };
1112
93424B012256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93424B002256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift */; };
1213
93540AB0282E25D90008BD6F /* ScreenPixelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93540AAF282E25D90008BD6F /* ScreenPixelAlignment.swift */; };
@@ -58,6 +59,7 @@
5859
/* End PBXContainerItemProxy section */
5960

6061
/* Begin PBXFileReference section */
62+
60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentInsetAdjustingContentOffsetTests.swift; sourceTree = "<group>"; };
6163
9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowOffsetTrackerTests.swift; sourceTree = "<group>"; };
6264
93424B002256878B003D00C0 /* MagazineLayoutFooterVisibilityMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagazineLayoutFooterVisibilityMode.swift; sourceTree = "<group>"; };
6365
93540AAF282E25D90008BD6F /* ScreenPixelAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenPixelAlignment.swift; sourceTree = "<group>"; };
@@ -156,6 +158,7 @@
156158
9332FB0622969AB200483D99 /* RowOffsetTrackerTests.swift */,
157159
93A1C00E21ACED0100DED67D /* TestingSupport.swift */,
158160
93540AB1282E26340008BD6F /* ScreenPixelAlignmentTests.swift */,
161+
60432D942E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift */,
159162
);
160163
path = Tests;
161164
sourceTree = "<group>";
@@ -382,6 +385,7 @@
382385
files = (
383386
FDE08E162B2CC47800C9D24D /* TargetContentOffsetAnchorTests.swift in Sources */,
384387
93A1C04B21ACED1100DED67D /* ModelStateInitiallSetUpTests.swift in Sources */,
388+
60432D952E05DB41001728F0 /* ContentInsetAdjustingContentOffsetTests.swift in Sources */,
385389
9332FB0822969B5600483D99 /* RowOffsetTrackerTests.swift in Sources */,
386390
93A1C04C21ACED1100DED67D /* ModelStateLayoutTests.swift in Sources */,
387391
93A1C04921ACED1100DED67D /* ModelStateEmptySectionLayoutTests.swift in Sources */,

MagazineLayout/Public/MagazineLayout.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,22 @@ public final class MagazineLayout: UICollectionViewLayout {
787787
return
788788
}
789789

790+
// If our layout direction is `bottomToTop`, allow changes to the top and bottom content insets
791+
// to automatically adjust the content offset. `UICollectionView` behaves this way by default
792+
// when the top content inset changes, so this adds the same behavior.
793+
if
794+
case .bottomToTop = verticalLayoutDirection,
795+
let previousContentInset
796+
{
797+
if previousContentInset.top != contentInset.top {
798+
context.contentOffsetAdjustment.y += contentInset.top - previousContentInset.top
799+
}
800+
if previousContentInset.bottom != contentInset.bottom {
801+
context.contentOffsetAdjustment.y += contentInset.bottom - previousContentInset.bottom
802+
}
803+
}
804+
previousContentInset = contentInset
805+
790806
let shouldInvalidateLayoutMetrics = !context.invalidateEverything &&
791807
!context.invalidateDataSourceCounts
792808

@@ -899,6 +915,7 @@ public final class MagazineLayout: UICollectionViewLayout {
899915
private var isPerformingAnimatedBoundsChange = false
900916
private var targetContentOffsetAnchor: TargetContentOffsetAnchor?
901917
private var stagedContentOffsetAdjustment: CGPoint?
918+
private var previousContentInset: UIEdgeInsets?
902919

903920
// Used to provide the model state with the current visible bounds for the sole purpose of
904921
// supporting pinned headers and footers.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Created by Bryn Bodayle on 6/20/25.
2+
// Copyright © 2025 Airbnb Inc. All rights reserved.
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import XCTest
17+
@testable import MagazineLayout
18+
19+
final class ContentInsetAdjustingContentOffsetTests: XCTestCase {
20+
21+
func testContentOffsetIsNotAdjustedForTopInsetChangeWithToTopBottomLayout() {
22+
let layout = MagazineLayout(verticalLayoutDirection: .topToBottom)
23+
let collectionView = StubCollectionView(
24+
frame: .zero,
25+
collectionViewLayout: layout)
26+
let context = MagazineLayoutInvalidationContext()
27+
layout.invalidateLayout(with: context)
28+
XCTAssertEqual(context.contentOffsetAdjustment, .zero)
29+
30+
collectionView.stubAdjustedContentInset = .init(top: 50, left: 0, bottom: 50, right: 0)
31+
layout.invalidateLayout(with: context)
32+
XCTAssertEqual(context.contentOffsetAdjustment, .zero)
33+
}
34+
35+
func testContentOffsetIsAdjustedForTopInsetChangeWithBottomToTopLayout() {
36+
let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop)
37+
let collectionView = StubCollectionView(
38+
frame: .zero,
39+
collectionViewLayout: layout)
40+
let context = MagazineLayoutInvalidationContext()
41+
layout.invalidateLayout(with: context)
42+
XCTAssertEqual(context.contentOffsetAdjustment, .zero)
43+
44+
collectionView.stubAdjustedContentInset = .init(top: 50, left: 0, bottom: 0, right: 0)
45+
layout.invalidateLayout(with: context)
46+
XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 50))
47+
}
48+
49+
func testContentOffsetIsAdjustedForBottomInsetChangeWithBottomToTopLayout() {
50+
let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop)
51+
let collectionView = StubCollectionView(
52+
frame: .zero,
53+
collectionViewLayout: layout)
54+
let context = MagazineLayoutInvalidationContext()
55+
layout.invalidateLayout(with: context)
56+
XCTAssertEqual(context.contentOffsetAdjustment, .zero)
57+
58+
collectionView.stubAdjustedContentInset = .init(top: 0, left: 0, bottom: 75, right: 0)
59+
layout.invalidateLayout(with: context)
60+
XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 75))
61+
}
62+
63+
func testContentOffsetIsAdjustedForTopAndBottomInsetChangesWithBottomToTopLayout() {
64+
let layout = MagazineLayout(verticalLayoutDirection: .bottomToTop)
65+
let collectionView = StubCollectionView(
66+
frame: .zero,
67+
collectionViewLayout: layout)
68+
let context = MagazineLayoutInvalidationContext()
69+
layout.invalidateLayout(with: context)
70+
XCTAssertEqual(context.contentOffsetAdjustment, .zero)
71+
72+
collectionView.stubAdjustedContentInset = .init(top: 100, left: 0, bottom: 100, right: 0)
73+
layout.invalidateLayout(with: context)
74+
XCTAssertEqual(context.contentOffsetAdjustment, .init(x: 0, y: 200))
75+
}
76+
}
77+
78+
private class StubCollectionView: UICollectionView {
79+
80+
var stubAdjustedContentInset: UIEdgeInsets = .zero
81+
override var adjustedContentInset: UIEdgeInsets {
82+
stubAdjustedContentInset
83+
}
84+
}

Tests/ScreenPixelAlignmentTests.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Created by Bryan Keller on 5/12/22.
22
// Copyright © 2022 Airbnb Inc. All rights reserved.
33

4-
// Created by Bryan Keller on 3/31/20.
5-
// Copyright © 2020 Airbnb Inc. All rights reserved.
6-
74
// Licensed under the Apache License, Version 2.0 (the "License");
85
// you may not use this file except in compliance with the License.
96
// You may obtain a copy of the License at

0 commit comments

Comments
 (0)