diff --git a/Example/SushiBelt.xcodeproj/project.pbxproj b/Example/SushiBelt.xcodeproj/project.pbxproj index 541c9fc..0563f2e 100644 --- a/Example/SushiBelt.xcodeproj/project.pbxproj +++ b/Example/SushiBelt.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 630220C72917A83000BB52BB /* TileListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630220C62917A83000BB52BB /* TileListViewController.swift */; }; 630220CA2917A86100BB52BB /* Sushi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630220C92917A86100BB52BB /* Sushi.swift */; }; 630220CC2917AE0D00BB52BB /* MainTestKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630220CB2917AE0D00BB52BB /* MainTestKind.swift */; }; + 6357DDEE2BF59B82002CF46F /* MatrixViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6357DDED2BF59B82002CF46F /* MatrixViewController.swift */; }; 64124CB3B1A70B8895D6CFC3 /* Pods_SushiBelt_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549A69A85E1A3E5CDD18BAB3 /* Pods_SushiBelt_Tests.framework */; }; 96837B9D5AAEC5191BD928C6 /* Pods_SushiBelt_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9582B77D1A71BE86C93C42A2 /* Pods_SushiBelt_Example.framework */; }; /* End PBXBuildFile section */ @@ -45,6 +46,7 @@ 630220C62917A83000BB52BB /* TileListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileListViewController.swift; sourceTree = ""; }; 630220C92917A86100BB52BB /* Sushi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sushi.swift; sourceTree = ""; }; 630220CB2917AE0D00BB52BB /* MainTestKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTestKind.swift; sourceTree = ""; }; + 6357DDED2BF59B82002CF46F /* MatrixViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixViewController.swift; sourceTree = ""; }; 75900424683F94C20A7CCC6A /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 842B83AA3961CCC887E18551 /* Pods-SushiBelt_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SushiBelt_Tests.release.xcconfig"; path = "Target Support Files/Pods-SushiBelt_Tests/Pods-SushiBelt_Tests.release.xcconfig"; sourceTree = ""; }; 9582B77D1A71BE86C93C42A2 /* Pods_SushiBelt_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SushiBelt_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -115,6 +117,7 @@ 630220C02917A7EE00BB52BB /* MainViewController.swift */, 630220C22917A7FB00BB52BB /* SingleListViewController.swift */, 630220C42917A80100BB52BB /* ScrollViewController.swift */, + 6357DDED2BF59B82002CF46F /* MatrixViewController.swift */, 630220C62917A83000BB52BB /* TileListViewController.swift */, 607FACDC1AFB9204008FA782 /* Images.xcassets */, 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, @@ -349,6 +352,7 @@ buildActionMask = 2147483647; files = ( 630220CA2917A86100BB52BB /* Sushi.swift in Sources */, + 6357DDEE2BF59B82002CF46F /* MatrixViewController.swift in Sources */, 630220C12917A7EE00BB52BB /* MainViewController.swift in Sources */, 630220CC2917AE0D00BB52BB /* MainTestKind.swift in Sources */, 630220C52917A80100BB52BB /* ScrollViewController.swift in Sources */, diff --git a/Example/SushiBelt/MainTestKind.swift b/Example/SushiBelt/MainTestKind.swift index 8f62731..493db91 100644 --- a/Example/SushiBelt/MainTestKind.swift +++ b/Example/SushiBelt/MainTestKind.swift @@ -13,6 +13,7 @@ enum MainTestKind: Int, CaseIterable { case single case scroll case tile + case matrix var image: UIImage? { switch self { @@ -22,6 +23,8 @@ enum MainTestKind: Int, CaseIterable { return UIImage(named: "orange_sushi") case .tile: return UIImage(named: "egg_sushi") + case .matrix: + return UIImage(named: "red_sushi") } } @@ -33,6 +36,8 @@ enum MainTestKind: Int, CaseIterable { return 0.5 case .tile: return 0.8 + case .matrix: + return 0.2 } } @@ -44,6 +49,8 @@ enum MainTestKind: Int, CaseIterable { return "ScrollView" case .tile: return "Tile List" + case .matrix: + return "Matrix" } } } diff --git a/Example/SushiBelt/MainViewController.swift b/Example/SushiBelt/MainViewController.swift index da0e04e..5e22960 100644 --- a/Example/SushiBelt/MainViewController.swift +++ b/Example/SushiBelt/MainViewController.swift @@ -144,6 +144,12 @@ extension MainViewController: UICollectionViewDelegate, UICollectionViewDelegate TileListViewController(), animated: true ) + + case .matrix: + self.navigationController?.pushViewController( + MatrixViewController(), + animated: true + ) } } diff --git a/Example/SushiBelt/MatrixViewController.swift b/Example/SushiBelt/MatrixViewController.swift new file mode 100644 index 0000000..a0b4242 --- /dev/null +++ b/Example/SushiBelt/MatrixViewController.swift @@ -0,0 +1,164 @@ +// +// MatrixViewController.swift +// SushiBelt_Example +// +// Created by david on 5/16/24. +// Copyright © 2024 CocoaPods. All rights reserved. +// + +import UIKit + +import SushiBelt + +fileprivate final class MatrixCell: UICollectionViewCell { + + static let identifier = "MatrixCell" + + private let imageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + self.contentView.addSubview(self.imageView) + self.contentView.backgroundColor = UIColor(red: 1.0, green: 0.83, blue: 0.47, alpha: 1.0) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.imageView.image = nil + } + + override func layoutSubviews() { + super.layoutSubviews() + self.imageView.frame.size = CGSize(width: 200.0, height: 200.0) + self.imageView.center = self.contentView.center + self.contentView.layer.cornerRadius = 24.0 + } + + func configure(sushi: Sushi) { + self.imageView.image = sushi.image + } +} + +class MatrixViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { + + var collectionView: UICollectionView! + private let tracker = SushiBeltTracker() + private let debugger = SushiBeltDebugger.shared + private let sections: [[Sushi]] = [ + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10), + Sushi.いらっしゃいませ(count: 10) + ] + + override func viewDidLoad() { + super.viewDidLoad() + let layout = UICollectionViewCompositionalLayout { (sectionIndex, _) -> NSCollectionLayoutSection? in + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(300), heightDimension: .absolute(300)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20) + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(300 * 10), heightDimension: .absolute(300)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 10) + let section = NSCollectionLayoutSection(group: group) + return section + } + + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + collectionView.backgroundColor = .white + collectionView.dataSource = self + collectionView.delegate = self + collectionView.register(MatrixCell.self, forCellWithReuseIdentifier: MatrixCell.identifier) + view.addSubview(collectionView) + self.tracker.delegate = self + self.tracker.dataSource = self + self.tracker.scrollContext = SushiBeltTrackerUIScrollContext(scrollView: self.collectionView) + self.tracker.registerDebugger(debugger: self.debugger) + self.debugger.show() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + trackingVisibleCellsIfNeeded() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + tracker.calculateItemsIfNeeded(items: []) + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return sections.count + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return sections[section].count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MatrixCell.identifier, for: indexPath) as? MatrixCell else { + return .init() + } + + let section = sections[indexPath.section] + let item = section[indexPath.item] + cell.configure(sushi: item) + return cell + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + trackingVisibleCellsIfNeeded() + } + + private func trackingVisibleCellsIfNeeded() { + let trackingItems = self.collectionView.visibleCells.compactMap { cell -> SushiBeltTrackerItem? in + guard let indexPath = self.collectionView.indexPath(for: cell) else { return nil } + + return SushiBeltTrackerItem( + id: .indexPath(indexPath), + rect: cell.sushiBeltTrackerItemRect() + ) + } + self.tracker.calculateItemsIfNeeded(items: trackingItems) + } +} + +extension MatrixViewController: SushiBeltTrackerDataSource { + + func trackingRect(_ tracker: SushiBeltTracker) -> CGRect { + collectionView.frame + } + + func visibleRatioForItem(_ tracker: SushiBeltTracker, item: SushiBeltTrackerItem) -> CGFloat { + guard case let .indexPath(indexPath) = item.id else { return 0.0 } + return sections[indexPath.section][indexPath.item].kind.visibleRatio + } +} + +extension MatrixViewController: SushiBeltTrackerDelegate { + + func willBeginTracking(_ tracker: SushiBeltTracker, item: SushiBeltTrackerItem) { + + print("🚀 begin tracking: \(item.debugDescription)") + } + + func didTrack(_ tracker: SushiBeltTracker, item: SushiBeltTrackerItem) { + + print("🎯 tracked: \(item.debugDescription)") + } + + func didEndTracking(_ tracker: SushiBeltTracker, item: SushiBeltTrackerItem) { + + print("👋 end tracking: \(item.debugDescription)") + } +} diff --git a/Sources/SushiBelt/Private/VisibleRatioCalculator.swift b/Sources/SushiBelt/Private/VisibleRatioCalculator.swift index 8faf2d3..504b6b5 100644 --- a/Sources/SushiBelt/Private/VisibleRatioCalculator.swift +++ b/Sources/SushiBelt/Private/VisibleRatioCalculator.swift @@ -24,20 +24,9 @@ public struct DefaultVisibleRatioCalculator: VisibleRatioCalculator { trackingRect: CGRect, scrollDirection: SushiBeltTrackerScrollDirection? ) -> CGFloat? { - - guard let scrollDirection = scrollDirection else { - return nil - } - let visibleRect = trackingRect.intersection(item.rect.frameInWindow) - - switch scrollDirection { - case .up, .down: - return min(1.0, visibleRect.height / item.rect.frameInWindow.height) - case .left, .right: - return min(1.0, visibleRect.width / item.rect.frameInWindow.width) - case .diagonal: - return nil - } + let itemRectPixels = item.rect.frameInWindow.height * item.rect.frameInWindow.width + let visiblePixels = visibleRect.height * visibleRect.width + return visiblePixels / itemRectPixels } } diff --git a/Tests/SushiBeltTests/Private/VisibleRatioCalculatorTests.swift b/Tests/SushiBeltTests/Private/VisibleRatioCalculatorTests.swift index 051af58..4fa552a 100644 --- a/Tests/SushiBeltTests/Private/VisibleRatioCalculatorTests.swift +++ b/Tests/SushiBeltTests/Private/VisibleRatioCalculatorTests.swift @@ -22,21 +22,6 @@ final class VisibleRatioCalculatorTests: XCTestCase { extension VisibleRatioCalculatorTests { - func test_early_exit_in_scrollDirection_null() { - // given - let calculator = self.defaultVisibleRatioCalculator() - - // when - let ratio = calculator.visibleRatio( - item: SushiBeltTrackerItem(id: .index(1), rect: .init(frame: .zero)), - trackingRect: CGRect(origin: .zero, size: .zero), - scrollDirection: nil - ) - - // then - XCTAssertNil(ratio) - } - func test_should_return_expected_visible_ratio_on_scroll_up() { // given let calculator = self.defaultVisibleRatioCalculator()