├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── AMClockView.podspec ├── AMClockView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── AMClockView.xcscheme ├── AMClockView ├── AMClockView.h └── Info.plist ├── LICENSE ├── Package.swift ├── README.md ├── SampleAMClock ├── SampleAMClock.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── SampleAMClock │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Classes │ │ ├── AppDelegate.swift │ │ └── ViewController.swift │ ├── Info.plist │ └── image │ │ ├── clock2@2x.png │ │ ├── clock@2x.png │ │ ├── h@2x.png │ │ ├── hourhand2@2x.png │ │ ├── hourhand@2x.png │ │ ├── m@2x.png │ │ ├── minutehand2@2x.png │ │ └── minutehand@2x.png └── SampleAMClockTests │ ├── Info.plist │ └── SampleAMClockTests.swift └── Source └── AMClockView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode (from gitignore.io) 2 | build/ 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | xcuserdata 12 | *.xccheckout 13 | *.moved-aside 14 | DerivedData 15 | *.hmap 16 | *.ipa 17 | *.xcuserstate 18 | 19 | # CocoaPod 20 | Pods/* 21 | Podfile.lock 22 | 23 | # Carthage 24 | Carthage/Build 25 | 26 | # others 27 | *.swp 28 | !.gitkeep 29 | .DS_Store 30 | UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AMClockView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "AMClockView" 3 | s.version = "2.1.0" 4 | s.summary = "AMClockView is a view can select time." 5 | s.license = { :type => 'MIT', :file => 'LICENSE' } 6 | s.homepage = "https://github.com/adventam10/AMClockView" 7 | s.author = { "am10" => "adventam10@gmail.com" } 8 | s.source = { :git => "https://github.com/adventam10/AMClockView.git", :tag => "#{s.version}" } 9 | s.platform = :ios, "9.0" 10 | s.requires_arc = true 11 | s.source_files = 'Source/*.{swift}' 12 | s.swift_version = "5.0" 13 | end 14 | -------------------------------------------------------------------------------- /AMClockView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0E47526C201E12F3002EA382 /* AMClockView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E47526A201E12F2002EA382 /* AMClockView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 0EB234D82354933300442A85 /* AMClockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB234D72354933300442A85 /* AMClockView.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 0E475267201E12F2002EA382 /* AMClockView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AMClockView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 0E47526A201E12F2002EA382 /* AMClockView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AMClockView.h; sourceTree = ""; }; 17 | 0E47526B201E12F3002EA382 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 18 | 0EB234D72354933300442A85 /* AMClockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AMClockView.swift; sourceTree = ""; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 0E475263201E12F2002EA382 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 0E47525D201E12F2002EA382 = { 33 | isa = PBXGroup; 34 | children = ( 35 | 0EB234D62354933300442A85 /* Source */, 36 | 0E475269201E12F2002EA382 /* AMClockView */, 37 | 0E475268201E12F2002EA382 /* Products */, 38 | ); 39 | sourceTree = ""; 40 | }; 41 | 0E475268201E12F2002EA382 /* Products */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | 0E475267201E12F2002EA382 /* AMClockView.framework */, 45 | ); 46 | name = Products; 47 | sourceTree = ""; 48 | }; 49 | 0E475269201E12F2002EA382 /* AMClockView */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 0E47526A201E12F2002EA382 /* AMClockView.h */, 53 | 0E47526B201E12F3002EA382 /* Info.plist */, 54 | ); 55 | path = AMClockView; 56 | sourceTree = ""; 57 | }; 58 | 0EB234D62354933300442A85 /* Source */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 0EB234D72354933300442A85 /* AMClockView.swift */, 62 | ); 63 | path = Source; 64 | sourceTree = ""; 65 | }; 66 | /* End PBXGroup section */ 67 | 68 | /* Begin PBXHeadersBuildPhase section */ 69 | 0E475264201E12F2002EA382 /* Headers */ = { 70 | isa = PBXHeadersBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 0E47526C201E12F3002EA382 /* AMClockView.h in Headers */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXHeadersBuildPhase section */ 78 | 79 | /* Begin PBXNativeTarget section */ 80 | 0E475266201E12F2002EA382 /* AMClockView */ = { 81 | isa = PBXNativeTarget; 82 | buildConfigurationList = 0E47526F201E12F3002EA382 /* Build configuration list for PBXNativeTarget "AMClockView" */; 83 | buildPhases = ( 84 | 0E475262201E12F2002EA382 /* Sources */, 85 | 0E475263201E12F2002EA382 /* Frameworks */, 86 | 0E475264201E12F2002EA382 /* Headers */, 87 | 0E475265201E12F2002EA382 /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = AMClockView; 94 | productName = AMClockView; 95 | productReference = 0E475267201E12F2002EA382 /* AMClockView.framework */; 96 | productType = "com.apple.product-type.framework"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 0E47525E201E12F2002EA382 /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | LastUpgradeCheck = 1020; 105 | ORGANIZATIONNAME = am10; 106 | TargetAttributes = { 107 | 0E475266201E12F2002EA382 = { 108 | CreatedOnToolsVersion = 9.2; 109 | LastSwiftMigration = 1020; 110 | ProvisioningStyle = Automatic; 111 | }; 112 | }; 113 | }; 114 | buildConfigurationList = 0E475261201E12F2002EA382 /* Build configuration list for PBXProject "AMClockView" */; 115 | compatibilityVersion = "Xcode 8.0"; 116 | developmentRegion = en; 117 | hasScannedForEncodings = 0; 118 | knownRegions = ( 119 | en, 120 | Base, 121 | ); 122 | mainGroup = 0E47525D201E12F2002EA382; 123 | productRefGroup = 0E475268201E12F2002EA382 /* Products */; 124 | projectDirPath = ""; 125 | projectRoot = ""; 126 | targets = ( 127 | 0E475266201E12F2002EA382 /* AMClockView */, 128 | ); 129 | }; 130 | /* End PBXProject section */ 131 | 132 | /* Begin PBXResourcesBuildPhase section */ 133 | 0E475265201E12F2002EA382 /* Resources */ = { 134 | isa = PBXResourcesBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXResourcesBuildPhase section */ 141 | 142 | /* Begin PBXSourcesBuildPhase section */ 143 | 0E475262201E12F2002EA382 /* Sources */ = { 144 | isa = PBXSourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 0EB234D82354933300442A85 /* AMClockView.swift in Sources */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXSourcesBuildPhase section */ 152 | 153 | /* Begin XCBuildConfiguration section */ 154 | 0E47526D201E12F3002EA382 /* Debug */ = { 155 | isa = XCBuildConfiguration; 156 | buildSettings = { 157 | ALWAYS_SEARCH_USER_PATHS = NO; 158 | CLANG_ANALYZER_NONNULL = YES; 159 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 160 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 161 | CLANG_CXX_LIBRARY = "libc++"; 162 | CLANG_ENABLE_MODULES = YES; 163 | CLANG_ENABLE_OBJC_ARC = YES; 164 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 165 | CLANG_WARN_BOOL_CONVERSION = YES; 166 | CLANG_WARN_COMMA = YES; 167 | CLANG_WARN_CONSTANT_CONVERSION = YES; 168 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 169 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 170 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 171 | CLANG_WARN_EMPTY_BODY = YES; 172 | CLANG_WARN_ENUM_CONVERSION = YES; 173 | CLANG_WARN_INFINITE_RECURSION = YES; 174 | CLANG_WARN_INT_CONVERSION = YES; 175 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 176 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 177 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 178 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 179 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 180 | CLANG_WARN_STRICT_PROTOTYPES = YES; 181 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 182 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 183 | CLANG_WARN_UNREACHABLE_CODE = YES; 184 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 185 | CODE_SIGN_IDENTITY = "iPhone Developer"; 186 | COPY_PHASE_STRIP = NO; 187 | CURRENT_PROJECT_VERSION = 1; 188 | DEBUG_INFORMATION_FORMAT = dwarf; 189 | ENABLE_STRICT_OBJC_MSGSEND = YES; 190 | ENABLE_TESTABILITY = YES; 191 | GCC_C_LANGUAGE_STANDARD = gnu11; 192 | GCC_DYNAMIC_NO_PIC = NO; 193 | GCC_NO_COMMON_BLOCKS = YES; 194 | GCC_OPTIMIZATION_LEVEL = 0; 195 | GCC_PREPROCESSOR_DEFINITIONS = ( 196 | "DEBUG=1", 197 | "$(inherited)", 198 | ); 199 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 200 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 201 | GCC_WARN_UNDECLARED_SELECTOR = YES; 202 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 203 | GCC_WARN_UNUSED_FUNCTION = YES; 204 | GCC_WARN_UNUSED_VARIABLE = YES; 205 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 206 | MTL_ENABLE_DEBUG_INFO = YES; 207 | ONLY_ACTIVE_ARCH = YES; 208 | SDKROOT = iphoneos; 209 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 210 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 211 | VERSIONING_SYSTEM = "apple-generic"; 212 | VERSION_INFO_PREFIX = ""; 213 | }; 214 | name = Debug; 215 | }; 216 | 0E47526E201E12F3002EA382 /* Release */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ALWAYS_SEARCH_USER_PATHS = NO; 220 | CLANG_ANALYZER_NONNULL = YES; 221 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 222 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 223 | CLANG_CXX_LIBRARY = "libc++"; 224 | CLANG_ENABLE_MODULES = YES; 225 | CLANG_ENABLE_OBJC_ARC = YES; 226 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 227 | CLANG_WARN_BOOL_CONVERSION = YES; 228 | CLANG_WARN_COMMA = YES; 229 | CLANG_WARN_CONSTANT_CONVERSION = YES; 230 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 231 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 232 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 233 | CLANG_WARN_EMPTY_BODY = YES; 234 | CLANG_WARN_ENUM_CONVERSION = YES; 235 | CLANG_WARN_INFINITE_RECURSION = YES; 236 | CLANG_WARN_INT_CONVERSION = YES; 237 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 238 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 239 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 240 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 242 | CLANG_WARN_STRICT_PROTOTYPES = YES; 243 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 244 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 245 | CLANG_WARN_UNREACHABLE_CODE = YES; 246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 247 | CODE_SIGN_IDENTITY = "iPhone Developer"; 248 | COPY_PHASE_STRIP = NO; 249 | CURRENT_PROJECT_VERSION = 1; 250 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 251 | ENABLE_NS_ASSERTIONS = NO; 252 | ENABLE_STRICT_OBJC_MSGSEND = YES; 253 | GCC_C_LANGUAGE_STANDARD = gnu11; 254 | GCC_NO_COMMON_BLOCKS = YES; 255 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 256 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 257 | GCC_WARN_UNDECLARED_SELECTOR = YES; 258 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 259 | GCC_WARN_UNUSED_FUNCTION = YES; 260 | GCC_WARN_UNUSED_VARIABLE = YES; 261 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 262 | MTL_ENABLE_DEBUG_INFO = NO; 263 | SDKROOT = iphoneos; 264 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 265 | VALIDATE_PRODUCT = YES; 266 | VERSIONING_SYSTEM = "apple-generic"; 267 | VERSION_INFO_PREFIX = ""; 268 | }; 269 | name = Release; 270 | }; 271 | 0E475270201E12F3002EA382 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | CODE_SIGN_IDENTITY = ""; 275 | CODE_SIGN_STYLE = Automatic; 276 | DEFINES_MODULE = YES; 277 | DEVELOPMENT_TEAM = ""; 278 | DYLIB_COMPATIBILITY_VERSION = 1; 279 | DYLIB_CURRENT_VERSION = 1; 280 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 281 | INFOPLIST_FILE = AMClockView/Info.plist; 282 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 283 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 284 | PRODUCT_BUNDLE_IDENTIFIER = am10.AMClockView; 285 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 286 | SKIP_INSTALL = YES; 287 | SWIFT_VERSION = 5.0; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | }; 290 | name = Debug; 291 | }; 292 | 0E475271201E12F3002EA382 /* Release */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | CODE_SIGN_IDENTITY = ""; 296 | CODE_SIGN_STYLE = Automatic; 297 | DEFINES_MODULE = YES; 298 | DEVELOPMENT_TEAM = ""; 299 | DYLIB_COMPATIBILITY_VERSION = 1; 300 | DYLIB_CURRENT_VERSION = 1; 301 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 302 | INFOPLIST_FILE = AMClockView/Info.plist; 303 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 304 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 305 | PRODUCT_BUNDLE_IDENTIFIER = am10.AMClockView; 306 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 307 | SKIP_INSTALL = YES; 308 | SWIFT_VERSION = 5.0; 309 | TARGETED_DEVICE_FAMILY = "1,2"; 310 | }; 311 | name = Release; 312 | }; 313 | /* End XCBuildConfiguration section */ 314 | 315 | /* Begin XCConfigurationList section */ 316 | 0E475261201E12F2002EA382 /* Build configuration list for PBXProject "AMClockView" */ = { 317 | isa = XCConfigurationList; 318 | buildConfigurations = ( 319 | 0E47526D201E12F3002EA382 /* Debug */, 320 | 0E47526E201E12F3002EA382 /* Release */, 321 | ); 322 | defaultConfigurationIsVisible = 0; 323 | defaultConfigurationName = Release; 324 | }; 325 | 0E47526F201E12F3002EA382 /* Build configuration list for PBXNativeTarget "AMClockView" */ = { 326 | isa = XCConfigurationList; 327 | buildConfigurations = ( 328 | 0E475270201E12F3002EA382 /* Debug */, 329 | 0E475271201E12F3002EA382 /* Release */, 330 | ); 331 | defaultConfigurationIsVisible = 0; 332 | defaultConfigurationName = Release; 333 | }; 334 | /* End XCConfigurationList section */ 335 | }; 336 | rootObject = 0E47525E201E12F2002EA382 /* Project object */; 337 | } 338 | -------------------------------------------------------------------------------- /AMClockView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AMClockView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AMClockView.xcodeproj/xcshareddata/xcschemes/AMClockView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /AMClockView/AMClockView.h: -------------------------------------------------------------------------------- 1 | // 2 | // AMClockView.h 3 | // AMClockView 4 | // 5 | // Created by am10 on 2018/01/28. 6 | // Copyright © 2018年 am10. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AMClockView. 12 | FOUNDATION_EXPORT double AMClockViewVersionNumber; 13 | 14 | //! Project version string for AMClockView. 15 | FOUNDATION_EXPORT const unsigned char AMClockViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /AMClockView/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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 adventam10 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 | // AMClockView, https://github.com/adventam10/AMClockView 5 | // 6 | // Created by am10 on 2019/10/14. 7 | // Copyright © 2019年 am10. All rights reserved. 8 | // 9 | 10 | import PackageDescription 11 | 12 | let package = Package(name: "AMClockView", 13 | platforms: [.iOS(.v9)], 14 | products: [.library(name: "AMClockView", 15 | targets: ["AMClockView"])], 16 | targets: [.target(name: "AMClockView", 17 | path: "Source")], 18 | swiftLanguageVersions: [.v5]) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMClockView 2 | 3 | ![Pod Platform](https://img.shields.io/cocoapods/p/AMClockView.svg?style=flat) 4 | ![Pod License](https://img.shields.io/cocoapods/l/AMClockView.svg?style=flat) 5 | [![Pod Version](https://img.shields.io/cocoapods/v/AMClockView.svg?style=flat)](http://cocoapods.org/pods/AMClockView) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 8 | 9 | `AMClockView` is a view can select time. 10 | 11 | ## Demo 12 | 13 | ![amclock](https://user-images.githubusercontent.com/34936885/34641894-0adbf71a-f34e-11e7-892a-86e5f3e51256.gif) 14 | 15 | ## Usage 16 | 17 | Create clockView. 18 | 19 | ```swift 20 | let clockView = AMClockView(frame: view.bounds) 21 | 22 | // customize here 23 | 24 | clockView.delegate = self 25 | view.addSubview(clockView) 26 | ``` 27 | 28 | Conform to the protocol in the class implementation. 29 | 30 | ```swift 31 | func clockView(_ clockView: AMClockView, didChangeDate date: Date) { 32 | // use selected date here 33 | } 34 | ``` 35 | 36 | The hour hand moves when you dragged inside of central circle. 37 | 38 | The minute hand moves when you draged outside of central circle. 39 | 40 | ### Customization 41 | `AMClockView` can be customized via the following properties. 42 | 43 | ```swift 44 | @IBInspectable public var clockBorderLineWidth: CGFloat = 5.0 45 | @IBInspectable public var smallClockIndexWidth: CGFloat = 1.0 46 | @IBInspectable public var clockIndexWidth: CGFloat = 2.0 47 | @IBInspectable public var hourHandWidth: CGFloat = 5.0 48 | @IBInspectable public var minuteHandWidth: CGFloat = 3.0 49 | @IBInspectable public var clockBorderLineColor: UIColor = .black 50 | @IBInspectable public var centerCircleLineColor: UIColor = .darkGray 51 | @IBInspectable public var hourHandColor: UIColor = .black 52 | @IBInspectable public var minuteHandColor: UIColor = .black 53 | @IBInspectable public var selectedTimeLabelTextColor: UIColor = .black 54 | @IBInspectable public var timeLabelTextColor: UIColor = .black 55 | @IBInspectable public var smallClockIndexColor: UIColor = .black 56 | @IBInspectable public var clockIndexColor: UIColor = .black 57 | @IBInspectable public var clockColor: UIColor = .clear 58 | @IBInspectable public var clockImage: UIImage? 59 | @IBInspectable public var minuteHandImage: UIImage? 60 | @IBInspectable public var hourHandImage: UIImage? 61 | @IBInspectable public var isShowSelectedTime: Bool = false 62 | public var clockType = AMCVClockType.arabic 63 | public var timeZone: TimeZone? // default is TimeZone.current 64 | public var selectedDate: Date? 65 | ``` 66 | 67 | ![clock](https://user-images.githubusercontent.com/34936885/66701955-20c3c680-ed3d-11e9-8e17-03544afab1bf.png) 68 | 69 | ## Installation 70 | 71 | ### CocoaPods 72 | 73 | Add this to your Podfile. 74 | 75 | ```ogdl 76 | pod 'AMClockView' 77 | ``` 78 | 79 | ### Carthage 80 | 81 | Add this to your Cartfile. 82 | 83 | ```ogdl 84 | github "adventam10/AMClockView" 85 | ``` 86 | 87 | ## License 88 | 89 | MIT 90 | 91 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0E47524C201DDCB4002EA382 /* clock2@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475244201DDCB4002EA382 /* clock2@2x.png */; }; 11 | 0E47524D201DDCB4002EA382 /* clock@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475245201DDCB4002EA382 /* clock@2x.png */; }; 12 | 0E47524E201DDCB4002EA382 /* h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475246201DDCB4002EA382 /* h@2x.png */; }; 13 | 0E47524F201DDCB4002EA382 /* hourhand2@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475247201DDCB4002EA382 /* hourhand2@2x.png */; }; 14 | 0E475250201DDCB4002EA382 /* hourhand@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475248201DDCB4002EA382 /* hourhand@2x.png */; }; 15 | 0E475251201DDCB4002EA382 /* m@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E475249201DDCB4002EA382 /* m@2x.png */; }; 16 | 0E475252201DDCB4002EA382 /* minutehand2@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E47524A201DDCB4002EA382 /* minutehand2@2x.png */; }; 17 | 0E475253201DDCB4002EA382 /* minutehand@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E47524B201DDCB4002EA382 /* minutehand@2x.png */; }; 18 | 0E475258201E1211002EA382 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E475256201E1211002EA382 /* AppDelegate.swift */; }; 19 | 0E475259201E1211002EA382 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E475257201E1211002EA382 /* ViewController.swift */; }; 20 | 0EB234DB2354935200442A85 /* AMClockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB234DA2354935200442A85 /* AMClockView.swift */; }; 21 | 0EB3D6382351CE450065073C /* SampleAMClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB3D6372351CE450065073C /* SampleAMClockTests.swift */; }; 22 | 0EB809962000A2420090810F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EB809942000A2420090810F /* Main.storyboard */; }; 23 | 0EB809982000A2420090810F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0EB809972000A2420090810F /* Assets.xcassets */; }; 24 | 0EB8099B2000A2420090810F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EB809992000A2420090810F /* LaunchScreen.storyboard */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | 0EB3D63A2351CE450065073C /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 0EB809852000A2420090810F /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 0EB8098C2000A2420090810F; 33 | remoteInfo = SampleAMClock; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 0E475244201DDCB4002EA382 /* clock2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "clock2@2x.png"; sourceTree = ""; }; 39 | 0E475245201DDCB4002EA382 /* clock@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "clock@2x.png"; sourceTree = ""; }; 40 | 0E475246201DDCB4002EA382 /* h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "h@2x.png"; sourceTree = ""; }; 41 | 0E475247201DDCB4002EA382 /* hourhand2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "hourhand2@2x.png"; sourceTree = ""; }; 42 | 0E475248201DDCB4002EA382 /* hourhand@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "hourhand@2x.png"; sourceTree = ""; }; 43 | 0E475249201DDCB4002EA382 /* m@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "m@2x.png"; sourceTree = ""; }; 44 | 0E47524A201DDCB4002EA382 /* minutehand2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "minutehand2@2x.png"; sourceTree = ""; }; 45 | 0E47524B201DDCB4002EA382 /* minutehand@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "minutehand@2x.png"; sourceTree = ""; }; 46 | 0E475256201E1211002EA382 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 47 | 0E475257201E1211002EA382 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 48 | 0EB234DA2354935200442A85 /* AMClockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AMClockView.swift; sourceTree = ""; }; 49 | 0EB3D6352351CE450065073C /* SampleAMClockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAMClockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 0EB3D6372351CE450065073C /* SampleAMClockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleAMClockTests.swift; sourceTree = ""; }; 51 | 0EB3D6392351CE450065073C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | 0EB8098D2000A2420090810F /* SampleAMClock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleAMClock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 0EB809952000A2420090810F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 54 | 0EB809972000A2420090810F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 55 | 0EB8099A2000A2420090810F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 56 | 0EB8099C2000A2420090810F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 0EB3D6322351CE450065073C /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 0EB8098A2000A2420090810F /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 0E475243201DDCB4002EA382 /* image */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 0E475244201DDCB4002EA382 /* clock2@2x.png */, 81 | 0E475245201DDCB4002EA382 /* clock@2x.png */, 82 | 0E475246201DDCB4002EA382 /* h@2x.png */, 83 | 0E475247201DDCB4002EA382 /* hourhand2@2x.png */, 84 | 0E475248201DDCB4002EA382 /* hourhand@2x.png */, 85 | 0E475249201DDCB4002EA382 /* m@2x.png */, 86 | 0E47524A201DDCB4002EA382 /* minutehand2@2x.png */, 87 | 0E47524B201DDCB4002EA382 /* minutehand@2x.png */, 88 | ); 89 | path = image; 90 | sourceTree = ""; 91 | }; 92 | 0E475255201E1211002EA382 /* Classes */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 0EB234D92354935200442A85 /* Source */, 96 | 0E475256201E1211002EA382 /* AppDelegate.swift */, 97 | 0E475257201E1211002EA382 /* ViewController.swift */, 98 | ); 99 | path = Classes; 100 | sourceTree = ""; 101 | }; 102 | 0EB234D92354935200442A85 /* Source */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 0EB234DA2354935200442A85 /* AMClockView.swift */, 106 | ); 107 | name = Source; 108 | path = ../../../Source; 109 | sourceTree = ""; 110 | }; 111 | 0EB3D6362351CE450065073C /* SampleAMClockTests */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 0EB3D6372351CE450065073C /* SampleAMClockTests.swift */, 115 | 0EB3D6392351CE450065073C /* Info.plist */, 116 | ); 117 | path = SampleAMClockTests; 118 | sourceTree = ""; 119 | }; 120 | 0EB809842000A2420090810F = { 121 | isa = PBXGroup; 122 | children = ( 123 | 0EB8098F2000A2420090810F /* SampleAMClock */, 124 | 0EB3D6362351CE450065073C /* SampleAMClockTests */, 125 | 0EB8098E2000A2420090810F /* Products */, 126 | ); 127 | sourceTree = ""; 128 | }; 129 | 0EB8098E2000A2420090810F /* Products */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 0EB8098D2000A2420090810F /* SampleAMClock.app */, 133 | 0EB3D6352351CE450065073C /* SampleAMClockTests.xctest */, 134 | ); 135 | name = Products; 136 | sourceTree = ""; 137 | }; 138 | 0EB8098F2000A2420090810F /* SampleAMClock */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 0E475255201E1211002EA382 /* Classes */, 142 | 0EB809942000A2420090810F /* Main.storyboard */, 143 | 0EB809972000A2420090810F /* Assets.xcassets */, 144 | 0EB809992000A2420090810F /* LaunchScreen.storyboard */, 145 | 0EB8099C2000A2420090810F /* Info.plist */, 146 | 0E475243201DDCB4002EA382 /* image */, 147 | ); 148 | path = SampleAMClock; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 0EB3D6342351CE450065073C /* SampleAMClockTests */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 0EB3D63E2351CE450065073C /* Build configuration list for PBXNativeTarget "SampleAMClockTests" */; 157 | buildPhases = ( 158 | 0EB3D6312351CE450065073C /* Sources */, 159 | 0EB3D6322351CE450065073C /* Frameworks */, 160 | 0EB3D6332351CE450065073C /* Resources */, 161 | ); 162 | buildRules = ( 163 | ); 164 | dependencies = ( 165 | 0EB3D63B2351CE450065073C /* PBXTargetDependency */, 166 | ); 167 | name = SampleAMClockTests; 168 | productName = SampleAMClockTests; 169 | productReference = 0EB3D6352351CE450065073C /* SampleAMClockTests.xctest */; 170 | productType = "com.apple.product-type.bundle.unit-test"; 171 | }; 172 | 0EB8098C2000A2420090810F /* SampleAMClock */ = { 173 | isa = PBXNativeTarget; 174 | buildConfigurationList = 0EB8099F2000A2420090810F /* Build configuration list for PBXNativeTarget "SampleAMClock" */; 175 | buildPhases = ( 176 | 0EB809892000A2420090810F /* Sources */, 177 | 0EB8098A2000A2420090810F /* Frameworks */, 178 | 0EB8098B2000A2420090810F /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = SampleAMClock; 185 | productName = SampleAMClock; 186 | productReference = 0EB8098D2000A2420090810F /* SampleAMClock.app */; 187 | productType = "com.apple.product-type.application"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 0EB809852000A2420090810F /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastSwiftUpdateCheck = 1100; 196 | LastUpgradeCheck = 1020; 197 | ORGANIZATIONNAME = am10; 198 | TargetAttributes = { 199 | 0EB3D6342351CE450065073C = { 200 | CreatedOnToolsVersion = 11.0; 201 | ProvisioningStyle = Automatic; 202 | TestTargetID = 0EB8098C2000A2420090810F; 203 | }; 204 | 0EB8098C2000A2420090810F = { 205 | CreatedOnToolsVersion = 9.2; 206 | LastSwiftMigration = 1020; 207 | ProvisioningStyle = Automatic; 208 | }; 209 | }; 210 | }; 211 | buildConfigurationList = 0EB809882000A2420090810F /* Build configuration list for PBXProject "SampleAMClock" */; 212 | compatibilityVersion = "Xcode 8.0"; 213 | developmentRegion = en; 214 | hasScannedForEncodings = 0; 215 | knownRegions = ( 216 | en, 217 | Base, 218 | ); 219 | mainGroup = 0EB809842000A2420090810F; 220 | productRefGroup = 0EB8098E2000A2420090810F /* Products */; 221 | projectDirPath = ""; 222 | projectRoot = ""; 223 | targets = ( 224 | 0EB8098C2000A2420090810F /* SampleAMClock */, 225 | 0EB3D6342351CE450065073C /* SampleAMClockTests */, 226 | ); 227 | }; 228 | /* End PBXProject section */ 229 | 230 | /* Begin PBXResourcesBuildPhase section */ 231 | 0EB3D6332351CE450065073C /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | 0EB8098B2000A2420090810F /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 0E47524D201DDCB4002EA382 /* clock@2x.png in Resources */, 243 | 0E47524F201DDCB4002EA382 /* hourhand2@2x.png in Resources */, 244 | 0E475251201DDCB4002EA382 /* m@2x.png in Resources */, 245 | 0E47524C201DDCB4002EA382 /* clock2@2x.png in Resources */, 246 | 0EB8099B2000A2420090810F /* LaunchScreen.storyboard in Resources */, 247 | 0EB809982000A2420090810F /* Assets.xcassets in Resources */, 248 | 0E475252201DDCB4002EA382 /* minutehand2@2x.png in Resources */, 249 | 0E475253201DDCB4002EA382 /* minutehand@2x.png in Resources */, 250 | 0E475250201DDCB4002EA382 /* hourhand@2x.png in Resources */, 251 | 0EB809962000A2420090810F /* Main.storyboard in Resources */, 252 | 0E47524E201DDCB4002EA382 /* h@2x.png in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 0EB3D6312351CE450065073C /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 0EB3D6382351CE450065073C /* SampleAMClockTests.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 0EB809892000A2420090810F /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 0EB234DB2354935200442A85 /* AMClockView.swift in Sources */, 272 | 0E475259201E1211002EA382 /* ViewController.swift in Sources */, 273 | 0E475258201E1211002EA382 /* AppDelegate.swift in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXSourcesBuildPhase section */ 278 | 279 | /* Begin PBXTargetDependency section */ 280 | 0EB3D63B2351CE450065073C /* PBXTargetDependency */ = { 281 | isa = PBXTargetDependency; 282 | target = 0EB8098C2000A2420090810F /* SampleAMClock */; 283 | targetProxy = 0EB3D63A2351CE450065073C /* PBXContainerItemProxy */; 284 | }; 285 | /* End PBXTargetDependency section */ 286 | 287 | /* Begin PBXVariantGroup section */ 288 | 0EB809942000A2420090810F /* Main.storyboard */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | 0EB809952000A2420090810F /* Base */, 292 | ); 293 | name = Main.storyboard; 294 | sourceTree = ""; 295 | }; 296 | 0EB809992000A2420090810F /* LaunchScreen.storyboard */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | 0EB8099A2000A2420090810F /* Base */, 300 | ); 301 | name = LaunchScreen.storyboard; 302 | sourceTree = ""; 303 | }; 304 | /* End PBXVariantGroup section */ 305 | 306 | /* Begin XCBuildConfiguration section */ 307 | 0EB3D63C2351CE450065073C /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | BUNDLE_LOADER = "$(TEST_HOST)"; 311 | CLANG_ENABLE_OBJC_WEAK = YES; 312 | CODE_SIGN_STYLE = Automatic; 313 | DEVELOPMENT_TEAM = H5QW7TLM9U; 314 | INFOPLIST_FILE = SampleAMClockTests/Info.plist; 315 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 316 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 317 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 318 | MTL_FAST_MATH = YES; 319 | PRODUCT_BUNDLE_IDENTIFIER = am10.SampleAMClockTests; 320 | PRODUCT_NAME = "$(TARGET_NAME)"; 321 | SWIFT_VERSION = 5.0; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleAMClock.app/SampleAMClock"; 324 | }; 325 | name = Debug; 326 | }; 327 | 0EB3D63D2351CE450065073C /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | BUNDLE_LOADER = "$(TEST_HOST)"; 331 | CLANG_ENABLE_OBJC_WEAK = YES; 332 | CODE_SIGN_STYLE = Automatic; 333 | DEVELOPMENT_TEAM = H5QW7TLM9U; 334 | INFOPLIST_FILE = SampleAMClockTests/Info.plist; 335 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 336 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 337 | MTL_FAST_MATH = YES; 338 | PRODUCT_BUNDLE_IDENTIFIER = am10.SampleAMClockTests; 339 | PRODUCT_NAME = "$(TARGET_NAME)"; 340 | SWIFT_VERSION = 5.0; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleAMClock.app/SampleAMClock"; 343 | }; 344 | name = Release; 345 | }; 346 | 0EB8099D2000A2420090810F /* Debug */ = { 347 | isa = XCBuildConfiguration; 348 | buildSettings = { 349 | ALWAYS_SEARCH_USER_PATHS = NO; 350 | CLANG_ANALYZER_NONNULL = YES; 351 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 352 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 353 | CLANG_CXX_LIBRARY = "libc++"; 354 | CLANG_ENABLE_MODULES = YES; 355 | CLANG_ENABLE_OBJC_ARC = YES; 356 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 357 | CLANG_WARN_BOOL_CONVERSION = YES; 358 | CLANG_WARN_COMMA = YES; 359 | CLANG_WARN_CONSTANT_CONVERSION = YES; 360 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 361 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 362 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INFINITE_RECURSION = YES; 366 | CLANG_WARN_INT_CONVERSION = YES; 367 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 369 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 372 | CLANG_WARN_STRICT_PROTOTYPES = YES; 373 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 374 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 375 | CLANG_WARN_UNREACHABLE_CODE = YES; 376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 377 | CODE_SIGN_IDENTITY = "iPhone Developer"; 378 | COPY_PHASE_STRIP = NO; 379 | DEBUG_INFORMATION_FORMAT = dwarf; 380 | ENABLE_STRICT_OBJC_MSGSEND = YES; 381 | ENABLE_TESTABILITY = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu11; 383 | GCC_DYNAMIC_NO_PIC = NO; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_OPTIMIZATION_LEVEL = 0; 386 | GCC_PREPROCESSOR_DEFINITIONS = ( 387 | "DEBUG=1", 388 | "$(inherited)", 389 | ); 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 397 | MTL_ENABLE_DEBUG_INFO = YES; 398 | ONLY_ACTIVE_ARCH = YES; 399 | SDKROOT = iphoneos; 400 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 401 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 402 | }; 403 | name = Debug; 404 | }; 405 | 0EB8099E2000A2420090810F /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ALWAYS_SEARCH_USER_PATHS = NO; 409 | CLANG_ANALYZER_NONNULL = YES; 410 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 411 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 412 | CLANG_CXX_LIBRARY = "libc++"; 413 | CLANG_ENABLE_MODULES = YES; 414 | CLANG_ENABLE_OBJC_ARC = YES; 415 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 416 | CLANG_WARN_BOOL_CONVERSION = YES; 417 | CLANG_WARN_COMMA = YES; 418 | CLANG_WARN_CONSTANT_CONVERSION = YES; 419 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 420 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 421 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 422 | CLANG_WARN_EMPTY_BODY = YES; 423 | CLANG_WARN_ENUM_CONVERSION = YES; 424 | CLANG_WARN_INFINITE_RECURSION = YES; 425 | CLANG_WARN_INT_CONVERSION = YES; 426 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 427 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 428 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 429 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 430 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 431 | CLANG_WARN_STRICT_PROTOTYPES = YES; 432 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 433 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 434 | CLANG_WARN_UNREACHABLE_CODE = YES; 435 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 436 | CODE_SIGN_IDENTITY = "iPhone Developer"; 437 | COPY_PHASE_STRIP = NO; 438 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 439 | ENABLE_NS_ASSERTIONS = NO; 440 | ENABLE_STRICT_OBJC_MSGSEND = YES; 441 | GCC_C_LANGUAGE_STANDARD = gnu11; 442 | GCC_NO_COMMON_BLOCKS = YES; 443 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 444 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 445 | GCC_WARN_UNDECLARED_SELECTOR = YES; 446 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 447 | GCC_WARN_UNUSED_FUNCTION = YES; 448 | GCC_WARN_UNUSED_VARIABLE = YES; 449 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 450 | MTL_ENABLE_DEBUG_INFO = NO; 451 | SDKROOT = iphoneos; 452 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 453 | VALIDATE_PRODUCT = YES; 454 | }; 455 | name = Release; 456 | }; 457 | 0EB809A02000A2420090810F /* Debug */ = { 458 | isa = XCBuildConfiguration; 459 | buildSettings = { 460 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 461 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 462 | CODE_SIGN_STYLE = Automatic; 463 | DEVELOPMENT_TEAM = ""; 464 | INFOPLIST_FILE = SampleAMClock/Info.plist; 465 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 466 | PRODUCT_BUNDLE_IDENTIFIER = am10.SampleAMClock; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | PROVISIONING_PROFILE_SPECIFIER = ""; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | 0EB809A12000A2420090810F /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 479 | CODE_SIGN_STYLE = Automatic; 480 | DEVELOPMENT_TEAM = ""; 481 | INFOPLIST_FILE = SampleAMClock/Info.plist; 482 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 483 | PRODUCT_BUNDLE_IDENTIFIER = am10.SampleAMClock; 484 | PRODUCT_NAME = "$(TARGET_NAME)"; 485 | PROVISIONING_PROFILE_SPECIFIER = ""; 486 | SWIFT_VERSION = 5.0; 487 | TARGETED_DEVICE_FAMILY = "1,2"; 488 | }; 489 | name = Release; 490 | }; 491 | /* End XCBuildConfiguration section */ 492 | 493 | /* Begin XCConfigurationList section */ 494 | 0EB3D63E2351CE450065073C /* Build configuration list for PBXNativeTarget "SampleAMClockTests" */ = { 495 | isa = XCConfigurationList; 496 | buildConfigurations = ( 497 | 0EB3D63C2351CE450065073C /* Debug */, 498 | 0EB3D63D2351CE450065073C /* Release */, 499 | ); 500 | defaultConfigurationIsVisible = 0; 501 | defaultConfigurationName = Release; 502 | }; 503 | 0EB809882000A2420090810F /* Build configuration list for PBXProject "SampleAMClock" */ = { 504 | isa = XCConfigurationList; 505 | buildConfigurations = ( 506 | 0EB8099D2000A2420090810F /* Debug */, 507 | 0EB8099E2000A2420090810F /* Release */, 508 | ); 509 | defaultConfigurationIsVisible = 0; 510 | defaultConfigurationName = Release; 511 | }; 512 | 0EB8099F2000A2420090810F /* Build configuration list for PBXNativeTarget "SampleAMClock" */ = { 513 | isa = XCConfigurationList; 514 | buildConfigurations = ( 515 | 0EB809A02000A2420090810F /* Debug */, 516 | 0EB809A12000A2420090810F /* Release */, 517 | ); 518 | defaultConfigurationIsVisible = 0; 519 | defaultConfigurationName = Release; 520 | }; 521 | /* End XCConfigurationList section */ 522 | }; 523 | rootObject = 0EB809852000A2420090810F /* Project object */; 524 | } 525 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/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 | } -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/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 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 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 | 121 | 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 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 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 | 259 | 260 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/Classes/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SampleAMClock 4 | // 5 | // Created by am10 on 2018/01/06. 6 | // Copyright © 2018年 am10. 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: [UIApplication.LaunchOptionsKey: 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 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/Classes/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SampleAMClock 4 | // 5 | // Created by am10 on 2018/01/06. 6 | // Copyright © 2018年 am10. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var cView1: AMClockView! 14 | @IBOutlet weak var cView2: AMClockView! 15 | @IBOutlet weak var cView3: AMClockView! 16 | @IBOutlet weak var cView4: AMClockView! 17 | 18 | @IBOutlet weak var cView5: AMClockView! 19 | @IBOutlet weak var cView6: AMClockView! 20 | 21 | @IBOutlet weak var timeLabel: UILabel! 22 | let dateFormatter = DateFormatter() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | // Do any additional setup after loading the view, typically from a nib. 27 | cView1.delegate = self 28 | cView2.delegate = self 29 | cView3.delegate = self 30 | cView4.delegate = self 31 | cView5.delegate = self 32 | cView6.delegate = self 33 | cView1.timeZone = TimeZone(identifier: "America/Toronto") 34 | cView2.timeZone = TimeZone(identifier: "Europe/Moscow") 35 | cView3.timeZone = TimeZone(identifier: "Asia/Tokyo") 36 | cView4.timeZone = TimeZone(identifier: "GMT") 37 | cView5.timeZone = TimeZone(identifier: "Africa/Cairo") 38 | cView6.timeZone = TimeZone(identifier: "Australia/Sydney") 39 | 40 | dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo") 41 | dateFormatter.dateFormat = "yyyy/MM/dd HH:mm" 42 | cView3.selectedDate = dateFormatter.date(from: "2018/01/01 10:10") 43 | } 44 | 45 | override func didReceiveMemoryWarning() { 46 | super.didReceiveMemoryWarning() 47 | // Dispose of any resources that can be recreated. 48 | } 49 | } 50 | 51 | extension ViewController: AMClockViewDelegate { 52 | func clockView(_ clockView: AMClockView, didChangeDate date: Date) { 53 | if let timeZone = clockView.timeZone { 54 | dateFormatter.timeZone = timeZone 55 | } 56 | timeLabel.text = "selected time: " + dateFormatter.string(from: date); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/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 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/clock2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/clock2@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/clock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/clock@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/h@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/hourhand2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/hourhand2@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/hourhand@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/hourhand@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/m@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/m@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/minutehand2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/minutehand2@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClock/image/minutehand@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adventam10/AMClockView/2b48ca4dcc2eb5f346e25a7cabb0cce68853e6cd/SampleAMClock/SampleAMClock/image/minutehand@2x.png -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClockTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SampleAMClock/SampleAMClockTests/SampleAMClockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleAMClockTests.swift 3 | // SampleAMClockTests 4 | // 5 | // Created by am10 on 2019/10/12. 6 | // Copyright © 2019 am10. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SampleAMClock 11 | 12 | class SampleAMClockTests: XCTestCase { 13 | 14 | private let dateFormatter: DateFormatter = { 15 | var df = DateFormatter() 16 | df.locale = .init(identifier: "ja_JP") 17 | df.dateFormat = "yyyyMMddHHmmss" 18 | return df 19 | }() 20 | private let accuracy: Float = 0.00001 21 | private let anglePerMinute = Float((2 * Double.pi) / 60) 22 | private let anglePerHour = Float((2 * Double.pi) / 12) 23 | private let angle270 = Float(Double.pi/2 + Double.pi) 24 | private let radius: CGFloat = 1.0 25 | override func setUp() { 26 | // Put setup code here. This method is called before the invocation of each test method in the class. 27 | } 28 | 29 | override func tearDown() { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | } 32 | 33 | func testCurrentMinuteAngle() { 34 | let model = AMClockModel() 35 | model.currentDate = dateFormatter.date(from: "20191210120000")! 36 | XCTAssertEqual(cosf(model.currentMinuteAngle), 0.0, accuracy: accuracy) 37 | model.currentDate = dateFormatter.date(from: "20191210121500")! 38 | XCTAssertEqual(cosf(model.currentMinuteAngle), 1.0, accuracy: accuracy) 39 | model.currentDate = dateFormatter.date(from: "20191210123000")! 40 | XCTAssertEqual(cosf(model.currentMinuteAngle), 0.0, accuracy: accuracy) 41 | model.currentDate = dateFormatter.date(from: "20191210124500")! 42 | XCTAssertEqual(cosf(model.currentMinuteAngle), -1.0, accuracy: accuracy) 43 | 44 | model.currentDate = dateFormatter.date(from: "20191215121300")! 45 | XCTAssertEqual(cosf(model.currentMinuteAngle), cosf(angle270 + anglePerMinute*13), accuracy: accuracy) 46 | model.currentDate = dateFormatter.date(from: "20191215122200")! 47 | XCTAssertEqual(cosf(model.currentMinuteAngle), cosf(angle270 + anglePerMinute*22), accuracy: accuracy) 48 | model.currentDate = dateFormatter.date(from: "20191215123600")! 49 | XCTAssertEqual(cosf(model.currentMinuteAngle), cosf(angle270 + anglePerMinute*36), accuracy: accuracy) 50 | model.currentDate = dateFormatter.date(from: "20191215125200")! 51 | XCTAssertEqual(cosf(model.currentMinuteAngle), cosf(angle270 + anglePerMinute*52), accuracy: accuracy) 52 | } 53 | 54 | func testCurrentHourAngle() { 55 | let model = AMClockModel() 56 | model.currentDate = dateFormatter.date(from: "20191210120000")! 57 | XCTAssertEqual(cosf(model.currentHourAngle), 0.0, accuracy: accuracy) 58 | model.currentDate = dateFormatter.date(from: "20191210030000")! 59 | XCTAssertEqual(cosf(model.currentHourAngle), 1.0, accuracy: accuracy) 60 | model.currentDate = dateFormatter.date(from: "20191210060000")! 61 | XCTAssertEqual(cosf(model.currentHourAngle), 0.0, accuracy: accuracy) 62 | model.currentDate = dateFormatter.date(from: "20191210090000")! 63 | XCTAssertEqual(cosf(model.currentHourAngle), -1.0, accuracy: accuracy) 64 | 65 | model.currentDate = dateFormatter.date(from: "20191215010000")! 66 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(angle270 + anglePerHour*1), accuracy: accuracy) 67 | model.currentDate = dateFormatter.date(from: "20191215040000")! 68 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(angle270 + anglePerHour*4), accuracy: accuracy) 69 | model.currentDate = dateFormatter.date(from: "20191215070000")! 70 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(angle270 + anglePerHour*7), accuracy: accuracy) 71 | model.currentDate = dateFormatter.date(from: "20191215110000")! 72 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(angle270 + anglePerHour*11), accuracy: accuracy) 73 | 74 | model.currentDate = dateFormatter.date(from: "20191210124040")! 75 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(Float(Double.pi/2 + Double.pi), byMinute: 40)), accuracy: accuracy) 76 | model.currentDate = dateFormatter.date(from: "20191210031540")! 77 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(0, byMinute: 15)), accuracy: accuracy) 78 | model.currentDate = dateFormatter.date(from: "20191210063040")! 79 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(Float(Double.pi/2), byMinute: 30)), accuracy: accuracy) 80 | model.currentDate = dateFormatter.date(from: "20191210094540")! 81 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(Float(Double.pi), byMinute: 45)), accuracy: accuracy) 82 | 83 | model.currentDate = dateFormatter.date(from: "20191215021325")! 84 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(angle270 + anglePerHour*2, byMinute: 13)), accuracy: accuracy) 85 | model.currentDate = dateFormatter.date(from: "20191215052230")! 86 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(angle270 + anglePerHour*5, byMinute: 22)), accuracy: accuracy) 87 | model.currentDate = dateFormatter.date(from: "20191215073655")! 88 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(angle270 + anglePerHour*7, byMinute: 36)), accuracy: accuracy) 89 | model.currentDate = dateFormatter.date(from: "20191215115244")! 90 | XCTAssertEqual(cosf(model.currentHourAngle), cosf(reviseAngle(angle270 + anglePerHour*11, byMinute: 52)), accuracy: accuracy) 91 | } 92 | 93 | func testFormattedTime() { 94 | let model = AMClockModel() 95 | model.currentDate = dateFormatter.date(from: "20191210123322")! 96 | XCTAssertEqual(model.formattedTime, "12:33") 97 | } 98 | 99 | func testCurrentMinute() { 100 | let model = AMClockModel() 101 | model.currentDate = dateFormatter.date(from: "20191210120022")! 102 | XCTAssertEqual(model.currentMinute, 0) 103 | model.currentDate = dateFormatter.date(from: "20191210121522")! 104 | XCTAssertEqual(model.currentMinute, 15) 105 | model.currentDate = dateFormatter.date(from: "20191210123022")! 106 | XCTAssertEqual(model.currentMinute, 30) 107 | model.currentDate = dateFormatter.date(from: "20191210124522")! 108 | XCTAssertEqual(model.currentMinute, 45) 109 | 110 | model.currentDate = dateFormatter.date(from: "20191210121222")! 111 | XCTAssertEqual(model.currentMinute, 12) 112 | model.currentDate = dateFormatter.date(from: "20191210122222")! 113 | XCTAssertEqual(model.currentMinute, 22) 114 | model.currentDate = dateFormatter.date(from: "20191210123722")! 115 | XCTAssertEqual(model.currentMinute, 37) 116 | model.currentDate = dateFormatter.date(from: "20191210125322")! 117 | XCTAssertEqual(model.currentMinute, 53) 118 | } 119 | 120 | func testUpdateCurrentDateWithMinuteMethod() { 121 | let model = AMClockModel() 122 | model.currentDate = dateFormatter.date(from: "20191210120022")! 123 | model.updateCurrentDate(minute: 30) 124 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210123022")) 125 | model.currentDate = dateFormatter.date(from: "20191210120022")! 126 | model.updateCurrentDate(minute: 65) 127 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210130522")) 128 | } 129 | 130 | func testAppendHourMethod() { 131 | let model = AMClockModel() 132 | model.currentDate = dateFormatter.date(from: "20191210120022")! 133 | model.appendHour(1) 134 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210130022")) 135 | model.currentDate = dateFormatter.date(from: "20191210120022")! 136 | model.appendHour(-1) 137 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210110022")) 138 | } 139 | 140 | func testCalculateHourAngleWithPointMethod() { 141 | let model = AMClockModel() 142 | model.currentDate = dateFormatter.date(from: "20191210120000")! 143 | var angle = model.calculateHourAngle(point: .init(x: radius, y: 0.0), radius: radius) 144 | XCTAssertEqual(cosf(angle), cosf(angle270), accuracy: accuracy) 145 | angle = model.calculateHourAngle(point: .init(x: radius*2, y: radius), radius: radius) 146 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 3), accuracy: accuracy) 147 | angle = model.calculateHourAngle(point: .init(x: radius, y: radius*2), radius: radius) 148 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 6), accuracy: accuracy) 149 | angle = model.calculateHourAngle(point: .init(x: 0.0, y: radius), radius: radius) 150 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 9), accuracy: accuracy) 151 | 152 | angle = model.calculateHourAngle(point: point(minute: 1), radius: radius) 153 | XCTAssertEqual(cosf(angle), cosf(angle270), accuracy: accuracy) 154 | angle = model.calculateHourAngle(point: point(minute: 2), radius: radius) 155 | XCTAssertEqual(cosf(angle), cosf(angle270), accuracy: accuracy) 156 | angle = model.calculateHourAngle(point: point(minute: 3), radius: radius) 157 | XCTAssertEqual(cosf(angle), cosf(angle270), accuracy: accuracy) 158 | angle = model.calculateHourAngle(point: point(minute: 4), radius: radius) 159 | XCTAssertEqual(cosf(angle), cosf(angle270), accuracy: accuracy) 160 | 161 | angle = model.calculateHourAngle(point: point(minute: 6), radius: radius) 162 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 1), accuracy: accuracy) 163 | angle = model.calculateHourAngle(point: point(minute: 7), radius: radius) 164 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 1), accuracy: accuracy) 165 | angle = model.calculateHourAngle(point: point(minute: 8), radius: radius) 166 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 1), accuracy: accuracy) 167 | angle = model.calculateHourAngle(point: point(minute: 9), radius: radius) 168 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 1), accuracy: accuracy) 169 | 170 | angle = model.calculateHourAngle(point: point(minute: 11), radius: radius) 171 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 2), accuracy: accuracy) 172 | angle = model.calculateHourAngle(point: point(minute: 12), radius: radius) 173 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 2), accuracy: accuracy) 174 | angle = model.calculateHourAngle(point: point(minute: 13), radius: radius) 175 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 2), accuracy: accuracy) 176 | angle = model.calculateHourAngle(point: point(minute: 14), radius: radius) 177 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 2), accuracy: accuracy) 178 | 179 | angle = model.calculateHourAngle(point: point(minute: 16), radius: radius) 180 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 3), accuracy: accuracy) 181 | angle = model.calculateHourAngle(point: point(minute: 17), radius: radius) 182 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 3), accuracy: accuracy) 183 | angle = model.calculateHourAngle(point: point(minute: 18), radius: radius) 184 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 3), accuracy: accuracy) 185 | angle = model.calculateHourAngle(point: point(minute: 19), radius: radius) 186 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 3), accuracy: accuracy) 187 | 188 | angle = model.calculateHourAngle(point: point(minute: 21), radius: radius) 189 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 4), accuracy: accuracy) 190 | angle = model.calculateHourAngle(point: point(minute: 22), radius: radius) 191 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 4), accuracy: accuracy) 192 | angle = model.calculateHourAngle(point: point(minute: 23), radius: radius) 193 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 4), accuracy: accuracy) 194 | angle = model.calculateHourAngle(point: point(minute: 24), radius: radius) 195 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 4), accuracy: accuracy) 196 | 197 | angle = model.calculateHourAngle(point: point(minute: 26), radius: radius) 198 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 5), accuracy: accuracy) 199 | angle = model.calculateHourAngle(point: point(minute: 27), radius: radius) 200 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 5), accuracy: accuracy) 201 | angle = model.calculateHourAngle(point: point(minute: 28), radius: radius) 202 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 5), accuracy: accuracy) 203 | angle = model.calculateHourAngle(point: point(minute: 29), radius: radius) 204 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 5), accuracy: accuracy) 205 | 206 | angle = model.calculateHourAngle(point: point(minute: 31), radius: radius) 207 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 6), accuracy: accuracy) 208 | angle = model.calculateHourAngle(point: point(minute: 32), radius: radius) 209 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 6), accuracy: accuracy) 210 | angle = model.calculateHourAngle(point: point(minute: 33), radius: radius) 211 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 6), accuracy: accuracy) 212 | angle = model.calculateHourAngle(point: point(minute: 34), radius: radius) 213 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 6), accuracy: accuracy) 214 | 215 | angle = model.calculateHourAngle(point: point(minute: 36), radius: radius) 216 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 7), accuracy: accuracy) 217 | angle = model.calculateHourAngle(point: point(minute: 37), radius: radius) 218 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 7), accuracy: accuracy) 219 | angle = model.calculateHourAngle(point: point(minute: 38), radius: radius) 220 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 7), accuracy: accuracy) 221 | angle = model.calculateHourAngle(point: point(minute: 39), radius: radius) 222 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 7), accuracy: accuracy) 223 | 224 | angle = model.calculateHourAngle(point: point(minute: 41), radius: radius) 225 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 8), accuracy: accuracy) 226 | angle = model.calculateHourAngle(point: point(minute: 42), radius: radius) 227 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 8), accuracy: accuracy) 228 | angle = model.calculateHourAngle(point: point(minute: 43), radius: radius) 229 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 8), accuracy: accuracy) 230 | angle = model.calculateHourAngle(point: point(minute: 44), radius: radius) 231 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 8), accuracy: accuracy) 232 | 233 | angle = model.calculateHourAngle(point: point(minute: 46), radius: radius) 234 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 9), accuracy: accuracy) 235 | angle = model.calculateHourAngle(point: point(minute: 47), radius: radius) 236 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 9), accuracy: accuracy) 237 | angle = model.calculateHourAngle(point: point(minute: 48), radius: radius) 238 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 9), accuracy: accuracy) 239 | angle = model.calculateHourAngle(point: point(minute: 49), radius: radius) 240 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 9), accuracy: accuracy) 241 | 242 | angle = model.calculateHourAngle(point: point(minute: 51), radius: radius) 243 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 10), accuracy: accuracy) 244 | angle = model.calculateHourAngle(point: point(minute: 52), radius: radius) 245 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 10), accuracy: accuracy) 246 | angle = model.calculateHourAngle(point: point(minute: 53), radius: radius) 247 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 10), accuracy: accuracy) 248 | angle = model.calculateHourAngle(point: point(minute: 54), radius: radius) 249 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 10), accuracy: accuracy) 250 | 251 | angle = model.calculateHourAngle(point: point(minute: 56), radius: radius) 252 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 11), accuracy: accuracy) 253 | angle = model.calculateHourAngle(point: point(minute: 57), radius: radius) 254 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 11), accuracy: accuracy) 255 | angle = model.calculateHourAngle(point: point(minute: 58), radius: radius) 256 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 11), accuracy: accuracy) 257 | angle = model.calculateHourAngle(point: point(minute: 59), radius: radius) 258 | XCTAssertEqual(cosf(angle), cosf(angle270 + anglePerHour * 11), accuracy: accuracy) 259 | } 260 | 261 | func testCalculateMinuteWithPointMethod() { 262 | let model = AMClockModel() 263 | XCTAssertEqual(model.calculateMinute(point: .init(x: radius, y: 0.0), radius: radius), 0) 264 | XCTAssertEqual(model.calculateMinute(point: .init(x: radius*2, y: radius), radius: radius), 15) 265 | XCTAssertEqual(model.calculateMinute(point: .init(x: radius, y: radius*2), radius: radius), 30) 266 | XCTAssertEqual(model.calculateMinute(point: .init(x: 0.0, y: radius), radius: radius), 45) 267 | 268 | XCTAssertEqual(model.calculateMinute(point: point(minute: 1), radius: radius), 1) 269 | XCTAssertEqual(model.calculateMinute(point: point(minute: 2), radius: radius), 2) 270 | XCTAssertEqual(model.calculateMinute(point: point(minute: 3), radius: radius), 3) 271 | XCTAssertEqual(model.calculateMinute(point: point(minute: 4), radius: radius), 4) 272 | XCTAssertEqual(model.calculateMinute(point: point(minute: 5), radius: radius), 5) 273 | XCTAssertEqual(model.calculateMinute(point: point(minute: 6), radius: radius), 6) 274 | XCTAssertEqual(model.calculateMinute(point: point(minute: 7), radius: radius), 7) 275 | XCTAssertEqual(model.calculateMinute(point: point(minute: 8), radius: radius), 8) 276 | XCTAssertEqual(model.calculateMinute(point: point(minute: 9), radius: radius), 9) 277 | XCTAssertEqual(model.calculateMinute(point: point(minute: 10), radius: radius), 10) 278 | XCTAssertEqual(model.calculateMinute(point: point(minute: 11), radius: radius), 11) 279 | XCTAssertEqual(model.calculateMinute(point: point(minute: 12), radius: radius), 12) 280 | XCTAssertEqual(model.calculateMinute(point: point(minute: 13), radius: radius), 13) 281 | XCTAssertEqual(model.calculateMinute(point: point(minute: 14), radius: radius), 14) 282 | 283 | XCTAssertEqual(model.calculateMinute(point: point(minute: 16), radius: radius), 16) 284 | XCTAssertEqual(model.calculateMinute(point: point(minute: 17), radius: radius), 17) 285 | XCTAssertEqual(model.calculateMinute(point: point(minute: 18), radius: radius), 18) 286 | XCTAssertEqual(model.calculateMinute(point: point(minute: 19), radius: radius), 19) 287 | XCTAssertEqual(model.calculateMinute(point: point(minute: 20), radius: radius), 20) 288 | XCTAssertEqual(model.calculateMinute(point: point(minute: 21), radius: radius), 21) 289 | XCTAssertEqual(model.calculateMinute(point: point(minute: 22), radius: radius), 22) 290 | XCTAssertEqual(model.calculateMinute(point: point(minute: 23), radius: radius), 23) 291 | XCTAssertEqual(model.calculateMinute(point: point(minute: 24), radius: radius), 24) 292 | XCTAssertEqual(model.calculateMinute(point: point(minute: 25), radius: radius), 25) 293 | XCTAssertEqual(model.calculateMinute(point: point(minute: 26), radius: radius), 26) 294 | XCTAssertEqual(model.calculateMinute(point: point(minute: 27), radius: radius), 27) 295 | XCTAssertEqual(model.calculateMinute(point: point(minute: 28), radius: radius), 28) 296 | XCTAssertEqual(model.calculateMinute(point: point(minute: 29), radius: radius), 29) 297 | 298 | XCTAssertEqual(model.calculateMinute(point: point(minute: 31), radius: radius), 31) 299 | XCTAssertEqual(model.calculateMinute(point: point(minute: 32), radius: radius), 32) 300 | XCTAssertEqual(model.calculateMinute(point: point(minute: 33), radius: radius), 33) 301 | XCTAssertEqual(model.calculateMinute(point: point(minute: 34), radius: radius), 34) 302 | XCTAssertEqual(model.calculateMinute(point: point(minute: 35), radius: radius), 35) 303 | XCTAssertEqual(model.calculateMinute(point: point(minute: 36), radius: radius), 36) 304 | XCTAssertEqual(model.calculateMinute(point: point(minute: 37), radius: radius), 37) 305 | XCTAssertEqual(model.calculateMinute(point: point(minute: 38), radius: radius), 38) 306 | XCTAssertEqual(model.calculateMinute(point: point(minute: 39), radius: radius), 39) 307 | XCTAssertEqual(model.calculateMinute(point: point(minute: 40), radius: radius), 40) 308 | XCTAssertEqual(model.calculateMinute(point: point(minute: 41), radius: radius), 41) 309 | XCTAssertEqual(model.calculateMinute(point: point(minute: 42), radius: radius), 42) 310 | XCTAssertEqual(model.calculateMinute(point: point(minute: 43), radius: radius), 43) 311 | XCTAssertEqual(model.calculateMinute(point: point(minute: 44), radius: radius), 44) 312 | 313 | XCTAssertEqual(model.calculateMinute(point: point(minute: 46), radius: radius), 46) 314 | XCTAssertEqual(model.calculateMinute(point: point(minute: 47), radius: radius), 47) 315 | XCTAssertEqual(model.calculateMinute(point: point(minute: 48), radius: radius), 48) 316 | XCTAssertEqual(model.calculateMinute(point: point(minute: 49), radius: radius), 49) 317 | XCTAssertEqual(model.calculateMinute(point: point(minute: 50), radius: radius), 50) 318 | XCTAssertEqual(model.calculateMinute(point: point(minute: 51), radius: radius), 51) 319 | XCTAssertEqual(model.calculateMinute(point: point(minute: 52), radius: radius), 52) 320 | XCTAssertEqual(model.calculateMinute(point: point(minute: 53), radius: radius), 53) 321 | XCTAssertEqual(model.calculateMinute(point: point(minute: 54), radius: radius), 54) 322 | XCTAssertEqual(model.calculateMinute(point: point(minute: 55), radius: radius), 55) 323 | XCTAssertEqual(model.calculateMinute(point: point(minute: 56), radius: radius), 56) 324 | XCTAssertEqual(model.calculateMinute(point: point(minute: 57), radius: radius), 57) 325 | XCTAssertEqual(model.calculateMinute(point: point(minute: 58), radius: radius), 58) 326 | XCTAssertEqual(model.calculateMinute(point: point(minute: 59), radius: radius), 59) 327 | } 328 | 329 | func testRevisedByAcrossHourWithStartAngleMethod() { 330 | let model = AMClockModel() 331 | model.currentDate = dateFormatter.date(from: "20191210120022")! 332 | model.revisedByAcrossHour(startAngle: anglePerMinute * 55, 333 | endAngle: anglePerMinute * 5) 334 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210130022")) 335 | model.currentDate = dateFormatter.date(from: "20191210120022")! 336 | model.revisedByAcrossHour(startAngle: anglePerMinute * 5, 337 | endAngle: anglePerMinute * 55) 338 | XCTAssertEqual(model.currentDate, dateFormatter.date(from: "20191210110022")) 339 | } 340 | 341 | func testCalculateElapsedTimeWithStartAngleMethod() { 342 | let model = AMClockModel() 343 | var hour = model.calculateElapsedTime(startAngle: anglePerMinute * 5, 344 | endAngle: anglePerMinute * 10) 345 | XCTAssertEqual(hour, 1) 346 | hour = model.calculateElapsedTime(startAngle: anglePerMinute * 5, 347 | endAngle: anglePerMinute * 8) 348 | XCTAssertEqual(hour, 0) 349 | hour = model.calculateElapsedTime(startAngle: anglePerMinute * 10, 350 | endAngle: anglePerMinute * 5) 351 | XCTAssertEqual(hour, -1) 352 | hour = model.calculateElapsedTime(startAngle: anglePerMinute * 8, 353 | endAngle: anglePerMinute * 5) 354 | XCTAssertEqual(hour, 0) 355 | hour = model.calculateElapsedTime(startAngle: anglePerMinute * 55, 356 | endAngle: anglePerMinute * 1) 357 | XCTAssertEqual(hour, 1) 358 | hour = model.calculateElapsedTime(startAngle: anglePerMinute * 5, 359 | endAngle: anglePerMinute * 59) 360 | XCTAssertEqual(hour, -1) 361 | } 362 | 363 | private func reviseAngle(_ angle: Float, byMinute minute: Float) -> Float { 364 | return angle + (minute / 60.0) * anglePerHour 365 | } 366 | 367 | private func angle(minute: Int) -> CGFloat { 368 | let angle = (Float(minute) + 0.001) * anglePerMinute + angle270 369 | return CGFloat(angle) 370 | } 371 | 372 | private func point(minute: Int) -> CGPoint { 373 | return .init(x: radius + cos(angle(minute: minute)) * radius, 374 | y: radius + sin(angle(minute: minute)) * radius) 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /Source/AMClockView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AMClockView.swift 3 | // AMClockView, https://github.com/adventam10/AMClockView 4 | // 5 | // Created by am10 on 2017/12/29. 6 | // Copyright © 2017年 am10. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum AMCVClockType { 12 | case none 13 | case arabic 14 | 15 | var times: [String] { 16 | switch self { 17 | case .none: 18 | return [] 19 | case .arabic: 20 | return ["12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"] 21 | } 22 | } 23 | 24 | func time(index: Int) -> String { 25 | switch self { 26 | case .none: 27 | return "" 28 | case .arabic: 29 | return times[index] 30 | } 31 | } 32 | } 33 | 34 | public protocol AMClockViewDelegate: AnyObject { 35 | func clockView(_ clockView: AMClockView, didChangeDate date: Date) 36 | } 37 | 38 | internal class AMClockModel { 39 | 40 | enum AMCVTimeEditType { 41 | case none 42 | case hour 43 | case minute 44 | } 45 | 46 | var timeZone: TimeZone? { 47 | didSet { 48 | if let timeZone = timeZone { 49 | calendar.timeZone = timeZone 50 | dateFormatter.timeZone = timeZone 51 | } else { 52 | calendar.timeZone = .current 53 | dateFormatter.timeZone = .current 54 | } 55 | } 56 | } 57 | var editType: AMCVTimeEditType = .none 58 | var startAngle: Float = 0.0 59 | var endAngle: Float = 0.0 60 | var currentDate = Date() 61 | var currentHourAngle: Float { 62 | return calculateAngle(hour: currentHour) 63 | } 64 | var currentMinuteAngle: Float { 65 | return angle270 + anglePerMinute * Float(currentMinute) 66 | } 67 | var currentMinute: Int { 68 | return currentComponents.minute! 69 | } 70 | var formattedTime: String { 71 | return dateFormatter.string(from: currentDate) 72 | } 73 | 74 | let angle30 = Float(Double.pi / 6) 75 | let angle270 = Float(Double.pi + Double.pi/2) 76 | 77 | private var currentHour: Int { 78 | return currentComponents.hour! 79 | } 80 | private var currentComponents: DateComponents { 81 | return calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], 82 | from: currentDate) 83 | } 84 | private var calendar = Calendar(identifier: .gregorian) 85 | 86 | private let angle360 = Float(Double.pi * 2) 87 | private let anglePerHour = Float(Double.pi * 2) / 12 88 | private let anglePerMinute = Float(Double.pi * 2) / 60 89 | private let dateFormatter: DateFormatter = { 90 | var df = DateFormatter() 91 | df.dateFormat = "HH:mm" 92 | return df 93 | }() 94 | 95 | func adjustFont(rect: CGRect) -> UIFont { 96 | let length = (rect.width > rect.height) ? rect.height : rect.width 97 | return .systemFont(ofSize: length * 0.8) 98 | } 99 | 100 | func updateCurrentDate(minute: Int) { 101 | var components = currentComponents 102 | components.minute = minute 103 | currentDate = calendar.date(from: components)! 104 | } 105 | 106 | func appendHour(_ hour: Int) { 107 | currentDate = currentDate.addingTimeInterval(60 * 60 * Double(hour)) 108 | } 109 | 110 | func calculateHourAngle(point: CGPoint, radius: CGFloat) -> Float { 111 | let radian = calculateRadian(point: point, radius: radius) 112 | let hour = Int((radian - angle270) / anglePerHour) 113 | return calculateAngle(hour: hour) 114 | } 115 | 116 | func calculateMinute(point: CGPoint, radius: CGFloat) -> Int { 117 | let radian = calculateRadian(point: point, radius: radius) 118 | return Int((radian - angle270) / anglePerMinute) 119 | } 120 | 121 | func revisedByAcrossHour(startAngle: Float, endAngle: Float) { 122 | if endAngle < startAngle { 123 | let gap = startAngle - endAngle 124 | if gap > angle270 { 125 | // case(through 12o'clock) 126 | // 1 hour ago 127 | appendHour(1) 128 | } 129 | } else { 130 | let gap = endAngle - startAngle 131 | if gap > angle270 { 132 | // case(through 12o'clock) 133 | // 1 hour later 134 | appendHour(-1) 135 | } 136 | } 137 | } 138 | 139 | func calculateElapsedTime(startAngle: Float, endAngle: Float) -> Int { 140 | var angleGap: Float = 0.0 141 | if startAngle > endAngle { 142 | let gap = startAngle - endAngle 143 | if gap > angle270 { 144 | angleGap = (endAngle + angle360) - startAngle 145 | } else { 146 | angleGap = endAngle - startAngle 147 | } 148 | } else { 149 | let gap = endAngle - startAngle 150 | if gap > angle270 { 151 | angleGap = ((startAngle + angle360) - endAngle) * -1 152 | } else { 153 | angleGap = endAngle - startAngle 154 | } 155 | } 156 | 157 | var degree = Int(angleGap*360 / angle360) 158 | degree = (degree < 0) ? degree - 5 : degree + 5 159 | let hour: Int = degree/30 160 | return hour 161 | } 162 | 163 | private func calculateRadian(point: CGPoint, radius: CGFloat) -> Float { 164 | // origin(view's center) 165 | let centerPoint = CGPoint(x: radius, y: radius) 166 | 167 | // Find difference in coordinates.Since the upper side of the screen is the Y coordinate +, the Y coordinate changes the sign. 168 | let x = Float(point.x - centerPoint.x) 169 | let y = -Float(point.y - centerPoint.y) 170 | 171 | var radian = atan2f(y, x) 172 | 173 | // To correct radian(3/2π~7/2π: 0 o'clock = 3/2π) 174 | radian = radian * -1 175 | if radian < 0 { 176 | radian += angle360 177 | } 178 | 179 | if radian >= 0 && radian < angle270 { 180 | radian += angle360 181 | } 182 | return radian 183 | } 184 | 185 | private func calculateAngle(hour: Int) -> Float { 186 | let hourInt = hour > 12 ? hour - 12 : hour 187 | let hourAngle = angle270 + anglePerHour * Float(hourInt) 188 | /// revise by minute 189 | return hourAngle + ((Float(currentMinute)/60.0) * anglePerHour) 190 | } 191 | } 192 | 193 | @IBDesignable public class AMClockView: UIView { 194 | 195 | @IBInspectable public var clockBorderLineWidth: CGFloat = 5.0 196 | @IBInspectable public var smallClockIndexWidth: CGFloat = 1.0 197 | @IBInspectable public var clockIndexWidth: CGFloat = 2.0 198 | @IBInspectable public var hourHandWidth: CGFloat = 5.0 199 | @IBInspectable public var minuteHandWidth: CGFloat = 3.0 200 | @IBInspectable public var clockBorderLineColor: UIColor = .black 201 | @IBInspectable public var centerCircleLineColor: UIColor = .darkGray 202 | @IBInspectable public var hourHandColor: UIColor = .black 203 | @IBInspectable public var minuteHandColor: UIColor = .black 204 | @IBInspectable public var selectedTimeLabelTextColor: UIColor = .black 205 | @IBInspectable public var timeLabelTextColor: UIColor = .black 206 | @IBInspectable public var smallClockIndexColor: UIColor = .black 207 | @IBInspectable public var clockIndexColor: UIColor = .black 208 | @IBInspectable public var clockColor: UIColor = .clear 209 | @IBInspectable public var clockImage: UIImage? 210 | @IBInspectable public var minuteHandImage: UIImage? 211 | @IBInspectable public var hourHandImage: UIImage? 212 | @IBInspectable public var isShowSelectedTime: Bool = false { 213 | didSet { 214 | selectedTimeLabel.isHidden = !isShowSelectedTime 215 | } 216 | } 217 | 218 | public weak var delegate: AMClockViewDelegate? 219 | /// watch dials 220 | public var clockType: AMCVClockType = .arabic 221 | /// Time zone 222 | /// 223 | /// default is TimeZone.current 224 | public var timeZone: TimeZone? { 225 | didSet { 226 | model.timeZone = timeZone 227 | } 228 | } 229 | public var selectedDate: Date? { 230 | didSet{ 231 | model.currentDate = selectedDate ?? Date() 232 | redrawClock() 233 | } 234 | } 235 | 236 | override public var bounds: CGRect { 237 | didSet { 238 | redrawClock() 239 | } 240 | } 241 | 242 | private let clockView = UIView() 243 | private let clockImageView = UIImageView() 244 | private let minuteHandImageView = UIImageView() 245 | private let hourHandImageView = UIImageView() 246 | private let selectedTimeLabel = UILabel() 247 | private let model = AMClockModel() 248 | 249 | private var drawLayer: CAShapeLayer? 250 | private var hourHandLayer: CAShapeLayer? 251 | private var minuteHandLayer: CAShapeLayer? 252 | private var panHourLayer: CAShapeLayer? 253 | private var panMinuteLayer: CAShapeLayer? 254 | private var radius: CGFloat { 255 | return clockView.frame.width/2 256 | } 257 | private var clockCenter: CGPoint { 258 | return CGPoint(x: radius, y: radius) 259 | } 260 | private var hourHandLength: CGFloat { 261 | return radius * 0.6 262 | } 263 | private var minuteHandLength: CGFloat { 264 | return radius * 0.8 265 | } 266 | 267 | //MARK: - Initialize 268 | required public init?(coder aDecoder: NSCoder) { 269 | super.init(coder:aDecoder) 270 | } 271 | 272 | override public init(frame: CGRect) { 273 | super.init(frame: frame) 274 | backgroundColor = .clear 275 | } 276 | 277 | convenience init() { 278 | self.init(frame: .zero) 279 | } 280 | 281 | override public func draw(_ rect: CGRect) { 282 | redrawClock() 283 | } 284 | 285 | //MARK: - Prepare View 286 | private func prepareClockView() { 287 | let length = (frame.width < frame.height) ? frame.width : frame.height 288 | clockView.frame = CGRect(x: frame.width/2 - length/2, 289 | y: frame.height/2 - length/2, 290 | width: length, height: length) 291 | addSubview(clockView) 292 | } 293 | 294 | private func prepareClockImageViews() { 295 | clockImageView.frame = clockView.bounds 296 | minuteHandImageView.frame = clockView.bounds 297 | hourHandImageView.frame = clockView.bounds 298 | 299 | clockImageView.image = clockImage 300 | minuteHandImageView.image = minuteHandImage 301 | hourHandImageView.image = hourHandImage 302 | 303 | clockView.addSubview(clockImageView) 304 | clockView.addSubview(hourHandImageView) 305 | clockView.addSubview(minuteHandImageView) 306 | } 307 | 308 | private func prepareSelectedTimeLabel() { 309 | selectedTimeLabel.frame = CGRect(x: clockCenter.x - (radius/2)/2, 310 | y: clockCenter.y - radius/3, 311 | width: radius/2, height: radius/3) 312 | clockView.addSubview(selectedTimeLabel) 313 | selectedTimeLabel.font = model.adjustFont(rect: selectedTimeLabel.frame) 314 | selectedTimeLabel.adjustsFontSizeToFitWidth = true 315 | selectedTimeLabel.textColor = selectedTimeLabelTextColor 316 | selectedTimeLabel.textAlignment = .center 317 | selectedTimeLabel.isHidden = !isShowSelectedTime 318 | } 319 | 320 | private func prepareTimeLabel() { 321 | var angle = model.angle270 322 | var smallRadius = radius - (radius/10 + clockBorderLineWidth) 323 | let length = radius/4 324 | smallRadius -= length/2 325 | 326 | // draw line (from center to out) 327 | for i in 0..<12 { 328 | let label = makeTimeLabel(length: length) 329 | label.text = clockType.time(index: i) 330 | label.font = model.adjustFont(rect: label.frame) 331 | clockView.addSubview(label) 332 | let point = CGPoint(x: clockCenter.x + smallRadius * CGFloat(cosf(angle)), 333 | y: clockCenter.y + smallRadius * CGFloat(sinf(angle))) 334 | label.center = point 335 | angle += model.angle30 336 | } 337 | } 338 | 339 | private func makeTimeLabel(length: CGFloat) -> UILabel { 340 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: length, height: length)) 341 | label.adjustsFontSizeToFitWidth = true 342 | label.textAlignment = .center 343 | label.textColor = timeLabelTextColor 344 | return label 345 | } 346 | 347 | //MARK: - Make Layer 348 | private func makeDrawLayer() -> CAShapeLayer { 349 | let drawLayer = CAShapeLayer() 350 | drawLayer.frame = clockView.bounds 351 | drawLayer.cornerRadius = radius 352 | drawLayer.masksToBounds = true 353 | if clockImage == nil { 354 | drawLayer.borderWidth = clockBorderLineWidth 355 | drawLayer.borderColor = clockBorderLineColor.cgColor 356 | } 357 | return drawLayer 358 | } 359 | 360 | private func makeSmallClockIndexLayer() -> CAShapeLayer { 361 | let layer = CAShapeLayer() 362 | layer.frame = drawLayer!.bounds 363 | layer.strokeColor = smallClockIndexColor.cgColor 364 | layer.fillColor = UIColor.clear.cgColor 365 | layer.lineWidth = smallClockIndexWidth 366 | 367 | let smallRadius = radius - (radius/20 + clockBorderLineWidth) 368 | var angle = model.angle270 369 | let path = UIBezierPath() 370 | // draw line (from center to out) 371 | for i in 0..<60 { 372 | if i%5 == 0 { 373 | angle += Float(Double.pi/30) 374 | continue 375 | } 376 | let startPoint = CGPoint(x: clockCenter.x + radius * CGFloat(cosf(angle)), 377 | y: clockCenter.y + radius * CGFloat(sinf(angle))) 378 | path.move(to: startPoint) 379 | let endPoint = CGPoint(x: clockCenter.x + smallRadius * CGFloat(cosf(angle)), 380 | y: clockCenter.y + smallRadius * CGFloat(sinf(angle))) 381 | path.addLine(to: endPoint) 382 | angle += Float(Double.pi/30) 383 | } 384 | layer.path = path.cgPath 385 | return layer 386 | } 387 | 388 | private func makeClockIndexLayer() -> CAShapeLayer { 389 | let layer = CAShapeLayer() 390 | layer.frame = drawLayer!.bounds 391 | layer.strokeColor = clockIndexColor.cgColor 392 | layer.fillColor = UIColor.clear.cgColor 393 | layer.lineWidth = clockIndexWidth 394 | 395 | let smallRadius = radius - (radius/10 + clockBorderLineWidth) 396 | var angle = model.angle270 397 | let path = UIBezierPath() 398 | // draw line (from center to out) 399 | for _ in 0..<12 { 400 | let startPoint = CGPoint(x: clockCenter.x + radius * CGFloat(cosf(angle)), 401 | y: clockCenter.y + radius * CGFloat(sinf(angle))) 402 | path.move(to: startPoint) 403 | let endPoint = CGPoint(x: clockCenter.x + smallRadius * CGFloat(cosf(angle)), 404 | y: clockCenter.y + smallRadius * CGFloat(sinf(angle))) 405 | path.addLine(to: endPoint) 406 | angle += model.angle30 407 | } 408 | layer.path = path.cgPath 409 | return layer 410 | } 411 | 412 | private func makeHourHandLayer() -> CAShapeLayer { 413 | let hourHandLayer = CAShapeLayer() 414 | hourHandLayer.frame = drawLayer!.bounds 415 | hourHandLayer.strokeColor = hourHandColor.cgColor 416 | hourHandLayer.fillColor = UIColor.clear.cgColor 417 | hourHandLayer.lineWidth = hourHandWidth 418 | hourHandLayer.path = makeHandPath(length: hourHandLength, 419 | angle: model.angle270).cgPath 420 | return hourHandLayer 421 | } 422 | 423 | private func makeMinuteHandLayer() -> CAShapeLayer { 424 | let minuteHandLayer = CAShapeLayer() 425 | minuteHandLayer.frame = drawLayer!.bounds 426 | minuteHandLayer.strokeColor = minuteHandColor.cgColor 427 | minuteHandLayer.fillColor = UIColor.clear.cgColor 428 | minuteHandLayer.lineWidth = minuteHandWidth 429 | minuteHandLayer.path = makeHandPath(length: minuteHandLength, 430 | angle: model.angle270).cgPath 431 | return minuteHandLayer 432 | } 433 | 434 | private func makePanMinuteLayer() -> CAShapeLayer { 435 | let panMinuteLayer = CAShapeLayer() 436 | let path = UIBezierPath(ovalIn: CGRect(x: clockCenter.x - radius, 437 | y: clockCenter.y - radius, 438 | width: radius * 2, height: radius * 2)) 439 | panMinuteLayer.frame = drawLayer!.bounds 440 | panMinuteLayer.strokeColor = UIColor.clear.cgColor 441 | panMinuteLayer.fillColor = clockColor.cgColor 442 | panMinuteLayer.path = path.cgPath 443 | return panMinuteLayer 444 | } 445 | 446 | private func makePanHourLayer() -> CAShapeLayer { 447 | let panHourLayer = CAShapeLayer() 448 | let smallRadius = radius/2 449 | let path = UIBezierPath(ovalIn: CGRect(x: clockCenter.x - smallRadius, 450 | y: clockCenter.y - smallRadius, 451 | width: smallRadius * 2, 452 | height: smallRadius * 2)) 453 | panHourLayer.frame = drawLayer!.bounds 454 | panHourLayer.strokeColor = centerCircleLineColor.cgColor 455 | panHourLayer.fillColor = UIColor.clear.cgColor 456 | panHourLayer.path = path.cgPath 457 | return panHourLayer 458 | } 459 | 460 | private func makeHandPath(length: CGFloat, angle: Float) -> UIBezierPath { 461 | let path = UIBezierPath() 462 | let point = CGPoint(x: clockCenter.x + length * CGFloat(cosf(angle)), 463 | y: clockCenter.y + length * CGFloat(sinf(angle))) 464 | path.move(to: clockCenter) 465 | path.addLine(to: point) 466 | return path 467 | } 468 | 469 | //MARK: - Gesture Action 470 | @objc func panAction(gesture: UIPanGestureRecognizer) { 471 | guard let panMinuteLayer = panMinuteLayer, 472 | let panHourLayer = panHourLayer else { 473 | return 474 | } 475 | 476 | let point = gesture.location(in: clockView) 477 | if gesture.state == .began { 478 | /// Set edit mode 479 | if UIBezierPath(cgPath: panHourLayer.path!).contains(point) { 480 | model.editType = .hour 481 | model.startAngle = model.currentHourAngle 482 | } else if UIBezierPath(cgPath: panMinuteLayer.path!).contains(point) { 483 | model.editType = .minute 484 | model.startAngle = model.currentMinuteAngle 485 | } else { 486 | model.editType = .none 487 | } 488 | } else { 489 | switch model.editType { 490 | case .none: 491 | /// Set edit mode 492 | if UIBezierPath(cgPath: panHourLayer.path!).contains(point) { 493 | model.editType = .hour 494 | model.startAngle = model.currentHourAngle 495 | } else if UIBezierPath(cgPath: panMinuteLayer.path!).contains(point) { 496 | model.editType = .minute 497 | model.startAngle = model.currentMinuteAngle 498 | } 499 | case .hour: 500 | editTimeHour(point: point) 501 | case .minute: 502 | editTimeMinute(point: point) 503 | } 504 | } 505 | } 506 | 507 | private func editTimeHour(point: CGPoint) { 508 | model.endAngle = model.calculateHourAngle(point: point, radius: radius) 509 | if model.startAngle == model.endAngle { 510 | return 511 | } 512 | 513 | let hour = model.calculateElapsedTime(startAngle: model.startAngle, 514 | endAngle: model.endAngle) 515 | model.appendHour(hour) 516 | 517 | drawHourHandLayer(angle: model.currentHourAngle) 518 | changedTime() 519 | model.startAngle = model.endAngle 520 | } 521 | 522 | private func editTimeMinute(point: CGPoint) { 523 | let minuteInt = model.calculateMinute(point: point, radius: radius) 524 | if minuteInt == model.currentMinute { 525 | return 526 | } 527 | 528 | model.updateCurrentDate(minute: minuteInt) 529 | model.endAngle = model.currentMinuteAngle 530 | model.revisedByAcrossHour(startAngle: model.startAngle, endAngle: model.endAngle) 531 | 532 | drawMinuteHandLayer(angle: model.currentMinuteAngle) 533 | drawHourHandLayer(angle: model.currentHourAngle) 534 | changedTime() 535 | model.startAngle = model.endAngle 536 | } 537 | 538 | //MARK: - Draw Hand 539 | private func drawMinuteHandLayer(angle: Float) { 540 | if minuteHandImage != nil { 541 | let rotation = angle - model.angle270 542 | minuteHandImageView.transform = CGAffineTransform(rotationAngle: CGFloat(rotation)) 543 | return 544 | } 545 | 546 | guard let minuteHandLayer = minuteHandLayer else { 547 | return 548 | } 549 | CATransaction.begin() 550 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 551 | minuteHandLayer.path = makeHandPath(length: minuteHandLength, angle: angle).cgPath 552 | CATransaction.commit() 553 | } 554 | 555 | private func drawHourHandLayer(angle: Float) { 556 | if hourHandImage != nil { 557 | clockImageView.image = clockImage 558 | minuteHandImageView.image = minuteHandImage 559 | hourHandImageView.image = hourHandImage 560 | let rotation = angle - model.angle270 561 | hourHandImageView.transform = CGAffineTransform(rotationAngle: CGFloat(rotation)) 562 | return 563 | } 564 | 565 | guard let hourHandLayer = hourHandLayer else { 566 | return 567 | } 568 | CATransaction.begin() 569 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 570 | hourHandLayer.path = makeHandPath(length: hourHandLength, angle: angle).cgPath 571 | CATransaction.commit() 572 | } 573 | 574 | //MARK: - Shwo/Clear 575 | private func clear() { 576 | clockView.subviews.forEach { $0.removeFromSuperview() } 577 | selectedTimeLabel.removeFromSuperview() 578 | clockImageView.removeFromSuperview() 579 | hourHandImageView.removeFromSuperview() 580 | minuteHandImageView.removeFromSuperview() 581 | clockView.removeFromSuperview() 582 | drawLayer?.removeFromSuperlayer() 583 | minuteHandImageView.transform = CGAffineTransform.identity 584 | hourHandImageView.transform = CGAffineTransform.identity 585 | 586 | hourHandLayer = nil 587 | minuteHandLayer = nil 588 | panHourLayer = nil 589 | panMinuteLayer = nil 590 | drawLayer = nil 591 | } 592 | 593 | private func relodClock() { 594 | clear() 595 | 596 | prepareClockView() 597 | prepareClockImageViews() 598 | 599 | drawLayer = makeDrawLayer() 600 | clockView.layer.addSublayer(drawLayer!) 601 | 602 | if clockImage == nil { 603 | drawLayer!.addSublayer(makeSmallClockIndexLayer()) 604 | drawLayer!.addSublayer(makeClockIndexLayer()) 605 | prepareTimeLabel() 606 | } 607 | 608 | panMinuteLayer = makePanMinuteLayer() 609 | drawLayer!.insertSublayer(panMinuteLayer!, at: 0) 610 | panHourLayer = makePanHourLayer() 611 | drawLayer!.addSublayer(panHourLayer!) 612 | let pan = UIPanGestureRecognizer(target: self, 613 | action: #selector(self.panAction(gesture:))) 614 | clockView.addGestureRecognizer(pan) 615 | 616 | if hourHandImage == nil { 617 | hourHandLayer = makeHourHandLayer() 618 | drawLayer!.addSublayer(hourHandLayer!) 619 | } 620 | 621 | if minuteHandImage == nil { 622 | minuteHandLayer = makeMinuteHandLayer() 623 | drawLayer!.addSublayer(minuteHandLayer!) 624 | } 625 | 626 | prepareSelectedTimeLabel() 627 | } 628 | 629 | private func changedTime() { 630 | selectedTimeLabel.text = model.formattedTime 631 | delegate?.clockView(self, didChangeDate: model.currentDate) 632 | } 633 | 634 | public func redrawClock() { 635 | relodClock() 636 | drawMinuteHandLayer(angle: model.currentMinuteAngle) 637 | drawHourHandLayer(angle: model.currentHourAngle) 638 | selectedTimeLabel.text = model.formattedTime 639 | } 640 | } 641 | --------------------------------------------------------------------------------