├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── ECTimelineView.xcscheme
├── Example
├── ECTimelineViewExample.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── ECTimelineViewExample
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── ECTimelineViewExampleTests
│ ├── ECTimelineViewExampleTests.swift
│ └── Info.plist
└── ECTimelineViewExampleUITests
│ ├── ECTimelineViewExampleUITests.swift
│ └── Info.plist
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── ECTimelineView.swift
├── ECTimelineViewDataSource.swift
├── Enums
│ ├── LoadDirection.swift
│ └── StartPosition.swift
└── Extensions
│ └── UICollectionView+Extensions.swift
└── Tests
├── ECTimelineViewTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ECTimelineView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7CCBFF9E252AA4EA00EE9573 /* ECTimelineView in Frameworks */ = {isa = PBXBuildFile; productRef = 7CCBFF9D252AA4EA00EE9573 /* ECTimelineView */; };
11 | 7CCE2464252A8B4B007557FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE2463252A8B4B007557FE /* AppDelegate.swift */; };
12 | 7CCE2466252A8B4B007557FE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE2465252A8B4B007557FE /* SceneDelegate.swift */; };
13 | 7CCE2468252A8B4B007557FE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE2467252A8B4B007557FE /* ViewController.swift */; };
14 | 7CCE246B252A8B4B007557FE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7CCE2469252A8B4B007557FE /* Main.storyboard */; };
15 | 7CCE246D252A8B4C007557FE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7CCE246C252A8B4C007557FE /* Assets.xcassets */; };
16 | 7CCE2470252A8B4C007557FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7CCE246E252A8B4C007557FE /* LaunchScreen.storyboard */; };
17 | 7CCE247B252A8B4C007557FE /* ECTimelineViewExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE247A252A8B4C007557FE /* ECTimelineViewExampleTests.swift */; };
18 | 7CCE2486252A8B4C007557FE /* ECTimelineViewExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE2485252A8B4C007557FE /* ECTimelineViewExampleUITests.swift */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXContainerItemProxy section */
22 | 7CCE2477252A8B4C007557FE /* PBXContainerItemProxy */ = {
23 | isa = PBXContainerItemProxy;
24 | containerPortal = 7CCE2458252A8B4B007557FE /* Project object */;
25 | proxyType = 1;
26 | remoteGlobalIDString = 7CCE245F252A8B4B007557FE;
27 | remoteInfo = ECTimelineViewExample;
28 | };
29 | 7CCE2482252A8B4C007557FE /* PBXContainerItemProxy */ = {
30 | isa = PBXContainerItemProxy;
31 | containerPortal = 7CCE2458252A8B4B007557FE /* Project object */;
32 | proxyType = 1;
33 | remoteGlobalIDString = 7CCE245F252A8B4B007557FE;
34 | remoteInfo = ECTimelineViewExample;
35 | };
36 | /* End PBXContainerItemProxy section */
37 |
38 | /* Begin PBXFileReference section */
39 | 7CCE2460252A8B4B007557FE /* ECTimelineViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ECTimelineViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
40 | 7CCE2463252A8B4B007557FE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
41 | 7CCE2465252A8B4B007557FE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
42 | 7CCE2467252A8B4B007557FE /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
43 | 7CCE246A252A8B4B007557FE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
44 | 7CCE246C252A8B4C007557FE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
45 | 7CCE246F252A8B4C007557FE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
46 | 7CCE2471252A8B4C007557FE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
47 | 7CCE2476252A8B4C007557FE /* ECTimelineViewExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ECTimelineViewExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
48 | 7CCE247A252A8B4C007557FE /* ECTimelineViewExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECTimelineViewExampleTests.swift; sourceTree = ""; };
49 | 7CCE247C252A8B4C007557FE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
50 | 7CCE2481252A8B4C007557FE /* ECTimelineViewExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ECTimelineViewExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
51 | 7CCE2485252A8B4C007557FE /* ECTimelineViewExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECTimelineViewExampleUITests.swift; sourceTree = ""; };
52 | 7CCE2487252A8B4C007557FE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
53 | 7CECCBDC252E899B00BF1511 /* ECTimelineView */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ECTimelineView; path = ..; sourceTree = ""; };
54 | /* End PBXFileReference section */
55 |
56 | /* Begin PBXFrameworksBuildPhase section */
57 | 7CCE245D252A8B4B007557FE /* Frameworks */ = {
58 | isa = PBXFrameworksBuildPhase;
59 | buildActionMask = 2147483647;
60 | files = (
61 | 7CCBFF9E252AA4EA00EE9573 /* ECTimelineView in Frameworks */,
62 | );
63 | runOnlyForDeploymentPostprocessing = 0;
64 | };
65 | 7CCE2473252A8B4C007557FE /* Frameworks */ = {
66 | isa = PBXFrameworksBuildPhase;
67 | buildActionMask = 2147483647;
68 | files = (
69 | );
70 | runOnlyForDeploymentPostprocessing = 0;
71 | };
72 | 7CCE247E252A8B4C007557FE /* Frameworks */ = {
73 | isa = PBXFrameworksBuildPhase;
74 | buildActionMask = 2147483647;
75 | files = (
76 | );
77 | runOnlyForDeploymentPostprocessing = 0;
78 | };
79 | /* End PBXFrameworksBuildPhase section */
80 |
81 | /* Begin PBXGroup section */
82 | 7CCBFF9C252AA4EA00EE9573 /* Frameworks */ = {
83 | isa = PBXGroup;
84 | children = (
85 | );
86 | name = Frameworks;
87 | sourceTree = "";
88 | };
89 | 7CCE2457252A8B4B007557FE = {
90 | isa = PBXGroup;
91 | children = (
92 | 7CECCBDC252E899B00BF1511 /* ECTimelineView */,
93 | 7CCE2462252A8B4B007557FE /* ECTimelineViewExample */,
94 | 7CCE2479252A8B4C007557FE /* ECTimelineViewExampleTests */,
95 | 7CCE2484252A8B4C007557FE /* ECTimelineViewExampleUITests */,
96 | 7CCE2461252A8B4B007557FE /* Products */,
97 | 7CCBFF9C252AA4EA00EE9573 /* Frameworks */,
98 | );
99 | sourceTree = "";
100 | };
101 | 7CCE2461252A8B4B007557FE /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | 7CCE2460252A8B4B007557FE /* ECTimelineViewExample.app */,
105 | 7CCE2476252A8B4C007557FE /* ECTimelineViewExampleTests.xctest */,
106 | 7CCE2481252A8B4C007557FE /* ECTimelineViewExampleUITests.xctest */,
107 | );
108 | name = Products;
109 | sourceTree = "";
110 | };
111 | 7CCE2462252A8B4B007557FE /* ECTimelineViewExample */ = {
112 | isa = PBXGroup;
113 | children = (
114 | 7CCE2463252A8B4B007557FE /* AppDelegate.swift */,
115 | 7CCE2465252A8B4B007557FE /* SceneDelegate.swift */,
116 | 7CCE2467252A8B4B007557FE /* ViewController.swift */,
117 | 7CCE2469252A8B4B007557FE /* Main.storyboard */,
118 | 7CCE246C252A8B4C007557FE /* Assets.xcassets */,
119 | 7CCE246E252A8B4C007557FE /* LaunchScreen.storyboard */,
120 | 7CCE2471252A8B4C007557FE /* Info.plist */,
121 | );
122 | path = ECTimelineViewExample;
123 | sourceTree = "";
124 | };
125 | 7CCE2479252A8B4C007557FE /* ECTimelineViewExampleTests */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 7CCE247A252A8B4C007557FE /* ECTimelineViewExampleTests.swift */,
129 | 7CCE247C252A8B4C007557FE /* Info.plist */,
130 | );
131 | path = ECTimelineViewExampleTests;
132 | sourceTree = "";
133 | };
134 | 7CCE2484252A8B4C007557FE /* ECTimelineViewExampleUITests */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 7CCE2485252A8B4C007557FE /* ECTimelineViewExampleUITests.swift */,
138 | 7CCE2487252A8B4C007557FE /* Info.plist */,
139 | );
140 | path = ECTimelineViewExampleUITests;
141 | sourceTree = "";
142 | };
143 | /* End PBXGroup section */
144 |
145 | /* Begin PBXNativeTarget section */
146 | 7CCE245F252A8B4B007557FE /* ECTimelineViewExample */ = {
147 | isa = PBXNativeTarget;
148 | buildConfigurationList = 7CCE248A252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExample" */;
149 | buildPhases = (
150 | 7CCE245C252A8B4B007557FE /* Sources */,
151 | 7CCE245D252A8B4B007557FE /* Frameworks */,
152 | 7CCE245E252A8B4B007557FE /* Resources */,
153 | );
154 | buildRules = (
155 | );
156 | dependencies = (
157 | );
158 | name = ECTimelineViewExample;
159 | packageProductDependencies = (
160 | 7CCBFF9D252AA4EA00EE9573 /* ECTimelineView */,
161 | );
162 | productName = ECTimelineViewExample;
163 | productReference = 7CCE2460252A8B4B007557FE /* ECTimelineViewExample.app */;
164 | productType = "com.apple.product-type.application";
165 | };
166 | 7CCE2475252A8B4C007557FE /* ECTimelineViewExampleTests */ = {
167 | isa = PBXNativeTarget;
168 | buildConfigurationList = 7CCE248D252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExampleTests" */;
169 | buildPhases = (
170 | 7CCE2472252A8B4C007557FE /* Sources */,
171 | 7CCE2473252A8B4C007557FE /* Frameworks */,
172 | 7CCE2474252A8B4C007557FE /* Resources */,
173 | );
174 | buildRules = (
175 | );
176 | dependencies = (
177 | 7CCE2478252A8B4C007557FE /* PBXTargetDependency */,
178 | );
179 | name = ECTimelineViewExampleTests;
180 | productName = ECTimelineViewExampleTests;
181 | productReference = 7CCE2476252A8B4C007557FE /* ECTimelineViewExampleTests.xctest */;
182 | productType = "com.apple.product-type.bundle.unit-test";
183 | };
184 | 7CCE2480252A8B4C007557FE /* ECTimelineViewExampleUITests */ = {
185 | isa = PBXNativeTarget;
186 | buildConfigurationList = 7CCE2490252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExampleUITests" */;
187 | buildPhases = (
188 | 7CCE247D252A8B4C007557FE /* Sources */,
189 | 7CCE247E252A8B4C007557FE /* Frameworks */,
190 | 7CCE247F252A8B4C007557FE /* Resources */,
191 | );
192 | buildRules = (
193 | );
194 | dependencies = (
195 | 7CCE2483252A8B4C007557FE /* PBXTargetDependency */,
196 | );
197 | name = ECTimelineViewExampleUITests;
198 | productName = ECTimelineViewExampleUITests;
199 | productReference = 7CCE2481252A8B4C007557FE /* ECTimelineViewExampleUITests.xctest */;
200 | productType = "com.apple.product-type.bundle.ui-testing";
201 | };
202 | /* End PBXNativeTarget section */
203 |
204 | /* Begin PBXProject section */
205 | 7CCE2458252A8B4B007557FE /* Project object */ = {
206 | isa = PBXProject;
207 | attributes = {
208 | LastSwiftUpdateCheck = 1200;
209 | LastUpgradeCheck = 1200;
210 | TargetAttributes = {
211 | 7CCE245F252A8B4B007557FE = {
212 | CreatedOnToolsVersion = 12.0;
213 | };
214 | 7CCE2475252A8B4C007557FE = {
215 | CreatedOnToolsVersion = 12.0;
216 | TestTargetID = 7CCE245F252A8B4B007557FE;
217 | };
218 | 7CCE2480252A8B4C007557FE = {
219 | CreatedOnToolsVersion = 12.0;
220 | TestTargetID = 7CCE245F252A8B4B007557FE;
221 | };
222 | };
223 | };
224 | buildConfigurationList = 7CCE245B252A8B4B007557FE /* Build configuration list for PBXProject "ECTimelineViewExample" */;
225 | compatibilityVersion = "Xcode 9.3";
226 | developmentRegion = en;
227 | hasScannedForEncodings = 0;
228 | knownRegions = (
229 | en,
230 | Base,
231 | );
232 | mainGroup = 7CCE2457252A8B4B007557FE;
233 | packageReferences = (
234 | );
235 | productRefGroup = 7CCE2461252A8B4B007557FE /* Products */;
236 | projectDirPath = "";
237 | projectRoot = "";
238 | targets = (
239 | 7CCE245F252A8B4B007557FE /* ECTimelineViewExample */,
240 | 7CCE2475252A8B4C007557FE /* ECTimelineViewExampleTests */,
241 | 7CCE2480252A8B4C007557FE /* ECTimelineViewExampleUITests */,
242 | );
243 | };
244 | /* End PBXProject section */
245 |
246 | /* Begin PBXResourcesBuildPhase section */
247 | 7CCE245E252A8B4B007557FE /* Resources */ = {
248 | isa = PBXResourcesBuildPhase;
249 | buildActionMask = 2147483647;
250 | files = (
251 | 7CCE2470252A8B4C007557FE /* LaunchScreen.storyboard in Resources */,
252 | 7CCE246D252A8B4C007557FE /* Assets.xcassets in Resources */,
253 | 7CCE246B252A8B4B007557FE /* Main.storyboard in Resources */,
254 | );
255 | runOnlyForDeploymentPostprocessing = 0;
256 | };
257 | 7CCE2474252A8B4C007557FE /* Resources */ = {
258 | isa = PBXResourcesBuildPhase;
259 | buildActionMask = 2147483647;
260 | files = (
261 | );
262 | runOnlyForDeploymentPostprocessing = 0;
263 | };
264 | 7CCE247F252A8B4C007557FE /* Resources */ = {
265 | isa = PBXResourcesBuildPhase;
266 | buildActionMask = 2147483647;
267 | files = (
268 | );
269 | runOnlyForDeploymentPostprocessing = 0;
270 | };
271 | /* End PBXResourcesBuildPhase section */
272 |
273 | /* Begin PBXSourcesBuildPhase section */
274 | 7CCE245C252A8B4B007557FE /* Sources */ = {
275 | isa = PBXSourcesBuildPhase;
276 | buildActionMask = 2147483647;
277 | files = (
278 | 7CCE2468252A8B4B007557FE /* ViewController.swift in Sources */,
279 | 7CCE2464252A8B4B007557FE /* AppDelegate.swift in Sources */,
280 | 7CCE2466252A8B4B007557FE /* SceneDelegate.swift in Sources */,
281 | );
282 | runOnlyForDeploymentPostprocessing = 0;
283 | };
284 | 7CCE2472252A8B4C007557FE /* Sources */ = {
285 | isa = PBXSourcesBuildPhase;
286 | buildActionMask = 2147483647;
287 | files = (
288 | 7CCE247B252A8B4C007557FE /* ECTimelineViewExampleTests.swift in Sources */,
289 | );
290 | runOnlyForDeploymentPostprocessing = 0;
291 | };
292 | 7CCE247D252A8B4C007557FE /* Sources */ = {
293 | isa = PBXSourcesBuildPhase;
294 | buildActionMask = 2147483647;
295 | files = (
296 | 7CCE2486252A8B4C007557FE /* ECTimelineViewExampleUITests.swift in Sources */,
297 | );
298 | runOnlyForDeploymentPostprocessing = 0;
299 | };
300 | /* End PBXSourcesBuildPhase section */
301 |
302 | /* Begin PBXTargetDependency section */
303 | 7CCE2478252A8B4C007557FE /* PBXTargetDependency */ = {
304 | isa = PBXTargetDependency;
305 | target = 7CCE245F252A8B4B007557FE /* ECTimelineViewExample */;
306 | targetProxy = 7CCE2477252A8B4C007557FE /* PBXContainerItemProxy */;
307 | };
308 | 7CCE2483252A8B4C007557FE /* PBXTargetDependency */ = {
309 | isa = PBXTargetDependency;
310 | target = 7CCE245F252A8B4B007557FE /* ECTimelineViewExample */;
311 | targetProxy = 7CCE2482252A8B4C007557FE /* PBXContainerItemProxy */;
312 | };
313 | /* End PBXTargetDependency section */
314 |
315 | /* Begin PBXVariantGroup section */
316 | 7CCE2469252A8B4B007557FE /* Main.storyboard */ = {
317 | isa = PBXVariantGroup;
318 | children = (
319 | 7CCE246A252A8B4B007557FE /* Base */,
320 | );
321 | name = Main.storyboard;
322 | sourceTree = "";
323 | };
324 | 7CCE246E252A8B4C007557FE /* LaunchScreen.storyboard */ = {
325 | isa = PBXVariantGroup;
326 | children = (
327 | 7CCE246F252A8B4C007557FE /* Base */,
328 | );
329 | name = LaunchScreen.storyboard;
330 | sourceTree = "";
331 | };
332 | /* End PBXVariantGroup section */
333 |
334 | /* Begin XCBuildConfiguration section */
335 | 7CCE2488252A8B4C007557FE /* Debug */ = {
336 | isa = XCBuildConfiguration;
337 | buildSettings = {
338 | ALWAYS_SEARCH_USER_PATHS = NO;
339 | CLANG_ANALYZER_NONNULL = YES;
340 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
341 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
342 | CLANG_CXX_LIBRARY = "libc++";
343 | CLANG_ENABLE_MODULES = YES;
344 | CLANG_ENABLE_OBJC_ARC = YES;
345 | CLANG_ENABLE_OBJC_WEAK = YES;
346 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
347 | CLANG_WARN_BOOL_CONVERSION = YES;
348 | CLANG_WARN_COMMA = YES;
349 | CLANG_WARN_CONSTANT_CONVERSION = YES;
350 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
351 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
352 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
353 | CLANG_WARN_EMPTY_BODY = YES;
354 | CLANG_WARN_ENUM_CONVERSION = YES;
355 | CLANG_WARN_INFINITE_RECURSION = YES;
356 | CLANG_WARN_INT_CONVERSION = YES;
357 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
358 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
359 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
360 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
361 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
363 | CLANG_WARN_STRICT_PROTOTYPES = YES;
364 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
366 | CLANG_WARN_UNREACHABLE_CODE = YES;
367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
368 | COPY_PHASE_STRIP = NO;
369 | DEBUG_INFORMATION_FORMAT = dwarf;
370 | ENABLE_STRICT_OBJC_MSGSEND = YES;
371 | ENABLE_TESTABILITY = YES;
372 | GCC_C_LANGUAGE_STANDARD = gnu11;
373 | GCC_DYNAMIC_NO_PIC = NO;
374 | GCC_NO_COMMON_BLOCKS = YES;
375 | GCC_OPTIMIZATION_LEVEL = 0;
376 | GCC_PREPROCESSOR_DEFINITIONS = (
377 | "DEBUG=1",
378 | "$(inherited)",
379 | );
380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
382 | GCC_WARN_UNDECLARED_SELECTOR = YES;
383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
384 | GCC_WARN_UNUSED_FUNCTION = YES;
385 | GCC_WARN_UNUSED_VARIABLE = YES;
386 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
387 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
388 | MTL_FAST_MATH = YES;
389 | ONLY_ACTIVE_ARCH = YES;
390 | SDKROOT = iphoneos;
391 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
392 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
393 | };
394 | name = Debug;
395 | };
396 | 7CCE2489252A8B4C007557FE /* Release */ = {
397 | isa = XCBuildConfiguration;
398 | buildSettings = {
399 | ALWAYS_SEARCH_USER_PATHS = NO;
400 | CLANG_ANALYZER_NONNULL = YES;
401 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
402 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
403 | CLANG_CXX_LIBRARY = "libc++";
404 | CLANG_ENABLE_MODULES = YES;
405 | CLANG_ENABLE_OBJC_ARC = YES;
406 | CLANG_ENABLE_OBJC_WEAK = YES;
407 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
408 | CLANG_WARN_BOOL_CONVERSION = YES;
409 | CLANG_WARN_COMMA = YES;
410 | CLANG_WARN_CONSTANT_CONVERSION = YES;
411 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
412 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
413 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
414 | CLANG_WARN_EMPTY_BODY = YES;
415 | CLANG_WARN_ENUM_CONVERSION = YES;
416 | CLANG_WARN_INFINITE_RECURSION = YES;
417 | CLANG_WARN_INT_CONVERSION = YES;
418 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
419 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
420 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
421 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
422 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
423 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
424 | CLANG_WARN_STRICT_PROTOTYPES = YES;
425 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
426 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
427 | CLANG_WARN_UNREACHABLE_CODE = YES;
428 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
429 | COPY_PHASE_STRIP = NO;
430 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
431 | ENABLE_NS_ASSERTIONS = NO;
432 | ENABLE_STRICT_OBJC_MSGSEND = YES;
433 | GCC_C_LANGUAGE_STANDARD = gnu11;
434 | GCC_NO_COMMON_BLOCKS = YES;
435 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
436 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
437 | GCC_WARN_UNDECLARED_SELECTOR = YES;
438 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
439 | GCC_WARN_UNUSED_FUNCTION = YES;
440 | GCC_WARN_UNUSED_VARIABLE = YES;
441 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
442 | MTL_ENABLE_DEBUG_INFO = NO;
443 | MTL_FAST_MATH = YES;
444 | SDKROOT = iphoneos;
445 | SWIFT_COMPILATION_MODE = wholemodule;
446 | SWIFT_OPTIMIZATION_LEVEL = "-O";
447 | VALIDATE_PRODUCT = YES;
448 | };
449 | name = Release;
450 | };
451 | 7CCE248B252A8B4C007557FE /* Debug */ = {
452 | isa = XCBuildConfiguration;
453 | buildSettings = {
454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
455 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
456 | CODE_SIGN_STYLE = Automatic;
457 | DEVELOPMENT_TEAM = HP22MNWU5C;
458 | INFOPLIST_FILE = ECTimelineViewExample/Info.plist;
459 | LD_RUNPATH_SEARCH_PATHS = (
460 | "$(inherited)",
461 | "@executable_path/Frameworks",
462 | );
463 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExample;
464 | PRODUCT_NAME = "$(TARGET_NAME)";
465 | SWIFT_VERSION = 5.0;
466 | TARGETED_DEVICE_FAMILY = "1,2";
467 | };
468 | name = Debug;
469 | };
470 | 7CCE248C252A8B4C007557FE /* Release */ = {
471 | isa = XCBuildConfiguration;
472 | buildSettings = {
473 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
474 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
475 | CODE_SIGN_STYLE = Automatic;
476 | DEVELOPMENT_TEAM = HP22MNWU5C;
477 | INFOPLIST_FILE = ECTimelineViewExample/Info.plist;
478 | LD_RUNPATH_SEARCH_PATHS = (
479 | "$(inherited)",
480 | "@executable_path/Frameworks",
481 | );
482 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExample;
483 | PRODUCT_NAME = "$(TARGET_NAME)";
484 | SWIFT_VERSION = 5.0;
485 | TARGETED_DEVICE_FAMILY = "1,2";
486 | };
487 | name = Release;
488 | };
489 | 7CCE248E252A8B4C007557FE /* Debug */ = {
490 | isa = XCBuildConfiguration;
491 | buildSettings = {
492 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
493 | BUNDLE_LOADER = "$(TEST_HOST)";
494 | CODE_SIGN_STYLE = Automatic;
495 | DEVELOPMENT_TEAM = HP22MNWU5C;
496 | INFOPLIST_FILE = ECTimelineViewExampleTests/Info.plist;
497 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
498 | LD_RUNPATH_SEARCH_PATHS = (
499 | "$(inherited)",
500 | "@executable_path/Frameworks",
501 | "@loader_path/Frameworks",
502 | );
503 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExampleTests;
504 | PRODUCT_NAME = "$(TARGET_NAME)";
505 | SWIFT_VERSION = 5.0;
506 | TARGETED_DEVICE_FAMILY = "1,2";
507 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ECTimelineViewExample.app/ECTimelineViewExample";
508 | };
509 | name = Debug;
510 | };
511 | 7CCE248F252A8B4C007557FE /* Release */ = {
512 | isa = XCBuildConfiguration;
513 | buildSettings = {
514 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
515 | BUNDLE_LOADER = "$(TEST_HOST)";
516 | CODE_SIGN_STYLE = Automatic;
517 | DEVELOPMENT_TEAM = HP22MNWU5C;
518 | INFOPLIST_FILE = ECTimelineViewExampleTests/Info.plist;
519 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
520 | LD_RUNPATH_SEARCH_PATHS = (
521 | "$(inherited)",
522 | "@executable_path/Frameworks",
523 | "@loader_path/Frameworks",
524 | );
525 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExampleTests;
526 | PRODUCT_NAME = "$(TARGET_NAME)";
527 | SWIFT_VERSION = 5.0;
528 | TARGETED_DEVICE_FAMILY = "1,2";
529 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ECTimelineViewExample.app/ECTimelineViewExample";
530 | };
531 | name = Release;
532 | };
533 | 7CCE2491252A8B4C007557FE /* Debug */ = {
534 | isa = XCBuildConfiguration;
535 | buildSettings = {
536 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
537 | CODE_SIGN_STYLE = Automatic;
538 | DEVELOPMENT_TEAM = HP22MNWU5C;
539 | INFOPLIST_FILE = ECTimelineViewExampleUITests/Info.plist;
540 | LD_RUNPATH_SEARCH_PATHS = (
541 | "$(inherited)",
542 | "@executable_path/Frameworks",
543 | "@loader_path/Frameworks",
544 | );
545 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExampleUITests;
546 | PRODUCT_NAME = "$(TARGET_NAME)";
547 | SWIFT_VERSION = 5.0;
548 | TARGETED_DEVICE_FAMILY = "1,2";
549 | TEST_TARGET_NAME = ECTimelineViewExample;
550 | };
551 | name = Debug;
552 | };
553 | 7CCE2492252A8B4C007557FE /* Release */ = {
554 | isa = XCBuildConfiguration;
555 | buildSettings = {
556 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
557 | CODE_SIGN_STYLE = Automatic;
558 | DEVELOPMENT_TEAM = HP22MNWU5C;
559 | INFOPLIST_FILE = ECTimelineViewExampleUITests/Info.plist;
560 | LD_RUNPATH_SEARCH_PATHS = (
561 | "$(inherited)",
562 | "@executable_path/Frameworks",
563 | "@loader_path/Frameworks",
564 | );
565 | PRODUCT_BUNDLE_IDENTIFIER = com.evancooper.ECTimelineViewExampleUITests;
566 | PRODUCT_NAME = "$(TARGET_NAME)";
567 | SWIFT_VERSION = 5.0;
568 | TARGETED_DEVICE_FAMILY = "1,2";
569 | TEST_TARGET_NAME = ECTimelineViewExample;
570 | };
571 | name = Release;
572 | };
573 | /* End XCBuildConfiguration section */
574 |
575 | /* Begin XCConfigurationList section */
576 | 7CCE245B252A8B4B007557FE /* Build configuration list for PBXProject "ECTimelineViewExample" */ = {
577 | isa = XCConfigurationList;
578 | buildConfigurations = (
579 | 7CCE2488252A8B4C007557FE /* Debug */,
580 | 7CCE2489252A8B4C007557FE /* Release */,
581 | );
582 | defaultConfigurationIsVisible = 0;
583 | defaultConfigurationName = Release;
584 | };
585 | 7CCE248A252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExample" */ = {
586 | isa = XCConfigurationList;
587 | buildConfigurations = (
588 | 7CCE248B252A8B4C007557FE /* Debug */,
589 | 7CCE248C252A8B4C007557FE /* Release */,
590 | );
591 | defaultConfigurationIsVisible = 0;
592 | defaultConfigurationName = Release;
593 | };
594 | 7CCE248D252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExampleTests" */ = {
595 | isa = XCConfigurationList;
596 | buildConfigurations = (
597 | 7CCE248E252A8B4C007557FE /* Debug */,
598 | 7CCE248F252A8B4C007557FE /* Release */,
599 | );
600 | defaultConfigurationIsVisible = 0;
601 | defaultConfigurationName = Release;
602 | };
603 | 7CCE2490252A8B4C007557FE /* Build configuration list for PBXNativeTarget "ECTimelineViewExampleUITests" */ = {
604 | isa = XCConfigurationList;
605 | buildConfigurations = (
606 | 7CCE2491252A8B4C007557FE /* Debug */,
607 | 7CCE2492252A8B4C007557FE /* Release */,
608 | );
609 | defaultConfigurationIsVisible = 0;
610 | defaultConfigurationName = Release;
611 | };
612 | /* End XCConfigurationList section */
613 |
614 | /* Begin XCSwiftPackageProductDependency section */
615 | 7CCBFF9D252AA4EA00EE9573 /* ECTimelineView */ = {
616 | isa = XCSwiftPackageProductDependency;
617 | productName = ECTimelineView;
618 | };
619 | /* End XCSwiftPackageProductDependency section */
620 | };
621 | rootObject = 7CCE2458252A8B4B007557FE /* Project object */;
622 | }
623 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "ECUICollectionViewMultiDelegate",
6 | "repositoryURL": "https://github.com/EvanCooper9/ECUICollectionViewMultiDelegate",
7 | "state": {
8 | "branch": null,
9 | "revision": "12fc074a5438b56fba802add2a2191cf6c8eb8ff",
10 | "version": "0.1.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // ECTimelineViewExample
4 | //
5 | // Created by Evan Cooper on 2020-10-04.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | true
15 | }
16 |
17 | // MARK: UISceneSession Lifecycle
18 |
19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Base.lproj/Main.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 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UIRequiredDeviceCapabilities
49 |
50 | armv7
51 |
52 | UISupportedInterfaceOrientations
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 | UISupportedInterfaceOrientations~ipad
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationPortraitUpsideDown
62 | UIInterfaceOrientationLandscapeLeft
63 | UIInterfaceOrientationLandscapeRight
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // ECTimelineViewExample
4 | //
5 | // Created by Evan Cooper on 2020-10-04.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | guard let _ = (scene as? UIWindowScene) else { return }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | import ECTimelineView
2 | import UIKit
3 |
4 | struct DataModel {
5 | let index: Int
6 | let value: String
7 | }
8 |
9 | class ViewController: UIViewController {
10 |
11 | // MARK: - Private Properties
12 |
13 | private lazy var timelineView: ECTimelineView = {
14 | let timelineView = ECTimelineView()
15 | timelineView.timelineDataSource = self
16 | timelineView.translatesAutoresizingMaskIntoConstraints = false
17 | return timelineView
18 | }()
19 |
20 | private var data = [Int: DataModel]()
21 |
22 | // MARK: - Lifecycle
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | view.addSubview(timelineView)
28 | NSLayoutConstraint.activate([
29 | view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: timelineView.leadingAnchor),
30 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: timelineView.trailingAnchor),
31 | view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: timelineView.topAnchor),
32 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: timelineView.bottomAnchor)
33 | ])
34 | }
35 | }
36 |
37 | // MARK: - ECTimelineViewDataSource
38 |
39 | extension ViewController: ECTimelineViewDataSource {
40 |
41 | func timelineView(_ timelineView: ECTimelineView, dataFor index: Int, asyncClosure: @escaping (T?) -> Void) -> T? {
42 | DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2) {
43 | asyncClosure(DataModel(index: index, value: "Async \(index)") as? T)
44 | }
45 |
46 | return DataModel(index: index, value: "\(index)") as? T
47 | }
48 |
49 | func configure(_ cell: U, withData data: T?) where U : UICollectionViewCell {
50 | guard let data = data as? DataModel else { return }
51 | cell.subviews.forEach { $0.removeFromSuperview() }
52 | let label = UILabel()
53 | label.textAlignment = .center
54 | label.textColor = .white
55 | label.text = data.value
56 | label.translatesAutoresizingMaskIntoConstraints = false
57 | cell.addSubview(label)
58 | cell.backgroundColor = data.index % 2 == 0 ? .red : .blue
59 | NSLayoutConstraint.activate([
60 | cell.centerYAnchor.constraint(equalTo: label.centerYAnchor),
61 | cell.centerXAnchor.constraint(equalTo: label.centerXAnchor)
62 | ])
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExampleTests/ECTimelineViewExampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ECTimelineViewExampleTests.swift
3 | // ECTimelineViewExampleTests
4 | //
5 | // Created by Evan Cooper on 2020-10-04.
6 | //
7 |
8 | import XCTest
9 | @testable import ECTimelineViewExample
10 |
11 | class ECTimelineViewExampleTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExampleUITests/ECTimelineViewExampleUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ECTimelineViewExampleUITests.swift
3 | // ECTimelineViewExampleUITests
4 | //
5 | // Created by Evan Cooper on 2020-10-04.
6 | //
7 |
8 | import XCTest
9 |
10 | class ECTimelineViewExampleUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Example/ECTimelineViewExampleUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "ECUICollectionViewMultiDelegate",
6 | "repositoryURL": "https://github.com/EvanCooper9/ECUICollectionViewMultiDelegate",
7 | "state": {
8 | "branch": null,
9 | "revision": "12fc074a5438b56fba802add2a2191cf6c8eb8ff",
10 | "version": "0.1.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ECTimelineView",
8 | products: [
9 | .library(
10 | name: "ECTimelineView",
11 | targets: ["ECTimelineView"]),
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/EvanCooper9/ECUICollectionViewMultiDelegate", from: "0.1.0")
15 | ],
16 | targets: [
17 | .target(
18 | name: "ECTimelineView",
19 | dependencies: [
20 | .init(stringLiteral: "ECUICollectionViewMultiDelegate")
21 | ],
22 | path: "Sources"
23 | )
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ECTimelineView
2 |
3 | An horizontal or vertical infinitely scrolling `UICollectionView` implementation. Loads data synchrounously and asynchronously.
4 |
5 | ## Installation
6 | ### Swift Package Manager
7 |
8 | ```swift
9 | .package(url: "https://github.com/EvanCooper9/ECTimelineView", from: "1.0.0")
10 | ```
11 |
12 | ### Dependencies
13 | - [ECUICollectionViewMultiDelegate](https://github.com/EvanCooper9/ECUICollectionViewMultiDelegate)
14 |
15 | ## Usage
16 | > Note: See inline documentation for more details
17 |
18 | ### Data Source
19 | Implement `ECTimelineViewDataSource`, and set the `timelineDataSource` property.
20 |
21 | ```swift
22 | // MARK: ECTimelineViewDataSource protocol
23 |
24 | // Asks for cell data that corresponds to the specified index
25 | func timelineView(_ timelineView: ECTimelineView, dataFor index: Int, asyncClosure: @escaping (_ data: T?) -> Void) -> T?
26 |
27 | // Configures the cell with the designated data
28 | func configure(_ cell: U, withData data: T?)
29 | ```
30 |
--------------------------------------------------------------------------------
/Sources/ECTimelineView.swift:
--------------------------------------------------------------------------------
1 | import ECUICollectionViewMultiDelegate
2 | import UIKit
3 |
4 | public final class ECTimelineView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
5 |
6 | // MARK: - Public properties
7 |
8 | /// Number of screens on either side of the visible cells to prefetch data for
9 | public var bufferScreens = 3 {
10 | didSet { reloadData() }
11 | }
12 |
13 | /// Number of cells visible on the screen
14 | public var visibleCellCount = 5 {
15 | didSet { reloadData() }
16 | }
17 |
18 | /// The direction dictates the axis fo scroll
19 | public var scrollDirection = ScrollDirection.vertical {
20 | didSet { collectionViewLayout = layout }
21 | }
22 |
23 | /// Where to start the visible content, considering the buffer content that is loaded
24 | public var startPosition: StartPosition = .middle
25 |
26 | /// Whether or not to re-ask the data source for new data when loading a cell who's data had been previously fetched
27 | public var refetchData = false
28 |
29 | /// Spacing between each cell
30 | public var cellSpacing: CGFloat = 0
31 |
32 | /// The data source
33 | public weak var timelineDataSource: ECTimelineViewDataSource? {
34 | didSet {
35 | data.removeAll()
36 | dataOffset = timelineDataSource?.lowerBound ?? 0
37 | displayedInitialCells = false
38 | fetchData(for: dataOffset...(pages * visibleCellCount) + dataOffset - 1)
39 | reloadData()
40 | }
41 | }
42 |
43 | // MARK: - Overrides
44 |
45 | /// Do not set this property. Use `timelineDataSource` instead
46 | public override var dataSource: UICollectionViewDataSource? {
47 | didSet {
48 | if !(dataSource is Self) { preconditionFailure("Do not set the dataSource property. Use `timelineDataSource` instead") }
49 | }
50 | }
51 |
52 | public override var delegate: UICollectionViewDelegate? {
53 | didSet {
54 | guard let delegate = delegate, !delegate.isEqual(multiDelegate) else { return }
55 | multiDelegate.add(delegate)
56 | self.delegate = multiDelegate
57 | }
58 | }
59 |
60 | // MARK: - Private properties
61 |
62 | private let multiDelegate = ECUICollectionViewMultiDelegate()
63 | private var displayedInitialCells = false
64 | private var pages: Int { (bufferScreens * 2) + 1 }
65 | private var bufferCells: Int { bufferScreens * visibleCellCount }
66 | private var cellCount: Int { visibleCellCount + (bufferCells * 2) }
67 | private var horizontal: Bool { scrollDirection == .horizontal }
68 | private var vertical: Bool { scrollDirection == .vertical }
69 |
70 | private let dataQueue = DispatchQueue(label: "com.evancooper.ectimelineview.data-queue")
71 | private var data = [Int: T]()
72 | private var dataOffset = 0
73 |
74 | private var areBoundsValid: Bool {
75 | guard let lowerBound = timelineDataSource?.lowerBound, let upperBound = timelineDataSource?.upperBound else { return true }
76 | return (lowerBound..= cellCount
77 | }
78 |
79 | private var cellSize: CGSize {
80 | let width = horizontal ? frame.width / CGFloat(visibleCellCount) : frame.width
81 | let height = horizontal ? frame.height : frame.height / CGFloat(visibleCellCount)
82 | return CGSize(width: width, height: height)
83 | }
84 |
85 | private var currentPosition: CGFloat {
86 | let offset = horizontal ? contentOffset.x : contentOffset.y
87 | return offset + (horizontal ? bounds.width : bounds.height) / 2
88 | }
89 |
90 | private var layout: UICollectionViewFlowLayout {
91 | let layout = UICollectionViewFlowLayout()
92 | layout.scrollDirection = scrollDirection
93 | return layout
94 | }
95 |
96 | private var loadDirection: LoadDirection {
97 | let contentMiddle = (horizontal ? contentSize.width : contentSize.height) / 2
98 | return (currentPosition > contentMiddle) ? .positive : .negative
99 | }
100 |
101 | private var visibleIndicies: [Int] {
102 | visibleCells
103 | .compactMap { indexPath(for: $0)?.row }
104 | .map { $0 + dataOffset }
105 | }
106 |
107 | // MARK: - Lifecycle
108 |
109 | public init(frame: CGRect = .zero) {
110 | super.init(frame: frame, collectionViewLayout: .init())
111 | commonInit()
112 | }
113 |
114 | required init?(coder aDecoder: NSCoder) {
115 | super.init(coder: aDecoder)
116 | commonInit()
117 | }
118 |
119 | private func commonInit() {
120 | collectionViewLayout = layout
121 | showsVerticalScrollIndicator = vertical
122 | showsHorizontalScrollIndicator = horizontal
123 | delegate = self
124 | dataSource = self
125 | multiDelegate.reductionDelegate = self
126 | register(cellType: U.self)
127 | dataOffset = bufferScreens * visibleCellCount
128 | }
129 |
130 | // MARK: - Public Methods
131 |
132 | public func add(_ delegate: UICollectionViewDelegate) {
133 | multiDelegate.add(delegate)
134 | }
135 |
136 | public func remove(_ delegate: UICollectionViewDelegate) {
137 | multiDelegate.remove(delegate)
138 | }
139 |
140 | public func refresh(dataAt index: Int) {
141 | data[index] = timelineDataSource?
142 | .timelineView(self, dataFor: index) { asyncData in
143 | self.data[index] = asyncData
144 | }
145 | }
146 |
147 | // MARK: - Private Methods
148 |
149 | private func adjustContentOffset() {
150 | guard let lowestVisibleIndex = visibleIndicies.min() else { return }
151 |
152 | let oldDataOffset = dataOffset
153 | dataOffset = lowestVisibleIndex - bufferCells
154 |
155 | if areBoundsValid {
156 | if let lowerBound = timelineDataSource?.lowerBound, lowerBound > dataOffset {
157 | dataOffset = lowerBound
158 | }
159 |
160 | if let upperBound = timelineDataSource?.upperBound, upperBound < dataOffset + cellCount {
161 | dataOffset = upperBound - cellCount
162 | }
163 | }
164 |
165 | let size = horizontal ? cellSize.width : cellSize.height
166 | let scrollAmount = (CGFloat(dataOffset - oldDataOffset) * size)
167 | let startOfContentOffset = horizontal ? contentOffset.x : contentOffset.y
168 | let scrollTo = startOfContentOffset - scrollAmount
169 |
170 | let pointX = horizontal ? scrollTo : 0
171 | let pointY = horizontal ? 0 : scrollTo
172 | setContentOffset(.init(x: pointX, y: pointY), animated: false)
173 | }
174 |
175 | private func fetchData() {
176 | let pointToAddData = loadDirection.isPositive ? dataOffset + cellCount : dataOffset - bufferCells - 1
177 |
178 | var min = pointToAddData
179 | var max = pointToAddData + (loadDirection.isPositive ? cellCount : bufferCells)
180 |
181 | if areBoundsValid {
182 | if let lowerBound = timelineDataSource?.lowerBound, lowerBound < min {
183 | min = lowerBound
184 | }
185 |
186 | if let upperBound = timelineDataSource?.upperBound, upperBound > max {
187 | max = upperBound
188 | }
189 | }
190 |
191 | fetchData(for: min...max)
192 | }
193 |
194 | private func fetchData(for range: ClosedRange) {
195 | range
196 | .filter { refetchData || !data.keys.contains($0) }
197 | .forEach { index in
198 | let dataFromDataSource = timelineDataSource?
199 | .timelineView(self, dataFor: index) { [weak self] asyncData in
200 | guard let self = self else { return }
201 | self.dataQueue.sync { self.data[index] = asyncData }
202 | DispatchQueue.main.async {
203 | guard self.visibleIndicies.contains(index) else { return }
204 | let indexPath = IndexPath(row: index - self.dataOffset, section: 0)
205 | self.reloadItems(at: [indexPath])
206 | }
207 | }
208 | dataQueue.sync { data[index] = dataFromDataSource }
209 | }
210 | }
211 |
212 | // MARK: - UICollectionViewDataSource
213 |
214 | public func numberOfSections(in collectionView: UICollectionView) -> Int {
215 | 1
216 | }
217 |
218 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
219 | visibleCellCount * pages
220 | }
221 |
222 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
223 | let adjustedIndex = indexPath.row + dataOffset
224 | let cell = dequeueReusableCell(withType: U.self, for: indexPath)
225 | timelineDataSource?.configure(cell, withData: data[adjustedIndex])
226 | return cell
227 | }
228 |
229 | // MARK: - UICollectionViewDelegate
230 |
231 | public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
232 | guard !displayedInitialCells else { return }
233 | displayedInitialCells.toggle()
234 | let startPosition: Int = {
235 | switch self.startPosition {
236 | case .beginning:
237 | return 0
238 | case .middle:
239 | return bufferCells
240 | case .end:
241 | return cellCount - visibleCellCount
242 | }
243 | }()
244 | scrollToItem(at: .init(row: startPosition, section: 0), at: .top, animated: false)
245 | }
246 |
247 | // MARK: - UICollectionViewDelegateFlowLayout
248 |
249 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
250 | cellSpacing
251 | }
252 |
253 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
254 | cellSize
255 | }
256 |
257 | // MARK: - UIScrollViewDelegate
258 |
259 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
260 | fetchData()
261 | adjustContentOffset()
262 | reloadData()
263 | }
264 | }
265 |
266 |
267 | extension ECTimelineView: ECReductionDelegate {
268 | public func reduce(_ first: ReductionType, _ second: ReductionType, selector: Selector) -> ReductionType {
269 | second
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/Sources/ECTimelineViewDataSource.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol ECTimelineViewDataSource: AnyObject {
4 |
5 | /// The lower bound (inclusive). The view will not scroll past this index or request data from the data source
6 | var lowerBound: Int? { get }
7 |
8 |
9 | /// The upper bound (exclusive). The view will not scroll past this index or request data from the data source
10 | var upperBound: Int? { get }
11 |
12 | /// Asks for cell data that corresponds to the specified index
13 | /// - Parameters:
14 | /// - timelineCollectionView: the opject requesting the data
15 | /// - index: the index of the data being stored in relation to all the other data
16 | /// - asyncClosure: a closure to return the requested data in an asynchronous fashion
17 | /// - Important: Data returned through asyncClosure will override any data previously returned
18 | func timelineView(_ timelineView: ECTimelineView, dataFor index: Int, asyncClosure: @escaping (_ data: T?) -> Void) -> T?
19 |
20 | /// Configures the cell with the designated data
21 | /// - Parameters:
22 | /// - cell: the cell to configure
23 | /// - data: the data that should be used to configure the cell
24 | func configure(_ cell: U, withData data: T?)
25 | }
26 |
27 | public extension ECTimelineViewDataSource {
28 | var lowerBound: Int? { nil }
29 | var upperBound: Int? { nil }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Enums/LoadDirection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal enum LoadDirection {
4 | case positive, negative
5 | }
6 |
7 | extension LoadDirection {
8 | var isPositive: Bool { self == .positive }
9 | var isNegative: Bool { self == .negative }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Enums/StartPosition.swift:
--------------------------------------------------------------------------------
1 | public enum StartPosition {
2 | case beginning
3 | case middle
4 | case end
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/Extensions/UICollectionView+Extensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UICollectionView {
4 | func dequeueReusableCell(withType type: T.Type, for indexPath: IndexPath) -> T {
5 | dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as! T
6 | }
7 |
8 | func register(cellType type: T.Type) {
9 | register(T.self, forCellWithReuseIdentifier: String(describing: T.self))
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/ECTimelineViewTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import ECTimelineView
3 |
4 | final class ECTimelineViewTests: XCTestCase {
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(ECTimelineViewTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------