";
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 | 
16 |
17 | ## HowTo
18 | 
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
--------------------------------------------------------------------------------