├── .DS_Store ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── BKCountDownTimer.podspec ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift └── Podfile ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── BKCountDownTimer │ ├── CircleBase.swift │ ├── CircleCount.swift │ ├── CircleTic.swift │ └── CircleTimer.swift ├── Tests ├── BKCountDownTimerTests │ ├── BKCountDownTimerTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── img-howto.png └── img-prev.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugkingK/CountDownTimer/b54e035a22a13687b3ef68be43a03fe5eba24427/.DS_Store -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /BKCountDownTimer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "BKCountDownTimer" 4 | s.version = "1.0.0" 5 | s.summary = "Circle shaped countdown timer." 6 | s.description = <<-DESC 7 | Circle shaped countdown timer. With Swift 8 | DESC 9 | 10 | s.homepage = "https://github.com/bugkingK/CountDownTimer" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "bugkingK" => "myway0710@naver.com" } 13 | s.platform = :ios, "10.0" 14 | s.source = { :git => "https://github.com/bugkingK/CountDownTimer.git", :tag => "#{s.version}" } 15 | s.source_files = "Classes", "Sources/**/*.{swift}" 16 | 17 | s.swift_version = '5.0' 18 | 19 | end -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D216BF4B234320930018F3C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D216BF4A234320930018F3C6 /* AppDelegate.swift */; }; 11 | D216BF4F234320930018F3C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D216BF4E234320930018F3C6 /* ViewController.swift */; }; 12 | D216BF52234320930018F3C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D216BF50234320930018F3C6 /* Main.storyboard */; }; 13 | D216BF54234320960018F3C6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D216BF53234320960018F3C6 /* Assets.xcassets */; }; 14 | D216BF57234320960018F3C6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D216BF55234320960018F3C6 /* LaunchScreen.storyboard */; }; 15 | D5A6431A238BE02E6F389727 /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1AD60EA84324E06EEF9029D9 /* Pods_Demo.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 1AD60EA84324E06EEF9029D9 /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 53316D16517E3E51C2A10E0B /* Pods-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.release.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig"; sourceTree = ""; }; 21 | C1152E1082DFF2BD1D395BC6 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; 22 | D216BF47234320930018F3C6 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | D216BF4A234320930018F3C6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | D216BF4E234320930018F3C6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | D216BF51234320930018F3C6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | D216BF53234320960018F3C6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | D216BF56234320960018F3C6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | D216BF58234320960018F3C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | D216BF44234320930018F3C6 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | D5A6431A238BE02E6F389727 /* Pods_Demo.framework in Frameworks */, 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 534DC9C85377D0D68F2223D6 /* Frameworks */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | 1AD60EA84324E06EEF9029D9 /* Pods_Demo.framework */, 47 | ); 48 | name = Frameworks; 49 | sourceTree = ""; 50 | }; 51 | A01B3314E146E3584C959E78 /* Pods */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | C1152E1082DFF2BD1D395BC6 /* Pods-Demo.debug.xcconfig */, 55 | 53316D16517E3E51C2A10E0B /* Pods-Demo.release.xcconfig */, 56 | ); 57 | name = Pods; 58 | path = Pods; 59 | sourceTree = ""; 60 | }; 61 | D216BF3E234320930018F3C6 = { 62 | isa = PBXGroup; 63 | children = ( 64 | D216BF49234320930018F3C6 /* Demo */, 65 | D216BF48234320930018F3C6 /* Products */, 66 | A01B3314E146E3584C959E78 /* Pods */, 67 | 534DC9C85377D0D68F2223D6 /* Frameworks */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | D216BF48234320930018F3C6 /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | D216BF47234320930018F3C6 /* Demo.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | D216BF49234320930018F3C6 /* Demo */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | D216BF4A234320930018F3C6 /* AppDelegate.swift */, 83 | D216BF4E234320930018F3C6 /* ViewController.swift */, 84 | D216BF50234320930018F3C6 /* Main.storyboard */, 85 | D216BF53234320960018F3C6 /* Assets.xcassets */, 86 | D216BF55234320960018F3C6 /* LaunchScreen.storyboard */, 87 | D216BF58234320960018F3C6 /* Info.plist */, 88 | ); 89 | path = Demo; 90 | sourceTree = ""; 91 | }; 92 | /* End PBXGroup section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | D216BF46234320930018F3C6 /* Demo */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = D216BF5B234320960018F3C6 /* Build configuration list for PBXNativeTarget "Demo" */; 98 | buildPhases = ( 99 | 58557038D94E8A94F1897F57 /* [CP] Check Pods Manifest.lock */, 100 | D216BF43234320930018F3C6 /* Sources */, 101 | D216BF44234320930018F3C6 /* Frameworks */, 102 | D216BF45234320930018F3C6 /* Resources */, 103 | 938569EFDD8F5A382C768D4C /* [CP] Embed Pods Frameworks */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = Demo; 110 | productName = Demo; 111 | productReference = D216BF47234320930018F3C6 /* Demo.app */; 112 | productType = "com.apple.product-type.application"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | D216BF3F234320930018F3C6 /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | LastSwiftUpdateCheck = 1100; 121 | LastUpgradeCheck = 1100; 122 | ORGANIZATIONNAME = Bugking; 123 | TargetAttributes = { 124 | D216BF46234320930018F3C6 = { 125 | CreatedOnToolsVersion = 11.0; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = D216BF42234320930018F3C6 /* Build configuration list for PBXProject "Demo" */; 130 | compatibilityVersion = "Xcode 9.3"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = D216BF3E234320930018F3C6; 138 | productRefGroup = D216BF48234320930018F3C6 /* Products */; 139 | projectDirPath = ""; 140 | projectRoot = ""; 141 | targets = ( 142 | D216BF46234320930018F3C6 /* Demo */, 143 | ); 144 | }; 145 | /* End PBXProject section */ 146 | 147 | /* Begin PBXResourcesBuildPhase section */ 148 | D216BF45234320930018F3C6 /* Resources */ = { 149 | isa = PBXResourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | D216BF57234320960018F3C6 /* LaunchScreen.storyboard in Resources */, 153 | D216BF54234320960018F3C6 /* Assets.xcassets in Resources */, 154 | D216BF52234320930018F3C6 /* Main.storyboard in Resources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXResourcesBuildPhase section */ 159 | 160 | /* Begin PBXShellScriptBuildPhase section */ 161 | 58557038D94E8A94F1897F57 /* [CP] Check Pods Manifest.lock */ = { 162 | isa = PBXShellScriptBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | ); 166 | inputFileListPaths = ( 167 | ); 168 | inputPaths = ( 169 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 170 | "${PODS_ROOT}/Manifest.lock", 171 | ); 172 | name = "[CP] Check Pods Manifest.lock"; 173 | outputFileListPaths = ( 174 | ); 175 | outputPaths = ( 176 | "$(DERIVED_FILE_DIR)/Pods-Demo-checkManifestLockResult.txt", 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | shellPath = /bin/sh; 180 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 181 | showEnvVarsInLog = 0; 182 | }; 183 | 938569EFDD8F5A382C768D4C /* [CP] Embed Pods Frameworks */ = { 184 | isa = PBXShellScriptBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | ); 188 | inputFileListPaths = ( 189 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-input-files.xcfilelist", 190 | ); 191 | name = "[CP] Embed Pods Frameworks"; 192 | outputFileListPaths = ( 193 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-output-files.xcfilelist", 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | shellPath = /bin/sh; 197 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh\"\n"; 198 | showEnvVarsInLog = 0; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | D216BF43234320930018F3C6 /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | D216BF4F234320930018F3C6 /* ViewController.swift in Sources */, 208 | D216BF4B234320930018F3C6 /* AppDelegate.swift in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | D216BF50234320930018F3C6 /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | D216BF51234320930018F3C6 /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | D216BF55234320960018F3C6 /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | D216BF56234320960018F3C6 /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | D216BF59234320960018F3C6 /* Debug */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 241 | CLANG_CXX_LIBRARY = "libc++"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_ENABLE_OBJC_WEAK = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 252 | CLANG_WARN_EMPTY_BODY = YES; 253 | CLANG_WARN_ENUM_CONVERSION = YES; 254 | CLANG_WARN_INFINITE_RECURSION = YES; 255 | CLANG_WARN_INT_CONVERSION = YES; 256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 | CLANG_WARN_STRICT_PROTOTYPES = YES; 262 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 264 | CLANG_WARN_UNREACHABLE_CODE = YES; 265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 | COPY_PHASE_STRIP = NO; 267 | DEBUG_INFORMATION_FORMAT = dwarf; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | ENABLE_TESTABILITY = YES; 270 | GCC_C_LANGUAGE_STANDARD = gnu11; 271 | GCC_DYNAMIC_NO_PIC = NO; 272 | GCC_NO_COMMON_BLOCKS = YES; 273 | GCC_OPTIMIZATION_LEVEL = 0; 274 | GCC_PREPROCESSOR_DEFINITIONS = ( 275 | "DEBUG=1", 276 | "$(inherited)", 277 | ); 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 285 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 286 | MTL_FAST_MATH = YES; 287 | ONLY_ACTIVE_ARCH = YES; 288 | SDKROOT = iphoneos; 289 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 290 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 291 | }; 292 | name = Debug; 293 | }; 294 | D216BF5A234320960018F3C6 /* Release */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ALWAYS_SEARCH_USER_PATHS = NO; 298 | CLANG_ANALYZER_NONNULL = YES; 299 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 300 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 301 | CLANG_CXX_LIBRARY = "libc++"; 302 | CLANG_ENABLE_MODULES = YES; 303 | CLANG_ENABLE_OBJC_ARC = YES; 304 | CLANG_ENABLE_OBJC_WEAK = YES; 305 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 306 | CLANG_WARN_BOOL_CONVERSION = YES; 307 | CLANG_WARN_COMMA = YES; 308 | CLANG_WARN_CONSTANT_CONVERSION = YES; 309 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 310 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 311 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INFINITE_RECURSION = YES; 315 | CLANG_WARN_INT_CONVERSION = YES; 316 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 318 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 319 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 320 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 321 | CLANG_WARN_STRICT_PROTOTYPES = YES; 322 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 323 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 324 | CLANG_WARN_UNREACHABLE_CODE = YES; 325 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 326 | COPY_PHASE_STRIP = NO; 327 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 328 | ENABLE_NS_ASSERTIONS = NO; 329 | ENABLE_STRICT_OBJC_MSGSEND = YES; 330 | GCC_C_LANGUAGE_STANDARD = gnu11; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 334 | GCC_WARN_UNDECLARED_SELECTOR = YES; 335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 336 | GCC_WARN_UNUSED_FUNCTION = YES; 337 | GCC_WARN_UNUSED_VARIABLE = YES; 338 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 339 | MTL_ENABLE_DEBUG_INFO = NO; 340 | MTL_FAST_MATH = YES; 341 | SDKROOT = iphoneos; 342 | SWIFT_COMPILATION_MODE = wholemodule; 343 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 344 | VALIDATE_PRODUCT = YES; 345 | }; 346 | name = Release; 347 | }; 348 | D216BF5C234320960018F3C6 /* Debug */ = { 349 | isa = XCBuildConfiguration; 350 | baseConfigurationReference = C1152E1082DFF2BD1D395BC6 /* Pods-Demo.debug.xcconfig */; 351 | buildSettings = { 352 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 353 | CODE_SIGN_STYLE = Automatic; 354 | DEVELOPMENT_TEAM = 6QG49X22JS; 355 | INFOPLIST_FILE = Demo/Info.plist; 356 | LD_RUNPATH_SEARCH_PATHS = ( 357 | "$(inherited)", 358 | "@executable_path/Frameworks", 359 | ); 360 | PRODUCT_BUNDLE_IDENTIFIER = tk.bugking.Demo; 361 | PRODUCT_NAME = "$(TARGET_NAME)"; 362 | SWIFT_VERSION = 5.0; 363 | TARGETED_DEVICE_FAMILY = "1,2"; 364 | }; 365 | name = Debug; 366 | }; 367 | D216BF5D234320960018F3C6 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | baseConfigurationReference = 53316D16517E3E51C2A10E0B /* Pods-Demo.release.xcconfig */; 370 | buildSettings = { 371 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 | CODE_SIGN_STYLE = Automatic; 373 | DEVELOPMENT_TEAM = 6QG49X22JS; 374 | INFOPLIST_FILE = Demo/Info.plist; 375 | LD_RUNPATH_SEARCH_PATHS = ( 376 | "$(inherited)", 377 | "@executable_path/Frameworks", 378 | ); 379 | PRODUCT_BUNDLE_IDENTIFIER = tk.bugking.Demo; 380 | PRODUCT_NAME = "$(TARGET_NAME)"; 381 | SWIFT_VERSION = 5.0; 382 | TARGETED_DEVICE_FAMILY = "1,2"; 383 | }; 384 | name = Release; 385 | }; 386 | /* End XCBuildConfiguration section */ 387 | 388 | /* Begin XCConfigurationList section */ 389 | D216BF42234320930018F3C6 /* Build configuration list for PBXProject "Demo" */ = { 390 | isa = XCConfigurationList; 391 | buildConfigurations = ( 392 | D216BF59234320960018F3C6 /* Debug */, 393 | D216BF5A234320960018F3C6 /* Release */, 394 | ); 395 | defaultConfigurationIsVisible = 0; 396 | defaultConfigurationName = Release; 397 | }; 398 | D216BF5B234320960018F3C6 /* Build configuration list for PBXNativeTarget "Demo" */ = { 399 | isa = XCConfigurationList; 400 | buildConfigurations = ( 401 | D216BF5C234320960018F3C6 /* Debug */, 402 | D216BF5D234320960018F3C6 /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Release; 406 | }; 407 | /* End XCConfigurationList section */ 408 | }; 409 | rootObject = D216BF3F234320930018F3C6 /* Project object */; 410 | } 411 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by moon on 01/10/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo/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 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/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 | -------------------------------------------------------------------------------- /Demo/Demo/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 | 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 | -------------------------------------------------------------------------------- /Demo/Demo/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 | 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 | -------------------------------------------------------------------------------- /Demo/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by moon on 01/10/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import BKCountDownTimer 11 | 12 | class ViewController: UIViewController { 13 | 14 | @IBOutlet weak var vwCircle:CircleCount! 15 | @IBOutlet weak var vwCircleTic:CircleTic! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view. 20 | 21 | vwCircle.startTimer(block: { (count, minute, second) in 22 | print("\(minute) : \(second)") 23 | }) { 24 | print("complete") 25 | } 26 | 27 | vwCircleTic.touchBeginEvent = { 28 | print("touch") 29 | } 30 | 31 | vwCircleTic.touchEndedEvent = { 32 | print("end") 33 | } 34 | 35 | vwCircleTic.touchMovedEvent = { 36 | print("move") 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Demo/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Demo' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Demo 9 | pod 'BKCountDownTimer' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ki Mun Kwon 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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: "BKCountDownTimer", 8 | dependencies: [ 9 | // Dependencies declare other packages that this package depends on. 10 | // .package(url: /* package url */, from: "1.0.0"), 11 | ], 12 | targets: [ 13 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 14 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 15 | .target( 16 | name: "BKCountDownTimer", 17 | dependencies: []), 18 | .testTarget( 19 | name: "BKCountDownTimerTests", 20 | dependencies: ["BKCountDownTimer"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CountDownTimer 2 | Circle shaped countdown timer. 3 | 4 | ## Installation 5 | CocoaPods: 6 |
 7 | pod 'BKCountDownTimer'
 8 | 
9 | Manual: 10 |
11 | Copy CircleBase.swift, CircleCount.swift, CircleTic.swift, CircleTimer.swift to your project.
12 | 
13 | 14 | ## Preview 15 | ![](/img-prev.png) 16 | 17 | ## HowTo 18 | ![](/img-howto.png) 19 | 20 | ## Using Timer 21 | ```swift 22 | vwCircle.startTimer(block: { (count, minute, second) in 23 | print("\(minute) : \(second)") 24 | }) { 25 | print("complete") 26 | } 27 | ``` 28 | 29 | ## License 30 | 31 | BKCountDownTimer is available under the MIT license. See the LICENSE file for more info. 32 | -------------------------------------------------------------------------------- /Sources/BKCountDownTimer/CircleBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleBase.swift 3 | // BKCountdownTImer 4 | // 5 | // Created by moon on 30/09/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | public class CircleBase: UIView { 13 | //MARK:- 🔶 @IBInspectable 14 | /// true는 시계방향, false 반시계방향 15 | @IBInspectable public var isClockwise: Bool = true 16 | /// Circle의 색을 지정합니다. 17 | @IBInspectable public var fillColor: UIColor? = UIColor.red 18 | /// 그린 원이 소수점으로 떨어지지 않도록 자동으로 계산합니다. 19 | @IBInspectable public var isAutoControl: Bool = false 20 | /// Circle의 각도를 지정합니다. 21 | @IBInspectable public var minuteValue:CGFloat = 45 { 22 | didSet { 23 | self.drawCircle(time: minuteValue) 24 | } 25 | } 26 | 27 | //MARK:- 🔶 @private 28 | /// 계산시 처음 0도 기준이 3시방향 이므로 12시방향 기준이 되려면 -90도 조정 29 | fileprivate var m_start_angle:CGFloat = -90 30 | /// 이 값이 변경됨에 따라 원의 부채꼴크기가 결정됨. 31 | fileprivate var m_end_angle:CGFloat = 180 32 | /// 원의 부채꼴을 채우는 레이어 33 | fileprivate var m_fill_layer:CAShapeLayer = CAShapeLayer() 34 | /// 시계방향일 때 원을 모두 채우는 값 35 | fileprivate let MAX_ANGLE_CW:CGFloat = 0.001 36 | /// 반 시계방향일 때 원을 모두 채우는 값 37 | fileprivate let MAX_ANGLE:CGFloat = 359.999 38 | 39 | //MARK:- 🔶 @internal 40 | /// 원의 반지름 41 | internal var radius:CGFloat = 0 42 | /// 가운데 좌표 43 | internal var centerPoint:CGPoint = CGPoint(x: 0, y: 0) { 44 | didSet { 45 | radius = min(centerPoint.x, centerPoint.y) 46 | } 47 | } 48 | 49 | //MARK:- 🔶 @public 50 | public var touchBeginEvent:(()->())? = nil 51 | public var touchMovedEvent:(()->())? = nil 52 | public var touchEndedEvent:(()->())? = nil 53 | 54 | //MARK:- 🔶 @override 55 | override public func draw(_ rect: CGRect) { 56 | super.draw(rect) 57 | self.centerPoint = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 58 | 59 | self.m_fill_layer.frame = self.bounds 60 | self.layer.addSublayer(self.m_fill_layer) 61 | self.drawCircle(time: minuteValue) 62 | } 63 | 64 | override public func layoutSubviews() { 65 | super.layoutSubviews() 66 | self.centerPoint = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 67 | self.m_fill_layer.frame = self.bounds 68 | self.drawCircle(time: minuteValue) 69 | } 70 | 71 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) { 72 | super.touchesBegan(touches, with: event) 73 | touchBeginEvent?() 74 | } 75 | 76 | override public func touchesMoved(_ touches: Set, with event: UIEvent?) { 77 | super.touchesMoved(touches, with: event) 78 | touchMovedEvent?() 79 | guard let touch = self.getLastValue(touches) else { 80 | return 81 | } 82 | let tapPoint = touch.location(in: self) 83 | let angle = self.angleToPoint(tapPoint) 84 | 85 | m_end_angle = CGFloat(angle).rounded(); 86 | self.drawCircle(angle:m_end_angle) 87 | } 88 | 89 | override public func touchesEnded(_ touches: Set, with event: UIEvent?) { 90 | super.touchesEnded(touches, with: event) 91 | touchEndedEvent?() 92 | var finalAngle = m_end_angle 93 | if isAutoControl { 94 | let rest = finalAngle.truncatingRemainder(dividingBy: 6) 95 | if rest > 2.5 { 96 | finalAngle = (finalAngle+(6-rest)).rounded() 97 | } else { 98 | finalAngle = (finalAngle-rest).rounded() 99 | } 100 | } 101 | 102 | let time = self.convertTime(angle: finalAngle) 103 | self.drawCircle(time: time) 104 | 105 | guard let touch = self.getLastValue(touches) else { 106 | return 107 | } 108 | let tapPoint = touch.location(in: self) 109 | self.touchesEnded(tapPoint, m_end_angle) 110 | } 111 | 112 | public func touchesEnded(_ tap: CGPoint, _ endAngle: CGFloat) { } 113 | 114 | //MARK:- 🔶 @public Method 115 | /// time을 입력한 값으로 Circle을 그립니다. 0보다 작으면 빈 공간을 그리고 60보다 크면 화면을 가득 채웁니다. 116 | public func drawCircle(time:CGFloat) { 117 | if time <= 0 { 118 | // Circle을 빈공간으로 그립니다. 119 | m_end_angle = isClockwise ? 360.0 : 0.0 120 | } else if time > 59 { 121 | // 60으로 고정 122 | m_end_angle = isClockwise ? MAX_ANGLE_CW : MAX_ANGLE 123 | } else { 124 | // 0 < time <= 59 125 | m_end_angle = isClockwise ? (360.0-(time*6.0)) : (time*6.0) 126 | } 127 | 128 | self.drawCircle(angle:m_end_angle) 129 | } 130 | 131 | //MARK:- 🔶 @public Method 132 | /* 133 | 시계방향 (isClockwise == true) 134 | m_endAngle 기준으로 1분당 6도, 1초당 0.1도 135 | 예) 1분 354도, 5분 330도, 15분 270도, 30분 180도, 45분 90도, 60분 0.01도 136 | 수식 : (2*pi)-(6*m_endAngle) 137 | 분 : 138 | 139 | 반시계방향 (isClockwise == false) 140 | m_endAngle 기준으로 1분당 6도, 1초당 0.1도 141 | 예) 1분 6도, 5분 30도, 15분 90도, 30분 180도, 45분 270도, 60분 359.9도 142 | 수식 : 6*m_endAngle 143 | 분 : m_endAngle=6 -> 6/6 = 1분 144 | */ 145 | public func startTimer(block:@escaping (_ countDown:CGFloat, _ minute:Int, _ second:Int)->(),completion:@escaping ()->()) { 146 | self.isUserInteractionEnabled = false 147 | CircleTimer.shared.startCountDown(time: minuteValue, block: { [weak self] (c, m, s) in 148 | guard let self = `self` else { return } 149 | block(c, m, s) 150 | self.drawCircle(time: c/60) 151 | }) { [weak self] in 152 | guard let self = `self` else { return } 153 | completion() 154 | self.isUserInteractionEnabled = true 155 | } 156 | } 157 | 158 | //MARK:- 🔶 @private Method 159 | /// angle을 time으로 변경하는 함수 160 | private func convertTime(angle:CGFloat) -> CGFloat { 161 | if isAutoControl { 162 | return isClockwise ? ((360 - m_end_angle)/6).rounded() : (m_end_angle/6).rounded() 163 | } else { 164 | return isClockwise ? ((360 - m_end_angle)/6) : (m_end_angle/6) 165 | } 166 | } 167 | 168 | /// Circle을 그리는 함수 169 | private func drawCircle(angle:CGFloat) { 170 | if angle > -1{ 171 | let standard = 360 + m_start_angle; 172 | let startAngle = CGFloat(standard).toRadians() 173 | var tmp = standard - angle 174 | if(tmp < 0) 175 | { 176 | tmp += 360 177 | } 178 | let endAngle = CGFloat(tmp).toRadians() 179 | 180 | let path = UIBezierPath() 181 | 182 | // Move to the centre 183 | path.move(to: centerPoint) 184 | // Draw a line to the circumference 185 | path.addLine(to: CGPoint(x: centerPoint.x + radius * cos(startAngle), y: centerPoint.y + radius * sin(startAngle))) 186 | // NOW draw the arc 187 | path.addArc(withCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: isClockwise) 188 | // Line back to the centre, where we started (or the stroke doesn't work, though the fill does) 189 | path.addLine(to: CGPoint(x: centerPoint.x, y: centerPoint.y)) 190 | // n.b. as @MartinR points out `cPath.close()` does the same! 191 | 192 | m_fill_layer.path = path.cgPath 193 | m_fill_layer.fillColor = fillColor?.cgColor 194 | } 195 | } 196 | 197 | private func angleToPoint(_ tapPoint : CGPoint) -> Float { 198 | let dx = tapPoint.x - centerPoint.x; 199 | let dy = tapPoint.y - centerPoint.y; 200 | let radians = atan2(dy,dx); // in radians 201 | var degrees = radians * 180 / .pi; // in degrees 202 | 203 | degrees -= m_start_angle 204 | 205 | if (degrees < 0){ return fabsf(Float(degrees))} 206 | else{ return 360 - Float(degrees) } 207 | } 208 | 209 | private func getLastValue(_ arr:Set) -> UITouch? { 210 | var rtn_val:UITouch? = nil; 211 | for tmp in arr { 212 | rtn_val = tmp 213 | } 214 | return rtn_val; 215 | } 216 | } 217 | 218 | fileprivate extension CGFloat { 219 | func toRadians() -> CGFloat { 220 | return self * CGFloat.pi / 180.0 221 | } 222 | } 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /Sources/BKCountDownTimer/CircleCount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleCount.swift 3 | // BKCountdownTImer 4 | // 5 | // Created by moon on 30/09/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | public class CircleCount: CircleTic { 13 | 14 | //MARK:- 🔶 @IBInspectable 15 | /// 라벨을 채우는 배경 색 16 | @IBInspectable public var labelFillColor: UIColor? = UIColor.white 17 | /// 라벨 색 18 | @IBInspectable public var labelColor: UIColor? = UIColor.black 19 | 20 | 21 | //MARK:- 🔶 @private 22 | /// 가운데 위치한 타이머 23 | private var lbMiddleTimer:UILabel = { 24 | let lb = UILabel() 25 | lb.textAlignment = .center 26 | lb.layer.backgroundColor = UIColor.white.cgColor 27 | return lb 28 | }() 29 | 30 | //MARK:- 🔶 @override 31 | override public func draw(_ rect: CGRect) { 32 | super.draw(rect) 33 | self.drawMiddleTimer() 34 | } 35 | 36 | public override func startTimer(block: @escaping (CGFloat, Int, Int) -> (), completion: @escaping () -> ()) { 37 | super.startTimer(block: { [weak self] (c, m, s) in 38 | guard let `self` = self else { return } 39 | var sM:String = "\(m)" 40 | var sS:String = "\(s)" 41 | if m < 10 { 42 | sM = "0\(m)" 43 | } 44 | 45 | if s < 10 { 46 | sS = "0\(s)" 47 | } 48 | let strCountDown:String = "\(sM):\(sS)" 49 | self.lbMiddleTimer.text = strCountDown 50 | block(c, m, s) 51 | }, completion: completion) 52 | } 53 | 54 | private func drawMiddleTimer() { 55 | let timerCenter:CGPoint = CGPoint(x: centerPoint.x - radius/2, y: centerPoint.y - radius/2) 56 | self.lbMiddleTimer.frame = CGRect(x: timerCenter.x, y: timerCenter.y, width: radius, height: radius) 57 | self.lbMiddleTimer.layer.cornerRadius = radius/2 58 | self.lbMiddleTimer.layer.backgroundColor = labelFillColor?.cgColor 59 | self.lbMiddleTimer.font = UIFont.boldSystemFont(ofSize: radius/4) 60 | self.lbMiddleTimer.textColor = labelColor 61 | let nMinute = Int(minuteValue) 62 | self.lbMiddleTimer.text = nMinute < 10 ? "0\(nMinute):00" : "\(nMinute):00" 63 | self.addSubview(self.lbMiddleTimer) 64 | } 65 | 66 | public override func touchesEnded(_ tap: CGPoint, _ endAngle: CGFloat) { 67 | var nMinute:Int = isClockwise ? Int(((360 - endAngle)/6).rounded()) : Int((endAngle/6).rounded()) 68 | if nMinute <= 0 { 69 | // Circle을 빈공간으로 그립니다. 70 | nMinute = 1 71 | } else if nMinute > 59 { 72 | // 60으로 고정 73 | nMinute = 60 74 | } 75 | 76 | self.minuteValue = CGFloat(nMinute) 77 | self.lbMiddleTimer.text = nMinute < 10 ? "0\(nMinute):00" : "\(nMinute):00" 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /Sources/BKCountDownTimer/CircleTic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleTic.swift 3 | // BKCountdownTImer 4 | // 5 | // Created by moon on 30/09/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | public class CircleTic: CircleBase { 13 | 14 | //MARK:- 🔶 @IBInspectable 15 | /// 시계 외각에 촘촘히 그려져 있는 선 중 가장 두꺼운 선의 색을 정합니다. 16 | @IBInspectable public var boldBezelColor:UIColor = .black 17 | /// 시계 외각에 촘촘히 그려져 있는 선 중 가장 얇은 선의 색을 정합니다. 18 | @IBInspectable public var thinBezelColor:UIColor = .black 19 | /// 20 | @IBInspectable public var expandTic:Bool = false 21 | 22 | //MARK:- 🔶 @public 23 | 24 | //MARK:- 🔶 @override 25 | override public func draw(_ rect: CGRect) { 26 | super.draw(rect) 27 | var frame: CGRect = rect 28 | if expandTic { 29 | frame.origin.x -= rect.width / 7 30 | frame.origin.y -= rect.height / 7 31 | frame.size.width += rect.width / 3.5 32 | frame.size.height += rect.height / 3.5 33 | } 34 | let ticView = CircleWithTic(frame: frame) 35 | ticView.backgroundColor = .clear 36 | self.addSubview(ticView) 37 | } 38 | 39 | } 40 | 41 | fileprivate class CircleWithTic: UIView { 42 | 43 | //MARK:- 🔶 @IBInspectable 44 | /// 시계 외각에 촘촘히 그려져 있는 선 중 가장 두꺼운 선의 색을 정합니다. 45 | var boldBezelColor:UIColor = .black 46 | /// 시계 외각에 촘촘히 그려져 있는 선 중 가장 얇은 선의 색을 정합니다. 47 | var thinBezelColor:UIColor = .black 48 | /// 반지름 49 | var radius:CGFloat = 5 50 | 51 | //MARK:- 🔶 @override 52 | override func draw(_ rect: CGRect) { 53 | super.draw(rect) 54 | let centerPoint = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 55 | radius = min(centerPoint.x, centerPoint.y) 56 | self.drawBoldBezel(rect) 57 | self.drawThinBezel(rect) 58 | } 59 | 60 | //MARK:- 🔶 @private Method 61 | private func drawBoldBezel(_ rect:CGRect) { 62 | let context = UIGraphicsGetCurrentContext()! 63 | let boldBezelWidth = self.radius / 30 64 | let boldBezelSize:CGFloat = boldBezelWidth * 3 65 | let boldBezelRect = CGRect(x: -boldBezelWidth/2, y: 0, width: boldBezelWidth, height: boldBezelSize) 66 | let boldBezelPath = UIBezierPath(rect: boldBezelRect) 67 | let angleDifference: CGFloat = .pi / 6 68 | 69 | context.saveGState() 70 | self.boldBezelColor.setFill() 71 | context.translateBy(x: rect.width / 2, y: rect.height / 2) 72 | 73 | for i in 1...12 { 74 | context.saveGState() 75 | let angle = angleDifference * CGFloat(i) + .pi 76 | context.rotate(by: angle) 77 | context.translateBy(x: 0, y: rect.height / 2.1 - boldBezelSize) 78 | boldBezelPath.fill() 79 | context.restoreGState() 80 | } 81 | context.restoreGState() 82 | } 83 | 84 | private func drawThinBezel(_ rect:CGRect) { 85 | let context = UIGraphicsGetCurrentContext()! 86 | let thinBezelWidth = self.radius / 70 87 | let thinBezelSize:CGFloat = thinBezelWidth * 7 88 | let thinBezelRect:CGRect = CGRect(x: -thinBezelWidth/2, y: 0, width: thinBezelWidth, height: thinBezelSize) 89 | let thinBezelPath = UIBezierPath(rect: thinBezelRect) 90 | let angleDifference:CGFloat = .pi / 30 91 | 92 | context.saveGState() 93 | self.thinBezelColor.setFill() 94 | context.translateBy(x: rect.width / 2, y: rect.height / 2) 95 | 96 | for i in 1...60 { 97 | if i % 5 == 0 { 98 | continue 99 | } 100 | context.saveGState() 101 | let angle = angleDifference * CGFloat(i) + .pi 102 | context.rotate(by: angle) 103 | context.translateBy(x: 0, y: rect.height / 2.1 - thinBezelSize) 104 | thinBezelPath.fill() 105 | context.restoreGState() 106 | } 107 | context.restoreGState() 108 | } 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Sources/BKCountDownTimer/CircleTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleTimer.swift 3 | // BKCountdownTImer 4 | // 5 | // Created by moon on 30/09/2019. 6 | // Copyright © 2019 Bugking. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CircleTimer: NSObject { 12 | static let shared = CircleTimer() 13 | 14 | public func startCountDown(time:CGFloat, block:@escaping(_ countDown:CGFloat, _ minute:Int, _ second:Int)->(), completion:@escaping()->()) { 15 | var countDown = time * 60 16 | 17 | let initCount = self.convert(time: countDown) 18 | block(countDown, initCount.share, initCount.rest) 19 | Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in 20 | if countDown < 1 { 21 | timer.invalidate() 22 | completion() 23 | return 24 | } 25 | countDown -= 1 26 | let result = self.convert(time: countDown) 27 | block(countDown, result.share, result.rest) 28 | } 29 | } 30 | 31 | private func convert(time:CGFloat) -> (share:Int, rest:Int) { 32 | let shareValue = Int(time/60) 33 | let resValue = Int(time.truncatingRemainder(dividingBy: 60).rounded()) 34 | 35 | return (shareValue, resValue) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/BKCountDownTimerTests/BKCountDownTimerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class BKCountDownTimerTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("BKCountDownTimer") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample), 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Tests/BKCountDownTimerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(BKCountDownTimerTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import BKCountDownTimerTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += BKCountDownTimerTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /img-howto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugkingK/CountDownTimer/b54e035a22a13687b3ef68be43a03fe5eba24427/img-howto.png -------------------------------------------------------------------------------- /img-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugkingK/CountDownTimer/b54e035a22a13687b3ef68be43a03fe5eba24427/img-prev.png --------------------------------------------------------------------------------