├── .gitignore ├── CellTimerDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── CellTimerDemo ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── TimerTableViewCell.swift ├── ViewController.swift └── YZTimerUtil.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /CellTimerDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8429FD721F677A8E0017FA8E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8429FD711F677A8E0017FA8E /* AppDelegate.swift */; }; 11 | 8429FD741F677A8E0017FA8E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8429FD731F677A8E0017FA8E /* ViewController.swift */; }; 12 | 8429FD771F677A8E0017FA8E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8429FD751F677A8E0017FA8E /* Main.storyboard */; }; 13 | 8429FD791F677A8E0017FA8E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8429FD781F677A8E0017FA8E /* Assets.xcassets */; }; 14 | 8429FD7C1F677A8E0017FA8E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8429FD7A1F677A8E0017FA8E /* LaunchScreen.storyboard */; }; 15 | 8429FD841F67B6BE0017FA8E /* YZTimerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8429FD831F67B6BE0017FA8E /* YZTimerUtil.swift */; }; 16 | 8429FD861F67CF240017FA8E /* TimerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8429FD851F67CF240017FA8E /* TimerTableViewCell.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 8429FD6E1F677A8E0017FA8E /* CellTimerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CellTimerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 8429FD711F677A8E0017FA8E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 8429FD731F677A8E0017FA8E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 8429FD761F677A8E0017FA8E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 8429FD781F677A8E0017FA8E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 8429FD7B1F677A8E0017FA8E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 8429FD7D1F677A8E0017FA8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 8429FD831F67B6BE0017FA8E /* YZTimerUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YZTimerUtil.swift; sourceTree = ""; }; 28 | 8429FD851F67CF240017FA8E /* TimerTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimerTableViewCell.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 8429FD6B1F677A8E0017FA8E /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 8429FD651F677A8E0017FA8E = { 43 | isa = PBXGroup; 44 | children = ( 45 | 8429FD701F677A8E0017FA8E /* CellTimerDemo */, 46 | 8429FD6F1F677A8E0017FA8E /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 8429FD6F1F677A8E0017FA8E /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 8429FD6E1F677A8E0017FA8E /* CellTimerDemo.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 8429FD701F677A8E0017FA8E /* CellTimerDemo */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 8429FD711F677A8E0017FA8E /* AppDelegate.swift */, 62 | 8429FD731F677A8E0017FA8E /* ViewController.swift */, 63 | 8429FD831F67B6BE0017FA8E /* YZTimerUtil.swift */, 64 | 8429FD751F677A8E0017FA8E /* Main.storyboard */, 65 | 8429FD781F677A8E0017FA8E /* Assets.xcassets */, 66 | 8429FD7A1F677A8E0017FA8E /* LaunchScreen.storyboard */, 67 | 8429FD7D1F677A8E0017FA8E /* Info.plist */, 68 | 8429FD851F67CF240017FA8E /* TimerTableViewCell.swift */, 69 | ); 70 | path = CellTimerDemo; 71 | sourceTree = ""; 72 | }; 73 | /* End PBXGroup section */ 74 | 75 | /* Begin PBXNativeTarget section */ 76 | 8429FD6D1F677A8E0017FA8E /* CellTimerDemo */ = { 77 | isa = PBXNativeTarget; 78 | buildConfigurationList = 8429FD801F677A8E0017FA8E /* Build configuration list for PBXNativeTarget "CellTimerDemo" */; 79 | buildPhases = ( 80 | 8429FD6A1F677A8E0017FA8E /* Sources */, 81 | 8429FD6B1F677A8E0017FA8E /* Frameworks */, 82 | 8429FD6C1F677A8E0017FA8E /* Resources */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = CellTimerDemo; 89 | productName = CellTimerDemo; 90 | productReference = 8429FD6E1F677A8E0017FA8E /* CellTimerDemo.app */; 91 | productType = "com.apple.product-type.application"; 92 | }; 93 | /* End PBXNativeTarget section */ 94 | 95 | /* Begin PBXProject section */ 96 | 8429FD661F677A8E0017FA8E /* Project object */ = { 97 | isa = PBXProject; 98 | attributes = { 99 | LastSwiftUpdateCheck = 0830; 100 | LastUpgradeCheck = 0830; 101 | ORGANIZATIONNAME = Yasin; 102 | TargetAttributes = { 103 | 8429FD6D1F677A8E0017FA8E = { 104 | CreatedOnToolsVersion = 8.3.1; 105 | ProvisioningStyle = Automatic; 106 | }; 107 | }; 108 | }; 109 | buildConfigurationList = 8429FD691F677A8E0017FA8E /* Build configuration list for PBXProject "CellTimerDemo" */; 110 | compatibilityVersion = "Xcode 3.2"; 111 | developmentRegion = English; 112 | hasScannedForEncodings = 0; 113 | knownRegions = ( 114 | en, 115 | Base, 116 | ); 117 | mainGroup = 8429FD651F677A8E0017FA8E; 118 | productRefGroup = 8429FD6F1F677A8E0017FA8E /* Products */; 119 | projectDirPath = ""; 120 | projectRoot = ""; 121 | targets = ( 122 | 8429FD6D1F677A8E0017FA8E /* CellTimerDemo */, 123 | ); 124 | }; 125 | /* End PBXProject section */ 126 | 127 | /* Begin PBXResourcesBuildPhase section */ 128 | 8429FD6C1F677A8E0017FA8E /* Resources */ = { 129 | isa = PBXResourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 8429FD7C1F677A8E0017FA8E /* LaunchScreen.storyboard in Resources */, 133 | 8429FD791F677A8E0017FA8E /* Assets.xcassets in Resources */, 134 | 8429FD771F677A8E0017FA8E /* Main.storyboard in Resources */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXSourcesBuildPhase section */ 141 | 8429FD6A1F677A8E0017FA8E /* Sources */ = { 142 | isa = PBXSourcesBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 8429FD841F67B6BE0017FA8E /* YZTimerUtil.swift in Sources */, 146 | 8429FD741F677A8E0017FA8E /* ViewController.swift in Sources */, 147 | 8429FD721F677A8E0017FA8E /* AppDelegate.swift in Sources */, 148 | 8429FD861F67CF240017FA8E /* TimerTableViewCell.swift in Sources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXSourcesBuildPhase section */ 153 | 154 | /* Begin PBXVariantGroup section */ 155 | 8429FD751F677A8E0017FA8E /* Main.storyboard */ = { 156 | isa = PBXVariantGroup; 157 | children = ( 158 | 8429FD761F677A8E0017FA8E /* Base */, 159 | ); 160 | name = Main.storyboard; 161 | sourceTree = ""; 162 | }; 163 | 8429FD7A1F677A8E0017FA8E /* LaunchScreen.storyboard */ = { 164 | isa = PBXVariantGroup; 165 | children = ( 166 | 8429FD7B1F677A8E0017FA8E /* Base */, 167 | ); 168 | name = LaunchScreen.storyboard; 169 | sourceTree = ""; 170 | }; 171 | /* End PBXVariantGroup section */ 172 | 173 | /* Begin XCBuildConfiguration section */ 174 | 8429FD7E1F677A8E0017FA8E /* Debug */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_ANALYZER_NONNULL = YES; 179 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 180 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 181 | CLANG_CXX_LIBRARY = "libc++"; 182 | CLANG_ENABLE_MODULES = YES; 183 | CLANG_ENABLE_OBJC_ARC = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 187 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 188 | CLANG_WARN_EMPTY_BODY = YES; 189 | CLANG_WARN_ENUM_CONVERSION = YES; 190 | CLANG_WARN_INFINITE_RECURSION = YES; 191 | CLANG_WARN_INT_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 194 | CLANG_WARN_UNREACHABLE_CODE = YES; 195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 196 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 197 | COPY_PHASE_STRIP = NO; 198 | DEBUG_INFORMATION_FORMAT = dwarf; 199 | ENABLE_STRICT_OBJC_MSGSEND = YES; 200 | ENABLE_TESTABILITY = YES; 201 | GCC_C_LANGUAGE_STANDARD = gnu99; 202 | GCC_DYNAMIC_NO_PIC = NO; 203 | GCC_NO_COMMON_BLOCKS = YES; 204 | GCC_OPTIMIZATION_LEVEL = 0; 205 | GCC_PREPROCESSOR_DEFINITIONS = ( 206 | "DEBUG=1", 207 | "$(inherited)", 208 | ); 209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 211 | GCC_WARN_UNDECLARED_SELECTOR = YES; 212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 213 | GCC_WARN_UNUSED_FUNCTION = YES; 214 | GCC_WARN_UNUSED_VARIABLE = YES; 215 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 216 | MTL_ENABLE_DEBUG_INFO = YES; 217 | ONLY_ACTIVE_ARCH = YES; 218 | SDKROOT = iphoneos; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | TARGETED_DEVICE_FAMILY = "1,2"; 222 | }; 223 | name = Debug; 224 | }; 225 | 8429FD7F1F677A8E0017FA8E /* Release */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | ALWAYS_SEARCH_USER_PATHS = NO; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_WARN_BOOL_CONVERSION = YES; 236 | CLANG_WARN_CONSTANT_CONVERSION = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 245 | CLANG_WARN_UNREACHABLE_CODE = YES; 246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 247 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 248 | COPY_PHASE_STRIP = NO; 249 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 250 | ENABLE_NS_ASSERTIONS = NO; 251 | ENABLE_STRICT_OBJC_MSGSEND = YES; 252 | GCC_C_LANGUAGE_STANDARD = gnu99; 253 | GCC_NO_COMMON_BLOCKS = YES; 254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 256 | GCC_WARN_UNDECLARED_SELECTOR = YES; 257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 258 | GCC_WARN_UNUSED_FUNCTION = YES; 259 | GCC_WARN_UNUSED_VARIABLE = YES; 260 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 261 | MTL_ENABLE_DEBUG_INFO = NO; 262 | SDKROOT = iphoneos; 263 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 264 | TARGETED_DEVICE_FAMILY = "1,2"; 265 | VALIDATE_PRODUCT = YES; 266 | }; 267 | name = Release; 268 | }; 269 | 8429FD811F677A8E0017FA8E /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 273 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 274 | DEVELOPMENT_TEAM = ""; 275 | INFOPLIST_FILE = CellTimerDemo/Info.plist; 276 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 277 | PRODUCT_BUNDLE_IDENTIFIER = com.yasin.CellTimerDemo; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | PROVISIONING_PROFILE_SPECIFIER = ""; 280 | SWIFT_VERSION = 3.0; 281 | }; 282 | name = Debug; 283 | }; 284 | 8429FD821F677A8E0017FA8E /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 289 | DEVELOPMENT_TEAM = ""; 290 | INFOPLIST_FILE = CellTimerDemo/Info.plist; 291 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 292 | PRODUCT_BUNDLE_IDENTIFIER = com.yasin.CellTimerDemo; 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | PROVISIONING_PROFILE_SPECIFIER = ""; 295 | SWIFT_VERSION = 3.0; 296 | }; 297 | name = Release; 298 | }; 299 | /* End XCBuildConfiguration section */ 300 | 301 | /* Begin XCConfigurationList section */ 302 | 8429FD691F677A8E0017FA8E /* Build configuration list for PBXProject "CellTimerDemo" */ = { 303 | isa = XCConfigurationList; 304 | buildConfigurations = ( 305 | 8429FD7E1F677A8E0017FA8E /* Debug */, 306 | 8429FD7F1F677A8E0017FA8E /* Release */, 307 | ); 308 | defaultConfigurationIsVisible = 0; 309 | defaultConfigurationName = Release; 310 | }; 311 | 8429FD801F677A8E0017FA8E /* Build configuration list for PBXNativeTarget "CellTimerDemo" */ = { 312 | isa = XCConfigurationList; 313 | buildConfigurations = ( 314 | 8429FD811F677A8E0017FA8E /* Debug */, 315 | 8429FD821F677A8E0017FA8E /* Release */, 316 | ); 317 | defaultConfigurationIsVisible = 0; 318 | }; 319 | /* End XCConfigurationList section */ 320 | }; 321 | rootObject = 8429FD661F677A8E0017FA8E /* Project object */; 322 | } 323 | -------------------------------------------------------------------------------- /CellTimerDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CellTimerDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CellTimerDemo 4 | // 5 | // Created by Yasin on 2017/9/12. 6 | // Copyright © 2017年 Yasin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /CellTimerDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /CellTimerDemo/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 | 27 | 28 | -------------------------------------------------------------------------------- /CellTimerDemo/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 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /CellTimerDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | -------------------------------------------------------------------------------- /CellTimerDemo/TimerTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerTableViewCell.swift 3 | // CellTimerDemo 4 | // 5 | // Created by Yasin on 2017/9/12. 6 | // Copyright © 2017年 Yasin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TimerTableViewCell: UITableViewCell, TimerListener { 12 | 13 | 14 | var time = NSDate().timeIntervalSince1970 { 15 | didSet { 16 | onTimer() 17 | } 18 | } 19 | 20 | deinit { 21 | YZTimerUtil.sharedInstance.removeListener(listener: self) 22 | } 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | YZTimerUtil.sharedInstance.addListener(listener: self) 26 | } 27 | 28 | func didOnTimer(announcer: YZTimerUtil, timeInterval: TimeInterval) { 29 | onTimer() 30 | } 31 | 32 | private func onTimer() { 33 | let leftTime = YZTimerUtil.sharedInstance.lefTimeInterval(time: time) 34 | if leftTime > 0 { 35 | self.detailTextLabel?.text = "倒计时 \(Int(leftTime)/3600):\(Int(leftTime)/60%60):\(Int(leftTime)%60)" 36 | } else { 37 | self.detailTextLabel?.text = "活动已开始" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CellTimerDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CellTimerDemo 4 | // 5 | // Created by Yasin on 2017/9/12. 6 | // Copyright © 2017年 Yasin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UITableViewController { 12 | 13 | fileprivate let data: [TimeInterval] = Array(0..<60).map { (item:Int) in 14 | var time = NSDate().timeIntervalSince1970 15 | time -= 60*60*3 16 | time += TimeInterval(60*60*item) 17 | return time 18 | } 19 | fileprivate let timerTableViewCellID = "TimerTableViewCell" 20 | 21 | 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | // 初始化YZTimerUtil,并且获取服务器当前时间 26 | YZTimerUtil.sharedInstance.resetServerTime() 27 | YZTimerUtil.sharedInstance.timerStart() 28 | // Do any additional setup after loading the view, typically from a nib. 29 | } 30 | 31 | 32 | } 33 | 34 | extension ViewController { 35 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 36 | return data.count 37 | } 38 | 39 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 40 | let cell = tableView.dequeueReusableCell(withIdentifier: timerTableViewCellID, for: indexPath) as! TimerTableViewCell 41 | cell.textLabel?.text = "第\(indexPath.row)个数据" 42 | cell.time = data[indexPath.row] 43 | return cell 44 | } 45 | 46 | // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 47 | // let timerCell = cell as! TimerTableViewCell 48 | // YZTimerUtil.sharedInstance.addListener(listener: timerCell) 49 | // } 50 | // override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { 51 | // let timerCell = cell as! TimerTableViewCell 52 | // YZTimerUtil.sharedInstance.removeListener(listener: timerCell) 53 | // } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /CellTimerDemo/YZTimerUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YZTimerUtil.swift 3 | // CellTimerDemo 4 | // 5 | // Created by Yasin on 2017/9/12. 6 | // Copyright © 2017年 Yasin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let rloadTimeInterval: TimeInterval = 15 12 | 13 | @objc 14 | protocol TimerListener: class { 15 | func didOnTimer(announcer: YZTimerUtil, timeInterval: TimeInterval) 16 | } 17 | class YZTimerUtil: NSObject { 18 | static let sharedInstance = YZTimerUtil() 19 | 20 | private let map: NSHashTable = NSHashTable.weakObjects() 21 | private var timer:Timer? 22 | 23 | 24 | /// 和服务器时间差,用于服务器时间同步 25 | var serverTimeInterval: TimeInterval = 0 26 | /// 和服务器时间对比后的当前时间戳 27 | var nowTimeInterval = NSDate().timeIntervalSince1970 28 | 29 | 30 | override init() { 31 | super.init() 32 | // 默认暂停定时器,定时器默认是加载到当前runloop中的,在进行UI界面操作比如滑动列表时,由于在main runloop中NSTimer是同步交付的被“阻塞”,就会导致NSTimer计时出现延误 33 | // 解决这种延误的方法,一种是在子线程中进行NSTimer的操作,在主线程中修改UI界面显示操作结果;另一种是仍然在主线程中进行NSTimer操作,但是将NSTimer实例加到main runloop的特定mode(模式)中。避免被复杂运算操作或者UI界面刷新所干扰。 34 | timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[unowned self] (_) in 35 | self.onTimer() 36 | }) 37 | //Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(onTimer), userInfo: nil, repeats: true) //这种方法要求onTimer是@objc 38 | timerPause() 39 | } 40 | 41 | func addListener(listener: TimerListener) { 42 | map.add(listener) 43 | } 44 | func removeListener(listener: TimerListener) { 45 | map.remove(listener) 46 | } 47 | 48 | private func onTimer() { 49 | nowTimeInterval = NSDate().timeIntervalSince1970 - serverTimeInterval 50 | 51 | for listener in self.map.allObjects { 52 | listener.didOnTimer(announcer: self, timeInterval: self.nowTimeInterval) 53 | } 54 | } 55 | 56 | /// 从服务器请求最新的时间 57 | func resetServerTime() { 58 | // 从服务器请求最新的时间 59 | // 。。。 60 | var success = true 61 | 62 | if success { 63 | // 请求成功 64 | serverTimeInterval = 0 65 | } else { 66 | // 如果请求失败,隔一段时间再请求一次 67 | perform(#selector(resetServerTime), with: nil, afterDelay: rloadTimeInterval) 68 | } 69 | } 70 | 71 | /// 当没有定时器需求的时候暂停定时器 72 | func timerPause() { 73 | timer?.fireDate = Date.distantFuture 74 | } 75 | 76 | /// 启动定时器 77 | func timerStart() { 78 | timer?.fireDate = Date.distantPast 79 | } 80 | 81 | 82 | /// 提供时间差值计算方法 83 | /// 84 | /// - Parameter time: 比如限时活动开始时间、结束时间 85 | /// - Returns: 时间差 86 | func lefTimeInterval(time: TimeInterval) -> TimeInterval { 87 | let leftTime = time - self.nowTimeInterval 88 | return leftTime 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CellTimerDemo 2 | 3 | 一个在多行cell中展示倒计时,使用定时器的Demo 4 | 5 | 文章地址:http://www.jianshu.com/p/97ec4b8f018c 6 | 7 | >本文借鉴了IGListKit中多cell通知方案 8 | 9 | 公司需要做限时抢购的业务,这里面有两个需求点: 10 | 11 | 1.在多个cell中显示倒计时 12 | 13 | 在每个cell中添加定时器是不现实的,必定会增加许多性能开销,所以肯定是使用一个定时器,关键在于如何通知到cell刷新UI 14 | 15 | 2.本地时间可能和服务器时间存在误差 16 | 17 | 有的手机可能时间没有和网络同步,或者用户故意调整了时间,所以本地时间存在错误的可能,所以就定下使用服务器时间 18 | 19 | #### 1.在多个cell中显示倒计时 20 | 思路是这样的:将需要接收定时器通知的对象注册到定时器单例中,存放在数组里面,当定时器更新的时候遍历数组回调通知 21 | ##### 定时器的创建 22 | **注意:**默认暂停定时器,定时器默认是加载到当前runloop中的,在进行UI界面操作比如滑动列表时,由于在main runloop中NSTimer是同步交付的被“阻塞”,就会导致NSTimer计时出现延误 23 | 解决这种延误的方法,一种是在子线程中进行NSTimer的操作,在主线程中修改UI界面显示操作结果;另一种是仍然在主线程中进行NSTimer操作,但是将NSTimer实例加到main runloop的特定mode(模式)中。避免被复杂运算操作或者UI界面刷新所干扰。 24 | ``` 25 | timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[unowned self] (_) in 26 | self.onTimer() 27 | }) 28 | //下面这种方法要求onTimer是@objc 29 | //Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(onTimer), userInfo: nil, repeats: true) 30 | ``` 31 | ##### 注册通知 32 | 这里使用NSHashTable存放注册对象的数组,可以防止循环引用注册对象释放不掉 33 | swift的protocol是一个很好的东西,这样可以更好的规范谁可以注册通知 34 | ``` 35 | @objc 36 | protocol TimerListener: class { 37 | func didOnTimer(announcer: YZTimerUtil, timeInterval: TimeInterval) 38 | } 39 | ``` 40 | ``` 41 | private let map: NSHashTable = NSHashTable.weakObjects() 42 | ``` 43 | ##### 何时注册删除通知 44 | 一开始是在willDisplay、didEndDisplaying方法中进行通知注册的,后来发现没有必要,因为cell创建后就是要接收通知的willDisplay、didEndDisplaying中还要进行cell类型的判断,所以就改为cell- init/deinit中 45 | ``` 46 | deinit { 47 | YZTimerUtil.sharedInstance.removeListener(listener: self) 48 | } 49 | //这里cell使用的是SB 50 | override func awakeFromNib() { 51 | super.awakeFromNib() 52 | YZTimerUtil.sharedInstance.addListener(listener: self) 53 | } 54 | ``` 55 | #### 2.本地时间可能和服务器时间存在误差 56 | 这里看项目需求吧,如果项目对时间要求没有那么严格,不做服务器时间对比也行,反正服务器那边会进行判断的,有些对时间要求严格的肯定是要做对比的,比如手机手令的动态码 57 | 我的思路是在定时器初始化的时候进行网络请求,拿到服务器的当前时间,然后计算本地和服务器时间的差值,后面就用这个差值进行计算。当然,受网络状态的影响,这个时间可能也不是准确的时间,但是这个时间误差会在一个可控范围内,为了精确时间差,可以每隔一段时间就校准一次,如果要更精准的,可以通过请求的requestTime/responseTime进行算法计算 58 | ``` 59 | /// 从服务器请求最新的时间,简单示例 60 | func resetServerTime() { 61 | // 从服务器请求最新的时间 62 | // 。。。 63 | var success = true 64 | 65 | if success { 66 | // 请求成功 67 | serverTimeInterval = 0 68 | } else { 69 | // 如果请求失败,隔一段时间再请求一次 70 | perform(#selector(resetServerTime), with: nil, afterDelay: rloadTimeInterval) 71 | } 72 | } 73 | ``` 74 | demo里面的代码很详细,也很简单,建议可以看看 75 | --------------------------------------------------------------------------------