Skip to content

Commit

Permalink
Merge pull request #9 from ra1028/keepsContentOffset
Browse files Browse the repository at this point in the history
Add new updater option keepsContentOffset
  • Loading branch information
ra1028 committed Mar 28, 2019
2 parents 9acc419 + a5ed570 commit a9a8d5e
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Carbon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
6B7ADEF221FDB12B003803BE /* UICollectionViewComponentCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7ADEEE21FDB12A003803BE /* UICollectionViewComponentCellTests.swift */; };
6B7ADEF321FDB12B003803BE /* UITableViewComponentCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7ADEEF21FDB12A003803BE /* UITableViewComponentCellTests.swift */; };
6B7ADEF421FDB12B003803BE /* UITableViewComponentHeaderFooterViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7ADEF021FDB12A003803BE /* UITableViewComponentHeaderFooterViewTests.swift */; };
6B7EED9E224CA5DD00060872 /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7EED9D224CA5DD00060872 /* UIScrollViewExtensions.swift */; };
6B7EEDA0224CE4E100060872 /* UIScrollViewExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7EED9F224CE4E000060872 /* UIScrollViewExtensionsTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -118,6 +120,8 @@
6B7ADEEE21FDB12A003803BE /* UICollectionViewComponentCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewComponentCellTests.swift; sourceTree = "<group>"; };
6B7ADEEF21FDB12A003803BE /* UITableViewComponentCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewComponentCellTests.swift; sourceTree = "<group>"; };
6B7ADEF021FDB12A003803BE /* UITableViewComponentHeaderFooterViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewComponentHeaderFooterViewTests.swift; sourceTree = "<group>"; };
6B7EED9D224CA5DD00060872 /* UIScrollViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtensions.swift; sourceTree = "<group>"; };
6B7EED9F224CE4E000060872 /* UIScrollViewExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtensionsTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -184,6 +188,7 @@
isa = PBXGroup;
children = (
6B65948821E2532100291AAF /* ComponentContainer.swift */,
6B7EED9D224CA5DD00060872 /* UIScrollViewExtensions.swift */,
);
path = Internal;
sourceTree = "<group>";
Expand Down Expand Up @@ -286,6 +291,7 @@
isa = PBXGroup;
children = (
6B7ADEE021FBA6D1003803BE /* ComponentContainerTests.swift */,
6B7EED9F224CE4E000060872 /* UIScrollViewExtensionsTests.swift */,
);
path = Internal;
sourceTree = "<group>";
Expand Down Expand Up @@ -434,6 +440,7 @@
buildActionMask = 2147483647;
files = (
6B6594BB21E2532100291AAF /* UITableViewComponentHeaderFooterView.swift in Sources */,
6B7EED9E224CA5DD00060872 /* UIScrollViewExtensions.swift in Sources */,
6B6594AC21E2532100291AAF /* Section.swift in Sources */,
6B7ADEE521FD9D11003803BE /* UITableViewCellContent.swift in Sources */,
6B6594B121E2532100291AAF /* UICollectionViewReloadDataUpdater.swift in Sources */,
Expand Down Expand Up @@ -475,6 +482,7 @@
6B7ADEDE21FBA59D003803BE /* UICollectionViewReloadDataTests.swift in Sources */,
6B7ADEBC21F8B464003803BE /* AnyComponentTests.swift in Sources */,
6B7ADEF221FDB12B003803BE /* UICollectionViewComponentCellTests.swift in Sources */,
6B7EEDA0224CE4E100060872 /* UIScrollViewExtensionsTests.swift in Sources */,
6B7ADEBE21F8BF89003803BE /* ViewNodeTests.swift in Sources */,
6B7ADECC21FA067E003803BE /* UITableViewAdapterTests.swift in Sources */,
6B7ADEF321FDB12B003803BE /* UITableViewComponentCellTests.swift in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ Indicates whether to always call `render` of the component in the visible area a
Should use this in case of using the above mentioned `skipReloadComponents`.
Default is `false`.

- **keepsContentOffset**
Indicating whether that to reset content offset after updated.
The content offset become unintended position after diffing updates in some case.
If set `true`, revert content offset after updates.
Default is `false`.

[See more](https://ra1028.github.io/Carbon/Updaters.html)

#### Element-Specific Behaviors
Expand Down
38 changes: 38 additions & 0 deletions Sources/Internal/UIScrollViewExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import UIKit

internal extension UIScrollView {
var _isContetRectContainsBounds: Bool {
return CGRect(origin: .zero, size: contentSize)
.inset(by: availableContentInset.inverted)
.contains(bounds)
}

var _maxContentOffsetX: CGFloat {
return contentSize.width + availableContentInset.right - bounds.width
}

var _maxContentOffsetY: CGFloat {
return contentSize.height + availableContentInset.bottom - bounds.height
}

var _isScrolling: Bool {
return isTracking || isDragging || isDecelerating
}
}

private extension UIScrollView {
var availableContentInset: UIEdgeInsets {
if #available(iOS 11.0, tvOS 11.0, *) {
return adjustedContentInset
}
else {
return contentInset
}
}
}

private extension UIEdgeInsets {
var inverted: UIEdgeInsets {
return UIEdgeInsets(top: -top, left: -left, bottom: -bottom, right: -right)
}
}
13 changes: 13 additions & 0 deletions Sources/Updaters/UICollectionViewUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ open class UICollectionViewUpdater<Adapter: Carbon.Adapter & UICollectionViewDel
/// after diffing updated. Default is false.
open var alwaysRenderVisibleComponents = false

/// A Bool value indicating whether that to reset content offset after
/// updated if not scrolling. Default is false.
open var keepsContentOffset = false

/// Max number of changes that can be animated for diffing updates. Default is 300.
open var animatableChangeCount = 300

Expand Down Expand Up @@ -84,6 +88,8 @@ open class UICollectionViewUpdater<Adapter: Carbon.Adapter & UICollectionViewDel
}

func performAnimatedUpdates() {
let contentOffsetBeforeUpdates = target.contentOffset

CATransaction.begin()
CATransaction.setCompletionBlock(completion)

Expand Down Expand Up @@ -128,6 +134,13 @@ open class UICollectionViewUpdater<Adapter: Carbon.Adapter & UICollectionViewDel
renderVisibleComponentsIfNeeded()

CATransaction.commit()

if keepsContentOffset && target._isContetRectContainsBounds && !target._isScrolling {
target.contentOffset = CGPoint(
x: min(target._maxContentOffsetX, contentOffsetBeforeUpdates.x),
y: min(target._maxContentOffsetY, contentOffsetBeforeUpdates.y)
)
}
}

if isAnimationEnabled {
Expand Down
10 changes: 10 additions & 0 deletions Sources/Updaters/UITableViewUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ open class UITableViewUpdater<Adapter: Carbon.Adapter & UITableViewDelegate & UI
/// after diffing updated. Default is false.
open var alwaysRenderVisibleComponents = false

/// A Bool value indicating whether that to reset content offset after
/// updated if not scrolling. Default is false.
open var keepsContentOffset = false

/// Max number of changes that can be animated for diffing updates. Default is 300.
open var animatableChangeCount = 300

Expand Down Expand Up @@ -114,6 +118,8 @@ open class UITableViewUpdater<Adapter: Carbon.Adapter & UITableViewDelegate & UI
}

func performAnimatedUpdates() {
let contentOffsetYBeforeUpdates = target.contentOffset.y

CATransaction.begin()
CATransaction.setCompletionBlock(completion)

Expand Down Expand Up @@ -158,6 +164,10 @@ open class UITableViewUpdater<Adapter: Carbon.Adapter & UITableViewDelegate & UI
renderVisibleComponentsIfNeeded()

CATransaction.commit()

if keepsContentOffset && target._isContetRectContainsBounds && !target._isScrolling {
target.contentOffset.y = min(target._maxContentOffsetY, contentOffsetYBeforeUpdates)
}
}

if isAnimationEnabled {
Expand Down
64 changes: 64 additions & 0 deletions Tests/Internal/UIScrollViewExtensionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import XCTest
@testable import Carbon

@available(iOS 11.0, *)
final class MockScrollViewExtensionsTests: XCTestCase {
func testIsContetRectContainsBounds() {
let scrollView1 = MockScrollView()
scrollView1.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
scrollView1.contentSize = CGSize(width: 100, height: 200)
scrollView1.bounds.size = CGSize(width: 30, height: 40)
scrollView1.contentOffset = CGPoint(x: 10, y: 20)

let scrollView2 = MockScrollView()
scrollView2.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
scrollView2.contentSize = CGSize(width: 100, height: 20)
scrollView2.bounds.size = CGSize(width: 30, height: 40)
scrollView2.contentOffset = CGPoint(x: 10, y: 20)

let scrollView3 = MockScrollView()
scrollView3.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 21, right: 11)
scrollView3.contentSize = CGSize(width: 100, height: 200)
scrollView3.bounds.size = CGSize(width: 100, height: 200)
scrollView3.contentOffset = CGPoint(x: 10, y: 20)

XCTAssertTrue(scrollView1._isContetRectContainsBounds)
XCTAssertFalse(scrollView2._isContetRectContainsBounds)
XCTAssertTrue(scrollView3._isContetRectContainsBounds)
}

func testIsScrolling() {
let scrollView1 = MockScrollView()
scrollView1._isTracking = true

let scrollView2 = MockScrollView()
scrollView2._isDragging = true

let scrollView3 = MockScrollView()
scrollView3._isDecelerating = true

XCTAssertTrue(scrollView1._isScrolling)
XCTAssertTrue(scrollView2._isScrolling)
XCTAssertTrue(scrollView3._isScrolling)
}

func textMaxContentOffsetX() {
let scrollView = MockScrollView()
scrollView.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
scrollView.contentSize = CGSize(width: 100, height: 200)
scrollView.bounds.size = CGSize(width: 30, height: 40)
scrollView.contentOffset = CGPoint(x: 10, y: 20)

XCTAssertEqual(scrollView._maxContentOffsetX, 74)
}

func textMaxContentOffsetY() {
let scrollView = MockScrollView()
scrollView.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
scrollView.contentSize = CGSize(width: 100, height: 200)
scrollView.bounds.size = CGSize(width: 30, height: 40)
scrollView.contentOffset = CGPoint(x: 10, y: 20)

XCTAssertEqual(scrollView._maxContentOffsetY, 163)
}
}
17 changes: 17 additions & 0 deletions Tests/TestTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,23 @@ final class MockCollectionReusableViewContent: UICollectionReusableViewContent {
}
}

final class MockScrollView: UIScrollView {
var _isTracking: Bool = false
override var isTracking: Bool {
return _isTracking
}

var _isDragging: Bool = false
override var isDragging: Bool {
return _isDragging
}

var _isDecelerating: Bool = false
override var isDecelerating: Bool {
return _isDecelerating
}
}

/// Extract `renderedContent` from specified container.
func renderedContent<T>(of container: Any, as type: T.Type) -> T? {
guard
Expand Down
28 changes: 28 additions & 0 deletions docs/Classes/UICollectionViewUpdater.html
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,34 @@ <h4>Declaration</h4>
</section>
</div>
</li>
<li class="item">
<div>
<code>
<a name="/s:6Carbon23UICollectionViewUpdaterC18keepsContentOffsetSbvp"></a>
<a name="//apple_ref/swift/Property/keepsContentOffset" class="dashAnchor"></a>
<a class="token" href="#/s:6Carbon23UICollectionViewUpdaterC18keepsContentOffsetSbvp">keepsContentOffset</a>
</code>
</div>
<div class="height-container">
<div class="pointer-container"></div>
<section class="section">
<div class="pointer"></div>
<div class="abstract">
<p>A Bool value indicating whether that to reset content offset after
updated if not scrolling. Default is false.</p>

</div>
<div class="declaration">
<h4>Declaration</h4>
<div class="language">
<p class="aside-title">Swift</p>
<pre class="highlight swift"><code><span class="kd">open</span> <span class="k">var</span> <span class="nv">keepsContentOffset</span><span class="p">:</span> <span class="kt">Bool</span></code></pre>

</div>
</div>
</section>
</div>
</li>
<li class="item">
<div>
<code>
Expand Down
28 changes: 28 additions & 0 deletions docs/Classes/UITableViewUpdater.html
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,34 @@ <h4>Declaration</h4>
</section>
</div>
</li>
<li class="item">
<div>
<code>
<a name="/s:6Carbon18UITableViewUpdaterC18keepsContentOffsetSbvp"></a>
<a name="//apple_ref/swift/Property/keepsContentOffset" class="dashAnchor"></a>
<a class="token" href="#/s:6Carbon18UITableViewUpdaterC18keepsContentOffsetSbvp">keepsContentOffset</a>
</code>
</div>
<div class="height-container">
<div class="pointer-container"></div>
<section class="section">
<div class="pointer"></div>
<div class="abstract">
<p>A Bool value indicating whether that to reset content offset after
updated if not scrolling. Default is false.</p>

</div>
<div class="declaration">
<h4>Declaration</h4>
<div class="language">
<p class="aside-title">Swift</p>
<pre class="highlight swift"><code><span class="kd">open</span> <span class="k">var</span> <span class="nv">keepsContentOffset</span><span class="p">:</span> <span class="kt">Bool</span></code></pre>

</div>
</div>
</section>
</div>
</li>
<li class="item">
<div>
<code>
Expand Down
5 changes: 5 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,11 @@ <h4 id='updater-customization' class='heading'>Updater Customization</h4>
Indicates whether to always call <code>render</code> of the component in the visible area after updating.<br>
Should use this in case of using the above mentioned <code>skipReloadComponents</code>.<br>
Default is <code>false</code>. </p></li>
<li><p><strong>keepsContentOffset</strong><br>
Indicating whether that to reset content offset after updated.<br>
The content offset become unintended position after diffing updates in some case.
If set <code>true</code>, revert content offset after updates.<br>
Default is <code>false</code>. </p></li>
</ul>

<p><a href="https://ra1028.github.io/Carbon/Updaters.html">See more</a></p>
Expand Down
2 changes: 1 addition & 1 deletion docs/search.json

Large diffs are not rendered by default.

0 comments on commit a9a8d5e

Please sign in to comment.