├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── CollectionKit.podspec ├── CollectionKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── CollectionKit.xcscheme ├── CollectionKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CollectionKitTests ├── BasicProviderSpec.swift ├── CollectionViewSpec.swift ├── ComposedHeaderProviderSpec.swift ├── ComposedProviderSpec.swift ├── EmptyStateProviderSpec.swift ├── FlowLayoutSpec.swift ├── Info.plist ├── RowLayoutSpec.swift ├── TestUils.swift └── WaterfallLayoutSpec.swift ├── Examples ├── CollectionKitExamples.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── CollectionKitExamples.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── CollectionKitExamples │ ├── AnimatorExample │ │ ├── AnimatorExampleViewController.swift │ │ ├── EdgeShrinkAnimator.swift │ │ ├── SelectionButton.swift │ │ └── ZoomAnimator.swift │ ├── ArticleExample │ │ ├── ArticleData.swift │ │ ├── ArticleExampleViewController.swift │ │ └── ArticleView.swift │ ├── ChatExample (Advance) │ │ ├── MessageCell.swift │ │ ├── Messages.swift │ │ └── MessagesViewController.swift │ ├── GridExample │ │ └── GridViewController.swift │ ├── HeaderExample │ │ └── HeaderExampleViewController.swift │ ├── HorizontalGalleryExample │ │ └── HorizontalGalleryViewController.swift │ ├── ReloadAnimationExample │ │ └── ReloadAnimationViewController.swift │ ├── ReloadDataExample │ │ └── ReloadDataViewController.swift │ ├── Supporting Files │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── 1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── winstonthewhitecorgi-600x600.jpg │ │ │ ├── 2.imageset │ │ │ │ ├── 4756075e3470b4af942edb6cba84f670.jpg │ │ │ │ └── Contents.json │ │ │ ├── 3.imageset │ │ │ │ ├── 5b1aa2ce2b5ca1e6bd0208e559c4e010 2.jpg │ │ │ │ └── Contents.json │ │ │ ├── 4.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── b5fd805cf2622c26d3f3d1691c4a8b40 2.jpg │ │ │ ├── 5.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── c7a8d1816c5da74b64878be25fad8722 2.jpg │ │ │ ├── 6.imageset │ │ │ │ ├── 652742ff93d1360b68eccb2826418d7a 2.jpg │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── ic_send.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── ic_send.png │ │ │ │ ├── ic_send_2x.png │ │ │ │ └── ic_send_3x.png │ │ │ ├── l1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── d3ee28bfeb925e231375a3904d6481c7.jpg │ │ │ ├── l2.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── af11b00f2142de8b8f53d2e5a05865bb 2.jpg │ │ │ └── l3.imageset │ │ │ │ ├── 2ea2dfb57c1ea26bf028bde407138fa9.jpg │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── CollectionViewController.swift │ │ ├── DynamicView.swift │ │ ├── ExampleView.swift │ │ ├── Info.plist │ │ ├── LabelProvider.swift │ │ ├── Random.swift │ │ ├── SquareView.swift │ │ ├── TestData.swift │ │ └── Util.swift │ └── ViewController.swift ├── Podfile └── Podfile.lock ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── Resources ├── alignContent.svg ├── alignItems.svg ├── example1.svg ├── example10.svg ├── example11.svg ├── example2.svg ├── example3.svg ├── example4.svg ├── example5.svg ├── example6.svg ├── example7.svg ├── example8.svg ├── example9.svg ├── justifyContents.svg └── v2_migration.md ├── Sources ├── Addon │ ├── EmptyStateProvider.swift │ ├── SimpleViewProvider.swift │ └── SpaceProvider.swift ├── Animator │ ├── Animator.swift │ ├── FadeAnimator.swift │ ├── ScaleAnimator.swift │ └── SimpleAnimator.swift ├── CollectionView.swift ├── DataSource │ ├── ArrayDataSource.swift │ ├── ClosureDataSource.swift │ └── DataSource.swift ├── Extensions │ ├── UIScrollView+CollectionKit.swift │ ├── UIView+CollectionKit.swift │ └── Util.swift ├── Info.plist ├── Layout │ ├── ClosureLayout.swift │ ├── FlowLayout.swift │ ├── InsetLayout.swift │ ├── Layout.swift │ ├── LayoutContext.swift │ ├── OverlayLayout.swift │ ├── RowLayout.swift │ ├── SimpleLayout.swift │ ├── StickyLayout.swift │ ├── TransposeLayout.swift │ ├── VisibleFrameInsetLayout.swift │ ├── WaterfallLayout.swift │ └── WrapperLayout.swift ├── Other │ ├── CollectionReloadable.swift │ ├── CollectionReuseViewManager.swift │ ├── Deprecated.swift │ └── LayoutHelper.swift ├── Protocol │ ├── ItemProvider.swift │ ├── LayoutableProvider.swift │ ├── Provider.swift │ └── SectionProvider.swift ├── Provider │ ├── BasicProvider+Convenience.swift │ ├── BasicProvider.swift │ ├── ComposedHeaderProvider+Convenience.swift │ ├── ComposedHeaderProvider.swift │ ├── ComposedProvider.swift │ ├── EmptyCollectionProvider.swift │ └── FlattenedProvider.swift ├── SizeSource │ ├── AutoLayoutSizeSource.swift │ ├── ClosureSizeSource.swift │ ├── SimpleViewSizeSource.swift │ ├── SizeSource.swift │ └── UIImageSizeSource.swift └── ViewSource │ ├── AnyViewSource.swift │ ├── ClosureViewSource.swift │ ├── ComposedViewSource.swift │ └── ViewSource.swift ├── WobbleAnimator └── WobbleAnimator.swift └── codecov.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | 21 | ## Other 22 | *.xccheckout 23 | *.moved-aside 24 | *.xcuserstate 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | 31 | # Swift Package Manager 32 | # 33 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 34 | # Packages/ 35 | .build/ 36 | 37 | # CocoaPods 38 | # 39 | # We recommend against adding the Pods directory to your .gitignore. However 40 | # you should judge for yourself, the pros and cons are mentioned at: 41 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 42 | # 43 | Pods/ 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | Carthage/Checkouts 49 | Carthage/Build 50 | !Carthage/Build/ 51 | Carthage/Build/* 52 | !Carthage/Build/iOS 53 | Carthage/Build/iOS/* 54 | !Carthage/Build/iOS/YetAnotherAnimationLibrary.* 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/screenshots -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | - force_cast 4 | 5 | opt_in_rules: 6 | - closure_spacing 7 | - empty_count 8 | 9 | included: 10 | - Sources 11 | 12 | identifier_name: 13 | allowed_symbols: 14 | - _ 15 | validates_start_lowercase: false 16 | min_length: 17 | error: 3 18 | excluded: 19 | - i 20 | - id 21 | - at 22 | - to 23 | - x 24 | - y 25 | - dx 26 | - dy -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10 3 | 4 | script: 5 | xcodebuild -workspace CollectionKit.xcworkspace -scheme CollectionKit -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone XR,OS=12.0' build test 6 | 7 | before_install: 8 | - pod repo update 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) -J 'CollectionKit' -------------------------------------------------------------------------------- /CollectionKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CollectionKit" 3 | s.version = "2.4.0" 4 | s.summary = "A modern swift framework for building data-driven reusable collection view components." 5 | 6 | s.description = <<-DESC 7 | ### Features 8 | 9 | * Declaritive API for building collection view components 10 | * Automatically update UI when data changes 11 | * Composable & hot swappable sections, layouts, & animations 12 | * Strong type checking powered by Swift Generics 13 | * Reuse everything! 14 | 15 | We think that populating collection view content should be as simple as building custom UIViews. Sections should be reusable and composable into one another. They should define their own layout be easily animatable as well. CollectionKit is our attempt in solving these problems. UICollectionView has been around for 10 years. It is time that we come up with something better **with Swift**. 16 | 17 | Unlike traditional UICollectionView's `datasource`/`delegate` methods, CollectionKit uses a single **Provider** object that tells `CollectionView` exactly how to display & handle a collection. 18 | 19 | These Providers are easy to construct, and infinitely composable. Providers also have their own animation and layout objects. You can have sections that layouts and behave differently with in a single `CollectionView`. 20 | 21 | CollectionKit already provides many of the commonly used Providers out of the box. But you can still easily create reuseable Provider classes that generalizes on different types on data. Checkout examples down below! 22 | DESC 23 | 24 | s.homepage = "https://github.com/SoySauceLab/CollectionKit" 25 | s.license = 'MIT' 26 | s.author = { "Luke" => "lzhaoyilun@gmail.com" } 27 | s.source = { :git => "https://github.com/SoySauceLab/CollectionKit.git", :tag => s.version.to_s } 28 | 29 | s.ios.deployment_target = '8.0' 30 | s.ios.frameworks = 'UIKit', 'Foundation' 31 | 32 | s.requires_arc = true 33 | 34 | s.default_subspecs = 'Core' 35 | 36 | s.subspec 'Core' do |cs| 37 | cs.source_files = 'Sources/**/*.swift' 38 | end 39 | 40 | s.subspec 'WobbleAnimator' do |cs| 41 | cs.dependency 'CollectionKit/Core' 42 | cs.dependency 'YetAnotherAnimationLibrary', "~> 1.4.0" 43 | cs.source_files = 'WobbleAnimator/**/*.swift' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /CollectionKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CollectionKit.xcodeproj/xcshareddata/xcschemes/CollectionKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /CollectionKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CollectionKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CollectionKitTests/BasicProviderSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicProviderSpec.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-10-17. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | @testable import CollectionKit 10 | import Quick 11 | import Nimble 12 | 13 | 14 | class BasicProviderSpec: QuickSpec { 15 | 16 | override func spec() { 17 | describe("BasicProvider") { 18 | var collectionView: CollectionView! 19 | beforeEach { 20 | collectionView = CollectionView() 21 | collectionView.showsVerticalScrollIndicator = false 22 | collectionView.showsHorizontalScrollIndicator = false 23 | } 24 | 25 | it("support all the initialization methods") { 26 | collectionView.provider = BasicProvider( 27 | dataSource: [0, 1, 2, 4], 28 | viewSource: { (label: UILabel, data: Int, index: Int) in }, 29 | sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 30 | return CGSize(width: 50, height: 50) 31 | }) 32 | 33 | collectionView.provider = BasicProvider( 34 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 35 | viewSource: { (label: UILabel, data: Int, index: Int) in }, 36 | sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 37 | return CGSize(width: 50, height: 50) 38 | }) 39 | 40 | collectionView.provider = BasicProvider( 41 | dataSource: [0, 1, 2, 4], 42 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }), 43 | sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 44 | return CGSize(width: 50, height: 50) 45 | }) 46 | 47 | collectionView.provider = BasicProvider( 48 | dataSource: [0, 1, 2, 4], 49 | viewSource: { (label: UILabel, data: Int, index: Int) in }, 50 | sizeSource: ClosureSizeSource(sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 51 | return CGSize(width: 50, height: 50) 52 | })) 53 | 54 | collectionView.provider = BasicProvider( 55 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 56 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }), 57 | sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 58 | return CGSize(width: 50, height: 50) 59 | }) 60 | 61 | collectionView.provider = BasicProvider( 62 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 63 | viewSource: { (label: UILabel, data: Int, index: Int) in }, 64 | sizeSource: ClosureSizeSource(sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 65 | return CGSize(width: 50, height: 50) 66 | })) 67 | 68 | collectionView.provider = BasicProvider( 69 | dataSource: [0, 1, 2, 4], 70 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }), 71 | sizeSource: ClosureSizeSource(sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 72 | return CGSize(width: 50, height: 50) 73 | })) 74 | 75 | collectionView.provider = BasicProvider( 76 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 77 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }), 78 | sizeSource: ClosureSizeSource(sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 79 | return CGSize(width: 50, height: 50) 80 | })) 81 | 82 | collectionView.provider = BasicProvider( 83 | dataSource: [0, 1, 2, 4], 84 | viewSource: { (label: UILabel, data: Int, index: Int) in } 85 | ) 86 | 87 | collectionView.provider = BasicProvider( 88 | dataSource: [0, 1, 2, 4], 89 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }) 90 | ) 91 | 92 | collectionView.provider = BasicProvider( 93 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 94 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Int, index: Int) in }) 95 | ) 96 | 97 | collectionView.provider = BasicProvider( 98 | dataSource: ArrayDataSource(data: [0, 1, 2, 4]), 99 | viewSource: { (label: UILabel, data: Int, index: Int) in } 100 | ) 101 | } 102 | } 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /CollectionKitTests/EmptyStateProviderSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyStateProviderSpec.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-11-01. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | @testable import CollectionKit 10 | import Quick 11 | import Nimble 12 | 13 | class EmptyStateProviderSpec: QuickSpec { 14 | 15 | override func spec() { 16 | describe("EmptyStateProvider") { 17 | var collectionView: CollectionView! 18 | beforeEach { 19 | collectionView = CollectionView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 20 | collectionView.showsVerticalScrollIndicator = false 21 | collectionView.showsHorizontalScrollIndicator = false 22 | } 23 | 24 | it("displays empty state view correctly") { 25 | let dataSource = ArrayDataSource(data: [0, 1, 2, 4]) 26 | let provider = BasicProvider( 27 | dataSource: dataSource, 28 | viewSource: { (label: UILabel, data: Int, index: Int) in }, 29 | sizeSource: { (index: Int, data: Int, collectionSize: CGSize) -> CGSize in 30 | return CGSize(width: 50, height: 50) 31 | }) 32 | let emptyLabel = UILabel() 33 | emptyLabel.text = "Empty Label" 34 | let emptyProvider = EmptyStateProvider(emptyStateView: emptyLabel, content: provider) 35 | collectionView.provider = emptyProvider 36 | collectionView.layoutIfNeeded() 37 | 38 | expect(collectionView.visibleCells.count) == 4 39 | dataSource.data = [] 40 | collectionView.layoutIfNeeded() 41 | 42 | expect(collectionView.visibleCells.count) == 1 43 | expect((collectionView.subviews[0] as! UILabel).text) == "Empty Label" 44 | 45 | dataSource.data = [1, 2] 46 | collectionView.layoutIfNeeded() 47 | expect(collectionView.visibleCells.count) == 2 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /CollectionKitTests/FlowLayoutSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowLayoutSpec.swift 3 | // CollectionKit 4 | // 5 | // Created by yansong li on 2017-09-04. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import CollectionKit 10 | import Quick 11 | import Nimble 12 | 13 | class FlowLayoutSpec: QuickSpec { 14 | 15 | override func spec() { 16 | describe("FlowLayout") { 17 | it("should not crash when parent size is zero") { 18 | let layout = FlowLayout() 19 | layout.mockLayout(parentSize: (0, 0)) 20 | expect(layout.frames.count).to(equal(0)) 21 | layout.mockLayout(parentSize: (0, 0), (200, 50)) 22 | expect(layout.frames.count).to(equal(1)) 23 | } 24 | 25 | it("should wrap child") { 26 | let layout = FlowLayout() 27 | layout.mockLayout(parentSize: (100, 100), (50, 50), (50, 50), (50, 50)) 28 | expect(layout.frames).to(equal(frames((0, 0, 50, 50), (50, 0, 50, 50), (0, 50, 50, 50)))) 29 | } 30 | 31 | it("should work with linespacing") { 32 | let layout = FlowLayout(lineSpacing: 10) 33 | layout.mockLayout(parentSize: (100, 300), (100, 100), (100, 100), (100, 100)) 34 | expect(layout.frames).to(equal(frames((0, 0, 100, 100), (0, 110, 100, 100), (0, 220, 100, 100)))) 35 | } 36 | 37 | it("should work with interitemspacing") { 38 | let layout = FlowLayout(interitemSpacing: 10) 39 | layout.mockLayout(parentSize: (130, 100), (50, 50), (50, 50), (50, 50)) 40 | expect(layout.frames).to(equal(frames((0, 0, 50, 50), (60, 0, 50, 50), (0, 50, 50, 50)))) 41 | } 42 | 43 | it("should work with interitemspacing and linespacing") { 44 | let layout = FlowLayout(lineSpacing: 10, interitemSpacing: 10) 45 | layout.mockLayout(parentSize: (130, 130), (50, 50), (50, 50), (50, 50)) 46 | expect(layout.frames).to(equal(frames((0, 0, 50, 50), (60, 0, 50, 50), (0, 60, 50, 50)))) 47 | } 48 | 49 | it("should not display cells outside of the visible area") { 50 | let layout = FlowLayout().transposed() 51 | layout.mockLayout(parentSize: (100, 50), (50, 50), (50, 50), (50, 50), (50, 50)) 52 | let visible = layout.visibleIndexes(visibleFrame: CGRect(x: 50, y: 0, width: 100, height: 50)) 53 | expect(visible).to(equal([1, 2])) 54 | } 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /CollectionKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CollectionKitTests/RowLayoutSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionKitTests.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke on 3/20/17. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import CollectionKit 10 | import Quick 11 | import Nimble 12 | 13 | class RowLayoutSpec: QuickSpec { 14 | 15 | override func spec() { 16 | describe("RowLayout") { 17 | it("should not crash when parent size is zero") { 18 | let layout = RowLayout() 19 | layout.mockLayout(parentSize: (0, 0)) 20 | expect(layout.frames.count).to(equal(0)) 21 | layout.mockLayout(parentSize: (0, 0), (200, 50)) 22 | expect(layout.frames.count).to(equal(1)) 23 | } 24 | 25 | it("should work without flex value") { 26 | let layout = RowLayout() 27 | layout.mockLayout((200, 50), (200, 50), (200, 50)) 28 | expect(layout.frames).to(equal(frames((0,0,200,50), (200,0,200,50), (400,0,200,50)))) 29 | } 30 | 31 | it("should expand flexed item when there is space") { 32 | let layout = RowLayout("1") 33 | layout.mockLayout(parentSize: (700, 50), (200, 50), (200, 50), (200, 50)) 34 | expect(layout.frames).to(equal(frames((0,0,200,50), (200,0, 300, 50), (500,0,200,50)))) 35 | } 36 | 37 | it("should not expand flex item where there is not enough space") { 38 | let layout = RowLayout("1", "2") 39 | layout.mockLayout(parentSize: (250, 300), (250, 200), (50, 200), (50, 200)) 40 | expect(layout.frames).to(equal(frames((0,0,250,200), (250,0,50,200), (300,0,50,200)))) 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /CollectionKitTests/TestUils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUils.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-30. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | import UIKit.UIGestureRecognizerSubclass 12 | 13 | extension Layout { 14 | 15 | struct MockLayoutContext: LayoutContext { 16 | var parentSize: (CGFloat, CGFloat) 17 | var childSizes: [(CGFloat, CGFloat)] 18 | var collectionSize: CGSize { 19 | return CGSize(width: parentSize.0, height: parentSize.1) 20 | } 21 | var numberOfItems: Int { 22 | return childSizes.count 23 | } 24 | func data(at: Int) -> Any { 25 | return childSizes[at] 26 | } 27 | func identifier(at: Int) -> String { 28 | return "\(at)" 29 | } 30 | func size(at: Int, collectionSize: CGSize) -> CGSize { 31 | let size = childSizes[at] 32 | return CGSize(width: size.0, height: size.1) 33 | } 34 | } 35 | 36 | func mockLayout(parentSize: (CGFloat, CGFloat) = (300, 300), _ childSizes: (CGFloat, CGFloat)...) { 37 | layout(context: MockLayoutContext(parentSize: parentSize, childSizes: childSizes)) 38 | } 39 | } 40 | 41 | class SimpleTestProvider: BasicProvider { 42 | 43 | var data: [Data] { 44 | get { return (dataSource as! ArrayDataSource).data } 45 | set { (dataSource as! ArrayDataSource).data = newValue } 46 | } 47 | 48 | convenience init(data: [Data]) { 49 | self.init( 50 | dataSource: ArrayDataSource(data: data, identifierMapper: { _, data in "\(data)" }), 51 | viewSource: ClosureViewSource(viewUpdater: { (label: UILabel, data: Data, index: Int) in 52 | label.backgroundColor = .red 53 | label.layer.cornerRadius = 8 54 | label.textAlignment = .center 55 | label.text = "\(data)" 56 | }), 57 | sizeSource: ClosureSizeSource(sizeSource: { (index: Int, data: Data, collectionSize: CGSize) -> CGSize in 58 | return CGSize(width: 50, height: 50) 59 | }) 60 | ) 61 | } 62 | 63 | } 64 | 65 | func sizes(_ s: [(CGFloat, CGFloat)]) -> [CGSize] { 66 | return s.map { CGSize(width: $0.0, height: $0.1) } 67 | } 68 | 69 | func sizes(_ s: (CGFloat, CGFloat)...) -> [CGSize] { 70 | return sizes(s) 71 | } 72 | 73 | func frames(_ f: [(CGFloat, CGFloat, CGFloat, CGFloat)]) -> [CGRect] { 74 | return f.map { CGRect(x: $0.0, y: $0.1, width: $0.2, height: $0.3) } 75 | } 76 | 77 | func frames(_ f: (CGFloat, CGFloat, CGFloat, CGFloat)...) -> [CGRect] { 78 | return frames(f) 79 | } 80 | 81 | 82 | extension UITapGestureRecognizer { 83 | static var testLocation: CGPoint? = { 84 | let swizzling: (AnyClass, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in 85 | let originalMethod = class_getInstanceMethod(forClass, originalSelector) 86 | let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) 87 | method_exchangeImplementations(originalMethod!, swizzledMethod!) 88 | } 89 | let originalSelector = #selector(location(in:)) 90 | let swizzledSelector = #selector(test_location(in:)) 91 | swizzling(UITapGestureRecognizer.self, originalSelector, swizzledSelector) 92 | return nil 93 | }() 94 | 95 | 96 | @objc dynamic func test_location(in view: UIView?) -> CGPoint { 97 | guard let testLocation = UITapGestureRecognizer.testLocation, let parent = self.view else { return test_location(in: view) } 98 | return (view ?? parent).convert(testLocation, from: parent) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CollectionKitTests/WaterfallLayoutSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaterfallLayoutSpec.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-10-17. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import CollectionKit 10 | import Quick 11 | import Nimble 12 | 13 | class WaterfallLayoutSpec: QuickSpec { 14 | 15 | override func spec() { 16 | describe("WaterfallLayout") { 17 | it("should not crash when parent size is zero") { 18 | let layout = WaterfallLayout() 19 | layout.mockLayout(parentSize: (0, 0)) 20 | expect(layout.frames.count).to(equal(0)) 21 | layout.mockLayout(parentSize: (0, 0), (200, 50)) 22 | expect(layout.frames.count).to(equal(1)) 23 | } 24 | 25 | it("should wrap child") { 26 | let layout = WaterfallLayout() 27 | layout.mockLayout(parentSize: (100, 100), (50, 50), (50, 50), (50, 50)) 28 | expect(layout.frames).to(equal(frames((0, 0, 50, 50), (50, 0, 50, 50), (0, 50, 50, 50)))) 29 | } 30 | 31 | it("should work with spacing") { 32 | let layout = WaterfallLayout(spacing: 10) 33 | layout.mockLayout(parentSize: (100, 300), (100, 100), (100, 100), (100, 100)) 34 | expect(layout.frames).to(equal(frames((0, 0, 45, 100), (55, 0, 45, 100), (0, 110, 45, 100)))) 35 | } 36 | 37 | it("should work with columns") { 38 | let layout = WaterfallLayout(columns: 1) 39 | layout.mockLayout(parentSize: (120, 100), (50, 50), (50, 50), (50, 50)) 40 | expect(layout.frames).to(equal(frames((0, 0, 120, 50), (0, 50, 120, 50), (0, 100, 120, 50)))) 41 | let layout2 = WaterfallLayout(columns: 3) 42 | layout2.mockLayout(parentSize: (120, 100), (50, 50), (50, 50), (50, 50)) 43 | expect(layout2.frames).to(equal(frames((0, 0, 40, 50), (40, 0, 40, 50), (80, 0, 40, 50)))) 44 | } 45 | 46 | it("should not display cells outside of the visible area") { 47 | let layout = WaterfallLayout() 48 | layout.mockLayout(parentSize: (100, 50), (50, 50), (50, 50), (50, 50), (50, 50)) 49 | let visible = layout.visibleIndexes(visibleFrame: CGRect(x: 0, y: 50, width: 100, height: 50)) 50 | expect(visible).to(equal([2, 3])) 51 | } 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/AnimatorExample/AnimatorExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatorExampleViewController.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2017-09-04. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class AnimatorExampleViewController: CollectionViewController { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | let animators = [ 17 | ("Default", Animator()), 18 | ("Wobble", WobbleAnimator()), 19 | ("Edge Shrink", EdgeShrinkAnimator()), 20 | ("Zoom", ZoomAnimator()), 21 | ] 22 | 23 | let imagesCollectionView = CollectionView() 24 | let visibleFrameInsets = UIEdgeInsets(top: 0, left: -100, bottom: 0, right: -100) 25 | 26 | let imageProvider = BasicProvider( 27 | dataSource: testImages, 28 | viewSource: ClosureViewSource(viewGenerator: { (data, index) -> UIImageView in 29 | let view = UIImageView() 30 | view.layer.cornerRadius = 5 31 | view.clipsToBounds = true 32 | return view 33 | }, viewUpdater: { (view: UIImageView, data: UIImage, at: Int) in 34 | view.image = data 35 | }), 36 | sizeSource: UIImageSizeSource(), 37 | layout: WaterfallLayout(columns: 2, spacing: 10).transposed().inset(by: bodyInset).insetVisibleFrame(by: visibleFrameInsets), 38 | animator: animators[0].1 39 | ) 40 | 41 | imagesCollectionView.provider = imageProvider 42 | 43 | let buttonsCollectionView = CollectionView() 44 | buttonsCollectionView.showsHorizontalScrollIndicator = false 45 | 46 | let buttonsProvider = BasicProvider( 47 | dataSource: animators, 48 | viewSource: { (view: SelectionButton, data: (String, Animator), at: Int) in 49 | view.label.text = data.0 50 | view.label.textColor = imageProvider.animator === data.1 ? .white : .black 51 | view.backgroundColor = imageProvider.animator === data.1 ? .lightGray : .white 52 | }, 53 | sizeSource: { _, data, maxSize in 54 | return CGSize(width: data.0.width(withConstraintedHeight: maxSize.height, font: UIFont.systemFont(ofSize:18)) + 20, height: maxSize.height) 55 | }, 56 | layout: FlowLayout(lineSpacing: 10).transposed() 57 | .inset(by: UIEdgeInsets(top: 10, left: 16, bottom: 0, right: 16)), 58 | animator: WobbleAnimator(), 59 | tapHandler: { context in 60 | imageProvider.animator = context.data.1 61 | 62 | // clear previous styles 63 | for cell in imagesCollectionView.visibleCells { 64 | cell.alpha = 1 65 | cell.transform = .identity 66 | } 67 | 68 | context.setNeedsReload() 69 | } 70 | ) 71 | 72 | buttonsCollectionView.provider = buttonsProvider 73 | 74 | let buttonsViewProvider = SimpleViewProvider( 75 | views: [buttonsCollectionView], 76 | sizeStrategy: (.fill, .absolute(44)) 77 | ) 78 | let providerViewProvider = SimpleViewProvider( 79 | identifier: "providerContent", 80 | views: [imagesCollectionView], 81 | sizeStrategy: (.fill, .fill) 82 | ) 83 | 84 | provider = ComposedProvider( 85 | layout: RowLayout("providerContent").transposed(), 86 | sections: [ 87 | buttonsViewProvider, 88 | providerViewProvider 89 | ] 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/AnimatorExample/EdgeShrinkAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeShrinkAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | open class EdgeShrinkAnimator: Animator { 13 | open override func update(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 14 | super.update(collectionView: collectionView, view: view, at: at, frame: frame) 15 | let effectiveRange: ClosedRange = -500...16 16 | let absolutePosition = frame.origin - collectionView.contentOffset 17 | if absolutePosition.x < effectiveRange.lowerBound { 18 | view.transform = .identity 19 | return 20 | } 21 | let scale = (absolutePosition.x.clamp(effectiveRange.lowerBound, effectiveRange.upperBound) - effectiveRange.lowerBound) / (effectiveRange.upperBound - effectiveRange.lowerBound) 22 | let translation = absolutePosition.x < effectiveRange.upperBound ? effectiveRange.upperBound - absolutePosition.x - (1 - scale) / 2 * frame.width : 0 23 | view.transform = CGAffineTransform.identity.translatedBy(x: translation, y: 0).scaledBy(x: scale, y: scale) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/AnimatorExample/SelectionButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionButton.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-25. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SelectionButton: DynamicView { 12 | let label = UILabel() 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | label.textAlignment = .center 16 | layer.cornerRadius = 5 17 | addSubview(label) 18 | } 19 | public required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | override func layoutSubviews() { 23 | super.layoutSubviews() 24 | label.frame = bounds 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/AnimatorExample/ZoomAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | open class ZoomAnimator: Animator { 13 | open override func update(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 14 | super.update(collectionView: collectionView, view: view, at: at, frame: frame) 15 | let bounds = CGRect(origin: .zero, size: collectionView.bounds.size) 16 | let absolutePosition = frame.center - collectionView.contentOffset 17 | let scale = 1 - max(0, absolutePosition.distance(bounds.center) - 150) / (max(bounds.width, bounds.height) - 150) 18 | view.transform = CGAffineTransform.identity.scaledBy(x: scale, y: scale) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ArticleExample/ArticleData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleData.swift 3 | // CollectionKit 4 | // 5 | // Created by yansong li on 2017-09-03. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct ArticleData { 12 | let hueValue: CGFloat 13 | let title: String 14 | let subTitle: String 15 | } 16 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ArticleExample/ArticleExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediumViewController.swift 3 | // CollectionKit 4 | // 5 | // Created by yansong li on 2017-09-03. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import CollectionKit 10 | import UIKit 11 | 12 | class ArticleExampleViewController: CollectionViewController { 13 | 14 | let articles: [ArticleData] = { 15 | let count = 20 16 | return (1...count).map { 17 | ArticleData(hueValue: CGFloat($0) / CGFloat(count), 18 | title: "Article \($0)", 19 | subTitle: "This is the subtitle for article \($0)") 20 | } 21 | }() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | collectionView.contentInset = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16) 27 | provider = BasicProvider( 28 | dataSource: articles, 29 | viewSource: { (view: ArticleView, data: ArticleData, at: Int) in 30 | view.populate(article: data) 31 | }, 32 | sizeSource: { (_, view, size) -> CGSize in 33 | return CGSize(width: size.width, height: 200) 34 | }, 35 | layout: FlowLayout(lineSpacing: 30) 36 | ) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ArticleExample/ArticleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleView.swift 3 | // CollectionKit 4 | // 5 | // Created by yansong li on 2017-09-03. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ArticleView: UIView { 12 | let colorView = UIView() 13 | 14 | let titleLabel: UILabel = { 15 | let titleLabel = UILabel() 16 | titleLabel.numberOfLines = 1 17 | titleLabel.font = .boldSystemFont(ofSize: 20) 18 | return titleLabel 19 | }() 20 | 21 | let subTitleLabel: UILabel = { 22 | let subTitleLabel = UILabel() 23 | subTitleLabel.numberOfLines = 2 24 | subTitleLabel.font = .systemFont(ofSize: 16) 25 | return subTitleLabel 26 | }() 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | backgroundColor = .white 31 | layer.shadowColor = UIColor.black.cgColor 32 | layer.shadowOffset = CGSize(width: 0, height: 12) 33 | layer.shadowRadius = 10 34 | layer.shadowOpacity = 0.1 35 | addSubview(colorView) 36 | addSubview(titleLabel) 37 | addSubview(subTitleLabel) 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | func populate(article data: ArticleData) { 45 | colorView.backgroundColor = 46 | UIColor(hue: data.hueValue, saturation: 0.68, brightness: 0.98, alpha: 1) 47 | titleLabel.text = data.title 48 | subTitleLabel.text = data.subTitle 49 | } 50 | 51 | override func layoutSubviews() { 52 | super.layoutSubviews() 53 | colorView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 120) 54 | titleLabel.frame = CGRect(x: 10, y: 130, width: bounds.width - 20, height: 30) 55 | subTitleLabel.frame = CGRect(x: 10, y: 160, width: bounds.width - 20, height: 30) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ChatExample (Advance)/MessageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageTextCell.swift 3 | // CollectionKitExample 4 | // 5 | // Created by YiLun Zhao on 2016-02-20. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class TextMessageCell: MessageCell { 13 | var textLabel = UILabel() 14 | 15 | override var message: Message! { 16 | didSet { 17 | textLabel.text = message.content 18 | textLabel.textColor = message.textColor 19 | textLabel.font = UIFont.systemFont(ofSize: message.fontSize) 20 | } 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | textLabel.frame = frame 26 | textLabel.numberOfLines = 0 27 | addSubview(textLabel) 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | override func layoutSubviews() { 35 | super.layoutSubviews() 36 | textLabel.frame = bounds.insetBy(dx: message.cellPadding, dy: message.cellPadding) 37 | } 38 | } 39 | 40 | class ImageMessageCell: MessageCell { 41 | var imageView = UIImageView() 42 | 43 | override var message: Message! { 44 | didSet { 45 | imageView.image = UIImage(named: message.content) 46 | } 47 | } 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | imageView.frame = bounds 52 | imageView.contentMode = .scaleAspectFill 53 | clipsToBounds = true 54 | addSubview(imageView) 55 | } 56 | 57 | required init?(coder aDecoder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | override func layoutSubviews() { 62 | super.layoutSubviews() 63 | imageView.frame = bounds 64 | } 65 | } 66 | 67 | class MessageCell: DynamicView { 68 | 69 | var message: Message! { 70 | didSet { 71 | layer.cornerRadius = message.roundedCornder ? 10 : 0 72 | 73 | if message.showShadow { 74 | layer.shadowOffset = CGSize(width: 0, height: 5) 75 | layer.shadowOpacity = 0.3 76 | layer.shadowRadius = 8 77 | layer.shadowColor = message.shadowColor.cgColor 78 | layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath 79 | } else { 80 | layer.shadowOpacity = 0 81 | layer.shadowColor = nil 82 | } 83 | 84 | backgroundColor = message.backgroundColor 85 | } 86 | } 87 | 88 | override init(frame: CGRect) { 89 | super.init(frame: frame) 90 | layer.shouldRasterize = true 91 | layer.rasterizationScale = UIScreen.main.scale 92 | isOpaque = true 93 | } 94 | 95 | required init?(coder aDecoder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | override func layoutSubviews() { 100 | super.layoutSubviews() 101 | if message?.showShadow ?? false { 102 | layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath 103 | } 104 | } 105 | 106 | static func sizeForText(_ text: String, fontSize: CGFloat, maxWidth: CGFloat, padding: CGFloat) -> CGSize { 107 | let maxSize = CGSize(width: maxWidth, height: 0) 108 | let font = UIFont.systemFont(ofSize: fontSize) 109 | var rect = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, 110 | attributes: [ NSAttributedString.Key.font: font ], context: nil) 111 | rect.size = CGSize(width: ceil(rect.size.width) + 2 * padding, height: ceil(rect.size.height) + 2 * padding) 112 | return rect.size 113 | } 114 | 115 | static func frameForMessage(_ message: Message, containerWidth: CGFloat) -> CGRect { 116 | if message.type == .image { 117 | var imageSize = UIImage(named: message.content)!.size 118 | let maxImageSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 120) 119 | if imageSize.width > maxImageSize.width { 120 | imageSize.height /= imageSize.width/maxImageSize.width 121 | imageSize.width = maxImageSize.width 122 | } 123 | if imageSize.height > maxImageSize.height { 124 | imageSize.width /= imageSize.height/maxImageSize.height 125 | imageSize.height = maxImageSize.height 126 | } 127 | return CGRect(origin: CGPoint(x: message.alignment == .right ? containerWidth - imageSize.width : 0, y: 0), size: imageSize) 128 | } 129 | if message.alignment == .center { 130 | let size = sizeForText(message.content, fontSize: message.fontSize, maxWidth: containerWidth, padding: message.cellPadding) 131 | return CGRect(x: (containerWidth - size.width)/2, y: 0, width: size.width, height: size.height) 132 | } else { 133 | let size = sizeForText(message.content, fontSize: message.fontSize, maxWidth: containerWidth - 50, padding: message.cellPadding) 134 | let origin = CGPoint(x: message.alignment == .right ? containerWidth - size.width : 0, y: 0) 135 | return CGRect(origin: origin, size: size) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ChatExample (Advance)/Messages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Messages.swift 3 | // CollectionKitExample 4 | // 5 | // Created by YiLun Zhao on 2016-02-23. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum MessageType { 12 | case text 13 | case announcement 14 | case status 15 | case image 16 | } 17 | enum MessageAlignment { 18 | case left 19 | case center 20 | case right 21 | } 22 | class Message { 23 | var identifier: String = UUID().uuidString 24 | var fromCurrentUser = false 25 | var content = "" 26 | var type: MessageType 27 | 28 | init(_ fromCurrentUser: Bool, content: String) { 29 | self.fromCurrentUser = fromCurrentUser 30 | self.type = .text 31 | self.content = content 32 | } 33 | init(_ fromCurrentUser: Bool, status: String) { 34 | self.fromCurrentUser = fromCurrentUser 35 | self.type = .status 36 | self.content = status 37 | } 38 | init(_ fromCurrentUser: Bool, image: String) { 39 | self.fromCurrentUser = fromCurrentUser 40 | self.type = .image 41 | self.content = image 42 | } 43 | init(announcement: String) { 44 | self.type = .announcement 45 | self.content = announcement 46 | } 47 | 48 | var fontSize: CGFloat { 49 | switch type { 50 | case .text: return 14 51 | default: return 12 52 | } 53 | } 54 | var cellPadding: CGFloat { 55 | switch type { 56 | case .announcement: return 4 57 | case .text: return 12 58 | case .status: return 2 59 | case .image: return 0 60 | } 61 | } 62 | var showShadow: Bool { 63 | switch type { 64 | case .text, .image: return true 65 | default: return false 66 | } 67 | } 68 | var roundedCornder: Bool { 69 | switch type { 70 | case .announcement: return false 71 | default: return true 72 | } 73 | } 74 | var textColor: UIColor { 75 | switch type { 76 | case .text: 77 | if fromCurrentUser { 78 | return UIColor.white 79 | } else { 80 | return UIColor(red: 131/255, green: 138/255, blue: 147/255, alpha: 1.0) 81 | } 82 | default: 83 | return UIColor(red: 131/255, green: 138/255, blue: 147/255, alpha: 1.0) 84 | } 85 | } 86 | 87 | var backgroundColor: UIColor { 88 | switch type { 89 | case .text: 90 | if fromCurrentUser { 91 | return .lightBlue 92 | } else { 93 | return UIColor(white: showShadow ? 1.0 : 0.95, alpha: 1.0) 94 | } 95 | default: 96 | return UIColor.clear 97 | } 98 | } 99 | var shadowColor: UIColor { 100 | switch type { 101 | case .text: 102 | if fromCurrentUser { 103 | return UIColor(red: 0.1, green: 140/255, blue: 1.0, alpha: 1.0) 104 | } else { 105 | return UIColor(white: 0.8, alpha: 1.0) 106 | } 107 | case .image: 108 | return UIColor(white: 0.4, alpha: 1.0) 109 | default: 110 | return UIColor.clear 111 | } 112 | } 113 | var alignment: MessageAlignment { 114 | switch type { 115 | case .announcement: return .center 116 | default: return (fromCurrentUser ? .right : .left) 117 | } 118 | } 119 | 120 | func verticalPaddingBetweenMessage(_ previousMessage: Message) -> CGFloat { 121 | if type == .image && previousMessage.type == .image { 122 | return 2 123 | } 124 | if type == .announcement { 125 | return 15 126 | } 127 | if previousMessage.type == .announcement { 128 | return 5 129 | } 130 | if type == .status { 131 | return 3 132 | } 133 | if type == .text && type == previousMessage.type && fromCurrentUser == previousMessage.fromCurrentUser { 134 | return 5 135 | } 136 | return 15 137 | } 138 | 139 | func copy() -> Message { 140 | switch type { 141 | case .image: 142 | return Message(fromCurrentUser, image: content) 143 | case .announcement: 144 | return Message(announcement: content) 145 | case .text: 146 | return Message(fromCurrentUser, content: content) 147 | case .status: 148 | return Message(fromCurrentUser, status: content) 149 | } 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/GridExample/GridViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridViewController.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2016-06-05. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | let kGridCellSize = CGSize(width: 50, height: 50) 13 | let kGridSize = (width: 20, height: 20) 14 | let kGridCellPadding:CGFloat = 10 15 | 16 | class GridViewController: CollectionViewController { 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | let dataSource = ArrayDataSource(data: Array(1...kGridSize.width * kGridSize.height), identifierMapper: { (_, data) in 21 | return "\(data)" 22 | }) 23 | let visibleFrameInsets = UIEdgeInsets(top: -150, left: -150, bottom: -150, right: -150) 24 | let layout = Closurelayout(frameProvider: { (i: Int, _) in 25 | CGRect(x: CGFloat(i % kGridSize.width) * (kGridCellSize.width + kGridCellPadding), 26 | y: CGFloat(i / kGridSize.width) * (kGridCellSize.height + kGridCellPadding), 27 | width: kGridCellSize.width, 28 | height: kGridCellSize.height) 29 | }).insetVisibleFrame(by: visibleFrameInsets) 30 | 31 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 32 | provider = BasicProvider( 33 | dataSource: dataSource, 34 | viewSource: { (view: SquareView, data: Int, index: Int) in 35 | view.backgroundColor = UIColor(hue: CGFloat(index) / CGFloat(kGridSize.width * kGridSize.height), 36 | saturation: 0.68, brightness: 0.98, alpha: 1) 37 | view.text = "\(data)" 38 | }, 39 | layout: layout, 40 | animator: WobbleAnimator() 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/HeaderExample/HeaderExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderExampleViewController.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2018-06-09. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class HeaderExampleViewController: CollectionViewController { 13 | 14 | let toggleButton: UIButton = { 15 | let button = UIButton() 16 | button.setTitle("Toggle Sticky Header", for: .normal) 17 | button.titleLabel?.font = .boldSystemFont(ofSize: 20) 18 | button.backgroundColor = UIColor(hue: 0.6, saturation: 0.68, brightness: 0.98, alpha: 1) 19 | button.layer.shadowColor = UIColor.black.cgColor 20 | button.layer.shadowOffset = CGSize(width: 0, height: -12) 21 | button.layer.shadowRadius = 10 22 | button.layer.shadowOpacity = 0.1 23 | return button 24 | }() 25 | 26 | var headerComposer: ComposedHeaderProvider! 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | toggleButton.addTarget(self, action: #selector(toggleSticky), for: .touchUpInside) 32 | view.addSubview(toggleButton) 33 | 34 | collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0) 35 | 36 | let sections: [Provider] = (1...10).map { _ in 37 | return BasicProvider( 38 | dataSource: ArrayDataSource(data: Array(1...9)), 39 | viewSource: ClosureViewSource(viewUpdater: { (view: SquareView, data: Int, index: Int) in 40 | view.backgroundColor = UIColor(hue: CGFloat(index) / 10, 41 | saturation: 0.68, brightness: 0.98, alpha: 1) 42 | view.text = "\(data)" 43 | }), 44 | sizeSource: { (index, data, maxSize) -> CGSize in 45 | return CGSize(width: 80, height: 80) 46 | }, 47 | layout: FlowLayout(spacing: 10, justifyContent: .spaceAround, alignItems: .center) 48 | .inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)) 49 | ) 50 | } 51 | 52 | let provider = ComposedHeaderProvider( 53 | headerViewSource: { (view: UILabel, data, index) in 54 | view.backgroundColor = UIColor.darkGray 55 | view.textColor = .white 56 | view.textAlignment = .center 57 | view.text = "Header \(data.index)" 58 | }, 59 | headerSizeSource: { (index, data, maxSize) -> CGSize in 60 | return CGSize(width: maxSize.width, height: 40) 61 | }, 62 | sections: sections 63 | ) 64 | 65 | self.headerComposer = provider 66 | self.provider = provider 67 | } 68 | 69 | @objc func toggleSticky() { 70 | headerComposer.isSticky = !headerComposer.isSticky 71 | } 72 | 73 | override func viewDidLayoutSubviews() { 74 | super.viewDidLayoutSubviews() 75 | toggleButton.frame = CGRect(x: 0, y: view.bounds.height - 44, width: view.bounds.width, height: 44) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/HorizontalGalleryExample/HorizontalGalleryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalGalleryViewController.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2016-06-14. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class HorizontalGalleryViewController: CollectionViewController { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 17 | 18 | let visibleFrameInsets = UIEdgeInsets(top: 0, left: -100, bottom: 0, right: -100) 19 | provider = BasicProvider( 20 | dataSource: testImages, 21 | viewSource: ClosureViewSource(viewGenerator: { (data, index) -> UIImageView in 22 | let view = UIImageView() 23 | view.layer.cornerRadius = 5 24 | view.clipsToBounds = true 25 | return view 26 | }, viewUpdater: { (view: UIImageView, data: UIImage, at: Int) in 27 | view.image = data 28 | }), 29 | sizeSource: UIImageSizeSource(), 30 | layout: WaterfallLayout(columns: 2, spacing: 10).transposed().insetVisibleFrame(by: visibleFrameInsets), 31 | animator: WobbleAnimator() 32 | ) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ReloadDataExample/ReloadDataViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReloadDataViewController.swift 3 | // CollectionKit 4 | // 5 | // Created by yansong li on 2017-09-04. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class ReloadDataViewController: CollectionViewController { 13 | 14 | let dataSource = ArrayDataSource(data: Array(0..<5)) { (_, data) in 15 | return "\(data)" 16 | } 17 | 18 | let addButton: UIButton = { 19 | let button = UIButton() 20 | button.setTitle("+", for: .normal) 21 | button.titleLabel?.font = .boldSystemFont(ofSize: 20) 22 | button.backgroundColor = UIColor(hue: 0.6, saturation: 0.68, brightness: 0.98, alpha: 1) 23 | button.layer.shadowColor = UIColor.black.cgColor 24 | button.layer.shadowOffset = CGSize(width: 0, height: -12) 25 | button.layer.shadowRadius = 10 26 | button.layer.shadowOpacity = 0.1 27 | return button 28 | }() 29 | 30 | var currentMax: Int = 5 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | addButton.addTarget(self, action: #selector(add), for: .touchUpInside) 35 | view.addSubview(addButton) 36 | 37 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 54, right: 10) 38 | 39 | provider = BasicProvider( 40 | dataSource: dataSource, 41 | viewSource: { (view: SquareView, data: Int, index: Int) in 42 | view.backgroundColor = UIColor(hue: CGFloat(data) / 30, 43 | saturation: 0.68, 44 | brightness: 0.98, 45 | alpha: 1) 46 | view.text = "\(data)" 47 | }, 48 | sizeSource: { (index, data, _) in 49 | return CGSize(width: 80, height: data % 3 == 0 ? 120 : 80) 50 | }, 51 | layout: FlowLayout(lineSpacing: 15, 52 | interitemSpacing: 15, 53 | justifyContent: .spaceAround, 54 | alignItems: .center, 55 | alignContent: .center), 56 | animator: ScaleAnimator(), 57 | tapHandler: { [weak self] context in 58 | self?.dataSource.data.remove(at: context.index) 59 | } 60 | ) 61 | } 62 | 63 | override func viewDidLayoutSubviews() { 64 | super.viewDidLayoutSubviews() 65 | addButton.frame = CGRect(x: 0, y: view.bounds.height - 44, 66 | width: view.bounds.width, height: 44) 67 | } 68 | 69 | @objc func add() { 70 | dataSource.data.append(currentMax) 71 | currentMax += 1 72 | // NOTE: Call reloadData() directly will make collectionView update immediately, so that contentSize 73 | // of collectionView will be updated. 74 | collectionView.reloadData() 75 | collectionView.scrollTo(edge: .bottom, animated:true) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CollectionKitExample 4 | // 5 | // Created by YiLun Zhao on 2016-02-12. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window!.backgroundColor = .white 20 | window!.rootViewController = ViewController() 21 | window!.makeKeyAndVisible() 22 | return true 23 | } 24 | 25 | func applicationWillResignActive(_ application: UIApplication) { 26 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 28 | } 29 | 30 | func applicationDidEnterBackground(_ application: UIApplication) { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(_ application: UIApplication) { 36 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 37 | } 38 | 39 | func applicationDidBecomeActive(_ application: UIApplication) { 40 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 41 | } 42 | 43 | func applicationWillTerminate(_ application: UIApplication) { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "winstonthewhitecorgi-600x600.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/1.imageset/winstonthewhitecorgi-600x600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/1.imageset/winstonthewhitecorgi-600x600.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/2.imageset/4756075e3470b4af942edb6cba84f670.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/2.imageset/4756075e3470b4af942edb6cba84f670.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "4756075e3470b4af942edb6cba84f670.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/3.imageset/5b1aa2ce2b5ca1e6bd0208e559c4e010 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/3.imageset/5b1aa2ce2b5ca1e6bd0208e559c4e010 2.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "5b1aa2ce2b5ca1e6bd0208e559c4e010 2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "b5fd805cf2622c26d3f3d1691c4a8b40 2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/4.imageset/b5fd805cf2622c26d3f3d1691c4a8b40 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/4.imageset/b5fd805cf2622c26d3f3d1691c4a8b40 2.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "c7a8d1816c5da74b64878be25fad8722 2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/5.imageset/c7a8d1816c5da74b64878be25fad8722 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/5.imageset/c7a8d1816c5da74b64878be25fad8722 2.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/6.imageset/652742ff93d1360b68eccb2826418d7a 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/6.imageset/652742ff93d1360b68eccb2826418d7a 2.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "652742ff93d1360b68eccb2826418d7a 2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_send.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_send_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_send_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send.png -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send_2x.png -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/ic_send.imageset/ic_send_3x.png -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "d3ee28bfeb925e231375a3904d6481c7.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l1.imageset/d3ee28bfeb925e231375a3904d6481c7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l1.imageset/d3ee28bfeb925e231375a3904d6481c7.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "af11b00f2142de8b8f53d2e5a05865bb 2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l2.imageset/af11b00f2142de8b8f53d2e5a05865bb 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l2.imageset/af11b00f2142de8b8f53d2e5a05865bb 2.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l3.imageset/2ea2dfb57c1ea26bf028bde407138fa9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoySauceLab/CollectionKit/daf0bc37651a9d335c1292390420f9bf4d8c1209/Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l3.imageset/2ea2dfb57c1ea26bf028bde407138fa9.jpg -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Assets.xcassets/l3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "2ea2dfb57c1ea26bf028bde407138fa9.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2017-09-04. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class CollectionViewController: UIViewController { 13 | let collectionView = CollectionView() 14 | 15 | var provider: Provider? { 16 | get { return collectionView.provider } 17 | set { collectionView.provider = newValue } 18 | } 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | view.addSubview(collectionView) 23 | } 24 | 25 | override func viewDidLayoutSubviews() { 26 | super.viewDidLayoutSubviews() 27 | collectionView.frame = view.bounds 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/DynamicView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicView.swift 3 | // CollectionKitExample 4 | // 5 | // Created by YiLun Zhao on 2016-02-21. 6 | // Copyright © 2016 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let kTiltAnimationVelocityListenerIdentifier = "kTiltAnimationVelocityListenerIdentifier" 12 | 13 | open class DynamicView: UIView { 14 | open var tapAnimation = true 15 | 16 | open var tiltAnimation = false { 17 | didSet { 18 | if tiltAnimation { 19 | yaal.center.velocity.changes.addListenerWith(identifier:kTiltAnimationVelocityListenerIdentifier) { [weak self] _, v in 20 | self?.velocityUpdated(v) 21 | } 22 | } else { 23 | yaal.center.velocity.changes.removeListenerWith(identifier: kTiltAnimationVelocityListenerIdentifier) 24 | yaal.rotationX.animateTo(0, stiffness: 150, damping: 7) 25 | yaal.rotationY.animateTo(0, stiffness: 150, damping: 7) 26 | } 27 | } 28 | } 29 | 30 | public override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | layer.yaal.perspective.setTo(-1/500) 33 | } 34 | 35 | public required init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | layer.yaal.perspective.setTo(-1/500) 38 | } 39 | 40 | func velocityUpdated(_ velocity: CGPoint) { 41 | let maxRotate = CGFloat.pi/6 42 | let rotateX = -(velocity.y / 3000).clamp(-maxRotate, maxRotate) 43 | let rotateY = (velocity.x / 3000).clamp(-maxRotate, maxRotate) 44 | yaal.rotationX.animateTo(rotateX, stiffness: 400, damping: 20) 45 | yaal.rotationY.animateTo(rotateY, stiffness: 400, damping: 20) 46 | } 47 | 48 | func touchAnim(touches:Set) { 49 | if let touch = touches.first, tapAnimation { 50 | var loc = touch.location(in: self) 51 | loc = CGPoint(x: loc.x.clamp(0, bounds.width), y: loc.y.clamp(0, bounds.height)) 52 | loc = loc - bounds.center 53 | let rotation = CGPoint(x: -loc.y / bounds.height, y: loc.x / bounds.width) 54 | if #available(iOS 9.0, *) { 55 | let force = touch.maximumPossibleForce == 0 ? 1 : touch.force 56 | let rotation = rotation * (0.21 + force * 0.04) 57 | yaal.scale.animateTo(0.95 - force*0.01) 58 | yaal.rotationX.animateTo(rotation.x) 59 | yaal.rotationY.animateTo(rotation.y) 60 | } else { 61 | let rotation = rotation * 0.25 62 | yaal.scale.animateTo(0.94) 63 | yaal.rotationX.animateTo(rotation.x) 64 | yaal.rotationY.animateTo(rotation.y) 65 | } 66 | } 67 | } 68 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 69 | super.touchesBegan(touches, with: event) 70 | touchAnim(touches: touches) 71 | } 72 | open override func touchesMoved(_ touches: Set, with event: UIEvent?) { 73 | super.touchesMoved(touches, with: event) 74 | touchAnim(touches: touches) 75 | } 76 | open override func touchesEnded(_ touches: Set, with event: UIEvent?) { 77 | super.touchesEnded(touches, with: event) 78 | if tapAnimation { 79 | yaal.scale.animateTo(1.0, stiffness: 150, damping: 7) 80 | yaal.rotationX.animateTo(0, stiffness: 150, damping: 7) 81 | yaal.rotationY.animateTo(0, stiffness: 150, damping: 7) 82 | } 83 | } 84 | open override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { 85 | super.touchesCancelled(touches!, with: event) 86 | if tapAnimation { 87 | yaal.scale.animateTo(1.0, stiffness: 150, damping: 7) 88 | yaal.rotationX.animateTo(0, stiffness: 150, damping: 7) 89 | yaal.rotationY.animateTo(0, stiffness: 150, damping: 7) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/ExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleView.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2017-09-04. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | class ExampleView: UIView { 13 | let titleLabel: UILabel = { 14 | let titleLabel = UILabel() 15 | titleLabel.font = .boldSystemFont(ofSize: 24) 16 | return titleLabel 17 | }() 18 | 19 | let cardView: UIView = { 20 | let cardView = UIView() 21 | cardView.backgroundColor = .white 22 | cardView.layer.cornerRadius = 8 23 | cardView.layer.shadowColor = UIColor.black.cgColor 24 | cardView.layer.shadowOffset = CGSize(width: 0, height: 12) 25 | cardView.layer.shadowRadius = 10 26 | cardView.layer.shadowOpacity = 0.1 27 | cardView.layer.borderColor = UIColor(white: 0, alpha: 0.1).cgColor 28 | cardView.layer.borderWidth = 0.5 29 | return cardView 30 | }() 31 | 32 | private var contentVC: UIViewController? 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | 37 | addSubview(titleLabel) 38 | addSubview(cardView) 39 | } 40 | 41 | public required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | override func layoutSubviews() { 46 | super.layoutSubviews() 47 | let size = titleLabel.sizeThatFits(bounds.size) 48 | titleLabel.frame = CGRect(origin: CGPoint(x: 0, y: 10), size: size) 49 | let labelHeight = titleLabel.frame.maxY + 10 50 | cardView.frame = CGRect(x: 0, y: labelHeight, width: bounds.width, height: bounds.height - labelHeight) 51 | contentVC?.view.frame = cardView.bounds 52 | } 53 | 54 | func populate(title: String, 55 | contentViewControllerType: UIViewController.Type) { 56 | titleLabel.text = title 57 | contentVC?.view.removeFromSuperview() 58 | contentVC = contentViewControllerType.init() 59 | let contentView = contentVC!.view! 60 | contentView.clipsToBounds = true 61 | contentView.layer.cornerRadius = 8 62 | cardView.addSubview(contentView) 63 | setNeedsLayout() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIRequiresFullScreen 32 | 33 | UIStatusBarHidden 34 | 35 | UIViewControllerBasedStatusBarAppearance 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/LabelProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelCollectionProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-23. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | open class LabelProvider: SimpleViewProvider { 13 | public var label: UILabel { 14 | return view(at: 0) as! UILabel 15 | } 16 | public init(identifier: String? = nil, insets: UIEdgeInsets = .zero) { 17 | let label = UILabel() 18 | label.numberOfLines = 0 19 | super.init(identifier: identifier, 20 | views: [label], 21 | sizeSource: SimpleViewSizeSource(sizeStrategy: (.fill, .fit)), 22 | layout: insets == .zero ? FlowLayout() : FlowLayout().inset(by: insets)) 23 | } 24 | public init(identifier: String? = nil, 25 | text: String, 26 | font: UIFont, 27 | color: UIColor = .black, 28 | insets: UIEdgeInsets = .zero) { 29 | let label = UILabel() 30 | label.font = font 31 | label.textColor = color 32 | label.text = text 33 | label.numberOfLines = 0 34 | super.init(identifier: identifier, 35 | views: [label], 36 | sizeSource: SimpleViewSizeSource(sizeStrategy: (.fill, .fit)), 37 | layout: insets == .zero ? FlowLayout() : FlowLayout().inset(by: insets)) 38 | } 39 | public init(identifier: String? = nil, attributedString: NSAttributedString, insets: UIEdgeInsets = .zero) { 40 | let label = UILabel() 41 | label.attributedText = attributedString 42 | label.numberOfLines = 0 43 | super.init(identifier: identifier, 44 | views: [label], 45 | sizeSource: SimpleViewSizeSource(sizeStrategy: (.fill, .fit)), 46 | layout: insets == .zero ? FlowLayout() : FlowLayout().inset(by: insets)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Random.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by DaRk-_-D0G on 13/04/2015. 3 | // Copyright (c) 2015 DaRk-_-D0G. All rights reserved. 4 | // Yannickstephan.com 5 | // redwolfstudio.fr 6 | 7 | import Foundation 8 | import CoreGraphics 9 | 10 | public extension Int { 11 | /// Returns a random Int point number between 0 and Int.max. 12 | static var random: Int { 13 | get { 14 | return Int.random(Int.max) 15 | } 16 | } 17 | /** 18 | Random integer between 0 and n-1. 19 | 20 | - parameter n: Int 21 | 22 | - returns: Int 23 | */ 24 | static func random(_ n: Int) -> Int { 25 | return Int(arc4random_uniform(UInt32(n))) 26 | } 27 | /** 28 | Random integer between min and max 29 | 30 | - parameter min: Int 31 | - parameter max: Int 32 | 33 | - returns: Int 34 | */ 35 | static func random(_ min: Int, max: Int) -> Int { 36 | return Int.random(max - min + 1) + min 37 | //Int(arc4random_uniform(UInt32(max - min + 1))) + min } 38 | } 39 | } 40 | public extension Double { 41 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 42 | static var random: Double { 43 | get { 44 | return Double(arc4random()) / 0xFFFFFFFF 45 | } 46 | } 47 | /** 48 | Create a random number Double 49 | 50 | - parameter min: Double 51 | - parameter max: Double 52 | 53 | - returns: Double 54 | */ 55 | static func random(_ min: Double, max: Double) -> Double { 56 | return Double.random * (max - min) + min 57 | } 58 | } 59 | public extension Float { 60 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 61 | static var random: Float { 62 | get { 63 | return Float(arc4random() / 0xFFFFFFFF) 64 | } 65 | } 66 | /** 67 | Create a random num Float 68 | 69 | - parameter min: Float 70 | - parameter max: Float 71 | 72 | - returns: Float 73 | */ 74 | static func random(min: Float, max: Float) -> Float { 75 | return Float.random * (max - min) + min 76 | } 77 | } 78 | public extension CGFloat { 79 | /// Randomly returns either 1.0 or -1.0. 80 | static var randomSign: CGFloat { 81 | get { 82 | return (arc4random_uniform(2) == 0) ? 1.0 : -1.0 83 | } 84 | } 85 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 86 | static var random: CGFloat { 87 | get { 88 | return CGFloat(Float.random) 89 | } 90 | } 91 | /** 92 | Create a random num CGFloat 93 | 94 | - parameter min: CGFloat 95 | - parameter max: CGFloat 96 | 97 | - returns: CGFloat random number 98 | */ 99 | static func random(_ min: CGFloat, max: CGFloat) -> CGFloat { 100 | return CGFloat.random * (max - min) + min 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/SquareView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SquareView.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2018-06-09. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SquareView: DynamicView { 12 | 13 | let textLabel = UILabel() 14 | 15 | var text: String? { 16 | get { return textLabel.text } 17 | set { textLabel.text = newValue } 18 | } 19 | 20 | public override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | layer.cornerRadius = 4 23 | 24 | textLabel.textColor = .white 25 | textLabel.textAlignment = .center 26 | addSubview(textLabel) 27 | } 28 | 29 | public required init?(coder aDecoder: NSCoder) { 30 | fatalError() 31 | } 32 | 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | textLabel.frame = bounds 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/TestData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestData.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-24. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let testMessages = [ 12 | Message(announcement: "CollectionView"), 13 | Message(false, content: "This is an advance example demostrating what CollectionView can do."), 14 | Message(false, content: "Checkout the source code to see how "), 15 | Message(false, content: "Nulla fringilla, dolor id congue elementum, urna diam rhoncus eros, sit amet hendrerit turpis velit eget nisl."), 16 | Message(false, content: "Quisque nulla sapien, dignissim ac risus nec, vehicula commodo lectus. Suspendisse lacinia mi sit amet nulla semper sollicitudin."), 17 | Message(true, content: "Test Content"), 18 | Message(announcement: "Today 9:30 AM"), 19 | Message(true, image: "l1"), 20 | Message(true, image: "l2"), 21 | Message(true, image: "l3"), 22 | Message(true, content: "Suspendisse ut turpis."), 23 | Message(true, content: "velit."), 24 | Message(false, content: "Suspendisse ut turpis velit."), 25 | Message(true, content: "Nullam placerat rhoncus erat ut placerat."), 26 | Message(false, content: "Fusce cursus metus viverra erat viverra, sed efficitur magna consequat. Ut tristique magna et sapien euismod, consequat maximus ipsum varius."), 27 | Message(false, content: "Nulla mattis odio a tortor fringilla pulvinar. Curabitur laoreet, velit nec malesuada finibus, massa arcu aliquam ex, a interdum justo massa eget erat. Curabitur facilisis molestie arcu id porta. Phasellus commodo rutrum mi a elementum. Etiam vestibulum volutpat sem, tincidunt auctor elit lobortis in. Pellentesque pellentesque tortor lectus, sed cursus augue porta vitae. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."), 28 | Message(false, content: "In bibendum nisl at arcu mollis volutpat vitae eu urna. Mauris sodales iaculis lorem, nec rutrum dui ullamcorper nec. Fusce nibh dolor, mollis ac efficitur condimentum, vulputate eget erat. Sed molestie neque eu blandit placerat. Fusce nec sagittis nulla. Sed aliquam elit sollicitudin egestas convallis. Vestibulum vel sem vel lectus porta tempus. Curabitur semper in nulla id lacinia. Sed consequat massa nisi, sed egestas quam facilisis id."), 29 | Message(false, image: "1"), 30 | Message(false, image: "2"), 31 | Message(false, image: "3"), 32 | Message(false, image: "4"), 33 | Message(false, image: "5"), 34 | Message(false, image: "6"), 35 | Message(true, content: "Etiam a leo nibh. Fusce cursus metus viverra erat viverra, sed efficitur magna consequat. Ut tristique magna et sapien euismod, consequat maximus ipsum varius."), 36 | Message(false, content: "Suspendisse ut turpis velit."), 37 | Message(true, content: "Vivamus et fermentum diam. Suspendisse vitae tempor lectus."), 38 | Message(true, content: "Duis eros eros"), 39 | Message(true, status: "Delivered"), 40 | ] 41 | 42 | let testImages: [UIImage] = [ 43 | UIImage(named: "l1")!, 44 | UIImage(named: "l2")!, 45 | UIImage(named: "l3")!, 46 | UIImage(named: "1")!, 47 | UIImage(named: "2")!, 48 | UIImage(named: "3")!, 49 | UIImage(named: "4")!, 50 | UIImage(named: "5")!, 51 | UIImage(named: "6")! 52 | ] 53 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/Supporting Files/Util.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | static var lightBlue: UIColor { 5 | return UIColor(red: 0, green: 184/255, blue: 1.0, alpha: 1.0) 6 | } 7 | } 8 | 9 | extension CGFloat { 10 | func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat { 11 | return self < a ? a : (self > b ? b : self) 12 | } 13 | } 14 | 15 | extension CGPoint { 16 | func translate(_ dx: CGFloat, dy: CGFloat) -> CGPoint { 17 | return CGPoint(x: self.x+dx, y: self.y+dy) 18 | } 19 | 20 | func transform(_ t: CGAffineTransform) -> CGPoint { 21 | return self.applying(t) 22 | } 23 | 24 | func distance(_ b: CGPoint) -> CGFloat { 25 | return sqrt(pow(self.x-b.x, 2)+pow(self.y-b.y, 2)) 26 | } 27 | } 28 | func +(left: CGPoint, right: CGPoint) -> CGPoint { 29 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 30 | } 31 | func += (left: inout CGPoint, right: CGPoint) { 32 | left.x += right.x 33 | left.y += right.y 34 | } 35 | func -(left: CGPoint, right: CGPoint) -> CGPoint { 36 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 37 | } 38 | func /(left: CGPoint, right: CGFloat) -> CGPoint { 39 | return CGPoint(x: left.x/right, y: left.y/right) 40 | } 41 | func *(left: CGPoint, right: CGFloat) -> CGPoint { 42 | return CGPoint(x: left.x*right, y: left.y*right) 43 | } 44 | func *(left: CGFloat, right: CGPoint) -> CGPoint { 45 | return right * left 46 | } 47 | func *(left: CGPoint, right: CGPoint) -> CGPoint { 48 | return CGPoint(x: left.x*right.x, y: left.y*right.y) 49 | } 50 | prefix func -(point: CGPoint) -> CGPoint { 51 | return CGPoint.zero - point 52 | } 53 | func /(left: CGSize, right: CGFloat) -> CGSize { 54 | return CGSize(width: left.width/right, height: left.height/right) 55 | } 56 | func -(left: CGPoint, right: CGSize) -> CGPoint { 57 | return CGPoint(x: left.x - right.width, y: left.y - right.height) 58 | } 59 | 60 | prefix func -(inset: UIEdgeInsets) -> UIEdgeInsets { 61 | return UIEdgeInsets(top: -inset.top, left: -inset.left, bottom: -inset.bottom, right: -inset.right) 62 | } 63 | 64 | extension CGRect { 65 | var center: CGPoint { 66 | return CGPoint(x: midX, y: midY) 67 | } 68 | var bounds: CGRect { 69 | return CGRect(origin: .zero, size: size) 70 | } 71 | init(center: CGPoint, size: CGSize) { 72 | self.init(origin: center - size / 2, size: size) 73 | } 74 | } 75 | 76 | func delay(_ delay: Double, closure:@escaping ()->Void) { 77 | DispatchQueue.main.asyncAfter( 78 | deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) 79 | } 80 | 81 | extension String { 82 | func width(withConstraintedHeight height: CGFloat, font: UIFont) -> CGFloat { 83 | let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) 84 | let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil) 85 | 86 | return boundingBox.width 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Examples/CollectionKitExamples/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-23. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionKit 11 | 12 | let bodyInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) 13 | let headerInset = UIEdgeInsets(top: 20, left: 16, bottom: 0, right: 16) 14 | 15 | func space(_ height: CGFloat) -> SpaceProvider { 16 | return SpaceProvider(sizeStrategy: (.fill, .absolute(height))) 17 | } 18 | 19 | class ViewController: CollectionViewController { 20 | 21 | let examples: [(String, UIViewController.Type)] = [ 22 | ("Horizontal Gallery", HorizontalGalleryViewController.self), 23 | ("Grid", GridViewController.self), 24 | ("Articles", ArticleExampleViewController.self), 25 | ("Reload", ReloadDataViewController.self), 26 | ("Reload Animation", ReloadAnimationViewController.self), 27 | ("Header", HeaderExampleViewController.self), 28 | ("Chat", MessagesViewController.self), 29 | ("Animators", AnimatorExampleViewController.self) 30 | ] 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | let examplesSection = BasicProvider( 36 | dataSource: examples, 37 | viewSource: { (view: ExampleView, data: (String, UIViewController.Type), at: Int) in 38 | view.populate(title: data.0, contentViewControllerType: data.1) 39 | }, 40 | sizeSource: { (_, _, size) -> CGSize in 41 | return CGSize(width: size.width, height: max(360, UIScreen.main.bounds.height * 0.7)) 42 | }, 43 | layout: FlowLayout(lineSpacing: 30).inset(by: bodyInset) 44 | ) 45 | 46 | provider = ComposedProvider(sections: [ 47 | space(100), 48 | LabelProvider(text: "CollectionKit", font: .boldSystemFont(ofSize: 38), insets: headerInset), 49 | LabelProvider(text: "A modern swift framework for building reusable collection view components.", font: .systemFont(ofSize: 20), insets: bodyInset), 50 | space(30), 51 | LabelProvider(text: "Examples", font: .boldSystemFont(ofSize: 30), insets: headerInset), 52 | examplesSection, 53 | space(30) 54 | ]) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Examples/Podfile: -------------------------------------------------------------------------------- 1 | 2 | use_frameworks! 3 | 4 | target "CollectionKitExamples" do 5 | pod "CollectionKit", :path => '../' 6 | pod "CollectionKit/WobbleAnimator", :path => '../' 7 | end 8 | -------------------------------------------------------------------------------- /Examples/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CollectionKit (2.4.0): 3 | - CollectionKit/Core (= 2.4.0) 4 | - CollectionKit/Core (2.4.0) 5 | - CollectionKit/WobbleAnimator (2.4.0): 6 | - CollectionKit/Core 7 | - YetAnotherAnimationLibrary (~> 1.4.0) 8 | - YetAnotherAnimationLibrary (1.4.0) 9 | 10 | DEPENDENCIES: 11 | - CollectionKit (from `../`) 12 | - CollectionKit/WobbleAnimator (from `../`) 13 | 14 | SPEC REPOS: 15 | https://github.com/cocoapods/specs.git: 16 | - YetAnotherAnimationLibrary 17 | 18 | EXTERNAL SOURCES: 19 | CollectionKit: 20 | :path: "../" 21 | 22 | SPEC CHECKSUMS: 23 | CollectionKit: 8f01e7629185bb81072c4aa734d105df5c2d1c8b 24 | YetAnotherAnimationLibrary: 7f808b10c2d193dd3e82407e722fa60aa0bdff2d 25 | 26 | PODFILE CHECKSUM: ebee1e253f63ed2523d8c23e296d4f3f5c625a3d 27 | 28 | COCOAPODS: 1.7.0.beta.3 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Luke Zhao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | 2 | use_frameworks! 3 | 4 | target "CollectionKitTests" do 5 | 6 | pod 'Quick', "= 1.3.2" 7 | pod 'Nimble', "= 7.3.1" 8 | end -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Nimble (7.3.1) 3 | - Quick (1.3.2) 4 | 5 | DEPENDENCIES: 6 | - Nimble (= 7.3.1) 7 | - Quick (= 1.3.2) 8 | 9 | SPEC REPOS: 10 | https://github.com/cocoapods/specs.git: 11 | - Nimble 12 | - Quick 13 | 14 | SPEC CHECKSUMS: 15 | Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae 16 | Quick: 2623cb30d7a7f41ca62f684f679586558f483d46 17 | 18 | PODFILE CHECKSUM: 056648a7e76b878641ac9a9fd2b13117bd03b5db 19 | 20 | COCOAPODS: 1.5.3 21 | -------------------------------------------------------------------------------- /Resources/alignItems.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | alignItems 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | .start 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | .center 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | .end 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | .stretch 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Resources/example1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 1 16 | 17 | 18 | 19 | 20 | 2 21 | 22 | 23 | 24 | 25 | 3 26 | 27 | 28 | 29 | 30 | 4 31 | 32 | -------------------------------------------------------------------------------- /Resources/example10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 | 27 | 1 28 | 29 | 30 | 31 | 32 | 1 33 | 34 | 35 | 36 | 37 | 1 38 | 39 | 40 | 41 | 42 | 2 43 | 44 | 45 | 46 | 47 | 48 | 3 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | -------------------------------------------------------------------------------- /Resources/example11.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | 30 | 31 | 32 | 33 | 1 34 | 35 | 36 | 37 | 38 | 1 39 | 40 | 41 | 42 | 43 | 1 44 | 45 | 46 | 47 | 48 | 2 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | 56 | 57 | 58 | 3 59 | 60 | 61 | 62 | 63 | 4 64 | 65 | 66 | 67 | 68 | 4 69 | 70 | -------------------------------------------------------------------------------- /Resources/example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 8 21 | 22 | 23 | 24 | 25 | 9 26 | 27 | -------------------------------------------------------------------------------- /Resources/example3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 8 21 | 22 | 23 | 24 | 25 | 9 26 | 27 | 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | 11 36 | 37 | 38 | 39 | 40 | 12 41 | 42 | -------------------------------------------------------------------------------- /Resources/example4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 8 21 | 22 | 23 | 24 | 25 | 9 26 | 27 | 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | 11 36 | 37 | 38 | 39 | 40 | 12 41 | 42 | -------------------------------------------------------------------------------- /Resources/example5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 8 21 | 22 | 23 | 24 | 25 | 9 26 | 27 | 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | 11 36 | 37 | 38 | 39 | 40 | 12 41 | 42 | -------------------------------------------------------------------------------- /Resources/example6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 9 21 | 22 | 23 | 24 | 25 | 11 26 | 27 | 28 | 29 | 30 | 12 31 | 32 | 33 | 34 | 35 | 8 36 | 37 | 38 | 39 | 40 | 10 41 | 42 | -------------------------------------------------------------------------------- /Resources/example7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 18 | 19 | 20 | 9 21 | 22 | 23 | 24 | 25 | 11 26 | 27 | 28 | 29 | 30 | 12 31 | 32 | 33 | 34 | 35 | 8 36 | 37 | 38 | 39 | 40 | 10 41 | 42 | -------------------------------------------------------------------------------- /Resources/example8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 | 27 | 1 28 | 29 | 30 | 31 | 32 | 1 33 | 34 | 35 | 36 | 37 | 1 38 | 39 | 40 | 41 | 42 | 2 43 | 44 | 45 | 46 | 47 | 2 48 | 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | 56 | 57 | 58 | 3 59 | 60 | -------------------------------------------------------------------------------- /Resources/example9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 | 27 | 1 28 | 29 | 30 | 31 | 32 | 1 33 | 34 | 35 | 36 | 37 | 1 38 | 39 | 40 | 41 | 42 | 2 43 | 44 | 45 | 46 | 47 | 3 48 | 49 | 50 | 51 | 52 | 3 53 | 54 | -------------------------------------------------------------------------------- /Sources/Addon/EmptyStateProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyStateProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-08. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class EmptyStateProvider: ComposedProvider { 12 | open var emptyStateView: UIView? 13 | open var emptyStateViewGetter: () -> UIView 14 | open var contentProvider: Provider 15 | open var emptyStateViewSectionIdentifier: String = "emptyStateView" 16 | 17 | public init(identifier: String? = nil, 18 | emptyStateView: @autoclosure @escaping () -> UIView, 19 | content: Provider) { 20 | self.emptyStateViewGetter = emptyStateView 21 | self.contentProvider = content 22 | super.init(identifier: identifier, 23 | layout: RowLayout(emptyStateViewSectionIdentifier).transposed(), 24 | sections: [content]) 25 | } 26 | 27 | open override func willReload() { 28 | contentProvider.willReload() 29 | if contentProvider.numberOfItems == 0, sections.first?.identifier != emptyStateViewSectionIdentifier { 30 | if emptyStateView == nil { 31 | emptyStateView = emptyStateViewGetter() 32 | } 33 | let viewSection = SimpleViewProvider( 34 | identifier: "emptyStateView", 35 | views: [emptyStateView!], 36 | sizeStrategy: (.fill, .fill) 37 | ) 38 | sections = [viewSection] 39 | super.willReload() 40 | } else if contentProvider.numberOfItems > 0, sections.first?.identifier == emptyStateViewSectionIdentifier { 41 | sections = [contentProvider] 42 | } else { 43 | super.willReload() 44 | } 45 | } 46 | 47 | open override func hasReloadable(_ reloadable: CollectionReloadable) -> Bool { 48 | return super.hasReloadable(reloadable) || contentProvider.hasReloadable(reloadable) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Addon/SimpleViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleViewCollectionProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-23. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SimpleViewProvider: BasicProvider { 12 | 13 | open var views: [UIView] { 14 | get { return viewDataSource.data } 15 | set { viewDataSource.data = newValue } 16 | } 17 | 18 | open var identifierMapper: IdentifierMapperFn { 19 | get { return viewDataSource.identifierMapper } 20 | set { viewDataSource.identifierMapper = newValue } 21 | } 22 | 23 | private let viewDataSource: ArrayDataSource 24 | 25 | private class SimpleViewProviderViewSource: ViewSource { 26 | override func view(data: UIView, index: Int) -> UIView { 27 | return data 28 | } 29 | } 30 | 31 | public init(identifier: String? = nil, 32 | views: [UIView] = [], 33 | identifierMapper: @escaping IdentifierMapperFn = { return "\($1.hash)" }, 34 | sizeSource: SizeSource = SizeSource(), 35 | layout: Layout = FlowLayout(), 36 | animator: Animator? = nil, 37 | tapHandler: TapHandler? = nil) { 38 | 39 | viewDataSource = ArrayDataSource(data: views, identifierMapper: identifierMapper) 40 | 41 | super.init(identifier: identifier, 42 | dataSource: viewDataSource, 43 | viewSource: SimpleViewProviderViewSource(), 44 | sizeSource: sizeSource, 45 | layout: layout, 46 | animator: animator, 47 | tapHandler: tapHandler) 48 | } 49 | 50 | public convenience init(identifier: String? = nil, 51 | views: [UIView] = [], 52 | sizeStrategy: (width: SimpleViewSizeSource.ViewSizeStrategy, 53 | height: SimpleViewSizeSource.ViewSizeStrategy) = (.fit, .fit), 54 | layout: Layout = FlowLayout(), 55 | identifierMapper: @escaping (Int, UIView) -> String = { index, view in 56 | return "\(view.hash)" 57 | }) { 58 | self.init(identifier: identifier, 59 | views: views, 60 | identifierMapper: identifierMapper, 61 | sizeSource: SimpleViewSizeSource(sizeStrategy: sizeStrategy), 62 | layout: layout) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Addon/SpaceProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpaceProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-23. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SpaceProvider: EmptyCollectionProvider { 12 | public enum SpaceSizeStrategy { 13 | case fill 14 | case absolute(CGFloat) 15 | } 16 | open var sizeStrategy: (SpaceSizeStrategy, SpaceSizeStrategy) 17 | public init(sizeStrategy: (SpaceSizeStrategy, SpaceSizeStrategy) = (.fill, .fill)) { 18 | self.sizeStrategy = sizeStrategy 19 | super.init() 20 | } 21 | var _contentSize: CGSize = .zero 22 | open override var contentSize: CGSize { 23 | return _contentSize 24 | } 25 | open override func layout(collectionSize: CGSize) { 26 | let width: CGFloat, height: CGFloat 27 | switch sizeStrategy.0 { 28 | case .fill: width = collectionSize.width 29 | case .absolute(let value): width = value 30 | } 31 | switch sizeStrategy.1 { 32 | case .fill: height = collectionSize.height 33 | case .absolute(let value): height = value 34 | } 35 | _contentSize = CGSize(width: width, height: height) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Animator/Animator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionAnimator.swift 3 | // CollectionView 4 | // 5 | // Created by Luke Zhao on 2017-07-19. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class Animator { 12 | 13 | /// Called when CollectionView inserts a view into its subviews. 14 | /// 15 | /// Perform any insertion animation when needed 16 | /// 17 | /// - Parameters: 18 | /// - collectionView: source CollectionView 19 | /// - view: the view being inserted 20 | /// - at: index of the view inside the CollectionView (after flattening step) 21 | /// - frame: frame provided by the layout 22 | open func insert(collectionView: CollectionView, 23 | view: UIView, 24 | at: Int, 25 | frame: CGRect) { 26 | view.bounds.size = frame.bounds.size 27 | view.center = frame.center 28 | } 29 | 30 | /// Called when CollectionView deletes a view from its subviews. 31 | /// 32 | /// Perform any deletion animation, then call `view.recycleForCollectionKitReuse()` 33 | /// after the animation finishes 34 | /// 35 | /// - Parameters: 36 | /// - collectionView: source CollectionView 37 | /// - view: the view being deleted 38 | open func delete(collectionView: CollectionView, 39 | view: UIView) { 40 | view.recycleForCollectionKitReuse() 41 | } 42 | 43 | /// Called when: 44 | /// * the view has just been inserted 45 | /// * the view's frame changed after `reloadData` 46 | /// * the view's screen position changed when user scrolls 47 | /// 48 | /// - Parameters: 49 | /// - collectionView: source CollectionView 50 | /// - view: the view being updated 51 | /// - at: index of the view inside the CollectionView (after flattening step) 52 | /// - frame: frame provided by the layout 53 | open func update(collectionView: CollectionView, 54 | view: UIView, 55 | at: Int, 56 | frame: CGRect) { 57 | if view.bounds.size != frame.bounds.size { 58 | view.bounds.size = frame.bounds.size 59 | } 60 | if view.center != frame.center { 61 | view.center = frame.center 62 | } 63 | } 64 | 65 | /// Called when contentOffset changes during reloadData 66 | /// 67 | /// - Parameters: 68 | /// - collectionView: source CollectionView 69 | /// - delta: changes in contentOffset 70 | /// - view: the view being updated 71 | /// - at: index of the view inside the CollectionView (after flattening step) 72 | /// - frame: frame provided by the layout 73 | open func shift(collectionView: CollectionView, 74 | delta: CGPoint, 75 | view: UIView, 76 | at: Int, 77 | frame: CGRect) { 78 | view.center += delta 79 | } 80 | 81 | public init() {} 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Animator/FadeAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FadeAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-16. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class FadeAnimator: SimpleAnimator { 12 | open var alpha: CGFloat = 0 13 | 14 | open override func hide(view: UIView) { 15 | view.alpha = alpha 16 | } 17 | 18 | open override func show(view: UIView) { 19 | view.alpha = 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Animator/ScaleAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaleAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-16. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ScaleAnimator: FadeAnimator { 12 | open var scale: CGFloat = 0.5 13 | 14 | open override func hide(view: UIView) { 15 | super.hide(view: view) 16 | view.transform = CGAffineTransform.identity.scaledBy(x: scale, y: scale) 17 | } 18 | 19 | open override func show(view: UIView) { 20 | super.show(view: view) 21 | view.transform = CGAffineTransform.identity 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Animator/SimpleAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-16. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SimpleAnimator: Animator { 12 | open var animationDuration: TimeInterval = 0.3 13 | open var animationOptions: UIView.AnimationOptions = [] 14 | open var useSpringAnimation: Bool = false 15 | open var springDamping: CGFloat = 0.8 16 | 17 | // override point for subclass 18 | open func hide(view: UIView) {} 19 | open func show(view: UIView) {} 20 | 21 | open override func insert(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 22 | super.insert(collectionView: collectionView, view: view, at: at, frame: frame) 23 | if collectionView.isReloading, collectionView.hasReloaded, collectionView.bounds.intersects(frame) { 24 | hide(view: view) 25 | animate { 26 | self.show(view: view) 27 | } 28 | } 29 | } 30 | 31 | open override func delete(collectionView: CollectionView, view: UIView) { 32 | if collectionView.isReloading, collectionView.bounds.intersects(view.frame) { 33 | animate({ 34 | self.hide(view: view) 35 | }, completion: { _ in 36 | if !collectionView.visibleCells.contains(view) { 37 | view.recycleForCollectionKitReuse() 38 | self.show(view: view) 39 | } 40 | }) 41 | } else { 42 | view.recycleForCollectionKitReuse() 43 | } 44 | } 45 | 46 | open override func update(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 47 | if view.bounds.size != frame.bounds.size { 48 | view.bounds.size = frame.bounds.size 49 | } 50 | animate { 51 | view.center = frame.center 52 | } 53 | } 54 | 55 | open func animate(_ animations: @escaping () -> Void) { 56 | animate(animations, completion: nil) 57 | } 58 | 59 | open func animate(_ animations: @escaping () -> Void, 60 | completion: ((Bool) -> Void)?) { 61 | if useSpringAnimation { 62 | UIView.animate(withDuration: animationDuration, 63 | delay: 0, 64 | usingSpringWithDamping: springDamping, 65 | initialSpringVelocity: 0, 66 | options: animationOptions, 67 | animations: animations, 68 | completion: completion) 69 | } else { 70 | UIView.animate(withDuration: animationDuration, 71 | delay: 0, 72 | options: animationOptions, 73 | animations: animations, 74 | completion: completion) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/DataSource/ArrayDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayDataSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias IdentifierMapperFn = (Int, Data) -> String 12 | 13 | open class ArrayDataSource: DataSource { 14 | 15 | open var data: [Data] { 16 | didSet { 17 | setNeedsReload() 18 | } 19 | } 20 | 21 | open var identifierMapper: IdentifierMapperFn { 22 | didSet { 23 | setNeedsReload() 24 | } 25 | } 26 | 27 | public init(data: [Data] = [], identifierMapper: @escaping IdentifierMapperFn = { index, _ in "\(index)" }) { 28 | self.data = data 29 | self.identifierMapper = identifierMapper 30 | } 31 | 32 | open override var numberOfItems: Int { 33 | return data.count 34 | } 35 | 36 | open override func identifier(at: Int) -> String { 37 | return identifierMapper(at, data[at]) 38 | } 39 | 40 | open override func data(at: Int) -> Data { 41 | return data[at] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/DataSource/ClosureDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureDataSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class ClosureDataSource: DataSource { 12 | 13 | open var getter: () -> [Data] { 14 | didSet { 15 | setNeedsReload() 16 | } 17 | } 18 | 19 | open var identifierMapper: IdentifierMapperFn { 20 | didSet { 21 | setNeedsReload() 22 | } 23 | } 24 | 25 | public init(getter: @escaping () -> [Data], 26 | identifierMapper: @escaping IdentifierMapperFn = { index, _ in "\(index)" }) { 27 | self.getter = getter 28 | self.identifierMapper = identifierMapper 29 | } 30 | 31 | open override var numberOfItems: Int { 32 | return getter().count 33 | } 34 | 35 | open override func identifier(at: Int) -> String { 36 | return identifierMapper(at, getter()[at]) 37 | } 38 | 39 | open override func data(at: Int) -> Data { 40 | return getter()[at] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DataSource/DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionDataProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-20. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class DataSource: CollectionReloadable { 12 | open var numberOfItems: Int { 13 | return 0 14 | } 15 | open func data(at: Int) -> Data { 16 | fatalError() 17 | } 18 | open func identifier(at: Int) -> String { 19 | return "\(at)" 20 | } 21 | public init() {} 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Extensions/UIScrollView+CollectionKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Addtion.swift 3 | // CollectionView 4 | // 5 | // Created by Luke on 4/16/17. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView { 12 | public var visibleFrame: CGRect { 13 | return bounds 14 | } 15 | public var visibleFrameLessInset: CGRect { 16 | return visibleFrame.inset(by: contentInset) 17 | } 18 | public var absoluteFrameLessInset: CGRect { 19 | return CGRect(origin: .zero, size: bounds.size).inset(by: contentInset) 20 | } 21 | public var innerSize: CGSize { 22 | return absoluteFrameLessInset.size 23 | } 24 | public var offsetFrame: CGRect { 25 | return CGRect(x: -contentInset.left, y: -contentInset.top, 26 | width: max(0, contentSize.width - bounds.width + contentInset.right + contentInset.left), 27 | height: max(0, contentSize.height - bounds.height + contentInset.bottom + contentInset.top)) 28 | } 29 | public func absoluteLocation(for point: CGPoint) -> CGPoint { 30 | return point - contentOffset 31 | } 32 | public func scrollTo(edge: UIRectEdge, animated: Bool) { 33 | let target: CGPoint 34 | switch edge { 35 | case UIRectEdge.top: 36 | target = CGPoint(x: contentOffset.x, y: offsetFrame.minY) 37 | case UIRectEdge.bottom: 38 | target = CGPoint(x: contentOffset.x, y: offsetFrame.maxY) 39 | case UIRectEdge.left: 40 | target = CGPoint(x: offsetFrame.minX, y: contentOffset.y) 41 | case UIRectEdge.right: 42 | target = CGPoint(x: offsetFrame.maxX, y: contentOffset.y) 43 | default: 44 | return 45 | } 46 | setContentOffset(target, animated: animated) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Extensions/UIView+CollectionKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+CollectionKit.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-24. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | private struct AssociatedKeys { 13 | static var reuseManager = "reuseManager" 14 | static var animator = "animator" 15 | static var currentAnimator = "currentAnimator" 16 | } 17 | 18 | internal var reuseManager: CollectionReuseViewManager? { 19 | get { return objc_getAssociatedObject(self, &AssociatedKeys.reuseManager) as? CollectionReuseViewManager } 20 | set { objc_setAssociatedObject(self, &AssociatedKeys.reuseManager, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 21 | } 22 | 23 | public var collectionAnimator: Animator? { 24 | get { return objc_getAssociatedObject(self, &AssociatedKeys.animator) as? Animator } 25 | set { 26 | if collectionAnimator === currentCollectionAnimator { 27 | currentCollectionAnimator = newValue 28 | } 29 | objc_setAssociatedObject(self, &AssociatedKeys.animator, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 30 | } 31 | } 32 | 33 | internal var currentCollectionAnimator: Animator? { 34 | get { return objc_getAssociatedObject(self, &AssociatedKeys.currentAnimator) as? Animator } 35 | set { objc_setAssociatedObject(self, &AssociatedKeys.currentAnimator, 36 | newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 37 | } 38 | 39 | public func recycleForCollectionKitReuse() { 40 | if let reuseManager = reuseManager { 41 | reuseManager.queue(view: self) 42 | } else { 43 | removeFromSuperview() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Extensions/Util.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension Array { 4 | func get(_ index: Int) -> Element? { 5 | if (0.. Bool) -> Index { 18 | var low = startIndex 19 | var high = endIndex 20 | while low != high { 21 | let mid = index(low, offsetBy: distance(from: low, to: high)/2) 22 | if predicate(self[mid]) { 23 | low = index(after: mid) 24 | } else { 25 | high = mid 26 | } 27 | } 28 | return low 29 | } 30 | } 31 | 32 | extension CGFloat { 33 | func clamp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { 34 | return self < minValue ? minValue : (self > maxValue ? maxValue : self) 35 | } 36 | } 37 | 38 | extension CGPoint { 39 | func translate(_ dx: CGFloat, dy: CGFloat) -> CGPoint { 40 | return CGPoint(x: self.x+dx, y: self.y+dy) 41 | } 42 | 43 | func transform(_ trans: CGAffineTransform) -> CGPoint { 44 | return self.applying(trans) 45 | } 46 | 47 | func distance(_ point: CGPoint) -> CGFloat { 48 | return sqrt(pow(self.x - point.x, 2)+pow(self.y - point.y, 2)) 49 | } 50 | 51 | var transposed: CGPoint { 52 | return CGPoint(x: y, y: x) 53 | } 54 | } 55 | 56 | extension CGSize { 57 | func insets(by insets: UIEdgeInsets) -> CGSize { 58 | return CGSize(width: width - insets.left - insets.right, height: height - insets.top - insets.bottom) 59 | } 60 | var transposed: CGSize { 61 | return CGSize(width: height, height: width) 62 | } 63 | } 64 | 65 | func abs(_ left: CGPoint) -> CGPoint { 66 | return CGPoint(x: abs(left.x), y: abs(left.y)) 67 | } 68 | func min(_ left: CGPoint, _ right: CGPoint) -> CGPoint { 69 | return CGPoint(x: min(left.x, right.x), y: min(left.y, right.y)) 70 | } 71 | func + (left: CGPoint, right: CGPoint) -> CGPoint { 72 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 73 | } 74 | func += (left: inout CGPoint, right: CGPoint) { 75 | left.x += right.x 76 | left.y += right.y 77 | } 78 | func + (left: CGRect, right: CGPoint) -> CGRect { 79 | return CGRect(origin: left.origin + right, size: left.size) 80 | } 81 | func - (left: CGPoint, right: CGPoint) -> CGPoint { 82 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 83 | } 84 | func - (left: CGRect, right: CGPoint) -> CGRect { 85 | return CGRect(origin: left.origin - right, size: left.size) 86 | } 87 | func / (left: CGPoint, right: CGFloat) -> CGPoint { 88 | return CGPoint(x: left.x/right, y: left.y/right) 89 | } 90 | func * (left: CGPoint, right: CGFloat) -> CGPoint { 91 | return CGPoint(x: left.x*right, y: left.y*right) 92 | } 93 | func * (left: CGFloat, right: CGPoint) -> CGPoint { 94 | return right * left 95 | } 96 | func * (left: CGPoint, right: CGPoint) -> CGPoint { 97 | return CGPoint(x: left.x*right.x, y: left.y*right.y) 98 | } 99 | prefix func - (point: CGPoint) -> CGPoint { 100 | return CGPoint.zero - point 101 | } 102 | func / (left: CGSize, right: CGFloat) -> CGSize { 103 | return CGSize(width: left.width/right, height: left.height/right) 104 | } 105 | func - (left: CGPoint, right: CGSize) -> CGPoint { 106 | return CGPoint(x: left.x - right.width, y: left.y - right.height) 107 | } 108 | 109 | prefix func - (inset: UIEdgeInsets) -> UIEdgeInsets { 110 | return UIEdgeInsets(top: -inset.top, left: -inset.left, bottom: -inset.bottom, right: -inset.right) 111 | } 112 | 113 | extension CGRect { 114 | var center: CGPoint { 115 | return CGPoint(x: midX, y: midY) 116 | } 117 | var bounds: CGRect { 118 | return CGRect(origin: .zero, size: size) 119 | } 120 | init(center: CGPoint, size: CGSize) { 121 | self.init(origin: center - size / 2, size: size) 122 | } 123 | var transposed: CGRect { 124 | return CGRect(origin: origin.transposed, size: size.transposed) 125 | } 126 | #if swift(>=4.2) 127 | #else 128 | func inset(by insets: UIEdgeInsets) -> CGRect { 129 | return UIEdgeInsetsInsetRect(self, insets) 130 | } 131 | #endif 132 | } 133 | 134 | func delay(_ delay: Double, closure:@escaping () -> Void) { 135 | DispatchQueue.main.asyncAfter( 136 | deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) 137 | } 138 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.3.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Layout/ClosureLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class Closurelayout: SimpleLayout { 12 | public var frameProvider: (Int, CGSize) -> CGRect 13 | 14 | public init(frameProvider: @escaping (Int, CGSize) -> CGRect) { 15 | self.frameProvider = frameProvider 16 | super.init() 17 | } 18 | 19 | public override func simpleLayout(context: LayoutContext) -> [CGRect] { 20 | var frames: [CGRect] = [] 21 | for i in 0.. [CGRect] { 44 | var frames: [CGRect] = [] 45 | 46 | let sizes = (0.. 80 | (totalHeight: CGFloat, lineData: [(lineSize: CGSize, count: Int)]) { 81 | var lineData: [(lineSize: CGSize, count: Int)] = [] 82 | var currentLineItemCount = 0 83 | var currentLineWidth: CGFloat = 0 84 | var currentLineMaxHeight: CGFloat = 0 85 | var totalHeight: CGFloat = 0 86 | for size in sizes { 87 | if currentLineWidth + size.width > maxWidth, currentLineItemCount != 0 { 88 | lineData.append((lineSize: CGSize(width: currentLineWidth - CGFloat(currentLineItemCount) * interitemSpacing, 89 | height: currentLineMaxHeight), 90 | count: currentLineItemCount)) 91 | totalHeight += currentLineMaxHeight 92 | currentLineMaxHeight = 0 93 | currentLineWidth = 0 94 | currentLineItemCount = 0 95 | } 96 | currentLineMaxHeight = max(currentLineMaxHeight, size.height) 97 | currentLineWidth += size.width + interitemSpacing 98 | currentLineItemCount += 1 99 | } 100 | if currentLineItemCount > 0 { 101 | lineData.append((lineSize: CGSize(width: currentLineWidth - CGFloat(currentLineItemCount) * interitemSpacing, 102 | height: currentLineMaxHeight), 103 | count: currentLineItemCount)) 104 | totalHeight += currentLineMaxHeight 105 | } 106 | return (totalHeight, lineData) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Layout/InsetLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsetLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-09-08. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class InsetLayout: WrapperLayout { 12 | public var insets: UIEdgeInsets 13 | public var insetProvider: ((CGSize) -> UIEdgeInsets)? 14 | 15 | struct InsetLayoutContext: LayoutContext { 16 | var original: LayoutContext 17 | var insets: UIEdgeInsets 18 | 19 | var collectionSize: CGSize { 20 | return original.collectionSize.insets(by: insets) 21 | } 22 | var numberOfItems: Int { 23 | return original.numberOfItems 24 | } 25 | func data(at: Int) -> Any { 26 | return original.data(at: at) 27 | } 28 | func identifier(at: Int) -> String { 29 | return original.identifier(at: at) 30 | } 31 | func size(at: Int, collectionSize: CGSize) -> CGSize { 32 | return original.size(at: at, collectionSize: collectionSize) 33 | } 34 | } 35 | 36 | public init(_ rootLayout: Layout, insets: UIEdgeInsets = .zero) { 37 | self.insets = insets 38 | super.init(rootLayout) 39 | } 40 | 41 | public init(_ rootLayout: Layout, insetProvider: @escaping ((CGSize) -> UIEdgeInsets)) { 42 | self.insets = .zero 43 | self.insetProvider = insetProvider 44 | super.init(rootLayout) 45 | } 46 | 47 | open override var contentSize: CGSize { 48 | return rootLayout.contentSize.insets(by: -insets) 49 | } 50 | 51 | open override func layout(context: LayoutContext) { 52 | if let insetProvider = insetProvider { 53 | insets = insetProvider(context.collectionSize) 54 | } 55 | rootLayout.layout(context: InsetLayoutContext(original: context, insets: insets)) 56 | } 57 | 58 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 59 | return rootLayout.visibleIndexes(visibleFrame: visibleFrame.inset(by: -insets)) 60 | } 61 | 62 | open override func frame(at: Int) -> CGRect { 63 | return rootLayout.frame(at: at) + CGPoint(x: insets.left, y: insets.top) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Layout/Layout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Layout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-20. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class Layout { 12 | 13 | open func layout(context: LayoutContext) { 14 | fatalError("Subclass should provide its own layout") 15 | } 16 | 17 | open var contentSize: CGSize { 18 | fatalError("Subclass should provide its own layout") 19 | } 20 | 21 | open func frame(at: Int) -> CGRect { 22 | fatalError("Subclass should provide its own layout") 23 | } 24 | 25 | open func visibleIndexes(visibleFrame: CGRect) -> [Int] { 26 | fatalError("Subclass should provide its own layout") 27 | } 28 | 29 | public init() {} 30 | } 31 | 32 | extension Layout { 33 | public func transposed() -> TransposeLayout { 34 | return TransposeLayout(self) 35 | } 36 | 37 | public func inset(by insets: UIEdgeInsets) -> InsetLayout { 38 | return InsetLayout(self, insets: insets) 39 | } 40 | 41 | public func insetVisibleFrame(by insets: UIEdgeInsets) -> VisibleFrameInsetLayout { 42 | return VisibleFrameInsetLayout(self, insets: insets) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Layout/LayoutContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutContext.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-07. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol LayoutContext { 12 | var collectionSize: CGSize { get } 13 | var numberOfItems: Int { get } 14 | func data(at: Int) -> Any 15 | func identifier(at: Int) -> String 16 | func size(at index: Int, collectionSize: CGSize) -> CGSize 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Layout/OverlayLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayLayout.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2017-08-29. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class OverlayLayout: SimpleLayout { 12 | public override func simpleLayout(context: LayoutContext) -> [CGRect] { 13 | var frames: [CGRect] = [] 14 | for i in 0.. 14 | public var alignItems: AlignItem 15 | public var justifyContent: JustifyContent 16 | 17 | /// always stretch filling item to fill empty space even if sizeSource returns a smaller size 18 | public var alwaysFillEmptySpaces: Bool = true 19 | 20 | public init(fillIdentifiers: Set, 21 | spacing: CGFloat = 0, 22 | justifyContent: JustifyContent = .start, 23 | alignItems: AlignItem = .start) { 24 | self.spacing = spacing 25 | self.fillIdentifiers = fillIdentifiers 26 | self.justifyContent = justifyContent 27 | self.alignItems = alignItems 28 | super.init() 29 | } 30 | 31 | public convenience init(_ fillIdentifiers: String..., 32 | spacing: CGFloat = 0, 33 | justifyContent: JustifyContent = .start, 34 | alignItems: AlignItem = .start) { 35 | self.init(fillIdentifiers: Set(fillIdentifiers), spacing: spacing, 36 | justifyContent: justifyContent, alignItems: alignItems) 37 | } 38 | 39 | public override func simpleLayout(context: LayoutContext) -> [CGRect] { 40 | 41 | let (sizes, totalWidth) = getCellSizes(context: context) 42 | 43 | let (offset, distributedSpacing) = LayoutHelper.distribute(justifyContent: justifyContent, 44 | maxPrimary: context.collectionSize.width, 45 | totalPrimary: totalWidth, 46 | minimunSpacing: spacing, 47 | numberOfItems: context.numberOfItems) 48 | 49 | let frames = LayoutHelper.alignItem(alignItems: alignItems, 50 | startingPrimaryOffset: offset, spacing: distributedSpacing, 51 | sizes: sizes, secondaryRange: 0...max(0, context.collectionSize.height)) 52 | 53 | return frames 54 | } 55 | } 56 | 57 | extension RowLayout { 58 | 59 | func getCellSizes(context: LayoutContext) -> (sizes: [CGSize], totalWidth: CGFloat) { 60 | var sizes: [CGSize] = [] 61 | let spacings = spacing * CGFloat(context.numberOfItems - 1) 62 | var freezedWidth = spacings 63 | var fillIndexes: [Int] = [] 64 | 65 | for i in 0.. [CGRect] { 16 | fatalError("Subclass should provide its own layout") 17 | } 18 | 19 | open func doneLayout() { 20 | 21 | } 22 | 23 | public final override func layout(context: LayoutContext) { 24 | frames = simpleLayout(context: context) 25 | _contentSize = frames.reduce(CGRect.zero) { (old, item) in 26 | old.union(item) 27 | }.size 28 | doneLayout() 29 | } 30 | 31 | public final override var contentSize: CGSize { 32 | return _contentSize 33 | } 34 | 35 | public final override func frame(at: Int) -> CGRect { 36 | return frames[at] 37 | } 38 | 39 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 40 | var result = [Int]() 41 | for (i, frame) in frames.enumerated() { 42 | if frame.intersects(visibleFrame) { 43 | result.append(i) 44 | } 45 | } 46 | return result 47 | } 48 | } 49 | 50 | open class VerticalSimpleLayout: SimpleLayout { 51 | private var maxFrameLength: CGFloat = 0 52 | 53 | open override func doneLayout() { 54 | maxFrameLength = frames.max { $0.height < $1.height }?.height ?? 0 55 | } 56 | 57 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 58 | var index = frames.binarySearch { $0.minY < visibleFrame.minY - maxFrameLength } 59 | var visibleIndexes = [Int]() 60 | while index < frames.count { 61 | let frame = frames[index] 62 | if frame.minY >= visibleFrame.maxY { 63 | break 64 | } 65 | if frame.maxY > visibleFrame.minY { 66 | visibleIndexes.append(index) 67 | } 68 | index += 1 69 | } 70 | return visibleIndexes 71 | } 72 | } 73 | 74 | open class HorizontalSimpleLayout: SimpleLayout { 75 | private var maxFrameLength: CGFloat = 0 76 | 77 | open override func doneLayout() { 78 | maxFrameLength = frames.max { $0.width < $1.width }?.width ?? 0 79 | } 80 | 81 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 82 | var index = frames.binarySearch { $0.minX < visibleFrame.minX - maxFrameLength } 83 | var visibleIndexes = [Int]() 84 | while index < frames.count { 85 | let frame = frames[index] 86 | if frame.minX >= visibleFrame.maxX { 87 | break 88 | } 89 | if frame.maxX > visibleFrame.minX { 90 | visibleIndexes.append(index) 91 | } 92 | index += 1 93 | } 94 | return visibleIndexes 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Layout/StickyLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StickyLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-31. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class StickyLayout: WrapperLayout { 12 | public var isStickyFn: (Int) -> Bool 13 | 14 | var stickyFrames: [(index: Int, frame: CGRect)] = [] 15 | var visibleFrame: CGRect = .zero 16 | var topFrameIndex: Int = 0 17 | 18 | public init(rootLayout: Layout, 19 | isStickyFn: @escaping (Int) -> Bool = { $0 % 2 == 0 }) { 20 | self.isStickyFn = isStickyFn 21 | super.init(rootLayout) 22 | } 23 | 24 | public override var contentSize: CGSize { 25 | return rootLayout.contentSize 26 | } 27 | 28 | public override func layout(context: LayoutContext) { 29 | rootLayout.layout(context: context) 30 | stickyFrames = (0.. [Int] { 38 | self.visibleFrame = visibleFrame 39 | topFrameIndex = stickyFrames.binarySearch { $0.frame.minY < visibleFrame.minY } - 1 40 | if let index = stickyFrames.get(topFrameIndex)?.index, index >= 0 { 41 | var oldVisible = rootLayout.visibleIndexes(visibleFrame: visibleFrame) 42 | if let index = oldVisible.firstIndex(of: index) { 43 | oldVisible.remove(at: index) 44 | } 45 | return oldVisible + [index] 46 | } 47 | return rootLayout.visibleIndexes(visibleFrame: visibleFrame) 48 | } 49 | 50 | public override func frame(at: Int) -> CGRect { 51 | let superFrame = rootLayout.frame(at: at) 52 | if superFrame.minY < visibleFrame.minY, let index = stickyFrames.get(topFrameIndex)?.index, index == at { 53 | let pushedY: CGFloat 54 | if topFrameIndex < stickyFrames.count - 1 { 55 | pushedY = rootLayout.frame(at: stickyFrames[topFrameIndex + 1].index).minY - superFrame.height 56 | } else { 57 | pushedY = visibleFrame.maxY - superFrame.height 58 | } 59 | return CGRect(origin: CGPoint(x: superFrame.minX, y: min(visibleFrame.minY, pushedY)), size: superFrame.size) 60 | } 61 | return superFrame 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Layout/TransposeLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransposeLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-09-08. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class TransposeLayout: WrapperLayout { 12 | struct TransposeLayoutContext: LayoutContext { 13 | var original: LayoutContext 14 | 15 | var collectionSize: CGSize { 16 | return original.collectionSize.transposed 17 | } 18 | var numberOfItems: Int { 19 | return original.numberOfItems 20 | } 21 | func data(at: Int) -> Any { 22 | return original.data(at: at) 23 | } 24 | func identifier(at: Int) -> String { 25 | return original.identifier(at: at) 26 | } 27 | func size(at: Int, collectionSize: CGSize) -> CGSize { 28 | return original.size(at: at, collectionSize: collectionSize.transposed).transposed 29 | } 30 | } 31 | 32 | open override var contentSize: CGSize { 33 | return rootLayout.contentSize.transposed 34 | } 35 | 36 | open override func layout(context: LayoutContext) { 37 | rootLayout.layout(context: TransposeLayoutContext(original: context)) 38 | } 39 | 40 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 41 | return rootLayout.visibleIndexes(visibleFrame: visibleFrame.transposed) 42 | } 43 | 44 | open override func frame(at: Int) -> CGRect { 45 | return rootLayout.frame(at: at).transposed 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Layout/VisibleFrameInsetLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisibleFrameInsetLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-03-23. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class VisibleFrameInsetLayout: WrapperLayout { 12 | public var insets: UIEdgeInsets 13 | public var insetProvider: ((CGSize) -> UIEdgeInsets)? 14 | 15 | public init(_ rootLayout: Layout, insets: UIEdgeInsets = .zero) { 16 | self.insets = insets 17 | super.init(rootLayout) 18 | } 19 | 20 | public init(_ rootLayout: Layout, insetProvider: @escaping ((CGSize) -> UIEdgeInsets)) { 21 | self.insets = .zero 22 | self.insetProvider = insetProvider 23 | super.init(rootLayout) 24 | } 25 | 26 | open override func layout(context: LayoutContext) { 27 | if let insetProvider = insetProvider { 28 | insets = insetProvider(context.collectionSize) 29 | } 30 | super.layout(context: context) 31 | } 32 | 33 | open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { 34 | return rootLayout.visibleIndexes(visibleFrame: visibleFrame.inset(by: insets)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Layout/WaterfallLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaterfallLayout.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class WaterfallLayout: VerticalSimpleLayout { 12 | public var columns: Int 13 | public var spacing: CGFloat 14 | private var columnWidth: [CGFloat] = [0, 0] 15 | private var maxSize = CGSize.zero 16 | 17 | public init(columns: Int = 2, spacing: CGFloat = 0) { 18 | self.columns = columns 19 | self.spacing = spacing 20 | super.init() 21 | } 22 | 23 | public override func simpleLayout(context: LayoutContext) -> [CGRect] { 24 | var frames: [CGRect] = [] 25 | 26 | let columnWidth = (context.collectionSize.width - CGFloat(columns - 1) * spacing) / CGFloat(columns) 27 | var columnHeight = [CGFloat](repeating: 0, count: columns) 28 | 29 | func getMinColomn() -> (Int, CGFloat) { 30 | var minHeight: (Int, CGFloat) = (0, columnHeight[0]) 31 | for (index, height) in columnHeight.enumerated() where height < minHeight.1 { 32 | minHeight = (index, height) 33 | } 34 | return minHeight 35 | } 36 | 37 | for i in 0.. [Int] { 27 | return rootLayout.visibleIndexes(visibleFrame: visibleFrame) 28 | } 29 | 30 | open override func frame(at: Int) -> CGRect { 31 | return rootLayout.frame(at: at) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Other/CollectionReloadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionReloadable.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-25. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol CollectionReloadable: class { 12 | var collectionView: CollectionView? { get } 13 | func reloadData() 14 | func setNeedsReload() 15 | } 16 | 17 | extension CollectionReloadable { 18 | public var collectionView: CollectionView? { 19 | return CollectionViewManager.shared.collectionView(for: self) 20 | } 21 | public func reloadData() { 22 | collectionView?.reloadData() 23 | } 24 | public func setNeedsReload() { 25 | collectionView?.setNeedsReload() 26 | } 27 | public func setNeedsInvalidateLayout() { 28 | collectionView?.setNeedsInvalidateLayout() 29 | } 30 | } 31 | 32 | internal class CollectionViewManager { 33 | static let shared: CollectionViewManager = { 34 | CollectionView.swizzleAdjustContentOffset() // smartly using dispatch_once 35 | return CollectionViewManager() 36 | }() 37 | 38 | var collectionViews = NSHashTable.weakObjects() 39 | 40 | func register(collectionView: CollectionView) { 41 | collectionViews.add(collectionView) 42 | } 43 | 44 | func collectionView(for reloadable: CollectionReloadable) -> CollectionView? { 45 | for collectionView in collectionViews.allObjects { 46 | if let provider = collectionView.provider, provider.hasReloadable(reloadable) { 47 | return collectionView 48 | } 49 | } 50 | return nil 51 | } 52 | } 53 | 54 | // https://github.com/SoySauceLab/CollectionKit/issues/63 55 | // UIScrollView has a weird behavior where its contentOffset resets to .zero when 56 | // frame is assigned. 57 | // this swizzling fixed the issue. where the scrollview would jump during scroll 58 | extension UIScrollView { 59 | @objc func collectionKitAdjustContentOffsetIfNecessary(_ animated: Bool) { 60 | guard !(self is CollectionView) || !isDragging && !isDecelerating else { return } 61 | self.perform(#selector(CollectionView.collectionKitAdjustContentOffsetIfNecessary)) 62 | } 63 | 64 | static func swizzleAdjustContentOffset() { 65 | let encoded = String("==QeyF2czV2Yl5kZJRXZzZmZPRnblRnbvNEdzVnakF2X".reversed()) 66 | let originalMethodName = String(data: Data(base64Encoded: encoded)!, encoding: .utf8)! 67 | let originalSelector = NSSelectorFromString(originalMethodName) 68 | let swizzledSelector = #selector(CollectionView.collectionKitAdjustContentOffsetIfNecessary) 69 | let originalMethod = class_getInstanceMethod(self, originalSelector) 70 | let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) 71 | method_exchangeImplementations(originalMethod!, swizzledMethod!) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Other/CollectionReuseViewManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionReuseViewManager.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-21. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol CollectionViewReusableView: class { 12 | func prepareForReuse() 13 | } 14 | 15 | public class CollectionReuseViewManager: NSObject { 16 | 17 | /// Time it takes for CollectionReuseViewManager to 18 | /// dump all reusableViews to save memory 19 | public var lifeSpan: TimeInterval = 5.0 20 | 21 | /// When `removeFromCollectionViewWhenReuse` is enabled, 22 | /// cells will always be removed from Collection View during reuse. 23 | /// This is slower but it doesn't influence the `isHidden` property 24 | /// of individual cells. 25 | public var removeFromCollectionViewWhenReuse = false 26 | 27 | var reusableViews: [String: [UIView]] = [:] 28 | var cleanupTimer: Timer? 29 | 30 | public func queue(view: UIView) { 31 | let identifier = NSStringFromClass(type(of: view)) 32 | view.reuseManager = nil 33 | if removeFromCollectionViewWhenReuse { 34 | view.removeFromSuperview() 35 | } else { 36 | view.isHidden = true 37 | } 38 | if reusableViews[identifier] != nil && !reusableViews[identifier]!.contains(view) { 39 | reusableViews[identifier]?.append(view) 40 | } else { 41 | reusableViews[identifier] = [view] 42 | } 43 | if let cleanupTimer = cleanupTimer { 44 | cleanupTimer.fireDate = Date().addingTimeInterval(lifeSpan) 45 | } else { 46 | cleanupTimer = Timer.scheduledTimer(timeInterval: lifeSpan, target: self, 47 | selector: #selector(cleanup), userInfo: nil, repeats: false) 48 | } 49 | } 50 | 51 | public func dequeue (_ defaultView: @autoclosure () -> T) -> T { 52 | let identifier = NSStringFromClass(T.self) 53 | let queuedView = reusableViews[identifier]?.popLast() as? T 54 | let view = queuedView ?? defaultView() 55 | if let view = view as? CollectionViewReusableView { 56 | view.prepareForReuse() 57 | } 58 | if !removeFromCollectionViewWhenReuse { 59 | view.isHidden = false 60 | } 61 | view.reuseManager = self 62 | return view 63 | } 64 | 65 | public func dequeue (type: T.Type) -> T { 66 | return dequeue(type.init()) 67 | } 68 | 69 | @objc func cleanup() { 70 | for views in reusableViews.values { 71 | for view in views { 72 | view.removeFromSuperview() 73 | } 74 | } 75 | reusableViews.removeAll() 76 | cleanupTimer?.invalidate() 77 | cleanupTimer = nil 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Other/LayoutHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutHelper.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-09-08. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum JustifyContent { 12 | case start, end, center, spaceBetween, spaceAround, spaceEvenly 13 | } 14 | 15 | public typealias AlignContent = JustifyContent 16 | 17 | public enum AlignItem { 18 | case start, end, center, stretch 19 | } 20 | 21 | struct LayoutHelper { 22 | 23 | static func alignItem(alignItems: AlignItem, 24 | startingPrimaryOffset: CGFloat, 25 | spacing: CGFloat, 26 | sizes: SizeArray, 27 | secondaryRange: ClosedRange) 28 | -> [CGRect] where SizeArray.Iterator.Element == CGSize { 29 | var frames: [CGRect] = [] 30 | var offset = startingPrimaryOffset 31 | for cellSize in sizes { 32 | let cellFrame: CGRect 33 | switch alignItems { 34 | case .start: 35 | cellFrame = CGRect(origin: CGPoint(x: offset, y: secondaryRange.lowerBound), size: cellSize) 36 | case .end: 37 | cellFrame = CGRect(origin: CGPoint(x: offset, 38 | y: secondaryRange.upperBound - cellSize.height), 39 | size: cellSize) 40 | case .center: 41 | let secondaryOffset = secondaryRange.lowerBound + 42 | (secondaryRange.upperBound - secondaryRange.lowerBound - cellSize.height) / 2 43 | cellFrame = CGRect(origin: CGPoint(x: offset, y: secondaryOffset), 44 | size: cellSize) 45 | case .stretch: 46 | cellFrame = CGRect(origin: CGPoint(x: offset, y: secondaryRange.lowerBound), 47 | size: CGSize(width: cellSize.width, 48 | height: secondaryRange.upperBound - secondaryRange.lowerBound)) 49 | } 50 | frames.append(cellFrame) 51 | offset += cellSize.width + spacing 52 | } 53 | return frames 54 | } 55 | 56 | static func distribute(justifyContent: JustifyContent, 57 | maxPrimary: CGFloat, 58 | totalPrimary: CGFloat, 59 | minimunSpacing: CGFloat, 60 | numberOfItems: Int) -> (offset: CGFloat, spacing: CGFloat) { 61 | var offset: CGFloat = 0 62 | var spacing = minimunSpacing 63 | guard numberOfItems > 0 else { return (offset, spacing) } 64 | if totalPrimary + CGFloat(numberOfItems - 1) * minimunSpacing < maxPrimary { 65 | let leftOverPrimary = maxPrimary - totalPrimary 66 | switch justifyContent { 67 | case .start: 68 | break 69 | case .center: 70 | offset += (leftOverPrimary - minimunSpacing * CGFloat(numberOfItems - 1)) / 2 71 | case .end: 72 | offset += (leftOverPrimary - minimunSpacing * CGFloat(numberOfItems - 1)) 73 | case .spaceBetween: 74 | guard numberOfItems > 1 else { break } 75 | spacing = leftOverPrimary / CGFloat(numberOfItems - 1) 76 | case .spaceAround: 77 | spacing = leftOverPrimary / CGFloat(numberOfItems) 78 | offset = spacing / 2 79 | case .spaceEvenly: 80 | spacing = leftOverPrimary / CGFloat(numberOfItems + 1) 81 | offset = spacing 82 | } 83 | } 84 | return (offset, spacing) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Protocol/ItemProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-13. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ItemProvider: Provider { 12 | func view(at: Int) -> UIView 13 | func update(view: UIView, at: Int) 14 | 15 | func didTap(view: UIView, at: Int) 16 | } 17 | 18 | extension ItemProvider { 19 | public func flattenedProvider() -> ItemProvider { 20 | return self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Protocol/LayoutableProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutableProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-13. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol LayoutableProvider { 12 | var layout: Layout { get } 13 | var internalLayout: Layout { get } 14 | func layoutContext(collectionSize: CGSize) -> LayoutContext 15 | } 16 | 17 | extension LayoutableProvider where Self: Provider { 18 | public var internalLayout: Layout { 19 | return layout 20 | } 21 | public func layout(collectionSize: CGSize) { 22 | internalLayout.layout(context: layoutContext(collectionSize: collectionSize)) 23 | } 24 | public func visibleIndexes(visibleFrame: CGRect) -> [Int] { 25 | return internalLayout.visibleIndexes(visibleFrame: visibleFrame) 26 | } 27 | public var contentSize: CGSize { 28 | return internalLayout.contentSize 29 | } 30 | public func frame(at: Int) -> CGRect { 31 | return internalLayout.frame(at: at) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Protocol/Provider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-23. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol Provider { 12 | var identifier: String? { get } 13 | 14 | // data 15 | var numberOfItems: Int { get } 16 | func identifier(at: Int) -> String 17 | 18 | // layout 19 | func layout(collectionSize: CGSize) 20 | func visibleIndexes(visibleFrame: CGRect) -> [Int] 21 | var contentSize: CGSize { get } 22 | func frame(at: Int) -> CGRect 23 | 24 | // event 25 | func willReload() 26 | func didReload() 27 | 28 | func animator(at: Int) -> Animator? 29 | 30 | // determines if a context belongs to current provider 31 | func hasReloadable(_ reloadable: CollectionReloadable) -> Bool 32 | 33 | func flattenedProvider() -> ItemProvider 34 | } 35 | 36 | extension Provider { 37 | public func willReload() {} 38 | public func didReload() {} 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Protocol/SectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-13. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SectionProvider: Provider { 12 | func section(at: Int) -> Provider? 13 | } 14 | 15 | extension SectionProvider { 16 | public func flattenedProvider() -> ItemProvider { 17 | return FlattenedProvider(provider: self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Provider/BasicProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicProvider.swift 3 | // CollectionView 4 | // 5 | // Created by Luke Zhao on 2017-07-18. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BasicProvider: ItemProvider, LayoutableProvider, CollectionReloadable { 12 | 13 | open var identifier: String? 14 | open var dataSource: DataSource { didSet { setNeedsReload() } } 15 | open var viewSource: ViewSource { didSet { setNeedsReload() } } 16 | open var sizeSource: SizeSource { didSet { setNeedsInvalidateLayout() } } 17 | open var layout: Layout { didSet { setNeedsInvalidateLayout() } } 18 | open var animator: Animator? { didSet { setNeedsReload() } } 19 | open var tapHandler: TapHandler? 20 | 21 | public typealias TapHandler = (TapContext) -> Void 22 | 23 | public struct TapContext { 24 | public let view: View 25 | public let index: Int 26 | public let dataSource: DataSource 27 | 28 | public var data: Data { 29 | return dataSource.data(at: index) 30 | } 31 | 32 | public func setNeedsReload() { 33 | dataSource.setNeedsReload() 34 | } 35 | } 36 | 37 | public init(identifier: String? = nil, 38 | dataSource: DataSource, 39 | viewSource: ViewSource, 40 | sizeSource: SizeSource = SizeSource(), 41 | layout: Layout = FlowLayout(), 42 | animator: Animator? = nil, 43 | tapHandler: TapHandler? = nil) { 44 | self.dataSource = dataSource 45 | self.viewSource = viewSource 46 | self.layout = layout 47 | self.sizeSource = sizeSource 48 | self.animator = animator 49 | self.tapHandler = tapHandler 50 | self.identifier = identifier 51 | } 52 | 53 | open var numberOfItems: Int { 54 | return dataSource.numberOfItems 55 | } 56 | open func view(at: Int) -> UIView { 57 | return viewSource.view(data: dataSource.data(at: at), index: at) 58 | } 59 | open func update(view: UIView, at: Int) { 60 | viewSource.update(view: view as! View, data: dataSource.data(at: at), index: at) 61 | } 62 | open func identifier(at: Int) -> String { 63 | return dataSource.identifier(at: at) 64 | } 65 | open func layoutContext(collectionSize: CGSize) -> LayoutContext { 66 | return BasicProviderLayoutContext(collectionSize: collectionSize, 67 | dataSource: dataSource, 68 | sizeSource: sizeSource) 69 | } 70 | open func animator(at: Int) -> Animator? { 71 | return animator 72 | } 73 | open func didTap(view: UIView, at: Int) { 74 | if let tapHandler = tapHandler { 75 | let context = TapContext(view: view as! View, index: at, dataSource: dataSource) 76 | tapHandler(context) 77 | } 78 | } 79 | open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool { 80 | return reloadable === self || reloadable === dataSource || reloadable === sizeSource 81 | } 82 | } 83 | 84 | struct BasicProviderLayoutContext: LayoutContext { 85 | var collectionSize: CGSize 86 | var dataSource: DataSource 87 | var sizeSource: SizeSource 88 | 89 | var numberOfItems: Int { 90 | return dataSource.numberOfItems 91 | } 92 | func data(at: Int) -> Any { 93 | return dataSource.data(at: at) 94 | } 95 | func identifier(at: Int) -> String { 96 | return dataSource.identifier(at: at) 97 | } 98 | func size(at index: Int, collectionSize: CGSize) -> CGSize { 99 | return sizeSource.size(at: index, data: dataSource.data(at: index), collectionSize: collectionSize) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Provider/ComposedHeaderProvider+Convenience.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposedHeaderProvider+Convenience.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-10-15. 6 | // 7 | 8 | extension ComposedHeaderProvider { 9 | public convenience init(identifier: String? = nil, 10 | layout: Layout = FlowLayout(), 11 | animator: Animator? = nil, 12 | headerViewSource: @escaping ViewUpdaterFn, 13 | headerSizeSource: @escaping ClosureSizeSourceFn, 14 | sections: [Provider] = [], 15 | tapHandler: TapHandler? = nil) { 16 | self.init(identifier: identifier, 17 | layout: layout, 18 | animator: animator, 19 | headerViewSource: ClosureViewSource(viewUpdater: headerViewSource), 20 | headerSizeSource: ClosureSizeSource(sizeSource: headerSizeSource), 21 | sections: sections, 22 | tapHandler: tapHandler) 23 | } 24 | 25 | public convenience init(identifier: String? = nil, 26 | layout: Layout = FlowLayout(), 27 | animator: Animator? = nil, 28 | headerViewSource: HeaderViewSource, 29 | headerSizeSource: @escaping ClosureSizeSourceFn, 30 | sections: [Provider] = [], 31 | tapHandler: TapHandler? = nil) { 32 | self.init(identifier: identifier, 33 | layout: layout, 34 | animator: animator, 35 | headerViewSource: headerViewSource, 36 | headerSizeSource: ClosureSizeSource(sizeSource: headerSizeSource), 37 | sections: sections, 38 | tapHandler: tapHandler) 39 | } 40 | 41 | public convenience init(identifier: String? = nil, 42 | layout: Layout = FlowLayout(), 43 | animator: Animator? = nil, 44 | headerViewSource: @escaping ViewUpdaterFn, 45 | headerSizeSource: HeaderSizeSource, 46 | sections: [Provider] = [], 47 | tapHandler: TapHandler? = nil) { 48 | self.init(identifier: identifier, 49 | layout: layout, 50 | animator: animator, 51 | headerViewSource: ClosureViewSource(viewUpdater: headerViewSource), 52 | headerSizeSource: headerSizeSource, 53 | sections: sections, 54 | tapHandler: tapHandler) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Provider/ComposedProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposedProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-20. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ComposedProvider: SectionProvider, LayoutableProvider, CollectionReloadable { 12 | 13 | open var identifier: String? 14 | open var sections: [Provider] { didSet { setNeedsReload() } } 15 | open var animator: Animator? { didSet { setNeedsReload() } } 16 | open var layout: Layout { didSet { setNeedsInvalidateLayout() } } 17 | 18 | public init(identifier: String? = nil, 19 | layout: Layout = FlowLayout(), 20 | animator: Animator? = nil, 21 | sections: [Provider] = []) { 22 | self.animator = animator 23 | self.layout = layout 24 | self.sections = sections 25 | self.identifier = identifier 26 | } 27 | 28 | open var numberOfItems: Int { 29 | return sections.count 30 | } 31 | 32 | open func section(at: Int) -> Provider? { 33 | return sections[at] 34 | } 35 | 36 | open func identifier(at: Int) -> String { 37 | return sections[at].identifier ?? "\(at)" 38 | } 39 | 40 | open func layoutContext(collectionSize: CGSize) -> LayoutContext { 41 | return CollectionComposerLayoutContext( 42 | collectionSize: collectionSize, 43 | sections: sections 44 | ) 45 | } 46 | 47 | open func animator(at: Int) -> Animator? { 48 | return animator 49 | } 50 | 51 | open func willReload() { 52 | for section in sections { 53 | section.willReload() 54 | } 55 | } 56 | 57 | open func didReload() { 58 | for section in sections { 59 | section.didReload() 60 | } 61 | } 62 | 63 | open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool { 64 | return reloadable === self || sections.contains(where: { $0.hasReloadable(reloadable) }) 65 | } 66 | } 67 | 68 | struct CollectionComposerLayoutContext: LayoutContext { 69 | var collectionSize: CGSize 70 | var sections: [Provider] 71 | 72 | var numberOfItems: Int { 73 | return sections.count 74 | } 75 | func data(at: Int) -> Any { 76 | return sections[at] 77 | } 78 | func identifier(at: Int) -> String { 79 | return sections[at].identifier ?? "\(at)" 80 | } 81 | func size(at: Int, collectionSize: CGSize) -> CGSize { 82 | sections[at].layout(collectionSize: collectionSize) 83 | return sections[at].contentSize 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Provider/EmptyCollectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseCollectionProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class EmptyCollectionProvider: ItemProvider, CollectionReloadable { 12 | open var identifier: String? 13 | 14 | public init(identifier: String? = nil) { 15 | self.identifier = identifier 16 | } 17 | 18 | open var numberOfItems: Int { 19 | return 0 20 | } 21 | open func view(at: Int) -> UIView { 22 | return UIView() 23 | } 24 | open func update(view: UIView, at: Int) {} 25 | open func identifier(at: Int) -> String { 26 | return "\(at)" 27 | } 28 | 29 | open var contentSize: CGSize { 30 | return .zero 31 | } 32 | open func layout(collectionSize: CGSize) {} 33 | open func frame(at: Int) -> CGRect { 34 | return .zero 35 | } 36 | open func visibleIndexes(visibleFrame: CGRect) -> [Int] { 37 | return [Int]() 38 | } 39 | 40 | open func animator(at: Int) -> Animator? { 41 | return nil 42 | } 43 | 44 | open func willReload() {} 45 | open func didReload() {} 46 | open func didTap(view: UIView, at: Int) {} 47 | 48 | open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool { 49 | return reloadable === self 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Provider/FlattenedProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlattenedProvider.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-08. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FlattenedProvider: ItemProvider { 12 | 13 | var provider: SectionProvider 14 | 15 | private var childSections: [(beginIndex: Int, sectionData: ItemProvider?)] 16 | 17 | init(provider: SectionProvider) { 18 | self.provider = provider 19 | var childSections: [(beginIndex: Int, sectionData: ItemProvider?)] = [] 20 | childSections.reserveCapacity(provider.numberOfItems) 21 | var count = 0 22 | for i in 0.. (Int, Int) { 36 | let sectionIndex = childSections.binarySearch { $0.beginIndex <= index } - 1 37 | return (sectionIndex, index - childSections[sectionIndex].beginIndex) 38 | } 39 | 40 | func apply(_ index: Int, with: (ItemProvider, Int) -> T) -> T { 41 | let (sectionIndex, item) = indexPath(index) 42 | if let sectionData = childSections[sectionIndex].sectionData { 43 | return with(sectionData, item) 44 | } else { 45 | assert(provider is ItemProvider, "Provider don't support view index") 46 | return with(provider as! ItemProvider, sectionIndex) 47 | } 48 | } 49 | 50 | var identifier: String? { 51 | return provider.identifier 52 | } 53 | 54 | var numberOfItems: Int { 55 | return (childSections.last?.beginIndex ?? 0) + (childSections.last?.sectionData?.numberOfItems ?? 0) 56 | } 57 | 58 | func view(at: Int) -> UIView { 59 | return apply(at) { 60 | $0.view(at: $1) 61 | } 62 | } 63 | 64 | func update(view: UIView, at: Int) { 65 | return apply(at) { 66 | $0.update(view: view, at: $1) 67 | } 68 | } 69 | 70 | func identifier(at: Int) -> String { 71 | let (sectionIndex, item) = indexPath(at) 72 | if let sectionData = childSections[sectionIndex].sectionData { 73 | return provider.identifier(at: sectionIndex) + "-" + sectionData.identifier(at: item) 74 | } else { 75 | return provider.identifier(at: sectionIndex) 76 | } 77 | } 78 | 79 | func layout(collectionSize: CGSize) { 80 | provider.layout(collectionSize: collectionSize) 81 | } 82 | 83 | var contentSize: CGSize { 84 | return provider.contentSize 85 | } 86 | 87 | func visibleIndexes(visibleFrame: CGRect) -> [Int] { 88 | var visible = [Int]() 89 | for sectionIndex in provider.visibleIndexes(visibleFrame: visibleFrame) { 90 | let beginIndex = childSections[sectionIndex].beginIndex 91 | if let sectionData = childSections[sectionIndex].sectionData { 92 | let sectionFrame = provider.frame(at: sectionIndex) 93 | let intersectFrame = visibleFrame.intersection(sectionFrame) 94 | let visibleFrameForCell = CGRect(origin: intersectFrame.origin - sectionFrame.origin, size: intersectFrame.size) 95 | let sectionVisible = sectionData.visibleIndexes(visibleFrame: visibleFrameForCell) 96 | for item in sectionVisible { 97 | visible.append(item + beginIndex) 98 | } 99 | } else { 100 | visible.append(beginIndex) 101 | } 102 | } 103 | return visible 104 | } 105 | 106 | func frame(at: Int) -> CGRect { 107 | let (sectionIndex, item) = indexPath(at) 108 | if let sectionData = childSections[sectionIndex].sectionData { 109 | var frame = sectionData.frame(at: item) 110 | frame.origin += provider.frame(at: sectionIndex).origin 111 | return frame 112 | } else { 113 | return provider.frame(at: sectionIndex) 114 | } 115 | } 116 | 117 | func animator(at: Int) -> Animator? { 118 | return apply(at) { 119 | $0.animator(at: $1) 120 | } ?? provider.animator(at: at) 121 | } 122 | 123 | func willReload() { 124 | provider.willReload() 125 | } 126 | 127 | func didReload() { 128 | provider.didReload() 129 | } 130 | 131 | func didTap(view: UIView, at: Int) { 132 | return apply(at) { 133 | $0.didTap(view: view, at: $1) 134 | } 135 | } 136 | 137 | func hasReloadable(_ reloadable: CollectionReloadable) -> Bool { 138 | return provider.hasReloadable(reloadable) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/SizeSource/AutoLayoutSizeSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public typealias ClosureViewUpdateFn = (View, Data, Int) -> Void 4 | 5 | open class AutoLayoutSizeSource: SizeSource { 6 | private let dummyView: View 7 | private let dummyViewUpdater: ClosureViewUpdateFn 8 | private let horizontalFittingPriority: UILayoutPriority 9 | private let verticalFittingPriority: UILayoutPriority 10 | public init(dummyView: View.Type, 11 | horizontalFittingPriority: UILayoutPriority = .defaultHigh, 12 | verticalFittingPriority: UILayoutPriority = .defaultLow, 13 | viewUpdater: @escaping ClosureViewUpdateFn) { 14 | 15 | self.dummyView = View() 16 | self.dummyViewUpdater = viewUpdater 17 | self.horizontalFittingPriority = horizontalFittingPriority 18 | self.verticalFittingPriority = verticalFittingPriority 19 | } 20 | open override func size(at index: Int, data: Data, collectionSize: CGSize) -> CGSize { 21 | self.dummyViewUpdater(self.dummyView, data, index) 22 | 23 | return self.dummyView.systemLayoutSizeFitting( 24 | collectionSize, 25 | withHorizontalFittingPriority: self.horizontalFittingPriority, 26 | verticalFittingPriority: self.verticalFittingPriority) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SizeSource/ClosureSizeSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureSizeSource.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-10-17. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ClosureSizeSourceFn = (Int, Data, CGSize) -> CGSize 12 | 13 | open class ClosureSizeSource: SizeSource { 14 | open var sizeSource: ClosureSizeSourceFn 15 | 16 | public init(sizeSource: @escaping ClosureSizeSourceFn) { 17 | self.sizeSource = sizeSource 18 | super.init() 19 | } 20 | 21 | open override func size(at index: Int, data: Data, collectionSize: CGSize) -> CGSize { 22 | return sizeSource(index, data, collectionSize) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SizeSource/SimpleViewSizeSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleViewSizeSource.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-10-17. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SimpleViewSizeSource: SizeSource, CollectionReloadable { 12 | public enum ViewSizeStrategy { 13 | case fill 14 | case fit 15 | case absolute(CGFloat) 16 | } 17 | 18 | public init(sizeStrategy: (width: ViewSizeStrategy, height: ViewSizeStrategy)) { 19 | self.sizeStrategy = sizeStrategy 20 | super.init() 21 | } 22 | 23 | open var sizeStrategy: (width: ViewSizeStrategy, height: ViewSizeStrategy) { 24 | didSet { setNeedsInvalidateLayout() } 25 | } 26 | 27 | open var sizeStrategyOverride: [UIView: (width: ViewSizeStrategy, height: ViewSizeStrategy)] = [:] { 28 | didSet { setNeedsInvalidateLayout() } 29 | } 30 | 31 | open override func size(at index: Int, data: UIView, collectionSize: CGSize) -> CGSize { 32 | let fitSize = data.sizeThatFits(collectionSize) 33 | let sizeStrategy = sizeStrategyOverride[data] ?? self.sizeStrategy 34 | let width: CGFloat, height: CGFloat 35 | 36 | switch sizeStrategy.width { 37 | case .fit: width = fitSize.width 38 | case .fill: width = collectionSize.width 39 | case .absolute(let value): width = value 40 | } 41 | switch sizeStrategy.height { 42 | case .fit: height = fitSize.height 43 | case .fill: height = collectionSize.height 44 | case .absolute(let value): height = value 45 | } 46 | 47 | return CGSize(width: width, height: height) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SizeSource/SizeSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizeSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-24. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SizeSource { 12 | 13 | public init() {} 14 | 15 | // override point for subclass 16 | open func size(at index: Int, data: Data, collectionSize: CGSize) -> CGSize { 17 | return collectionSize 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SizeSource/UIImageSizeSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageSizeSource.swift 3 | // CollectionKitTests 4 | // 5 | // Created by Luke Zhao on 2018-10-17. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class UIImageSizeSource: SizeSource { 12 | open override func size(at index: Int, data: UIImage, collectionSize: CGSize) -> CGSize { 13 | var imageSize = data.size 14 | if imageSize.width > collectionSize.width { 15 | imageSize.height /= imageSize.width / collectionSize.width 16 | imageSize.width = collectionSize.width 17 | } 18 | if imageSize.height > collectionSize.height { 19 | imageSize.width /= imageSize.height / collectionSize.height 20 | imageSize.height = collectionSize.height 21 | } 22 | return imageSize 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ViewSource/AnyViewSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewSource.swift 3 | // CollectionKitExample 4 | // 5 | // Created by Luke Zhao on 2018-06-06. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol AnyViewSource { 12 | func anyView(data: Any, index: Int) -> UIView 13 | func anyUpdate(view: UIView, data: Any, index: Int) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ViewSource/ClosureViewSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureViewSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ViewUpdaterFn = (View, Data, Int) -> Void 12 | public typealias ViewGeneratorFn = (Data, Int) -> View 13 | 14 | public class ClosureViewSource: ViewSource where View: UIView { 15 | public var viewGenerator: ViewGeneratorFn? 16 | public var viewUpdater: ViewUpdaterFn 17 | 18 | public init(viewGenerator: ViewGeneratorFn? = nil, 19 | viewUpdater: @escaping ViewUpdaterFn) { 20 | self.viewGenerator = viewGenerator 21 | self.viewUpdater = viewUpdater 22 | super.init() 23 | } 24 | 25 | public override func update(view: View, data: Data, index: Int) { 26 | viewUpdater(view, data, index) 27 | } 28 | 29 | public override func view(data: Data, index: Int) -> View { 30 | if let viewGenerator = viewGenerator { 31 | let view = reuseManager.dequeue(viewGenerator(data, index)) 32 | update(view: view, data: data, index: index) 33 | return view 34 | } else { 35 | return super.view(data: data, index: index) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ViewSource/ComposedViewSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposedViewSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2018-06-06. 6 | // Copyright © 2018 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class ComposedViewSource: ViewSource { 12 | public var viewSourceSelector: (Data) -> AnyViewSource 13 | 14 | public init(viewSourceSelector: @escaping (Data) -> AnyViewSource) { 15 | self.viewSourceSelector = viewSourceSelector 16 | } 17 | 18 | public override func update(view: UIView, data: Data, index: Int) { 19 | viewSourceSelector(data).anyUpdate(view: view, data: data, index: index) 20 | } 21 | 22 | public override func view(data: Data, index: Int) -> UIView { 23 | return viewSourceSelector(data).anyView(data: data, index: index) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ViewSource/ViewSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewSource.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-07-20. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ViewSource { 12 | public private(set) lazy var reuseManager = CollectionReuseViewManager() 13 | 14 | /// Should return a new view for the given data and index 15 | open func view(data: Data, index: Int) -> View { 16 | let view = reuseManager.dequeue(View()) 17 | update(view: view, data: data, index: index) 18 | return view 19 | } 20 | 21 | /// Should update the given view with the provided data and index 22 | open func update(view: View, data: Data, index: Int) {} 23 | 24 | public init() {} 25 | } 26 | 27 | extension ViewSource: AnyViewSource { 28 | public final func anyView(data: Any, index: Int) -> UIView { 29 | return view(data: data as! Data, index: index) 30 | } 31 | 32 | public final func anyUpdate(view: UIView, data: Any, index: Int) { 33 | return update(view: view as! View, data: data as! Data, index: index) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WobbleAnimator/WobbleAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WobbleAnimator.swift 3 | // CollectionKit 4 | // 5 | // Created by Luke Zhao on 2017-08-15. 6 | // Copyright © 2017 lkzhao. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import YetAnotherAnimationLibrary 11 | 12 | open class WobbleAnimator: Animator { 13 | public var sensitivity: CGPoint = CGPoint(x: 1, y: 1) 14 | 15 | func scrollVelocity(collectionView: CollectionView) -> CGPoint { 16 | guard collectionView.hasReloaded else { 17 | return .zero 18 | } 19 | let velocity = collectionView.bounds.origin - collectionView.lastLoadBounds.origin 20 | if collectionView.isReloading { 21 | return velocity - collectionView.contentOffsetChange 22 | } else { 23 | return velocity 24 | } 25 | } 26 | 27 | open override func shift(collectionView: CollectionView, delta: CGPoint, view: UIView, at: Int, frame: CGRect) { 28 | view.center += delta 29 | view.yaal.center.updateWithCurrentState() 30 | } 31 | 32 | open override func insert(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 33 | super.insert(collectionView: collectionView, view: view, at: at, frame: frame) 34 | let screenDragLocation = collectionView.absoluteLocation(for: collectionView.panGestureRecognizer.location(in: collectionView)) 35 | let delta = scrollVelocity(collectionView: collectionView) * 8 36 | view.bounds.size = frame.bounds.size 37 | let cellDiff = frame.center - collectionView.contentOffset - screenDragLocation 38 | let resistance = (cellDiff * sensitivity).distance(.zero) / 1000 39 | let newCenterDiff = delta * resistance 40 | let constrainted = CGPoint(x: delta.x > 0 ? min(delta.x, newCenterDiff.x) : max(delta.x, newCenterDiff.x), 41 | y: delta.y > 0 ? min(delta.y, newCenterDiff.y) : max(delta.y, newCenterDiff.y)) 42 | view.center += constrainted 43 | view.yaal.center.updateWithCurrentState() 44 | view.yaal.center.animateTo(frame.center, stiffness: 250, damping: 30, threshold:0.5) 45 | } 46 | 47 | open override func update(collectionView: CollectionView, view: UIView, at: Int, frame: CGRect) { 48 | let screenDragLocation = collectionView.absoluteLocation(for: collectionView.panGestureRecognizer.location(in: collectionView)) 49 | let delta = scrollVelocity(collectionView: collectionView) 50 | view.bounds.size = frame.bounds.size 51 | let cellDiff = frame.center - collectionView.contentOffset - screenDragLocation 52 | let resistance = (cellDiff * sensitivity).distance(.zero) / 1000 53 | let newCenterDiff = delta * resistance 54 | let constrainted = CGPoint(x: delta.x > 0 ? min(delta.x, newCenterDiff.x) : max(delta.x, newCenterDiff.x), 55 | y: delta.y > 0 ? min(delta.y, newCenterDiff.y) : max(delta.y, newCenterDiff.y)) 56 | view.center += constrainted 57 | view.yaal.center.updateWithCurrentState() 58 | view.yaal.center.animateTo(frame.center, stiffness: 250, damping: 30, threshold:0.5) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Examples" 3 | - "CollectionKitTests" 4 | - "Sources/Addon" 5 | - "Sources/Other" --------------------------------------------------------------------------------