-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
π²[Native Checkout] "Peek" functionality using hidden scroll view #665
Changes from all commits
9406300
b0aa974
85db760
48a3e1f
5fcf780
21216ac
2e4ddc2
8b7f848
a405611
7aa0272
6c92d7d
992f1e4
21ba760
88ef739
a31a1ae
d60f4aa
1037763
e5d3515
c9eb6ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,13 @@ import Library | |
import KsApi | ||
import Prelude | ||
|
||
final class RewardsCollectionViewController: UICollectionViewController { | ||
private enum Layout { | ||
enum Card { | ||
static let width: CGFloat = 249 | ||
} | ||
} | ||
|
||
final class RewardsCollectionViewController: UICollectionViewController { | ||
// MARK: - Properties | ||
|
||
private let dataSource = RewardsCollectionViewDataSource() | ||
|
@@ -19,16 +24,15 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
private let layout: UICollectionViewFlowLayout = { | ||
UICollectionViewFlowLayout() | ||
|> \.minimumLineSpacing .~ Styles.grid(3) | ||
|> \.sectionInset .~ .init(all: Styles.grid(6)) | ||
|> \.minimumInteritemSpacing .~ 0 | ||
|> \.sectionInset .~ .init(topBottom: Styles.grid(6)) | ||
|> \.scrollDirection .~ .horizontal | ||
}() | ||
|
||
private var flowLayout: UICollectionViewFlowLayout? { | ||
return self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout | ||
} | ||
|
||
private let peekAmountInset = Styles.grid(3) | ||
|
||
static func instantiate(with project: Project, refTag: RefTag?) -> RewardsCollectionViewController { | ||
let rewardsCollectionVC = RewardsCollectionViewController() | ||
rewardsCollectionVC.viewModel.inputs.configure(with: project, refTag: refTag) | ||
|
@@ -66,6 +70,8 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
|
||
self.collectionView.register(RewardCell.self) | ||
|
||
self.configureHiddenScrollView() | ||
|
||
self.viewModel.inputs.viewDidLoad() | ||
} | ||
|
||
|
@@ -74,18 +80,13 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
|
||
guard let layout = self.flowLayout else { return } | ||
|
||
let sectionInsets = layout.sectionInset | ||
let topBottomInsets = sectionInsets.top + sectionInsets.bottom | ||
let collectionViewSize = self.collectionView.frame.size | ||
|
||
let itemHeight = self.collectionView.contentSize.height - topBottomInsets | ||
var itemWidth = collectionViewSize.width - sectionInsets.left - 2 * peekAmountInset | ||
self.updateHiddenScrollViewBoundsIfNeeded(for: layout) | ||
} | ||
|
||
if [.landscapeLeft, .landscapeRight].contains(UIDevice.current.orientation) { | ||
itemWidth = collectionViewSize.width / 3 - sectionInsets.left - 2 * peekAmountInset | ||
} | ||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { | ||
super.viewWillTransition(to: size, with: coordinator) | ||
|
||
layout.itemSize = CGSize(width: itemWidth, height: itemHeight) | ||
self.flowLayout?.invalidateLayout() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've noticed that no matter what the current horizontal offset rotating the device always resets it to 0 therefore scrolls back to the very first reward. There should be a way to either cache current page and upon rotation restore the scroll offset (maybe inside the coordinator completion handler)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd prefer to implement this separately as an enhancement. I'll create a card π |
||
} | ||
|
||
override func bindStyles() { | ||
|
@@ -96,6 +97,9 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
|
||
_ = self.collectionView | ||
|> collectionViewStyle | ||
|
||
_ = self.collectionView.panGestureRecognizer | ||
|> \.isEnabled .~ false | ||
} | ||
|
||
override func bindViewModel() { | ||
|
@@ -105,7 +109,89 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
.observeForUI() | ||
.observeValues { [weak self] rewards in | ||
self?.dataSource.load(rewards: rewards) | ||
self?.collectionView.reloadData() | ||
} | ||
} | ||
|
||
// MARK: - Private Helpers | ||
|
||
private func configureHiddenScrollView() { | ||
_ = self.hiddenPagingScrollView | ||
|> \.delegate .~ self | ||
|
||
_ = (self.hiddenPagingScrollView, self.view) | ||
|> ksr_insertSubviewInParent(at: 0) | ||
|
||
self.collectionView.addGestureRecognizer(self.hiddenPagingScrollView.panGestureRecognizer) | ||
} | ||
|
||
private func updateHiddenScrollViewBoundsIfNeeded(for layout: UICollectionViewFlowLayout) { | ||
let (contentSize, pageSize, contentInsetLeftRight) = self.hiddenScrollViewData(from: layout, | ||
using: self.collectionView) | ||
let needsUpdate = self.collectionView.contentInset.left != contentInsetLeftRight | ||
|| self.hiddenPagingScrollView.contentSize != contentSize | ||
|
||
// Check if orientation or frame has changed | ||
guard needsUpdate else { | ||
return | ||
} | ||
|
||
_ = self.hiddenPagingScrollView | ||
|> \.frame .~ self.collectionView.frame | ||
|> \.bounds .~ CGRect(x: 0, y: 0, width: pageSize.width, height: pageSize.height) | ||
|> \.contentSize .~ CGSize(width: contentSize.width, height: contentSize.height) | ||
|
||
let (top, bottom) = self.collectionView.contentInset.topBottom | ||
|
||
_ = self.collectionView | ||
|> \.contentInset .~ .init(top: top, | ||
left: contentInsetLeftRight, | ||
bottom: bottom, | ||
right: contentInsetLeftRight) | ||
|
||
self.collectionView.contentOffset.x = -contentInsetLeftRight | ||
} | ||
|
||
private typealias HiddenScrollViewData = (contentSize: CGSize, pageSize: CGSize, | ||
contentInsetLeftRight: CGFloat) | ||
|
||
private func hiddenScrollViewData(from layout: UICollectionViewFlowLayout, | ||
using collectionView: UICollectionView) -> HiddenScrollViewData { | ||
let itemSize = layout.itemSize | ||
let lineSpacing = layout.minimumLineSpacing | ||
let totalItemWidth = itemSize.width + lineSpacing | ||
|
||
let pageWidth = totalItemWidth | ||
let pageHeight = itemSize.height | ||
let pageSize = CGSize(width: pageWidth, height: pageHeight) | ||
|
||
let contentSize = CGSize(width: collectionView.contentSize.width + lineSpacing, | ||
height: collectionView.contentSize.height) | ||
|
||
let contentInsetLeftRight = (collectionView.frame.width - itemSize.width) / 2 | ||
|
||
return (contentSize, pageSize, contentInsetLeftRight) | ||
} | ||
|
||
private func calculateItemSize(from layout: UICollectionViewFlowLayout, | ||
using collectionView: UICollectionView) -> CGSize { | ||
let cardWidth = Layout.Card.width | ||
|
||
let sectionInsets = layout.sectionInset | ||
var adjustedContentInset = UIEdgeInsets.zero | ||
|
||
if #available(iOS 11.0, *) { | ||
adjustedContentInset = collectionView.adjustedContentInset | ||
} | ||
|
||
let topBottomSectionInsets = sectionInsets.top + sectionInsets.bottom | ||
let topBottomContentInsets = adjustedContentInset.top + adjustedContentInset.bottom | ||
let leftRightInsets = sectionInsets.left + sectionInsets.right | ||
|
||
let itemHeight = collectionView.frame.height - topBottomSectionInsets - topBottomContentInsets | ||
let itemWidth = cardWidth - leftRightInsets | ||
|
||
return CGSize(width: itemWidth, height: itemHeight) | ||
} | ||
|
||
// MARK: - Public Functions | ||
|
@@ -115,13 +201,39 @@ final class RewardsCollectionViewController: UICollectionViewController { | |
} | ||
} | ||
|
||
// MARK: - Styles | ||
// MARK: - UIScrollViewDelegate | ||
|
||
extension RewardsCollectionViewController { | ||
override func scrollViewDidScroll(_ scrollView: UIScrollView) { | ||
guard scrollView == self.hiddenPagingScrollView else { return } | ||
|
||
let leftInset = self.collectionView.contentInset.left | ||
|
||
self.collectionView.contentOffset.x = scrollView.contentOffset.x - leftInset | ||
} | ||
} | ||
|
||
// MARK: - UICollectionViewDelegateFlowLayout | ||
|
||
extension RewardsCollectionViewController: UICollectionViewDelegateFlowLayout { | ||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, | ||
sizeForItemAt indexPath: IndexPath) -> CGSize { | ||
guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { | ||
return .zero | ||
} | ||
|
||
// Cache the itemSize so we can recalculate hidden scroll view data efficiently | ||
layout.itemSize = self.calculateItemSize(from: layout, using: collectionView) | ||
|
||
return layout.itemSize | ||
} | ||
} | ||
|
||
// MARK: Styles | ||
|
||
private var collectionViewStyle: CollectionViewStyle = { collectionView -> UICollectionView in | ||
collectionView | ||
|> \.alwaysBounceHorizontal .~ true | ||
|> \.backgroundColor .~ .ksr_grey_200 | ||
|> \.isPagingEnabled .~ true | ||
|> \.clipsToBounds .~ false | ||
|> \.allowsSelection .~ true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,13 @@ public func ksr_addSubviewToParent() -> ((UIView, UIView) -> (UIView, UIView)) { | |
} | ||
} | ||
|
||
public func ksr_insertSubviewInParent(at index: Int) -> ((UIView, UIView) -> (UIView, UIView)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized we don't have tests for these? Shouldn't be so hard to add? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually don't have tests for any of these functions, but sure I can add one. |
||
return { (subview, parent) in | ||
parent.insertSubview(subview, at: index) | ||
return (subview, parent) | ||
} | ||
} | ||
|
||
public func ksr_constrainViewToEdgesInParent(priority: UILayoutPriority = .required) | ||
-> ((UIView, UIView) -> (UIView, UIView)) { | ||
return { (subview, parent) in | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import XCTest | ||
@testable import Library | ||
|
||
final class UIViewAutoLayoutExtensionTests: TestCase { | ||
func test_insertSubviewInParentAtIndex_oneSubview() { | ||
let view1 = UIView(frame: .zero) | ||
let view2 = UIView(frame: .zero) | ||
|
||
_ = (view2, view1) | ||
|
||
_ = ksr_insertSubviewInParent(at: 0)(view2, view1) | ||
|
||
XCTAssertEqual(view1.subviews.count, 1) | ||
XCTAssertEqual(view1.subviews[0], view2) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In an ideal scenario...this
guard
is repeated twice in the flow...first time here...second time inupdateHiddenScrollViewBoundsIfNeeded
. In order to make each function independent of the state of the view controller could we do thisor even something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it, will update the
updateHiddenScrollViewBoundsIfNeeded
to acceptlayout
as an argument π