├── .swift-version ├── KSTimeline.podspec ├── KSTimeline.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── Shih.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── KSTimeline ├── Info.plist ├── KSTimeline.h ├── KSTimelineContentView.swift ├── KSTimelineRulerView.swift └── KSTimelineView.swift ├── KSTimelineDemo ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── BigBunny.mp4 ├── F35.mp4 ├── Info.plist ├── Toystory.mp4 ├── VideoHelper.swift ├── ViewController1.swift └── ViewController2.swift ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /KSTimeline.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "KSTimeline" 4 | s.version = "0.1.1" 5 | s.summary = "KSTimeline, written in swift, is a simple and customizable view which supports showing a series of events in a vertically time-sorted structure." 6 | s.homepage = "https://github.com/KenShih522/KSTimeline" 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { "KenShih522" => "kenshih522@gmail.com" } 9 | s.platform = :ios, "10.0" 10 | s.source = { :git => "https://github.com/KenShih522/KSTimeline", :tag => "0.1.1" } 11 | s.source_files = Dir['KSTimeline/*'] 12 | s.requires_arc = true 13 | 14 | end 15 | -------------------------------------------------------------------------------- /KSTimeline.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 514A31641FE115C300795B16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A31631FE115C300795B16 /* AppDelegate.swift */; }; 11 | 514A31661FE115C300795B16 /* ViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A31651FE115C300795B16 /* ViewController2.swift */; }; 12 | 514A31691FE115C300795B16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 514A31671FE115C300795B16 /* Main.storyboard */; }; 13 | 514A316B1FE115C300795B16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 514A316A1FE115C300795B16 /* Assets.xcassets */; }; 14 | 514A316E1FE115C300795B16 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 514A316C1FE115C300795B16 /* LaunchScreen.storyboard */; }; 15 | 514A317E1FE115F900795B16 /* KSTimeline.h in Headers */ = {isa = PBXBuildFile; fileRef = 514A317C1FE115F900795B16 /* KSTimeline.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | 514A31811FE115F900795B16 /* KSTimeline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 514A317A1FE115F900795B16 /* KSTimeline.framework */; }; 17 | 514A31821FE115F900795B16 /* KSTimeline.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 514A317A1FE115F900795B16 /* KSTimeline.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 18 | 514A318A1FE1163D00795B16 /* KSTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A31871FE1163D00795B16 /* KSTimelineView.swift */; }; 19 | 514A318B1FE1163D00795B16 /* KSTimelineContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A31881FE1163D00795B16 /* KSTimelineContentView.swift */; }; 20 | 514A318C1FE1163D00795B16 /* KSTimelineRulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A31891FE1163D00795B16 /* KSTimelineRulerView.swift */; }; 21 | 514A318E1FE116C000795B16 /* VideoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A318D1FE116C000795B16 /* VideoHelper.swift */; }; 22 | 514A31921FE1195100795B16 /* BigBunny.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 514A318F1FE1195100795B16 /* BigBunny.mp4 */; }; 23 | 514A31931FE1195100795B16 /* Toystory.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 514A31901FE1195100795B16 /* Toystory.mp4 */; }; 24 | 514A31941FE1195100795B16 /* F35.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 514A31911FE1195100795B16 /* F35.mp4 */; }; 25 | 515662111FE1434600E6CED8 /* ViewController1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515662101FE1434600E6CED8 /* ViewController1.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 514A317F1FE115F900795B16 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 514A31581FE115C300795B16 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 514A31791FE115F900795B16; 34 | remoteInfo = KSTimeline; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXCopyFilesBuildPhase section */ 39 | 514A31861FE115F900795B16 /* Embed Frameworks */ = { 40 | isa = PBXCopyFilesBuildPhase; 41 | buildActionMask = 2147483647; 42 | dstPath = ""; 43 | dstSubfolderSpec = 10; 44 | files = ( 45 | 514A31821FE115F900795B16 /* KSTimeline.framework in Embed Frameworks */, 46 | ); 47 | name = "Embed Frameworks"; 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXCopyFilesBuildPhase section */ 51 | 52 | /* Begin PBXFileReference section */ 53 | 514A31601FE115C300795B16 /* KSTimelineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KSTimelineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 514A31631FE115C300795B16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 55 | 514A31651FE115C300795B16 /* ViewController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController2.swift; sourceTree = ""; }; 56 | 514A31681FE115C300795B16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 57 | 514A316A1FE115C300795B16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 58 | 514A316D1FE115C300795B16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 59 | 514A316F1FE115C300795B16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 514A317A1FE115F900795B16 /* KSTimeline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KSTimeline.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 514A317C1FE115F900795B16 /* KSTimeline.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSTimeline.h; sourceTree = ""; }; 62 | 514A317D1FE115F900795B16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | 514A31871FE1163D00795B16 /* KSTimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSTimelineView.swift; sourceTree = ""; }; 64 | 514A31881FE1163D00795B16 /* KSTimelineContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSTimelineContentView.swift; sourceTree = ""; }; 65 | 514A31891FE1163D00795B16 /* KSTimelineRulerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSTimelineRulerView.swift; sourceTree = ""; }; 66 | 514A318D1FE116C000795B16 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = ""; }; 67 | 514A318F1FE1195100795B16 /* BigBunny.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = BigBunny.mp4; sourceTree = ""; }; 68 | 514A31901FE1195100795B16 /* Toystory.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Toystory.mp4; sourceTree = ""; }; 69 | 514A31911FE1195100795B16 /* F35.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = F35.mp4; sourceTree = ""; }; 70 | 515662101FE1434600E6CED8 /* ViewController1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController1.swift; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | 514A315D1FE115C300795B16 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | 514A31811FE115F900795B16 /* KSTimeline.framework in Frameworks */, 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | 514A31761FE115F900795B16 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXFrameworksBuildPhase section */ 90 | 91 | /* Begin PBXGroup section */ 92 | 514A31571FE115C300795B16 = { 93 | isa = PBXGroup; 94 | children = ( 95 | 514A317B1FE115F900795B16 /* KSTimeline */, 96 | 514A31621FE115C300795B16 /* KSTimelineDemo */, 97 | 514A31611FE115C300795B16 /* Products */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | 514A31611FE115C300795B16 /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 514A31601FE115C300795B16 /* KSTimelineDemo.app */, 105 | 514A317A1FE115F900795B16 /* KSTimeline.framework */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | 514A31621FE115C300795B16 /* KSTimelineDemo */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 514A31901FE1195100795B16 /* Toystory.mp4 */, 114 | 514A318F1FE1195100795B16 /* BigBunny.mp4 */, 115 | 514A31911FE1195100795B16 /* F35.mp4 */, 116 | 514A31631FE115C300795B16 /* AppDelegate.swift */, 117 | 514A318D1FE116C000795B16 /* VideoHelper.swift */, 118 | 515662101FE1434600E6CED8 /* ViewController1.swift */, 119 | 514A31651FE115C300795B16 /* ViewController2.swift */, 120 | 514A31671FE115C300795B16 /* Main.storyboard */, 121 | 514A316A1FE115C300795B16 /* Assets.xcassets */, 122 | 514A316C1FE115C300795B16 /* LaunchScreen.storyboard */, 123 | 514A316F1FE115C300795B16 /* Info.plist */, 124 | ); 125 | path = KSTimelineDemo; 126 | sourceTree = ""; 127 | }; 128 | 514A317B1FE115F900795B16 /* KSTimeline */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 514A31881FE1163D00795B16 /* KSTimelineContentView.swift */, 132 | 514A31891FE1163D00795B16 /* KSTimelineRulerView.swift */, 133 | 514A31871FE1163D00795B16 /* KSTimelineView.swift */, 134 | 514A317C1FE115F900795B16 /* KSTimeline.h */, 135 | 514A317D1FE115F900795B16 /* Info.plist */, 136 | ); 137 | path = KSTimeline; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXHeadersBuildPhase section */ 143 | 514A31771FE115F900795B16 /* Headers */ = { 144 | isa = PBXHeadersBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 514A317E1FE115F900795B16 /* KSTimeline.h in Headers */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXHeadersBuildPhase section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 514A315F1FE115C300795B16 /* KSTimelineDemo */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 514A31721FE115C300795B16 /* Build configuration list for PBXNativeTarget "KSTimelineDemo" */; 157 | buildPhases = ( 158 | 514A315C1FE115C300795B16 /* Sources */, 159 | 514A315D1FE115C300795B16 /* Frameworks */, 160 | 514A315E1FE115C300795B16 /* Resources */, 161 | 514A31861FE115F900795B16 /* Embed Frameworks */, 162 | ); 163 | buildRules = ( 164 | ); 165 | dependencies = ( 166 | 514A31801FE115F900795B16 /* PBXTargetDependency */, 167 | ); 168 | name = KSTimelineDemo; 169 | productName = KSTimelineDemo; 170 | productReference = 514A31601FE115C300795B16 /* KSTimelineDemo.app */; 171 | productType = "com.apple.product-type.application"; 172 | }; 173 | 514A31791FE115F900795B16 /* KSTimeline */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = 514A31831FE115F900795B16 /* Build configuration list for PBXNativeTarget "KSTimeline" */; 176 | buildPhases = ( 177 | 514A31751FE115F900795B16 /* Sources */, 178 | 514A31761FE115F900795B16 /* Frameworks */, 179 | 514A31771FE115F900795B16 /* Headers */, 180 | 514A31781FE115F900795B16 /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | ); 186 | name = KSTimeline; 187 | productName = KSTimeline; 188 | productReference = 514A317A1FE115F900795B16 /* KSTimeline.framework */; 189 | productType = "com.apple.product-type.framework"; 190 | }; 191 | /* End PBXNativeTarget section */ 192 | 193 | /* Begin PBXProject section */ 194 | 514A31581FE115C300795B16 /* Project object */ = { 195 | isa = PBXProject; 196 | attributes = { 197 | LastSwiftUpdateCheck = 0910; 198 | LastUpgradeCheck = 0910; 199 | ORGANIZATIONNAME = kenshih; 200 | TargetAttributes = { 201 | 514A315F1FE115C300795B16 = { 202 | CreatedOnToolsVersion = 9.1; 203 | ProvisioningStyle = Automatic; 204 | }; 205 | 514A31791FE115F900795B16 = { 206 | CreatedOnToolsVersion = 9.1; 207 | LastSwiftMigration = 0910; 208 | ProvisioningStyle = Automatic; 209 | }; 210 | }; 211 | }; 212 | buildConfigurationList = 514A315B1FE115C300795B16 /* Build configuration list for PBXProject "KSTimeline" */; 213 | compatibilityVersion = "Xcode 8.0"; 214 | developmentRegion = en; 215 | hasScannedForEncodings = 0; 216 | knownRegions = ( 217 | en, 218 | Base, 219 | ); 220 | mainGroup = 514A31571FE115C300795B16; 221 | productRefGroup = 514A31611FE115C300795B16 /* Products */; 222 | projectDirPath = ""; 223 | projectRoot = ""; 224 | targets = ( 225 | 514A31791FE115F900795B16 /* KSTimeline */, 226 | 514A315F1FE115C300795B16 /* KSTimelineDemo */, 227 | ); 228 | }; 229 | /* End PBXProject section */ 230 | 231 | /* Begin PBXResourcesBuildPhase section */ 232 | 514A315E1FE115C300795B16 /* Resources */ = { 233 | isa = PBXResourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 514A31941FE1195100795B16 /* F35.mp4 in Resources */, 237 | 514A316E1FE115C300795B16 /* LaunchScreen.storyboard in Resources */, 238 | 514A31921FE1195100795B16 /* BigBunny.mp4 in Resources */, 239 | 514A31931FE1195100795B16 /* Toystory.mp4 in Resources */, 240 | 514A316B1FE115C300795B16 /* Assets.xcassets in Resources */, 241 | 514A31691FE115C300795B16 /* Main.storyboard in Resources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | 514A31781FE115F900795B16 /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | 514A315C1FE115C300795B16 /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 515662111FE1434600E6CED8 /* ViewController1.swift in Sources */, 260 | 514A318E1FE116C000795B16 /* VideoHelper.swift in Sources */, 261 | 514A31661FE115C300795B16 /* ViewController2.swift in Sources */, 262 | 514A31641FE115C300795B16 /* AppDelegate.swift in Sources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | 514A31751FE115F900795B16 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | 514A318A1FE1163D00795B16 /* KSTimelineView.swift in Sources */, 271 | 514A318B1FE1163D00795B16 /* KSTimelineContentView.swift in Sources */, 272 | 514A318C1FE1163D00795B16 /* KSTimelineRulerView.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXTargetDependency section */ 279 | 514A31801FE115F900795B16 /* PBXTargetDependency */ = { 280 | isa = PBXTargetDependency; 281 | target = 514A31791FE115F900795B16 /* KSTimeline */; 282 | targetProxy = 514A317F1FE115F900795B16 /* PBXContainerItemProxy */; 283 | }; 284 | /* End PBXTargetDependency section */ 285 | 286 | /* Begin PBXVariantGroup section */ 287 | 514A31671FE115C300795B16 /* Main.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 514A31681FE115C300795B16 /* Base */, 291 | ); 292 | name = Main.storyboard; 293 | sourceTree = ""; 294 | }; 295 | 514A316C1FE115C300795B16 /* LaunchScreen.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | 514A316D1FE115C300795B16 /* Base */, 299 | ); 300 | name = LaunchScreen.storyboard; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXVariantGroup section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | 514A31701FE115C300795B16 /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | CLANG_ANALYZER_NONNULL = YES; 311 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 321 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 329 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 330 | CLANG_WARN_STRICT_PROTOTYPES = YES; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | CODE_SIGN_IDENTITY = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | DEBUG_INFORMATION_FORMAT = dwarf; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | ENABLE_TESTABILITY = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu11; 341 | GCC_DYNAMIC_NO_PIC = NO; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_OPTIMIZATION_LEVEL = 0; 344 | GCC_PREPROCESSOR_DEFINITIONS = ( 345 | "DEBUG=1", 346 | "$(inherited)", 347 | ); 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 355 | MTL_ENABLE_DEBUG_INFO = YES; 356 | ONLY_ACTIVE_ARCH = YES; 357 | SDKROOT = iphoneos; 358 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 359 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 360 | }; 361 | name = Debug; 362 | }; 363 | 514A31711FE115C300795B16 /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 369 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 370 | CLANG_CXX_LIBRARY = "libc++"; 371 | CLANG_ENABLE_MODULES = YES; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 374 | CLANG_WARN_BOOL_CONVERSION = YES; 375 | CLANG_WARN_COMMA = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 378 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 379 | CLANG_WARN_EMPTY_BODY = YES; 380 | CLANG_WARN_ENUM_CONVERSION = YES; 381 | CLANG_WARN_INFINITE_RECURSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | CODE_SIGN_IDENTITY = "iPhone Developer"; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu11; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | SDKROOT = iphoneos; 408 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 409 | VALIDATE_PRODUCT = YES; 410 | }; 411 | name = Release; 412 | }; 413 | 514A31731FE115C300795B16 /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 417 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 418 | CODE_SIGN_STYLE = Automatic; 419 | DEVELOPMENT_TEAM = 79E68ZK73C; 420 | INFOPLIST_FILE = KSTimelineDemo/Info.plist; 421 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 423 | PRODUCT_BUNDLE_IDENTIFIER = com.kenshih.KSTimelineDemo; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | SWIFT_VERSION = 4.0; 426 | TARGETED_DEVICE_FAMILY = "1,2"; 427 | }; 428 | name = Debug; 429 | }; 430 | 514A31741FE115C300795B16 /* Release */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 434 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 435 | CODE_SIGN_STYLE = Automatic; 436 | DEVELOPMENT_TEAM = 79E68ZK73C; 437 | INFOPLIST_FILE = KSTimelineDemo/Info.plist; 438 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 439 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 440 | PRODUCT_BUNDLE_IDENTIFIER = com.kenshih.KSTimelineDemo; 441 | PRODUCT_NAME = "$(TARGET_NAME)"; 442 | SWIFT_VERSION = 4.0; 443 | TARGETED_DEVICE_FAMILY = "1,2"; 444 | }; 445 | name = Release; 446 | }; 447 | 514A31841FE115F900795B16 /* Debug */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | CLANG_ENABLE_MODULES = YES; 451 | CODE_SIGN_IDENTITY = ""; 452 | CODE_SIGN_STYLE = Automatic; 453 | CURRENT_PROJECT_VERSION = 1; 454 | DEFINES_MODULE = YES; 455 | DYLIB_COMPATIBILITY_VERSION = 1; 456 | DYLIB_CURRENT_VERSION = 1; 457 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 458 | INFOPLIST_FILE = KSTimeline/Info.plist; 459 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 460 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 461 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 462 | PRODUCT_BUNDLE_IDENTIFIER = com.kenshih.KSTimeline; 463 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 464 | SKIP_INSTALL = YES; 465 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 466 | SWIFT_VERSION = 4.0; 467 | TARGETED_DEVICE_FAMILY = "1,2"; 468 | VERSIONING_SYSTEM = "apple-generic"; 469 | VERSION_INFO_PREFIX = ""; 470 | }; 471 | name = Debug; 472 | }; 473 | 514A31851FE115F900795B16 /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | CLANG_ENABLE_MODULES = YES; 477 | CODE_SIGN_IDENTITY = ""; 478 | CODE_SIGN_STYLE = Automatic; 479 | CURRENT_PROJECT_VERSION = 1; 480 | DEFINES_MODULE = YES; 481 | DYLIB_COMPATIBILITY_VERSION = 1; 482 | DYLIB_CURRENT_VERSION = 1; 483 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 484 | INFOPLIST_FILE = KSTimeline/Info.plist; 485 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 486 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 487 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 488 | PRODUCT_BUNDLE_IDENTIFIER = com.kenshih.KSTimeline; 489 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 490 | SKIP_INSTALL = YES; 491 | SWIFT_VERSION = 4.0; 492 | TARGETED_DEVICE_FAMILY = "1,2"; 493 | VERSIONING_SYSTEM = "apple-generic"; 494 | VERSION_INFO_PREFIX = ""; 495 | }; 496 | name = Release; 497 | }; 498 | /* End XCBuildConfiguration section */ 499 | 500 | /* Begin XCConfigurationList section */ 501 | 514A315B1FE115C300795B16 /* Build configuration list for PBXProject "KSTimeline" */ = { 502 | isa = XCConfigurationList; 503 | buildConfigurations = ( 504 | 514A31701FE115C300795B16 /* Debug */, 505 | 514A31711FE115C300795B16 /* Release */, 506 | ); 507 | defaultConfigurationIsVisible = 0; 508 | defaultConfigurationName = Release; 509 | }; 510 | 514A31721FE115C300795B16 /* Build configuration list for PBXNativeTarget "KSTimelineDemo" */ = { 511 | isa = XCConfigurationList; 512 | buildConfigurations = ( 513 | 514A31731FE115C300795B16 /* Debug */, 514 | 514A31741FE115C300795B16 /* Release */, 515 | ); 516 | defaultConfigurationIsVisible = 0; 517 | defaultConfigurationName = Release; 518 | }; 519 | 514A31831FE115F900795B16 /* Build configuration list for PBXNativeTarget "KSTimeline" */ = { 520 | isa = XCConfigurationList; 521 | buildConfigurations = ( 522 | 514A31841FE115F900795B16 /* Debug */, 523 | 514A31851FE115F900795B16 /* Release */, 524 | ); 525 | defaultConfigurationIsVisible = 0; 526 | defaultConfigurationName = Release; 527 | }; 528 | /* End XCConfigurationList section */ 529 | }; 530 | rootObject = 514A31581FE115C300795B16 /* Project object */; 531 | } 532 | -------------------------------------------------------------------------------- /KSTimeline.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KSTimeline.xcodeproj/xcuserdata/Shih.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | KSTimeline.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | KSTimelineDemo.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /KSTimeline/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /KSTimeline/KSTimeline.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSTimeline.h 3 | // KSTimeline 4 | // 5 | // Created by Shih on 13/12/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for KSTimeline. 12 | FOUNDATION_EXPORT double KSTimelineVersionNumber; 13 | 14 | //! Project version string for KSTimeline. 15 | FOUNDATION_EXPORT const unsigned char KSTimelineVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /KSTimeline/KSTimelineContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KSTimelineContentView.swift 3 | // KSTimeline 4 | // 5 | // Created by Shih on 24/11/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable open class KSTimelineContentView: UIScrollView, UIScrollViewDelegate { 12 | 13 | public let rulerView = KSTimelineRulerView() 14 | 15 | override open func draw(_ rect: CGRect) { 16 | 17 | super.draw(rect) 18 | 19 | } 20 | 21 | func updateRuler() { 22 | 23 | self.rulerView.frame = CGRect(x: 0, y: 0, width: self.contentSize.width, height: self.contentSize.height) 24 | 25 | } 26 | 27 | override open func layoutSubviews() { 28 | 29 | super.layoutSubviews() 30 | 31 | self.backgroundColor = UIColor.clear 32 | 33 | self.updateRuler() 34 | 35 | } 36 | 37 | override init(frame: CGRect) { 38 | 39 | super.init(frame: frame) 40 | 41 | self.commonInit() 42 | 43 | } 44 | 45 | required public init?(coder aDecoder: NSCoder) { 46 | 47 | super.init(coder: aDecoder) 48 | 49 | self.commonInit() 50 | 51 | } 52 | 53 | override open func prepareForInterfaceBuilder() { 54 | 55 | super.prepareForInterfaceBuilder() 56 | 57 | self.commonInit() 58 | 59 | } 60 | 61 | internal func commonInit() { 62 | 63 | self.showsVerticalScrollIndicator = false 64 | 65 | self.showsHorizontalScrollIndicator = false 66 | 67 | self.isOpaque = true 68 | 69 | self.addSubview(self.rulerView) 70 | 71 | self.rulerView.drawWave = true 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /KSTimeline/KSTimelineRulerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KSTimelineRulerView.swift 3 | // KSTimeline 4 | // 5 | // Created by Shih on 24/11/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol KSTimelineRulerEventDataSource: NSObjectProtocol { 12 | 13 | func numberOfEvents(_ ruler: KSTimelineRulerView) -> Int 14 | 15 | func timelineRuler(_ ruler: KSTimelineRulerView, eventAt index: Int) -> KSTimelineEvent 16 | 17 | } 18 | 19 | @IBDesignable open class KSTimelineRulerView: UIView { 20 | 21 | var dataSource: KSTimelineRulerEventDataSource? 22 | 23 | var drawWave: Bool = false { 24 | 25 | didSet { 26 | 27 | self.setNeedsDisplay() 28 | 29 | } 30 | 31 | } 32 | 33 | internal func drawEvent(rect: CGRect) { 34 | 35 | guard let dataSource = self.dataSource else { return } 36 | 37 | let numberOfEvents = dataSource.numberOfEvents(self) 38 | 39 | let padding = UIScreen.main.widthOfSafeArea() 40 | 41 | let contentWidth = self.bounds.width - padding 42 | 43 | let unit_hour_width = contentWidth / 24 44 | 45 | let unit_minute_width = unit_hour_width / 60 46 | 47 | let unit_second_width = unit_minute_width / 60 48 | 49 | let unit_gap_height = CGFloat(20) 50 | 51 | let wave_height = CGFloat(5) 52 | 53 | for index in 0.. 10 ? true : false 102 | 103 | let show_minute = unit_minute_width > 10 ? true : false 104 | 105 | let show_second = unit_second_width > 10 ? true : false 106 | 107 | let unit_gap_height = CGFloat(20) 108 | 109 | let extra_padding = padding / 2 110 | 111 | let textFontAttributes = [ 112 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 12), 113 | NSAttributedStringKey.foregroundColor: UIColor.lightGray, 114 | NSAttributedStringKey.paragraphStyle: NSParagraphStyle.default 115 | ] 116 | 117 | let text_width = CGFloat(36) 118 | 119 | let text_height = CGFloat(12) 120 | 121 | UIColor.lightGray.setFill() 122 | 123 | if show_hour == true { 124 | 125 | for hour in 0...23 { 126 | 127 | let hour_x = CGFloat(hour) * unit_hour_width + extra_padding 128 | 129 | let hour_y = rect.size.height - unit_hour_height 130 | 131 | UIColor.lightGray.setFill() 132 | 133 | UIRectFill(CGRect(x: hour_x, y: hour_y - unit_gap_height, width: 1, height: unit_hour_height)) 134 | 135 | if show_minute == true { 136 | 137 | for minute in 0..<6 { 138 | 139 | let minute_x = CGFloat(minute) * unit_minute_width 140 | 141 | let minute_y = rect.size.height - unit_minute_height 142 | 143 | UIColor.lightGray.setFill() 144 | 145 | UIRectFill(CGRect(x: hour_x + minute_x, y: minute_y - unit_gap_height, width: 1, height: unit_minute_height)) 146 | 147 | if show_second == true { 148 | 149 | for second in 0..<5 { 150 | 151 | let second_x = CGFloat(second) * unit_second_width 152 | 153 | let second_y = rect.size.height - unit_sec_height 154 | 155 | UIColor.lightGray.setFill() 156 | 157 | UIRectFill(CGRect(x: hour_x + minute_x + second_x, y: second_y - unit_gap_height, width: 1, height: unit_sec_height)) 158 | 159 | } 160 | 161 | } 162 | 163 | if unit_minute_width > text_width { 164 | 165 | let text_x = hour_x + minute_x - (text_width / 2) 166 | 167 | let text_y = rect.size.height - 17 168 | 169 | (String(format: "%02d:%02d", hour, minute*10) as NSString).draw(in: CGRect(x: text_x, y: text_y, width: text_width, height: text_height), withAttributes: textFontAttributes) 170 | 171 | } 172 | 173 | } 174 | 175 | } 176 | 177 | let text_x = hour_x - (text_width / 2) 178 | 179 | let text_y = rect.size.height - 17 180 | 181 | (String(format: "%02d:00", hour) as NSString).draw(in: CGRect(x: text_x, y: text_y, width: text_width, height: text_height), withAttributes: textFontAttributes) 182 | 183 | } 184 | 185 | UIColor.lightGray.setFill() 186 | 187 | UIRectFill(CGRect(x: extra_padding, y: 0, width: rect.size.width - extra_padding*2, height: 0.5)) 188 | 189 | UIRectFill(CGRect(x: extra_padding, y: rect.size.height - 20, width: rect.size.width - extra_padding*2, height: 0.5)) 190 | 191 | } 192 | 193 | if self.drawWave { 194 | 195 | self.drawEvent(rect: rect) 196 | 197 | } 198 | 199 | } 200 | 201 | override open func layoutSubviews() { 202 | 203 | super.layoutSubviews() 204 | 205 | self.backgroundColor = UIColor.clear 206 | 207 | } 208 | 209 | override init(frame: CGRect) { 210 | 211 | super.init(frame: frame) 212 | 213 | } 214 | 215 | required public init?(coder aDecoder: NSCoder) { 216 | 217 | super.init(coder: aDecoder) 218 | 219 | } 220 | 221 | override open func prepareForInterfaceBuilder() { 222 | 223 | super.prepareForInterfaceBuilder() 224 | 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /KSTimeline/KSTimelineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KSTimelineView.swift 3 | // KSTimeline 4 | // 5 | // Created by Shih on 24/11/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol KSTimelineDelegate: NSObjectProtocol { 12 | 13 | func timelineStartScroll(_ timeline: KSTimelineView) 14 | 15 | func timelineEndScroll(_ timeline: KSTimelineView) 16 | 17 | func timeline(_ timeline: KSTimelineView, didScrollTo date: Date) 18 | 19 | } 20 | 21 | @objc public protocol KSTimelineDatasource: NSObjectProtocol { 22 | 23 | func numberOfEvents(_ timeline: KSTimelineView) -> Int 24 | 25 | func event(_ timeline: KSTimelineView, at index: Int) -> KSTimelineEvent 26 | 27 | } 28 | 29 | @objc public class KSTimelineEvent: NSObject { 30 | 31 | public var start: Date 32 | 33 | public var end: Date 34 | 35 | public var duration: Double 36 | 37 | public var videoURL: URL 38 | 39 | public init(start: Date, end: Date, duration: Double, videoURL: URL) { 40 | 41 | self.start = start 42 | 43 | self.end = end 44 | 45 | self.duration = duration 46 | 47 | self.videoURL = videoURL 48 | 49 | super.init() 50 | 51 | } 52 | 53 | } 54 | 55 | extension UIScreen { 56 | 57 | func widthOfSafeArea() -> CGFloat { 58 | 59 | guard let rootView = UIApplication.shared.keyWindow else { return 0 } 60 | 61 | if #available(iOS 11.0, *) { 62 | 63 | let leftInset = rootView.safeAreaInsets.left 64 | 65 | let rightInset = rootView.safeAreaInsets.right 66 | 67 | return rootView.bounds.width - leftInset - rightInset 68 | 69 | } else { 70 | 71 | return rootView.bounds.width 72 | 73 | } 74 | 75 | } 76 | 77 | func heightOfSafeArea() -> CGFloat { 78 | 79 | guard let rootView = UIApplication.shared.keyWindow else { return 0 } 80 | 81 | if #available(iOS 11.0, *) { 82 | 83 | let topInset = rootView.safeAreaInsets.top 84 | 85 | let bottomInset = rootView.safeAreaInsets.bottom 86 | 87 | return rootView.bounds.height - topInset - bottomInset 88 | 89 | } else { 90 | 91 | return rootView.bounds.height 92 | 93 | } 94 | 95 | } 96 | 97 | } 98 | 99 | @IBDesignable open class KSTimelineView: UIView { 100 | 101 | public var delegate: KSTimelineDelegate? 102 | 103 | public var datasource: KSTimelineDatasource? 104 | 105 | public var basedDate: Date! 106 | 107 | public var currentDate: Date! 108 | 109 | public var isScrollingLocked = false 110 | 111 | public let contentView = KSTimelineContentView() 112 | 113 | let currentIndicator: CAShapeLayer = CAShapeLayer() 114 | 115 | var pinchGesture: UIPinchGestureRecognizer! 116 | 117 | var lastScale: CGFloat = 1.0 118 | 119 | var scale: CGFloat = 1.0 120 | 121 | var isPinching = false 122 | 123 | @IBInspectable var contentWidth: CGFloat = 2400 124 | 125 | // MARK: Public Methods 126 | 127 | public func scrollToDate(date: Date) { 128 | 129 | let hour = Calendar.current.component(.hour, from: date) 130 | 131 | let minute = Calendar.current.component(.minute, from: date) 132 | 133 | let second = Calendar.current.component(.second, from: date) 134 | 135 | guard let target_date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: self.basedDate) else { return } 136 | 137 | let contentWidth = self.contentView.contentSize.width - UIScreen.main.widthOfSafeArea() 138 | 139 | let unit_hour_width = contentWidth / 24.0 140 | 141 | let unit_minute_width = unit_hour_width / 60.0 142 | 143 | let unit_second_width = unit_minute_width / 60.0 144 | 145 | let newOffset = (unit_hour_width * CGFloat(hour)) + (unit_minute_width * CGFloat(minute)) + (unit_second_width * CGFloat(second)) 146 | 147 | let delegate = self.contentView.delegate 148 | 149 | self.contentView.delegate = nil; 150 | 151 | self.contentView.contentOffset = CGPoint(x: newOffset, y: 0) 152 | 153 | self.contentView.delegate = delegate; 154 | 155 | if let date = Calendar.current.date(bySettingHour: hour, minute: minute, second: second, of: target_date) { 156 | 157 | self.currentDate = date 158 | 159 | } 160 | else { 161 | 162 | self.currentDate = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: target_date) 163 | 164 | } 165 | 166 | } 167 | 168 | override open func draw(_ rect: CGRect) { 169 | 170 | super.draw(rect) 171 | 172 | self.backgroundColor = UIColor.clear 173 | 174 | } 175 | 176 | override init(frame: CGRect) { 177 | 178 | super.init(frame: frame) 179 | 180 | self.commonInit() 181 | 182 | } 183 | 184 | required public init?(coder aDecoder: NSCoder) { 185 | 186 | super.init(coder: aDecoder) 187 | 188 | self.commonInit() 189 | 190 | } 191 | 192 | override open func prepareForInterfaceBuilder() { 193 | 194 | super.prepareForInterfaceBuilder() 195 | 196 | self.commonInit() 197 | 198 | } 199 | 200 | override open func layoutSubviews() { 201 | 202 | super.layoutSubviews() 203 | 204 | self.contentView.setNeedsDisplay() 205 | 206 | let x = (self.bounds.size.width / 2) 207 | 208 | let y = CGFloat(0) 209 | 210 | let width = CGFloat(10) 211 | 212 | let height = self.bounds.height 213 | 214 | let frame = CGRect(x: x, y: y, width: width, height: height) 215 | 216 | self.currentIndicator.frame = frame 217 | 218 | } 219 | 220 | func commonInit() { 221 | 222 | let now = Date() 223 | 224 | self.basedDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: now) 225 | 226 | self.currentDate = basedDate 227 | 228 | self.setupView() 229 | 230 | self.pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(KSTimelineView.pinch(gesture:))) 231 | 232 | self.contentView.addGestureRecognizer(self.pinchGesture) 233 | 234 | let padding = UIScreen.main.bounds.width - UIScreen.main.widthOfSafeArea() 235 | 236 | self.contentView.contentSize = CGSize(width: self.contentWidth + padding, height: self.bounds.height) 237 | 238 | } 239 | 240 | @objc func pinch(gesture: UIPinchGestureRecognizer) { 241 | 242 | if gesture.state == .began { 243 | 244 | lastScale = gesture.scale 245 | 246 | self.isPinching = true 247 | 248 | } 249 | 250 | let kMaxScale: CGFloat = 10.0 251 | 252 | let kMinScale: CGFloat = 1.0 253 | 254 | let currentScale = max(min(gesture.scale * scale, kMaxScale), kMinScale) 255 | 256 | self.contentView.contentSize = CGSize(width: self.contentWidth*currentScale, height: self.bounds.size.height) 257 | 258 | self.contentView.rulerView.frame.size = self.contentView.contentSize 259 | 260 | let hour = Calendar.current.component(.hour, from: self.currentDate) 261 | 262 | let minute = Calendar.current.component(.minute, from: self.currentDate) 263 | 264 | let second = Calendar.current.component(.second, from: self.currentDate) 265 | 266 | let padding: CGFloat = UIScreen.main.widthOfSafeArea() 267 | 268 | let contentWidth = self.contentView.contentSize.width - padding 269 | 270 | let unit_hour_width = contentWidth / 24.0 271 | 272 | let unit_minute_width = unit_hour_width / 60.0 273 | 274 | let unit_second_width = unit_minute_width / 60.0 275 | 276 | let newOffset = (unit_hour_width * CGFloat(hour)) + (unit_minute_width * CGFloat(minute)) + (unit_second_width * CGFloat(second)) 277 | 278 | self.contentView.contentOffset = CGPoint(x: newOffset, y: 0) 279 | 280 | self.contentView.rulerView.setNeedsDisplay() 281 | 282 | lastScale = currentScale 283 | 284 | if gesture.state == .ended || gesture.state == .cancelled || gesture.state == .failed { 285 | 286 | scale = currentScale 287 | 288 | self.isPinching = false 289 | 290 | } 291 | 292 | } 293 | 294 | internal func setupView() { 295 | 296 | self.addSubview(self.contentView) 297 | 298 | self.contentView.delegate = self 299 | 300 | self.contentView.translatesAutoresizingMaskIntoConstraints = false 301 | 302 | self.contentView.rulerView.dataSource = self 303 | 304 | self.addConstraint(NSLayoutConstraint(item: self.contentView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0)) 305 | 306 | self.addConstraint(NSLayoutConstraint(item: self.contentView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0)) 307 | 308 | self.addConstraint(NSLayoutConstraint(item: self.contentView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)) 309 | 310 | self.addConstraint(NSLayoutConstraint(item: self.contentView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)) 311 | 312 | self.contentView.bounces = false 313 | 314 | self.setupCurrentIndicator() 315 | 316 | } 317 | 318 | internal func setupCurrentIndicator() { 319 | 320 | let triangle = UIBezierPath() 321 | 322 | triangle.move(to: CGPoint(x: -5, y: 0)) 323 | 324 | triangle.addLine(to: CGPoint(x: 5, y: 0)) 325 | 326 | triangle.addLine(to: CGPoint(x: 0, y: 10)) 327 | 328 | triangle.close() 329 | 330 | let line = CALayer() 331 | 332 | line.frame = CGRect(x: -0.5, y: 0, width: 1, height: self.bounds.height) 333 | 334 | line.backgroundColor = UIColor.red.cgColor 335 | 336 | self.currentIndicator.path = triangle.cgPath 337 | 338 | self.currentIndicator.fillColor = UIColor.red.cgColor 339 | 340 | self.currentIndicator.addSublayer(line) 341 | 342 | self.layer.addSublayer(self.currentIndicator) 343 | 344 | } 345 | 346 | } 347 | 348 | extension KSTimelineView: UIScrollViewDelegate { 349 | 350 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 351 | 352 | self.delegate?.timelineStartScroll(self) 353 | 354 | } 355 | 356 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 357 | 358 | guard isPinching == false && isScrollingLocked == false else { return } 359 | 360 | guard let target_date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: self.basedDate) else { return } 361 | 362 | let padding = self.bounds.width 363 | 364 | let contentWidth = scrollView.contentSize.width - padding 365 | 366 | let unit_hour_width = contentWidth / 24 367 | 368 | let unit_minute_width = unit_hour_width / 60 369 | 370 | let unit_second_width = unit_minute_width / 60 371 | 372 | let timeline_x = scrollView.contentOffset.x 373 | 374 | let hour = Int(floor(timeline_x / unit_hour_width)) 375 | 376 | let minute = Int(floor((timeline_x - (CGFloat(hour) * unit_hour_width)) / unit_minute_width)) 377 | 378 | let second = Int(floor((timeline_x - (CGFloat(hour) * unit_hour_width) - (CGFloat(minute) * unit_minute_width)) / unit_second_width)) 379 | 380 | if let date = Calendar.current.date(bySettingHour: hour, minute: minute, second: second, of: target_date) { 381 | 382 | self.currentDate = date 383 | 384 | } 385 | else { 386 | 387 | self.currentDate = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: target_date) 388 | 389 | } 390 | 391 | self.delegate?.timeline(self, didScrollTo: self.currentDate) 392 | 393 | } 394 | 395 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 396 | 397 | self.delegate?.timelineEndScroll(self) 398 | 399 | } 400 | 401 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 402 | 403 | self.delegate?.timelineEndScroll(self) 404 | 405 | } 406 | 407 | } 408 | 409 | extension KSTimelineView: KSTimelineRulerEventDataSource { 410 | 411 | func numberOfEvents(_ ruler: KSTimelineRulerView) -> Int { 412 | 413 | guard let datasource = self.datasource else { return 0 } 414 | 415 | return datasource.numberOfEvents(self) 416 | 417 | } 418 | 419 | func timelineRuler(_ ruler: KSTimelineRulerView, eventAt index: Int) -> KSTimelineEvent { 420 | 421 | return self.datasource!.event(self, at: index) 422 | 423 | } 424 | 425 | } 426 | -------------------------------------------------------------------------------- /KSTimelineDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // KSTimelineDemo 4 | // 5 | // Created by Shih on 13/12/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /KSTimelineDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /KSTimelineDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /KSTimelineDemo/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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 174 | 184 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /KSTimelineDemo/BigBunny.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenShih522/KSTimeline/c09e5d00b8242b857ef62bb79be8f13c4ff41a28/KSTimelineDemo/BigBunny.mp4 -------------------------------------------------------------------------------- /KSTimelineDemo/F35.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenShih522/KSTimeline/c09e5d00b8242b857ef62bb79be8f13c4ff41a28/KSTimelineDemo/F35.mp4 -------------------------------------------------------------------------------- /KSTimelineDemo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /KSTimelineDemo/Toystory.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenShih522/KSTimeline/c09e5d00b8242b857ef62bb79be8f13c4ff41a28/KSTimelineDemo/Toystory.mp4 -------------------------------------------------------------------------------- /KSTimelineDemo/VideoHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoHelper.swift 3 | // KSTimelineDemo 4 | // 5 | // Created by Shih on 13/12/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | class VideoHelper: NSObject { 13 | 14 | class func getMediaDuration(url: URL) -> Double{ 15 | 16 | let asset = AVURLAsset(url: url) 17 | 18 | let duration: CMTime = asset.duration 19 | 20 | return CMTimeGetSeconds(duration) 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /KSTimelineDemo/ViewController1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController1.swift 3 | // KSTimelineDemo 4 | // 5 | // Created by Shih on 13/12/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import KSTimeline 11 | 12 | class ViewController1: UIViewController { 13 | 14 | @IBOutlet weak var timeline: KSTimelineView! 15 | 16 | @IBOutlet weak var currentTime: UILabel! 17 | 18 | var currentDate: Date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())! { 19 | 20 | didSet { 21 | 22 | let dateString = self.dateFormatter.string(from: currentDate) 23 | 24 | self.currentTime.text = dateString 25 | 26 | } 27 | 28 | } 29 | 30 | lazy var dateFormatter: DateFormatter = { 31 | 32 | var dateFormatter = DateFormatter() 33 | 34 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 35 | 36 | return dateFormatter 37 | 38 | }() 39 | 40 | override func viewDidLoad() { 41 | 42 | super.viewDidLoad() 43 | 44 | self.timeline.delegate = self 45 | 46 | self.currentTime.text = self.dateFormatter.string(from: Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())!) 47 | 48 | } 49 | 50 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 51 | 52 | super.viewWillTransition(to: size, with: coordinator) 53 | 54 | self.timeline.isScrollingLocked = true 55 | 56 | coordinator.animate(alongsideTransition: { (context) in 57 | 58 | self.timeline.contentView.rulerView.setNeedsDisplay() 59 | 60 | self.timeline.scrollToDate(date: self.currentDate) 61 | 62 | }) { (context) in 63 | 64 | self.timeline.isScrollingLocked = false 65 | 66 | } 67 | 68 | } 69 | 70 | override func didReceiveMemoryWarning() { 71 | 72 | super.didReceiveMemoryWarning() 73 | 74 | } 75 | 76 | } 77 | 78 | extension ViewController1: KSTimelineDelegate { 79 | 80 | func timelineStartScroll(_ timeline: KSTimelineView) { 81 | 82 | } 83 | 84 | func timelineEndScroll(_ timeline: KSTimelineView) { 85 | 86 | } 87 | 88 | func timeline(_ timeline: KSTimelineView, didScrollTo date: Date) { 89 | 90 | self.currentDate = date 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /KSTimelineDemo/ViewController2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController2.swift 3 | // KSTimelineDemo 4 | // 5 | // Created by Shih on 13/12/2017. 6 | // Copyright © 2017 kenshih. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | import KSTimeline 11 | 12 | extension Date { 13 | 14 | func isBetween(_ date1: Date, and date2: Date) -> Bool { 15 | 16 | return (min(date1, date2) ... max(date1, date2)).contains(self) 17 | 18 | } 19 | 20 | } 21 | 22 | class ViewController2: UIViewController { 23 | 24 | @IBOutlet weak var timeline: KSTimelineView! 25 | 26 | @IBOutlet weak var currentTime: UILabel! 27 | 28 | @IBOutlet weak var videoContainer: UIView! 29 | 30 | @IBOutlet var playBtn: UIBarButtonItem! 31 | 32 | @IBOutlet weak var prevousBtn: UIButton! 33 | 34 | @IBOutlet weak var nextBtn: UIButton! 35 | 36 | var currentDate: Date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())! { 37 | 38 | didSet { 39 | 40 | let dateString = self.dateFormatter.string(from: currentDate) 41 | 42 | self.currentTime.text = dateString 43 | 44 | } 45 | 46 | } 47 | 48 | var player: AVPlayer? 49 | 50 | var playerLayer: AVPlayerLayer? 51 | 52 | var currentEvent: KSTimelineEvent? 53 | 54 | var events: [KSTimelineEvent] = [KSTimelineEvent]() 55 | 56 | var displayLink: CADisplayLink? 57 | 58 | var isScrolling: Bool = false 59 | 60 | lazy var pauseBtn: UIBarButtonItem = { 61 | 62 | let barButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(ViewController2.didPressPauseBtn(_:))) 63 | 64 | return barButton 65 | 66 | }() 67 | 68 | lazy var dateFormatter: DateFormatter = { 69 | 70 | var dateFormatter = DateFormatter() 71 | 72 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 73 | 74 | return dateFormatter 75 | 76 | }() 77 | 78 | // MARK: IBAction 79 | 80 | @IBAction func didPressPreviousVideoBtn(_ sender: Any) { 81 | 82 | guard self.events.count > 0 else { return } 83 | 84 | if let event = self.getPreviousEvent(date: self.currentDate) { 85 | 86 | if self.events.index(of: event) == 0 { 87 | 88 | self.prevousBtn.isEnabled = false 89 | 90 | } 91 | 92 | self.playEvent(event: event) 93 | 94 | self.nextBtn.isEnabled = true 95 | 96 | } 97 | 98 | } 99 | 100 | @IBAction func didPressNextVideoBtn(_ sender: Any) { 101 | 102 | guard self.events.count > 0 else { return } 103 | 104 | if let event = self.getNextEvent(date: self.currentDate) { 105 | 106 | if self.events.index(of: event) == self.events.count - 1 { 107 | 108 | self.nextBtn.isEnabled = false 109 | 110 | } 111 | 112 | self.playEvent(event: event) 113 | 114 | if event == self.events.first { 115 | 116 | self.prevousBtn.isEnabled = false 117 | 118 | } 119 | else { 120 | 121 | self.prevousBtn.isEnabled = true 122 | 123 | } 124 | 125 | } 126 | else { 127 | 128 | let event = self.events[0] 129 | 130 | self.playEvent(event: event) 131 | 132 | self.prevousBtn.isEnabled = false 133 | 134 | } 135 | 136 | } 137 | 138 | @IBAction func didPressPlayBtn(_ sender: Any) { 139 | 140 | guard let event = self.findEvents(date: self.currentDate) else { 141 | 142 | guard let event = self.getNextEvent(date: self.currentDate) else { 143 | 144 | guard self.events.count > 0 else { return } 145 | 146 | self.playEvent(event: self.events[0]) 147 | 148 | return 149 | 150 | } 151 | 152 | self.playEvent(event: event) 153 | 154 | return 155 | 156 | } 157 | 158 | if event == self.currentEvent { 159 | 160 | self.player?.play() 161 | 162 | } 163 | else { 164 | 165 | self.playEvent(event: event) 166 | 167 | } 168 | 169 | self.navigationItem.rightBarButtonItem = self.pauseBtn 170 | 171 | } 172 | 173 | @objc func didPressPauseBtn(_ sender: Any) { 174 | 175 | self.player?.pause() 176 | 177 | self.navigationItem.rightBarButtonItem = self.playBtn 178 | 179 | } 180 | 181 | // MARK: Internal Function 182 | 183 | @objc func playerDidFinishPlaying(_ note: NSNotification) { 184 | 185 | self.navigationItem.rightBarButtonItem = self.playBtn 186 | 187 | } 188 | 189 | internal func setupEvents() { 190 | 191 | var startDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())! 192 | 193 | var url = Bundle.main.url(forResource: "Toystory", withExtension: "mp4")! 194 | 195 | var duration = VideoHelper.getMediaDuration(url: url) 196 | 197 | var endDate = Calendar.current.date(byAdding: .second, value: Int(floor(duration)), to: startDate)! 198 | 199 | var event = KSTimelineEvent(start: startDate, end: endDate, duration: duration, videoURL: url) 200 | 201 | self.events.append(event) 202 | 203 | startDate = Calendar.current.date(byAdding: .hour, value: 1, to: startDate)! 204 | 205 | url = Bundle.main.url(forResource: "BigBunny", withExtension: "mp4")! 206 | 207 | duration = VideoHelper.getMediaDuration(url: url) 208 | 209 | endDate = Calendar.current.date(byAdding: .second, value: Int(floor(duration)), to: startDate)! 210 | 211 | event = KSTimelineEvent(start: startDate, end: endDate, duration: duration, videoURL: url) 212 | 213 | self.events.append(event) 214 | 215 | startDate = Calendar.current.date(byAdding: .hour, value: 2, to: startDate)! 216 | 217 | startDate = Calendar.current.date(byAdding: .minute, value: 30, to: startDate)! 218 | 219 | url = Bundle.main.url(forResource: "F35", withExtension: "mp4")! 220 | 221 | duration = VideoHelper.getMediaDuration(url: url) 222 | 223 | endDate = Calendar.current.date(byAdding: .second, value: Int(floor(duration)), to: startDate)! 224 | 225 | event = KSTimelineEvent(start: startDate, end: endDate, duration: duration, videoURL: url) 226 | 227 | self.events.append(event) 228 | 229 | } 230 | 231 | internal func findEvents(date: Date) -> KSTimelineEvent? { 232 | 233 | for event in self.events { 234 | 235 | if date.isBetween(event.start, and: event.end) { 236 | 237 | return event 238 | 239 | } 240 | 241 | } 242 | 243 | return nil 244 | 245 | } 246 | 247 | internal func getNextEvent(date: Date) -> KSTimelineEvent? { 248 | 249 | for event in self.events { 250 | 251 | if Int(event.start.timeIntervalSince1970) > Int(date.timeIntervalSince1970) { 252 | 253 | return event 254 | 255 | } 256 | 257 | } 258 | 259 | return nil 260 | 261 | } 262 | 263 | internal func getPreviousEvent(date: Date) -> KSTimelineEvent? { 264 | 265 | for event in self.events.reversed() { 266 | 267 | if Int(event.end.timeIntervalSince1970) < Int(date.timeIntervalSince1970) { 268 | 269 | return event 270 | 271 | } 272 | 273 | } 274 | 275 | return nil 276 | 277 | } 278 | 279 | internal func playEvent(event: KSTimelineEvent) { 280 | 281 | self.displayLink?.invalidate() 282 | 283 | self.displayLink = nil 284 | 285 | self.playerLayer?.removeFromSuperlayer() 286 | 287 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem) 288 | 289 | let asset = AVURLAsset(url: event.videoURL) 290 | 291 | let playerItem = AVPlayerItem(asset: asset) 292 | 293 | self.player = AVPlayer(playerItem: playerItem) 294 | 295 | NotificationCenter.default.addObserver(self, selector: #selector(ViewController2.playerDidFinishPlaying(_:)), 296 | name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player!.currentItem) 297 | 298 | self.playerLayer = AVPlayerLayer(player: self.player!) 299 | 300 | self.playerLayer!.frame = self.videoContainer.bounds 301 | 302 | self.videoContainer.layer.addSublayer(self.playerLayer!) 303 | 304 | self.currentEvent = event 305 | 306 | self.player!.play() 307 | 308 | self.navigationItem.rightBarButtonItem = self.pauseBtn 309 | 310 | self.displayLink = CADisplayLink(target: self, selector: #selector(ViewController2.displayLinkDidFire(_:))) 311 | 312 | self.displayLink!.add(to: RunLoop.current, forMode: RunLoopMode.commonModes) 313 | 314 | } 315 | 316 | // MARK: CADisplayLink Task 317 | 318 | @objc func displayLinkDidFire(_ sender: CADisplayLink) { 319 | 320 | guard self.isScrolling == false else { return } 321 | 322 | guard let player = self.player else { return } 323 | 324 | guard let currentItem = player.currentItem else { return } 325 | 326 | guard let currentEvent = self.currentEvent else { return } 327 | 328 | if UIApplication.shared.applicationState == .background { return } 329 | 330 | let seconds = CMTimeGetSeconds(currentItem.currentTime()) 331 | 332 | guard seconds > 0 else { return } 333 | 334 | let date = currentEvent.start.addingTimeInterval(seconds) 335 | 336 | if date >= currentEvent.end { 337 | 338 | guard let index = self.events.index(of: currentEvent) else { return } 339 | 340 | let newIndex = index + 1 341 | 342 | if newIndex < self.events.count { 343 | 344 | self.currentEvent = self.events[newIndex] 345 | 346 | self.playEvent(event: self.currentEvent!) 347 | 348 | self.prevousBtn.isEnabled = true 349 | 350 | if self.currentEvent == self.events.last { 351 | 352 | self.nextBtn.isEnabled = false 353 | 354 | } 355 | else { 356 | 357 | self.nextBtn.isEnabled = true 358 | 359 | } 360 | 361 | } 362 | 363 | } 364 | 365 | self.timeline.scrollToDate(date: date) 366 | 367 | self.currentDate = date 368 | 369 | } 370 | 371 | // MARK: View Function 372 | 373 | override func viewDidLoad() { 374 | 375 | super.viewDidLoad() 376 | 377 | self.timeline.delegate = self 378 | 379 | self.timeline.datasource = self 380 | 381 | self.currentTime.text = self.dateFormatter.string(from: Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())!) 382 | 383 | self.setupEvents() 384 | 385 | guard self.events.count > 0 else { return } 386 | 387 | self.prevousBtn.isEnabled = false 388 | 389 | self.nextBtn.isEnabled = true 390 | 391 | self.playBtn.isEnabled = true 392 | 393 | self.pauseBtn.isEnabled = true 394 | 395 | guard let event = self.findEvents(date: self.currentDate) else { return } 396 | 397 | self.playEvent(event: event) 398 | 399 | self.automaticallyAdjustsScrollViewInsets = true 400 | 401 | } 402 | 403 | override func viewWillDisappear(_ animated: Bool) { 404 | 405 | super.viewWillDisappear(animated) 406 | 407 | self.player?.pause() 408 | 409 | } 410 | 411 | override func viewDidLayoutSubviews() { 412 | 413 | super.viewDidLayoutSubviews() 414 | 415 | self.playerLayer?.frame = self.videoContainer.bounds 416 | 417 | } 418 | 419 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 420 | 421 | super.viewWillTransition(to: size, with: coordinator) 422 | 423 | self.timeline.isScrollingLocked = true 424 | 425 | coordinator.animate(alongsideTransition: { (context) in 426 | 427 | self.timeline.contentView.rulerView.setNeedsDisplay() 428 | 429 | self.timeline.scrollToDate(date: self.currentDate) 430 | 431 | }) { (context) in 432 | 433 | self.timeline.isScrollingLocked = false 434 | 435 | } 436 | 437 | } 438 | 439 | override func didReceiveMemoryWarning() { 440 | 441 | super.didReceiveMemoryWarning() 442 | 443 | } 444 | 445 | } 446 | 447 | extension ViewController2: KSTimelineDelegate { 448 | 449 | func timelineStartScroll(_ timeline: KSTimelineView) { 450 | 451 | self.isScrolling = true 452 | 453 | } 454 | 455 | func timelineEndScroll(_ timeline: KSTimelineView) { 456 | 457 | self.isScrolling = false 458 | 459 | } 460 | 461 | func timeline(_ timeline: KSTimelineView, didScrollTo date: Date) { 462 | 463 | self.isScrolling = true 464 | 465 | self.currentDate = date 466 | 467 | guard let lastEvent = self.events.last else { return } 468 | 469 | guard let firstEvent = self.events.first else { return } 470 | 471 | if lastEvent == firstEvent { 472 | 473 | self.prevousBtn.isEnabled = false 474 | 475 | self.nextBtn.isEnabled = false 476 | 477 | } 478 | else if self.currentDate < firstEvent.end { 479 | 480 | self.prevousBtn.isEnabled = false 481 | 482 | self.nextBtn.isEnabled = true 483 | 484 | } 485 | else if self.currentDate > lastEvent.start { 486 | 487 | self.nextBtn.isEnabled = false 488 | 489 | self.prevousBtn.isEnabled = true 490 | 491 | } 492 | else { 493 | 494 | self.nextBtn.isEnabled = true 495 | 496 | self.prevousBtn.isEnabled = true 497 | 498 | } 499 | 500 | guard let event = self.findEvents(date: self.currentDate) else { 501 | 502 | self.player?.pause() 503 | 504 | self.currentEvent = nil 505 | 506 | self.navigationItem.rightBarButtonItem = self.playBtn 507 | 508 | return 509 | 510 | } 511 | 512 | guard self.currentEvent == event else { 513 | 514 | self.playEvent(event: event) 515 | 516 | return 517 | 518 | } 519 | 520 | let interval = date.timeIntervalSince(event.start) 521 | 522 | let timeScale = self.player?.currentItem?.asset.duration.timescale 523 | 524 | let time = CMTimeMakeWithSeconds(interval, timeScale!) 525 | 526 | self.player?.seek(to: time, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) 527 | 528 | } 529 | 530 | } 531 | 532 | extension ViewController2: KSTimelineDatasource { 533 | 534 | func numberOfEvents(_ timeline: KSTimelineView) -> Int { 535 | 536 | return self.events.count 537 | 538 | } 539 | 540 | func event(_ timeline: KSTimelineView, at index: Int) -> KSTimelineEvent { 541 | 542 | return self.events[index] 543 | 544 | } 545 | 546 | } 547 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 KenShih522 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KSTimeline 2 | 3 | KSTimeline, written in swift, is a simple and customizable view which supports showing a series of events in a vertically time-sorted structure. 4 | 5 | ## Preview 6 | 7 | ### Demo1 8 | 9 | - adjust the time scale by pinch gesture
10 | - display different time mode according to time scale
11 | 12 |

