From 61fc9cb7973be252ba16083bd7d41bbb23c74e68 Mon Sep 17 00:00:00 2001 From: Arnaud Dorgans Date: Sat, 23 Dec 2017 20:40:46 +0100 Subject: [PATCH] 0.1.0 --- .gitignore | 2 + .swift-version | 1 + .../InfiniteLayout.xcodeproj/project.pbxproj | 16 +- .../InfiniteLayout/Base.lproj/Main.storyboard | 90 +++++-- Example/InfiniteLayout/Cell.swift | 60 +++++ .../AppIcon.appiconset/Contents.json | 17 +- .../Images.xcassets/Contents.json | 6 + Example/InfiniteLayout/ViewController.swift | 51 +++- Example/Podfile.lock | 16 ++ InfiniteLayout.podspec | 6 +- .../Classes/InfiniteCollectionView.swift | 161 ++++++++++++ .../InfiniteCollectionViewController.swift | 23 ++ .../Classes/InfiniteCollectionViewProxy.h | 19 ++ .../Classes/InfiniteCollectionViewProxy.m | 52 ++++ InfiniteLayout/Classes/InfiniteLayout.swift | 248 ++++++++++++++++++ InfiniteLayout/Classes/Proxy.swift | 16 ++ InfiniteLayout/Classes/ReplaceMe.swift | 0 17 files changed, 751 insertions(+), 33 deletions(-) create mode 100644 .swift-version create mode 100644 Example/InfiniteLayout/Cell.swift create mode 100644 Example/InfiniteLayout/Images.xcassets/Contents.json create mode 100644 Example/Podfile.lock create mode 100644 InfiniteLayout/Classes/InfiniteCollectionView.swift create mode 100644 InfiniteLayout/Classes/InfiniteCollectionViewController.swift create mode 100644 InfiniteLayout/Classes/InfiniteCollectionViewProxy.h create mode 100644 InfiniteLayout/Classes/InfiniteCollectionViewProxy.m create mode 100644 InfiniteLayout/Classes/InfiniteLayout.swift create mode 100644 InfiniteLayout/Classes/Proxy.swift delete mode 100644 InfiniteLayout/Classes/ReplaceMe.swift diff --git a/.gitignore b/.gitignore index e7b722d..9924cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ Carthage # `pod install` in .travis.yml # # Pods/ +/Example/InfiniteLayout.xcworkspace +/Example/Pods diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..5186d07 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +4.0 diff --git a/Example/InfiniteLayout.xcodeproj/project.pbxproj b/Example/InfiniteLayout.xcodeproj/project.pbxproj index 124329f..3dfe31e 100644 --- a/Example/InfiniteLayout.xcodeproj/project.pbxproj +++ b/Example/InfiniteLayout.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; + D403973F1FEBFA9D006C41D2 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D403973E1FEBFA9D006C41D2 /* Cell.swift */; }; E997A539C8278E7A661C9F91 /* Pods_InfiniteLayout_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C50F79B0DFFDE95D322480B2 /* Pods_InfiniteLayout_Tests.framework */; }; /* End PBXBuildFile section */ @@ -30,7 +31,7 @@ /* Begin PBXFileReference section */ 17E9A86BF3432BF233E3D297 /* Pods-InfiniteLayout_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example.debug.xcconfig"; sourceTree = ""; }; 2E68337E21F08944069DC023 /* Pods-InfiniteLayout_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Tests/Pods-InfiniteLayout_Tests.release.xcconfig"; sourceTree = ""; }; - 5DA98D9A451E79A872E96E69 /* InfiniteLayout.podspec */ = {isa = PBXFileReference; includeInIndex = 1; name = InfiniteLayout.podspec; path = ../InfiniteLayout.podspec; sourceTree = ""; }; + 5DA98D9A451E79A872E96E69 /* InfiniteLayout.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = InfiniteLayout.podspec; path = ../InfiniteLayout.podspec; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* InfiniteLayout_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InfiniteLayout_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -44,9 +45,10 @@ A6830D8B970E919B725A0D5E /* Pods_InfiniteLayout_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InfiniteLayout_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AD3D900D0067605DEAFF148B /* Pods-InfiniteLayout_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example.release.xcconfig"; sourceTree = ""; }; C50F79B0DFFDE95D322480B2 /* Pods_InfiniteLayout_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InfiniteLayout_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EF577FD009F0E2F96F1CECC6 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + D403973E1FEBFA9D006C41D2 /* Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; + EF577FD009F0E2F96F1CECC6 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; F900DDE615FA1056EB58998B /* Pods-InfiniteLayout_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Tests/Pods-InfiniteLayout_Tests.debug.xcconfig"; sourceTree = ""; }; - FD6AC573B93C313B5BDFF3FE /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; name = README.md; path = ../README.md; sourceTree = ""; }; + FD6AC573B93C313B5BDFF3FE /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,6 +97,7 @@ children = ( 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 607FACD71AFB9204008FA782 /* ViewController.swift */, + D403973E1FEBFA9D006C41D2 /* Cell.swift */, 607FACD91AFB9204008FA782 /* Main.storyboard */, 607FACDC1AFB9204008FA782 /* Images.xcassets */, 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, @@ -215,10 +218,12 @@ TargetAttributes = { 607FACCF1AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = CPE39ZT2AY; LastSwiftMigration = 0900; }; 607FACE41AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = CPE39ZT2AY; LastSwiftMigration = 0900; TestTargetID = 607FACCF1AFB9204008FA782; }; @@ -372,6 +377,7 @@ files = ( 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, + D403973F1FEBFA9D006C41D2 /* Cell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -517,6 +523,7 @@ baseConfigurationReference = 17E9A86BF3432BF233E3D297 /* Pods-InfiniteLayout_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = CPE39ZT2AY; INFOPLIST_FILE = InfiniteLayout/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; @@ -532,6 +539,7 @@ baseConfigurationReference = AD3D900D0067605DEAFF148B /* Pods-InfiniteLayout_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = CPE39ZT2AY; INFOPLIST_FILE = InfiniteLayout/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; @@ -546,6 +554,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = F900DDE615FA1056EB58998B /* Pods-InfiniteLayout_Tests.debug.xcconfig */; buildSettings = { + DEVELOPMENT_TEAM = CPE39ZT2AY; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", @@ -567,6 +576,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2E68337E21F08944069DC023 /* Pods-InfiniteLayout_Tests.release.xcconfig */; buildSettings = { + DEVELOPMENT_TEAM = CPE39ZT2AY; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", diff --git a/Example/InfiniteLayout/Base.lproj/Main.storyboard b/Example/InfiniteLayout/Base.lproj/Main.storyboard index 52ea29e..b0ece48 100644 --- a/Example/InfiniteLayout/Base.lproj/Main.storyboard +++ b/Example/InfiniteLayout/Base.lproj/Main.storyboard @@ -1,25 +1,81 @@ - - + + + + + - + + + - - + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/InfiniteLayout/Cell.swift b/Example/InfiniteLayout/Cell.swift new file mode 100644 index 0000000..74a4d96 --- /dev/null +++ b/Example/InfiniteLayout/Cell.swift @@ -0,0 +1,60 @@ +// +// Cell.swift +// InfiniteLayout_Example +// +// Created by Arnaud Dorgans on 21/12/2017. +// Copyright © 2017 CocoaPods. All rights reserved. +// + +import UIKit + +class Cell: UICollectionViewCell { + + @IBOutlet weak var cellView: CellView! + + func update(index: Int) { + cellView.update(index: index) + } +} + +@IBDesignable class CellView: UIView { + + let titleLabel = UILabel() + + let colors = [#colorLiteral(red: 0.5254901961, green: 0.6901960784, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.6196078431, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.6078431373, green: 0.5254901961, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.5254901961, blue: 0.8392156863, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.5254901961, blue: 0.6, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.6784313725, blue: 0.5254901961, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.9058823529, blue: 0.5254901961, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.9137254902, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.8, blue: 0.9137254902, alpha: 1)] + + override init(frame: CGRect) { + super.init(frame: frame) + sharedInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInit() + + } + + override func awakeFromNib() { + super.awakeFromNib() + } + + func sharedInit() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.textColor = .white + titleLabel.font = UIFont.boldSystemFont(ofSize: 32) + self.addSubview(titleLabel) + titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + + self.layer.cornerRadius = 8 + } + + func update(index: Int) { + self.titleLabel.text = String(index + 1) + self.backgroundColor = colors[index % colors.count] + } + + override func prepareForInterfaceBuilder() { + self.update(index: 0) + } +} diff --git a/Example/InfiniteLayout/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/InfiniteLayout/Images.xcassets/AppIcon.appiconset/Contents.json index d3942e9..19882d5 100644 --- a/Example/InfiniteLayout/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/InfiniteLayout/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -29,10 +39,15 @@ "idiom" : "iphone", "size" : "60x60", "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Example/InfiniteLayout/Images.xcassets/Contents.json b/Example/InfiniteLayout/Images.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/InfiniteLayout/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InfiniteLayout/ViewController.swift b/Example/InfiniteLayout/ViewController.swift index c46f550..a75e413 100644 --- a/Example/InfiniteLayout/ViewController.swift +++ b/Example/InfiniteLayout/ViewController.swift @@ -7,18 +7,49 @@ // import UIKit +import InfiniteLayout -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. +class ViewController: InfiniteCollectionViewController { + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard let collectionView = self.collectionView else { + return + } + + let cells = collectionView.indexPathsForVisibleItems.sorted() + for i in 0.. Int { + return 14 + } + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell + cell.update(index: self.infiniteCollectionView!.indexPath(from: indexPath).row) + return cell } - } +extension ViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let height: CGFloat = 100 + let width: CGFloat = height * 2 + return CGSize(width: width, height: height) + } +} diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 0000000..fed76dc --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - InfiniteLayout (0.1.0) + +DEPENDENCIES: + - InfiniteLayout (from `../`) + +EXTERNAL SOURCES: + InfiniteLayout: + :path: ../ + +SPEC CHECKSUMS: + InfiniteLayout: 82a21b8255623e2d72174f3082bef9e576140621 + +PODFILE CHECKSUM: 3a658536624f41ec07c5a7a2c55407b9b8f48528 + +COCOAPODS: 1.4.0.beta.2 diff --git a/InfiniteLayout.podspec b/InfiniteLayout.podspec index 96fd6c1..3a7ab96 100644 --- a/InfiniteLayout.podspec +++ b/InfiniteLayout.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = 'InfiniteLayout' s.version = '0.1.0' - s.summary = 'A short description of InfiniteLayout.' + s.summary = 'Beautiful CollectionView Layout providing InfiniteSrolling, Paging and NSProxy delegate' # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? @@ -26,11 +26,13 @@ TODO: Add long description of the pod here. s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'Arnoymous' => 'ineox@me.com' } s.source = { :git => 'https://github.com/Arnoymous/InfiniteLayout.git', :tag => s.version.to_s } - # s.social_media_url = 'https://twitter.com/' + s.social_media_url = 'https://twitter.com/arnauddorgans' s.ios.deployment_target = '8.0' + s.tvos.deployment_target = '9.0' s.source_files = 'InfiniteLayout/Classes/**/*' +#s.xcconfig = { 'SWIFT_OBJC_BRIDGING_HEADER' => '${POD_ROOT}/InfiniteLayout/BridgeHeader.h' } # s.resource_bundles = { # 'InfiniteLayout' => ['InfiniteLayout/Assets/*.png'] diff --git a/InfiniteLayout/Classes/InfiniteCollectionView.swift b/InfiniteLayout/Classes/InfiniteCollectionView.swift new file mode 100644 index 0000000..27de5cb --- /dev/null +++ b/InfiniteLayout/Classes/InfiniteCollectionView.swift @@ -0,0 +1,161 @@ +// +// InfiniteCollectionView.swift +// InfiniteLayout +// +// Created by Arnaud Dorgans on 20/12/2017. +// + +import UIKit + +open class InfiniteCollectionView: UICollectionView { + + lazy var delegateProxy = InfiniteCollectionViewDelegateProxy(self, exceptions: ["scrollViewDidScroll:", + "scrollViewWillEndDragging:withVelocity:targetContentOffset:"]) + lazy var dataSourceProxy = InfiniteCollectionViewDataSourceProxy(self, exceptions: ["numberOfSectionsInCollectionView:", + "collectionView:numberOfItemsInSection:"]) + + @IBInspectable var isItemPagingEnabled: Bool = false + @IBInspectable var velocityMultiplier: CGFloat = 500 { + didSet { + self.infiniteLayout.velocityMultiplier = velocityMultiplier + } + } + + override open var delegate: UICollectionViewDelegate? { + get { return super.delegate } + set { + guard let newValue = newValue else { + super.delegate = nil + return + } + let isProxy = newValue is InfiniteCollectionViewDelegateProxy + let delegate = isProxy ? newValue : delegateProxy + if !isProxy { + delegateProxy.delegate = newValue + } + super.delegate = delegate + } + } + override open var dataSource: UICollectionViewDataSource? { + get { return super.dataSource } + set { + guard let newValue = newValue else { + super.dataSource = nil + return + } + let isProxy = newValue is InfiniteCollectionViewDataSourceProxy + let dataSource = isProxy ? newValue : dataSourceProxy + if !isProxy { + dataSourceProxy.delegate = newValue + } + super.dataSource = dataSource + } + } + + public var infiniteLayout: InfiniteLayout! { + return self.collectionViewLayout as? InfiniteLayout + } + + public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: InfiniteLayout(layout: layout)) + sharedInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + let layout = self.collectionViewLayout + if !(layout is InfiniteLayout) { + self.collectionViewLayout = InfiniteLayout(layout: layout) + } + sharedInit() + } + + private func sharedInit() { + delegateProxy.collectionView = self + dataSourceProxy.collectionView = self + } + + open override func layoutSubviews() { + super.layoutSubviews() + + self.centerCollectionViewIfNeeded() + self.loopCollectionViewIfNeeded() + } +} + +extension InfiniteCollectionView: UICollectionViewDelegate { + + // MARK: Loop + func loopCollectionViewIfNeeded() { + self.infiniteLayout.loopCollectionViewIfNeeded() + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + delegateProxy.delegate?.scrollViewDidScroll?(scrollView) + self.loopCollectionViewIfNeeded() + } + + // MARK: Paging + func centerCollectionViewIfNeeded() { + guard isItemPagingEnabled, + !self.isDragging && !self.isDecelerating else { + return + } + self.infiniteLayout.centerCollectionViewIfNeeded() + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if isItemPagingEnabled { + self.infiniteLayout.centerCollectionView(withVelocity: velocity, targetContentOffset: targetContentOffset) + } + } +} + +// MARK: DataSource +extension InfiniteCollectionView: UICollectionViewDataSource { + + private var delegateNumberOfSections: Int { + guard let sections = dataSourceProxy.delegate.flatMap({ $0.numberOfSections?(in: self) ?? 1 }) else { + fatalError("collectionView dataSource is required") + } + return sections + } + + private func delegateNumberOfItems(in section: Int) -> Int { + guard let items = dataSourceProxy.delegate.flatMap({ $0.collectionView(self, numberOfItemsInSection: self.section(from: section)) }) else { + fatalError("collectionView dataSource is required") + } + return items + } + + public func section(from infiniteSection: Int) -> Int { + return infiniteSection % delegateNumberOfSections + } + + public func indexPath(from infiniteIndexPath: IndexPath) -> IndexPath { + let items = delegateNumberOfItems(in: infiniteIndexPath.section) + return IndexPath(item: infiniteIndexPath.item % items, section: self.section(from: infiniteIndexPath.section)) + } + + private var multiplier: Int { + let min = Swift.min(self.infiniteLayout.itemSize.width, self.infiniteLayout.itemSize.height) + let count = ceil(InfiniteLayout.minimumContentSize.width / min) + return Int(count) * delegateNumberOfSections + } + + public func numberOfSections(in collectionView: UICollectionView) -> Int { + let delegateNumberOfSections = self.delegateNumberOfSections + return delegateNumberOfSections > 1 ? delegateNumberOfSections * multiplier : delegateNumberOfSections + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + let delegateNumberOfSections = self.delegateNumberOfSections + let delegateNumberOfItems = self.delegateNumberOfItems(in: section) + return delegateNumberOfSections > 1 ? delegateNumberOfItems : delegateNumberOfItems * multiplier + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + fatalError("collectionView dataSource is required") + } +} diff --git a/InfiniteLayout/Classes/InfiniteCollectionViewController.swift b/InfiniteLayout/Classes/InfiniteCollectionViewController.swift new file mode 100644 index 0000000..d3c0d71 --- /dev/null +++ b/InfiniteLayout/Classes/InfiniteCollectionViewController.swift @@ -0,0 +1,23 @@ +// +// InfiniteCollectionViewController.swift +// InfiniteLayout +// +// Created by Arnaud Dorgans on 23/12/2017. +// + +import UIKit + +open class InfiniteCollectionViewController: UICollectionViewController { + + public var infiniteCollectionView: InfiniteCollectionView? { + return self.collectionView as? InfiniteCollectionView + } + + override open func viewDidLoad() { + super.viewDidLoad() + + guard let _ = self.infiniteCollectionView else { + fatalError("InfiniteCollectionView is needed") + } + } +} diff --git a/InfiniteLayout/Classes/InfiniteCollectionViewProxy.h b/InfiniteLayout/Classes/InfiniteCollectionViewProxy.h new file mode 100644 index 0000000..c5f5026 --- /dev/null +++ b/InfiniteLayout/Classes/InfiniteCollectionViewProxy.h @@ -0,0 +1,19 @@ +// +// InfiniteCollectionViewProxy.h +// InfiniteLayout +// +// Created by Arnaud Dorgans on 21/12/2017. +// + +#import + +@interface _InfiniteCollectionViewProxy> : NSProxy + +-(nonnull instancetype)init:(nullable T)collectionView exceptions:(nonnull NSArray*)exceptions; + +@property (nonatomic, weak, nullable) T collectionView; +@property (nonatomic, weak, nullable) T delegate; +@property (nonatomic, nonnull, retain) NSArray* exceptions; + + +@end diff --git a/InfiniteLayout/Classes/InfiniteCollectionViewProxy.m b/InfiniteLayout/Classes/InfiniteCollectionViewProxy.m new file mode 100644 index 0000000..806bfe8 --- /dev/null +++ b/InfiniteLayout/Classes/InfiniteCollectionViewProxy.m @@ -0,0 +1,52 @@ +// +// InfiniteCollectionViewProxy.m +// InfiniteLayout +// +// Created by Arnaud Dorgans on 21/12/2017. +// + +#import "InfiniteCollectionViewProxy.h" + +@implementation _InfiniteCollectionViewProxy + +-(instancetype)init:(nullable id)collectionView exceptions:(nonnull NSArray*)exceptions { + self.collectionView = collectionView; + self.exceptions = exceptions; + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + return ([self.collectionView respondsToSelector:aSelector] || [self.delegate respondsToSelector:aSelector]); +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + NSObject *delegateForResonse = [self.delegate respondsToSelector:aSelector] ? self.delegate : self.collectionView; + NSMethodSignature *signature = [delegateForResonse respondsToSelector:aSelector] ? [delegateForResonse methodSignatureForSelector:aSelector] : nil; + return signature; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + NSString *selectorName = NSStringFromSelector(invocation.selector); + + NSArray> *delegates = @[self.delegate, self.collectionView]; + if ([self.exceptions containsObject:selectorName]) { + delegates = [[delegates reverseObjectEnumerator] allObjects]; + } + for (int i = 0; i < delegates.count; i++) { + if ([delegates[i] respondsToSelector:invocation.selector]) { + [self invokeInvocation:invocation onDelegate:delegates[i]]; + return; + } + } +} + +- (void)invokeInvocation:(NSInvocation *)invocation onDelegate:(id)delegate +{ + if ([delegate respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:delegate]; + } +} + +@end diff --git a/InfiniteLayout/Classes/InfiniteLayout.swift b/InfiniteLayout/Classes/InfiniteLayout.swift new file mode 100644 index 0000000..8782f2d --- /dev/null +++ b/InfiniteLayout/Classes/InfiniteLayout.swift @@ -0,0 +1,248 @@ +// +// InfiniteCollectionView.swift +// InfiniteLayout +// +// Created by Arnaud Dorgans on 20/12/2017. +// + +import UIKit + +open class InfiniteLayout: UICollectionViewFlowLayout { + + public var velocityMultiplier: CGFloat = 500 // used to simulate paging + + private let multiplier: CGFloat = 100 // contentOffset multiplier + + private var contentSize: CGSize = .zero + + private (set) var isEnabled: Bool = false + + open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + return true + } + + public convenience init(layout: UICollectionViewLayout) { + self.init() + guard let layout = layout as? UICollectionViewFlowLayout else { + return + } + self.scrollDirection = layout.scrollDirection + self.minimumLineSpacing = layout.minimumLineSpacing + self.minimumInteritemSpacing = layout.minimumInteritemSpacing + self.itemSize = layout.itemSize + self.sectionInset = layout.sectionInset + self.headerReferenceSize = layout.headerReferenceSize + self.footerReferenceSize = layout.footerReferenceSize + } + + static var minimumContentSize: CGSize { + let max = Swift.max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 4 + return CGSize(width: max, height: max) + } + + override open func prepare() { + let collectionViewContentSize = super.collectionViewContentSize + self.contentSize = CGSize(width: collectionViewContentSize.width, height: collectionViewContentSize.height) + self.isEnabled = (scrollDirection == .horizontal ? self.contentSize.width : self.contentSize.height) >= + InfiniteLayout.minimumContentSize.width + super.prepare() + } + + override open var collectionViewContentSize: CGSize { + guard isEnabled else { + return super.collectionViewContentSize + } + return CGSize(width: scrollDirection == .horizontal ? self.contentSize.width * multiplier : self.contentSize.width, + height: scrollDirection == .vertical ? self.contentSize.height * multiplier : self.contentSize.height) + } + + override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard isEnabled else { + return super.layoutAttributesForElements(in: rect) + } + let page = self.page(for: rect.origin) + var elements = [UICollectionViewLayoutAttributes]() + var rect = self.rect(from: rect) + if (self.scrollDirection == .horizontal && rect.maxX > contentSize.width) || + (self.scrollDirection == .vertical && rect.maxY > contentSize.height) { + let diffRect = CGRect(origin: .zero, size: CGSize(width: self.scrollDirection == .horizontal ? rect.maxX - contentSize.width : rect.width, + height: self.scrollDirection == .vertical ? rect.maxY - contentSize.height : rect.height)) + elements.append(contentsOf: self.elements(in: diffRect, page: self.page(from: page, offset: 1))) + if self.scrollDirection == .horizontal { + rect.size.width -= diffRect.width + } else { + rect.size.height -= diffRect.height + } + } + elements.append(contentsOf: self.elements(in: rect, page: page)) + return elements + } + + private func page(for point: CGPoint) -> CGPoint { + let xPage: CGFloat = floor(point.x / contentSize.width) + let yPage: CGFloat = floor(point.y / contentSize.height) + + return CGPoint(x: self.scrollDirection == .horizontal ? xPage : 0, + y: self.scrollDirection == .vertical ? yPage : 0) + } + + private func page(from page: CGPoint, offset: CGFloat) -> CGPoint { + return CGPoint(x: self.scrollDirection == .horizontal ? page.x + offset : page.x, + y: self.scrollDirection == .vertical ? page.y + offset : page.y) + } + + private func pageIndex(from page: CGPoint) -> CGFloat { + return self.scrollDirection == .horizontal ? page.x : page.y + } + + public func rect(from rect: CGRect, page: CGPoint = .zero) -> CGRect { + var rect = rect + if self.scrollDirection == .horizontal && rect.origin.x < 0 { + rect.origin.x += abs(floor(contentSize.width / rect.origin.x)) * contentSize.width + } else if self.scrollDirection == .vertical && rect.origin.y < 0 { + rect.origin.y += abs(floor(contentSize.height / rect.origin.y)) * contentSize.height + } + rect.origin.x = rect.origin.x.truncatingRemainder(dividingBy: contentSize.width) + rect.origin.y = rect.origin.y.truncatingRemainder(dividingBy: contentSize.height) + rect.origin.x += page.x * contentSize.width + rect.origin.y += page.y * contentSize.height + return rect + } + + private func elements(in rect: CGRect, page: CGPoint) -> [UICollectionViewLayoutAttributes] { + let rect = self.rect(from: rect) + let elements = super.layoutAttributesForElements(in: rect)? + .map { self.layoutAttributes(from: $0, page: page) } + .filter { $0 != nil } + .map { $0! } ?? [] + return elements + } + + private func layoutAttributes(from layoutAttributes: UICollectionViewLayoutAttributes, page: CGPoint) -> UICollectionViewLayoutAttributes! { + guard let attributes = layoutAttributes.copy() as? UICollectionViewLayoutAttributes else { + return nil + } + attributes.frame = rect(from: attributes.frame, page: page) + return attributes + } + + // MARK: Loop + private func preferredContentOffset(forContentOffset contentOffset: CGPoint) -> CGPoint { + return rect(from: CGRect(origin: contentOffset, size: .zero), page: self.page(from: .zero, offset: multiplier / 2)).origin + } + + public func loopCollectionViewIfNeeded() { + guard let collectionView = self.collectionView, self.isEnabled else { + return + } + let page = self.pageIndex(from: self.page(for: collectionView.contentOffset)) + let offset = self.preferredContentOffset(forContentOffset: collectionView.contentOffset) + if (page < 2 || page > self.multiplier - 2) && collectionView.contentOffset != offset { + DispatchQueue.main.async { + collectionView.contentOffset = offset + } + } + } + + // MARK: Paging + func collectionViewRect() -> CGRect? { + guard let collectionView = self.collectionView else { + return nil + } + let margins = UIEdgeInsets(top: collectionView.contentInset.top + collectionView.layoutMargins.top, + left: collectionView.contentInset.left + collectionView.layoutMargins.left, + bottom: collectionView.contentInset.bottom + collectionView.layoutMargins.bottom, + right: collectionView.contentInset.right + collectionView.layoutMargins.right) + + var visibleRect = CGRect() + visibleRect.origin.x = margins.left + visibleRect.origin.y = margins.top + visibleRect.size.width = collectionView.bounds.width - visibleRect.origin.x - margins.right + visibleRect.size.height = collectionView.bounds.height - visibleRect.origin.y - margins.bottom + return visibleRect + } + + func visibleCollectionViewRect() -> CGRect? { + guard let collectionView = self.collectionView, + var collectionViewRect = self.collectionViewRect() else { + return nil + } + collectionViewRect.origin.x += collectionView.contentOffset.x + collectionViewRect.origin.y += collectionView.contentOffset.y + return collectionViewRect + } + + public func visibleLayoutAttributes(at offset: CGPoint? = nil) -> [UICollectionViewLayoutAttributes] { + guard let collectionView = self.collectionView else { + return [] + } + return (self.layoutAttributesForElements(in: CGRect(origin: offset ?? collectionView.contentOffset, size: collectionView.frame.size)) ?? []) + .sorted(by: { lhs, rhs in + guard let lhs = self.centerredContentOffset(forRect: lhs.frame) else { + return false + } + guard let rhs = self.centerredContentOffset(forRect: rhs.frame) else { + return true + } + let value: (CGPoint)->CGFloat = { + return self.scrollDirection == .horizontal ? abs(collectionView.contentOffset.x - $0.x) : abs(collectionView.contentOffset.y - $0.y) + } + return value(lhs) < value(rhs) + }) + } + + public func preferredVisibleLayoutAttributes(at offset: CGPoint? = nil, velocity: CGPoint = .zero, targetOffset: CGPoint? = nil) -> UICollectionViewLayoutAttributes? { + guard let currentOffset = self.collectionView?.contentOffset else { + return nil + } + let direction: (CGPoint)->Bool = { + return self.scrollDirection == .horizontal ? $0.x > currentOffset.x : $0.y > currentOffset.y + } + let velocity = self.scrollDirection == .horizontal ? velocity.x : velocity.y + let targetDirection = direction(targetOffset ?? currentOffset) + return self.visibleLayoutAttributes(at: offset) + .first { attributes in + guard let offset = self.centerredContentOffset(forRect: attributes.frame) else { + return false + } + return direction(offset) == targetDirection || velocity == 0 + } + } + + func centerredContentOffset(forRect rect: CGRect) -> CGPoint? { + guard let collectionView = self.collectionView, + let collectionRect = self.collectionViewRect() else { + return nil + } + return CGPoint(x: self.scrollDirection == .horizontal ? abs(rect.midX - collectionRect.origin.x - collectionRect.width/2) : collectionView.contentOffset.x, + y: self.scrollDirection == .vertical ? abs(rect.midY - collectionRect.origin.y - collectionRect.height/2) : collectionView.contentOffset.y) + } + + public func centerCollectionView(withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let collectionView = self.collectionView, self.isEnabled else { + return + } + let newTarget = CGPoint(x: self.scrollDirection == .horizontal ? collectionView.contentOffset.x + velocity.x * velocityMultiplier : targetContentOffset.pointee.x, + y: self.scrollDirection == .vertical ? collectionView.contentOffset.y + velocity.y * velocityMultiplier : targetContentOffset.pointee.y) + + guard let preferredAttributes = self.preferredVisibleLayoutAttributes(at: newTarget, velocity: velocity, targetOffset: targetContentOffset.pointee), + let offset = self.centerredContentOffset(forRect: preferredAttributes.frame) else { + return + } + targetContentOffset.pointee = offset + } + + public func centerCollectionViewIfNeeded() { + guard let collectionView = self.collectionView, self.isEnabled else { + return + } + guard let preferredAttributes = self.preferredVisibleLayoutAttributes(), + let offset = self.centerredContentOffset(forRect: preferredAttributes.frame), + collectionView.contentOffset != offset else { + return + } + DispatchQueue.main.async { + collectionView.contentOffset = offset + } + } +} diff --git a/InfiniteLayout/Classes/Proxy.swift b/InfiniteLayout/Classes/Proxy.swift new file mode 100644 index 0000000..5c9b926 --- /dev/null +++ b/InfiniteLayout/Classes/Proxy.swift @@ -0,0 +1,16 @@ +// +// Proxy.swift +// InfiniteLayout +// +// Created by Arnaud Dorgans on 20/12/2017. +// + +import UIKit + +class InfiniteCollectionViewDelegateProxy: _InfiniteCollectionViewProxy { + +} + +class InfiniteCollectionViewDataSourceProxy: _InfiniteCollectionViewProxy { + +} diff --git a/InfiniteLayout/Classes/ReplaceMe.swift b/InfiniteLayout/Classes/ReplaceMe.swift deleted file mode 100644 index e69de29..0000000