Skip to content

Commit

Permalink
💲[Native Checkout] "Peek" functionality using hidden scroll view (#665)
Browse files Browse the repository at this point in the history
* New files

* removing scroll view stuff for now

* Tests

* Cleaning up insets

* More cleanup

* Swiftlint

* Adding “deprecated” to more file names

* Removing duplicated file

* Increasing hit area of close button

* Hidden scroll view

* Hidden scroll view working

* Refactoring & cleanup

* Use collection view’s content size

* PR updates from comments

* Fix autolayout warnings on device rotation

* Cleanup

* More cleanup

* More PR feedback
  • Loading branch information
Isabel Barrera authored and ifbarrera committed Aug 5, 2019
1 parent 2f77467 commit 366db9c
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 17 deletions.
146 changes: 129 additions & 17 deletions Kickstarter-iOS/Views/Controllers/RewardsCollectionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -66,6 +70,8 @@ final class RewardsCollectionViewController: UICollectionViewController {

self.collectionView.register(RewardCell.self)

self.configureHiddenScrollView()

self.viewModel.inputs.viewDidLoad()
}

Expand All @@ -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()
}

override func bindStyles() {
Expand All @@ -96,6 +97,9 @@ final class RewardsCollectionViewController: UICollectionViewController {

_ = self.collectionView
|> collectionViewStyle

_ = self.collectionView.panGestureRecognizer
|> \.isEnabled .~ false
}

override func bindViewModel() {
Expand All @@ -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
Expand All @@ -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
}
4 changes: 4 additions & 0 deletions Kickstarter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
778215EB20F79FA300F3D09F /* HelpDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778215EA20F79FA300F3D09F /* HelpDataSourceTests.swift */; };
778215EE20F7AB8300F3D09F /* HelpViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778215EC20F7AA5F00F3D09F /* HelpViewControllerTests.swift */; };
77891BDE20CEB6DB00B46D5D /* ThanksProjectsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77891BA720CEB6DB00B46D5D /* ThanksProjectsDataSourceTests.swift */; };
778CCCDA2285BF8900FB8D35 /* UIView+AutolayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778CCCD92285BF8900FB8D35 /* UIView+AutolayoutTests.swift */; };
7790DF502200D395005DBB11 /* SettingsFormFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7790DF4F2200D395005DBB11 /* SettingsFormFieldView.swift */; };
7790DF892200D3BD005DBB11 /* SettingsFormFieldView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7790DF882200D3BD005DBB11 /* SettingsFormFieldView.xib */; };
7793B16D21077AEB007857C0 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7793B16C21077AEB007857C0 /* SettingsHeaderView.swift */; };
Expand Down Expand Up @@ -1436,6 +1437,7 @@
778215EA20F79FA300F3D09F /* HelpDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpDataSourceTests.swift; sourceTree = "<group>"; };
778215EC20F7AA5F00F3D09F /* HelpViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewControllerTests.swift; sourceTree = "<group>"; };
77891BA720CEB6DB00B46D5D /* ThanksProjectsDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThanksProjectsDataSourceTests.swift; sourceTree = "<group>"; };
778CCCD92285BF8900FB8D35 /* UIView+AutolayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+AutolayoutTests.swift"; sourceTree = "<group>"; };
7790DF4F2200D395005DBB11 /* SettingsFormFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFormFieldView.swift; sourceTree = "<group>"; };
7790DF882200D3BD005DBB11 /* SettingsFormFieldView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsFormFieldView.xib; sourceTree = "<group>"; };
7793B16C21077AEB007857C0 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2949,6 +2951,7 @@
D0A787BE2204D975006AE4F4 /* UITableView+AutoLayoutHeaderView.swift */,
01A7A4BF1C9690220036E553 /* UITextField+LocalizedPlaceholderKey.swift */,
37BB45C82243031100E7392A /* UIView+AutoLayout.swift */,
778CCCD92285BF8900FB8D35 /* UIView+AutolayoutTests.swift */,
A733795F1D0EDFEE00C91445 /* UIViewController-Preparation.swift */,
370BE71522541C8100B44DB2 /* UIViewController+URL.swift */,
370BE74E22541C8F00B44DB2 /* UIViewController+URLTests.swift */,
Expand Down Expand Up @@ -4615,6 +4618,7 @@
A7ED1FC31E831C5C00BFFA01 /* ProfileViewModelTests.swift in Sources */,
A7ED20011E831C5C00BFFA01 /* SignupViewModelTests.swift in Sources */,
A7ED1FF71E831C5C00BFFA01 /* ResetPasswordViewModelTests.swift in Sources */,
778CCCDA2285BF8900FB8D35 /* UIView+AutolayoutTests.swift in Sources */,
373AB25F222A0DAC00769FC2 /* PasswordValidationTests.swift in Sources */,
D6C9A20E1F755FE200981E64 /* GraphSchemaTests.swift in Sources */,
A7ED1FF51E831C5C00BFFA01 /* ProjectNavBarViewModelTests.swift in Sources */,
Expand Down
7 changes: 7 additions & 0 deletions Library/UIView+AutoLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ public func ksr_addSubviewToParent() -> ((UIView, UIView) -> (UIView, UIView)) {
}
}

public func ksr_insertSubviewInParent(at index: Int) -> ((UIView, UIView) -> (UIView, UIView)) {
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
Expand Down
16 changes: 16 additions & 0 deletions Library/UIView+AutolayoutTests.swift
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)
}
}

0 comments on commit 366db9c

Please sign in to comment.