13 | 14 |

15 | 16 | ### Demo2 17 | 18 | - play next event automatically
19 | - scroll to seek video
20 | - press > to next video
21 | - press < to previous video
22 | 23 |

24 | 25 |

26 | 27 | ## Usage 28 | 29 | #### Delegate 30 | 31 | ```swift 32 | @objc public protocol KSTimelineDelegate: NSObjectProtocol { 33 | 34 | func timelineStartScroll(_ timeline: KSTimelineView) 35 | 36 | func timelineEndScroll(_ timeline: KSTimelineView) 37 | 38 | func timeline(_ timeline: KSTimelineView, didScrollTo date: Date) 39 | 40 | } 41 | ``` 42 | 43 | #### Datasource 44 | 45 | ```swift 46 | @objc public protocol KSTimelineDatasource: NSObjectProtocol { 47 | 48 | func numberOfEvents(_ timeline: KSTimelineView) -> Int 49 | 50 | func event(_ timeline: KSTimelineView, at index: Int) -> KSTimelineEvent 51 | 52 | } 53 | ``` 54 | 55 | #### KSTimelineEvent 56 | 57 | ```swift 58 | @objc public class KSTimelineEvent: NSObject { 59 | 60 | public var start: Date 61 | 62 | public var end: Date 63 | 64 | public var duration: Double 65 | 66 | public var videoURL: URL 67 | 68 | public init(start: Date, end: Date, duration: Double, videoURL: URL) { 69 | 70 | self.start = start 71 | 72 | self.end = end 73 | 74 | self.duration = duration 75 | 76 | self.videoURL = videoURL 77 | 78 | super.init() 79 | 80 | } 81 | 82 | } 83 | ``` 84 | 85 | # Installation 86 | ### Cocoapods 87 | KSTimeline can be added to your project using CocoaPods by adding the following line to your `Podfile`: 88 | 89 | ```ruby 90 | pod 'KSTimeline' 91 | ``` 92 | --------------------------------------------------------------------------------