├── .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 |
--------------------------------------------------------------------------------
/Resources/example1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------