├── .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 | --------------------------------------------------------------------------------