Skip to content
Draft
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
8 changes: 8 additions & 0 deletions ListableUI/Sources/Behavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public struct Behavior : Equatable

/// How to adjust the `contentInset` of the list when the keyboard visibility changes.
public var keyboardAdjustmentMode : KeyboardAdjustmentMode

/// Additional insets to apply while adjusting the list for an overlapping keyboard.
///
/// This is useful for persistent overlays, such as floating action bars, that should be treated
/// as unavailable space when UIKit scrolls the first responder into view.
public var keyboardAdjustmentAdditionalInsets : UIEdgeInsets

/// How the list should react when the user taps the application status bar.
/// The default value of this enables scrolling to top.
Expand Down Expand Up @@ -64,6 +70,7 @@ public struct Behavior : Equatable
isScrollEnabled: Bool = true,
keyboardDismissMode : UIScrollView.KeyboardDismissMode = .interactive,
keyboardAdjustmentMode : KeyboardAdjustmentMode = .adjustsWhenVisible,
keyboardAdjustmentAdditionalInsets : UIEdgeInsets = .zero,
scrollsToTop : ScrollsToTop = .enabled,
selectionMode : SelectionMode = .single,
underflow : Underflow = Underflow(),
Expand All @@ -77,6 +84,7 @@ public struct Behavior : Equatable
self.isScrollEnabled = isScrollEnabled
self.keyboardDismissMode = keyboardDismissMode
self.keyboardAdjustmentMode = keyboardAdjustmentMode
self.keyboardAdjustmentAdditionalInsets = keyboardAdjustmentAdditionalInsets

self.scrollsToTop = scrollsToTop

Expand Down
51 changes: 46 additions & 5 deletions ListableUI/Sources/ListView/ListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,27 @@ public final class ListView : UIView
name: UITextField.textDidBeginEditingNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(textDidBeginEditingNotification(_:)),
name: UITextView.textDidBeginEditingNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(textDidEndEditingNotification(_:)),
name: UITextField.textDidEndEditingNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(textDidEndEditingNotification(_:)),
name: UITextView.textDidEndEditingNotification,
object: nil
)
}

deinit
Expand Down Expand Up @@ -416,6 +430,9 @@ public final class ListView : UIView
/// whenever insets require an update.
public func updateScrollViewInsets()
{
let previousContentInset = self.collectionView.contentInset
let previousAdjustedContentInset = self.collectionView.adjustedContentInset

let insets: ScrollViewInsets
if case .custom = self.behavior.keyboardAdjustmentMode {
insets = self.customScrollViewInsets()
Expand All @@ -436,6 +453,16 @@ public final class ListView : UIView
if self.collectionView.verticalScrollIndicatorInsets != insets.verticalScroll {
self.collectionView.verticalScrollIndicatorInsets = insets.verticalScroll
}

let nextAdjustedContentInset = self.collectionView.adjustedContentInset
let didChangeInsets =
previousContentInset != self.collectionView.contentInset ||
previousAdjustedContentInset != nextAdjustedContentInset

if didChangeInsets {
self.collectionViewLayout.setNeedsRelayout()
self.collectionView.layoutIfNeeded()
}
}

func calculateScrollViewInsets(with keyboardFrame : KeyboardFrame?) -> ScrollViewInsets {
Expand Down Expand Up @@ -467,12 +494,19 @@ public final class ListView : UIView
}
}()

let keyboardAdjustmentAdditionalInsets: UIEdgeInsets = keyboardBottomInset > 0.0
? self.behavior.keyboardAdjustmentAdditionalInsets
: .zero

let scrollInsets = modified(self.scrollIndicatorInsets) {
$0.bottom = max($0.bottom, keyboardBottomInset)
$0.bottom = max($0.bottom, keyboardBottomInset + keyboardAdjustmentAdditionalInsets.bottom)
}

let contentInsets = modified(self.collectionView.contentInset) {
$0.bottom = keyboardBottomInset
$0.top = keyboardAdjustmentAdditionalInsets.top
$0.left = keyboardAdjustmentAdditionalInsets.left
$0.bottom = keyboardBottomInset + keyboardAdjustmentAdditionalInsets.bottom
$0.right = keyboardAdjustmentAdditionalInsets.right
}

return .init(
Expand Down Expand Up @@ -659,7 +693,6 @@ public final class ListView : UIView
completion: ScrollCompletion? = nil
) -> Bool
{

let storageContent = storage.allContent

// Make sure the section identifier is valid.
Expand Down Expand Up @@ -1129,9 +1162,9 @@ public final class ListView : UIView
override public func layoutSubviews()
{
super.layoutSubviews()

self.collectionView.frame = self.bounds

/// Our layout changed, update the keyboard inset in case the inset should now be different.
self.updateScrollViewInsets()
}
Expand Down Expand Up @@ -1996,6 +2029,14 @@ final class CollectionView : ListView.IOS16_4_First_Responder_Bug_CollectionView
}
}

override func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
// UIKit can ask for a non-animated focus scroll from inside the keyboard animation
// transaction. Promote that case to the scroll view's animated path so cells move
// through UICollectionView's normal scrolling lifecycle instead of jumping mid-transaction.
let shouldPromoteToScrollAnimation = !animated && UIView.inheritedAnimationDuration > 0
super.scrollRectToVisible(rect, animated: animated || shouldPromoteToScrollAnimation)
}

/// Returns true when the content size is large enough that scrolling is possible
/// without bouncing back to it's original position.
var isContentScrollable: Bool {
Expand Down
1 change: 1 addition & 0 deletions ListableUI/Tests/BehaviorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BehaviorTests: XCTestCase

XCTAssertEqual(behavior.keyboardDismissMode, .interactive)
XCTAssertEqual(behavior.keyboardAdjustmentMode, .adjustsWhenVisible)
XCTAssertEqual(behavior.keyboardAdjustmentAdditionalInsets, .zero)

XCTAssertEqual(behavior.selectionMode, .single)

Expand Down
37 changes: 37 additions & 0 deletions ListableUI/Tests/ListView/ListViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,43 @@ class ListViewTests: XCTestCase
UIEdgeInsets(top: 10, left: 0, bottom: 200, right: 0)
)
}

self.testcase("Overlapping Keyboard Frame With Additional Insets") {
listView.behavior.keyboardAdjustmentAdditionalInsets = UIEdgeInsets(top: 1, left: 2, bottom: 50, right: 4)

let insets = listView.calculateScrollViewInsets(
with:.overlapping(frame: CGRect(x: 0, y: 200, width: 200, height: 200))
)

XCTAssertEqual(
insets.content,
UIEdgeInsets(top: 1, left: 2, bottom: 250, right: 4)
)

XCTAssertEqual(
insets.horizontalScroll,
UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 40)
)

XCTAssertEqual(
insets.verticalScroll,
UIEdgeInsets(top: 10, left: 0, bottom: 250, right: 0)
)
}

self.testcase("Non-Overlapping Keyboard Frame With Additional Insets") {
let insets = listView.calculateScrollViewInsets(with: .nonOverlapping)

XCTAssertEqual(
insets.content,
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
)

XCTAssertEqual(
insets.verticalScroll,
UIEdgeInsets(top: 10, left: 0, bottom: 30, right: 0)
)
}
}

func test_change_size() {
Expand Down
Loading