├── .gitignore ├── CHANGELOG.md ├── Classes ├── MyLilTimer.h └── MyLilTimer.m ├── LICENSE ├── MyLilTimer.podspec ├── README.md ├── Rakefile └── TimerTest ├── TimerTest.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── TimerTest.xcscheme └── TimerTest ├── Base.lproj └── Main.storyboard ├── Images.xcassets ├── AppIcon.appiconset │ └── Contents.json └── LaunchImage.launchimage │ └── Contents.json ├── TimerTest-Info.plist ├── TimerTestAppDelegate.h ├── TimerTestAppDelegate.m ├── TimerTestViewController.h ├── TimerTestViewController.m └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 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 | profile 13 | *.moved-aside 14 | DerivedData 15 | .idea/ 16 | *.hmap 17 | .*.swp 18 | .svn/ 19 | *.o 20 | *.d 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # MyLilTimer CHANGELOG 2 | 3 | ## 0.1.0 4 | 5 | Initial release. 6 | -------------------------------------------------------------------------------- /Classes/MyLilTimer.h: -------------------------------------------------------------------------------- 1 | // 2 | // MyLilTimer.h 3 | // TimerTest 4 | // 5 | // Created by Jonathon Mah on 2014-01-01. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import 10 | 11 | 12 | typedef NS_ENUM(NSInteger, MyLilTimerClock) { 13 | MyLilTimerClockRealtime = 0, 14 | MyLilTimerClockMonotonic, 15 | MyLilTimerClockBoottime, 16 | }; 17 | 18 | NSString *NSStringFromMyLilTimerClock(MyLilTimerClock clock); 19 | 20 | 21 | typedef NS_ENUM(NSInteger, MyLilTimerBehavior) { 22 | /// The timer fires after an interval has elapsed, regardless of system clock changes and system sleep. 23 | MyLilTimerBehaviorHourglass = MyLilTimerClockBoottime, 24 | 25 | /// The timer fires after the system has run for some duration; this is paused while the operating system was asleep. 26 | /// This behavior is appropriate for measuring the speed of long-running operations, such as "3 of 7 items processed, 30 seconds remaining". 27 | /// 28 | /// This is the behavior of \p NSTimer and \p NSRunLoop / \p CFRunLoop. 29 | MyLilTimerBehaviorPauseOnSystemSleep = MyLilTimerClockMonotonic, 30 | 31 | /// The timer fires when the time on the system clock passes the fire date. 32 | /// This behavior is appropriate for an alarm that fires when the clock shows a particular time. 33 | MyLilTimerBehaviorObeySystemClockChanges = MyLilTimerClockRealtime, 34 | }; 35 | 36 | NSString *NSStringFromMyLilTimerBehavior(MyLilTimerBehavior behavior); 37 | 38 | 39 | MyLilTimerBehavior MyLilTimerBehaviorFromClock(MyLilTimerClock clock); 40 | MyLilTimerClock MyLilTimerClockFromBehavior(MyLilTimerBehavior behavior); 41 | 42 | 43 | @interface MyLilTimer : NSObject 44 | 45 | /** 46 | * Returns the current value of a clock. 47 | * A single value is arbitrary; use the difference between two invocations of this method. 48 | */ 49 | + (NSTimeInterval)nowValueForClock:(MyLilTimerClock)clock; 50 | 51 | - (instancetype)init NS_UNAVAILABLE; 52 | 53 | /** 54 | * Creates a new timer and schedules it on the main run loop in NSRunLoopCommonModes. 55 | * 56 | * \param behavior determines how time is measured. 57 | * 58 | * \param intervalSinceNow the number of seconds before the timer fires. 59 | * Like NSTimer, this is measured from now, regardless when the timer is scheduled with a run loop. 60 | * The minimum value is 0.0001 for consistency with NSTimer. 61 | * 62 | * \param target the object to which to send the message specified by \p action when the timer fires. 63 | * The timer maintains a strong reference to target until the timer is invalidated. 64 | * 65 | * \param action the message to send to \p target when the timer fires. 66 | * This method should have the signature: 67 | * \p - (void)timerFired:(MyLilTimer *)timer 68 | * 69 | * \param userInfo an arbitrary object associated with the timer. 70 | * The timer releases this object when it's invalidated (after the action has been sent to the target). 71 | */ 72 | + (instancetype)scheduledTimerWithBehavior:(MyLilTimerBehavior)behavior timeInterval:(NSTimeInterval)intervalSinceNow target:(id)target selector:(SEL)action userInfo:(id)userInfo; 73 | 74 | /** 75 | * Creates a new timer, without scheduling it on a run loop. 76 | * \em (Designated initializer.) 77 | * 78 | * \param behavior determines how time is measured. 79 | * 80 | * \param intervalSinceNow the number of seconds before the timer fires. 81 | * Like NSTimer, this is measured from now, regardless when the timer is scheduled with a run loop. 82 | * The minimum value is 0.0001 for consistency with NSTimer. 83 | * 84 | * \param target the object to which to send the message specified by \p action when the timer fires. 85 | * The timer maintains a strong reference to target until the timer is invalidated. 86 | * 87 | * \param action the message to send to \p target when the timer fires. 88 | * This method should have the signature: 89 | * \p - (void)timerFired:(MyLilTimer *)timer 90 | * 91 | * \param userInfo an arbitrary object associated with the timer. 92 | * The timer releases this object when it's invalidated (after the action has been sent to the target). 93 | */ 94 | - (instancetype)initWithBehavior:(MyLilTimerBehavior)behavior timeInterval:(NSTimeInterval)intervalSinceNow target:(id)target selector:(SEL)action userInfo:(id)userInfo; 95 | 96 | /** 97 | * Currently only timers on the main thread (using the main loop) are supported. 98 | */ 99 | - (void)scheduleOnMainRunLoopForModes:(NSSet *)modes; 100 | 101 | /** 102 | * Fires the timer immediately, sending the action to the target, then invalidates. 103 | * Does nothing if the timer has been invalidated. 104 | */ 105 | - (void)fire; 106 | 107 | @property (nonatomic, readonly) MyLilTimerBehavior behavior; 108 | 109 | /** 110 | * An arbitrary object associated with the timer. 111 | * 112 | * \returns the argument for the \p userInfo parameter of an init method, 113 | * or nil if the timer has been invalidated. 114 | */ 115 | @property (nonatomic, readonly) id userInfo; 116 | 117 | /** 118 | * Returns the date at which the timer is currently scheduled to fire. 119 | * This value can change depending on the timer behavior, system clock changes, and system sleep. 120 | */ 121 | - (NSDate *)fireDate; 122 | 123 | /** 124 | * Returns the duration that has elapsed since the timer's fire date. 125 | * If the timer has not yet fired, the return value will be negative indicating a time in the future. 126 | */ 127 | - (NSTimeInterval)timeSinceFireDate; 128 | 129 | /** 130 | * A larger value allows the timer to fire later, in sync with other system activity to reduce power use. 131 | * Has no effect if \p NSTimer does not support setting tolerance (prior to Mac OS X 10.9 and iOS 7.0). 132 | * \see -[NSTimer tolerance] 133 | */ 134 | @property (nonatomic) NSTimeInterval tolerance; 135 | 136 | - (void)invalidate; 137 | @property (nonatomic, readonly, getter = isValid) BOOL valid; 138 | 139 | @end 140 | -------------------------------------------------------------------------------- /Classes/MyLilTimer.m: -------------------------------------------------------------------------------- 1 | // 2 | // MyLilTimer.m 3 | // TimerTest 4 | // 5 | // Created by Jonathon Mah on 2014-01-01. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import "MyLilTimer.h" 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | 16 | #if ! __has_feature(objc_arc) 17 | #error This file needs Automatic Reference Counting (ARC). Use -fobjc-arc flag (or convert project to ARC). 18 | #endif 19 | 20 | 21 | NSString *NSStringFromMyLilTimerClock(MyLilTimerClock clock) 22 | { 23 | switch (clock) { 24 | #define CASE_RETURN(x) case x: return @#x 25 | CASE_RETURN(MyLilTimerClockRealtime); 26 | CASE_RETURN(MyLilTimerClockMonotonic); 27 | CASE_RETURN(MyLilTimerClockBoottime); 28 | #undef CASE_RETURN 29 | } 30 | return nil; 31 | } 32 | 33 | NSString *NSStringFromMyLilTimerBehavior(MyLilTimerBehavior behavior) 34 | { 35 | switch (behavior) { 36 | #define CASE_RETURN(x) case x: return @#x 37 | CASE_RETURN(MyLilTimerBehaviorHourglass); 38 | CASE_RETURN(MyLilTimerBehaviorPauseOnSystemSleep); 39 | CASE_RETURN(MyLilTimerBehaviorObeySystemClockChanges); 40 | #undef CASE_RETURN 41 | } 42 | return nil; 43 | } 44 | 45 | static BOOL __unused isValidClock(MyLilTimerClock b) 46 | { 47 | return (NSStringFromMyLilTimerClock(b) != nil); 48 | } 49 | 50 | static BOOL __unused isValidBehavior(MyLilTimerBehavior b) 51 | { 52 | return (NSStringFromMyLilTimerBehavior(b) != nil); 53 | } 54 | 55 | static NSTimeInterval timeIntervalSinceBoot(void) 56 | { 57 | // TODO: Potentially a race condition if the system clock changes between reading `bootTime` and `now` 58 | int status; 59 | 60 | struct timeval bootTime; 61 | status = sysctl((int[]){CTL_KERN, KERN_BOOTTIME}, 2, 62 | &bootTime, &(size_t){sizeof(bootTime)}, 63 | NULL, 0); 64 | NSCAssert(status == 0, nil); 65 | 66 | struct timeval now; 67 | status = gettimeofday(&now, NULL); 68 | NSCAssert(status == 0, nil); 69 | 70 | struct timeval difference; 71 | timersub(&now, &bootTime, &difference); 72 | 73 | return (difference.tv_sec + difference.tv_usec * 1.e-6); 74 | } 75 | 76 | static void assertMainThread(void) 77 | { 78 | NSCAssert([NSThread isMainThread], @"MyLilTimer does not currently support background threads."); 79 | } 80 | 81 | 82 | static NSString *const MyLilTimerHostCalendarChangedNotification = @"MyLilTimerHostCalendarChanged"; 83 | 84 | 85 | MyLilTimerBehavior MyLilTimerBehaviorFromClock(MyLilTimerClock clock) 86 | { return (MyLilTimerBehavior)clock; } 87 | 88 | MyLilTimerClock MyLilTimerClockFromBehavior(MyLilTimerBehavior behavior) 89 | { return (MyLilTimerClock)behavior; } 90 | 91 | 92 | @interface MyLilTimerHostCalendarChangedNotifier : NSObject 93 | + (instancetype)sharedInstance; 94 | @end 95 | 96 | 97 | @interface MyLilTimer () 98 | @property (nonatomic, readwrite, getter = isValid) BOOL valid; 99 | @end 100 | 101 | 102 | @implementation MyLilTimer { 103 | id _target; 104 | SEL _action; 105 | 106 | NSTimeInterval _fireClockValue; 107 | NSSet *_runLoopModes; 108 | NSTimer *_nextCheckTimer; 109 | 110 | MyLilTimerHostCalendarChangedNotifier *_notifier; 111 | } 112 | 113 | 114 | #pragma mark NSObject 115 | 116 | - (instancetype)init 117 | { 118 | NSAssert(NO, @"Bad initializer, use %s", sel_getName(@selector(initWithBehavior:timeInterval:target:selector:userInfo:))); 119 | return nil; 120 | } 121 | 122 | - (void)dealloc 123 | { 124 | [self invalidate]; 125 | } 126 | 127 | - (NSString *)description 128 | { 129 | NSString *fireInfo = self.valid ? (_runLoopModes ? @"scheduled" : @"unscheduled") : @"fired"; 130 | NSTimeInterval timeSinceFireDate = self.timeSinceFireDate; 131 | NSString *ago = (timeSinceFireDate < 0) ? @"from now" : @"ago"; 132 | return [NSString stringWithFormat:@"<%@ %p %@ %fs %@>", [self class], self, fireInfo, ABS(timeSinceFireDate), ago]; 133 | } 134 | 135 | 136 | #pragma mark MyLilTimer: API 137 | 138 | + (NSTimeInterval)nowValueForClock:(MyLilTimerClock)clock 139 | { 140 | NSParameterAssert(isValidClock(clock)); 141 | switch (clock) { 142 | case MyLilTimerClockRealtime: 143 | return [NSDate timeIntervalSinceReferenceDate]; 144 | case MyLilTimerClockMonotonic: 145 | // a.k.a. CACurrentMediaTime() 146 | // a.k.a. [NSProcessInfo processInfo].systemUptime 147 | // a.k.a. _CFGetSystemUptime() 148 | // a.k.a. mach_absolute_time() (in different units) 149 | return [NSProcessInfo processInfo].systemUptime; 150 | case MyLilTimerClockBoottime: 151 | return timeIntervalSinceBoot(); 152 | } 153 | return NAN; // assertions disabled 154 | } 155 | 156 | + (instancetype)scheduledTimerWithBehavior:(MyLilTimerBehavior)behavior timeInterval:(NSTimeInterval)intervalSinceNow target:(id)target selector:(SEL)action userInfo:(id)userInfo 157 | { 158 | assertMainThread(); 159 | MyLilTimer *timer = [[self alloc] initWithBehavior:behavior timeInterval:intervalSinceNow target:target selector:action userInfo:userInfo]; 160 | [timer scheduleOnMainRunLoopForModes:[NSSet setWithObject:NSRunLoopCommonModes]]; 161 | return timer; 162 | } 163 | 164 | - (instancetype)initWithBehavior:(MyLilTimerBehavior)behavior timeInterval:(NSTimeInterval)intervalSinceNow target:(id)target selector:(SEL)action userInfo:(id)userInfo 165 | { 166 | if (!(self = [super init])) { 167 | return nil; 168 | } 169 | 170 | assertMainThread(); 171 | NSParameterAssert(isValidBehavior(behavior)); 172 | NSParameterAssert(target != nil); 173 | NSParameterAssert(action != NULL); 174 | if (!isValidBehavior(behavior) || !target || !action) { // assertions diabled 175 | return nil; 176 | } 177 | 178 | // NSTimer behavior 179 | intervalSinceNow = MAX(0.1e-3, intervalSinceNow); 180 | 181 | _behavior = behavior; 182 | _target = target; 183 | _action = action; 184 | _userInfo = userInfo; 185 | 186 | _fireClockValue = [[self class] nowValueForClock:MyLilTimerClockFromBehavior(self.behavior)] + intervalSinceNow; 187 | 188 | self.valid = YES; 189 | 190 | return self; 191 | } 192 | 193 | - (void)scheduleOnMainRunLoopForModes:(NSSet *)modes 194 | { 195 | assertMainThread(); 196 | if (_runLoopModes) { 197 | [NSException raise:NSInvalidArgumentException format:@"Timer can only be scheduled once"]; 198 | } 199 | NSParameterAssert(modes.count > 0); 200 | _runLoopModes = [modes copy]; 201 | 202 | _notifier = [MyLilTimerHostCalendarChangedNotifier sharedInstance]; 203 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkExpirationAndRescheduleIfNeeded:) name:MyLilTimerHostCalendarChangedNotification object:nil]; 204 | 205 | [self checkExpirationAndRescheduleIfNeeded:self]; 206 | } 207 | 208 | - (void)fire 209 | { 210 | assertMainThread(); 211 | if (!self.valid) { 212 | return; 213 | } 214 | 215 | ((void(*)(id, SEL, id))objc_msgSend)(_target, _action, self); 216 | //[_target performSelector:_action withObject:self]; 217 | 218 | [self invalidate]; 219 | } 220 | 221 | - (NSDate *)fireDate 222 | { return [NSDate dateWithTimeIntervalSinceNow:-self.timeSinceFireDate]; } 223 | 224 | - (NSTimeInterval)timeSinceFireDate 225 | { 226 | assertMainThread(); 227 | return [[self class] nowValueForClock:MyLilTimerClockFromBehavior(self.behavior)] - _fireClockValue; 228 | } 229 | 230 | - (void)setTolerance:(NSTimeInterval)tolerance 231 | { 232 | _tolerance = tolerance; 233 | [self checkExpirationAndRescheduleIfNeeded:self]; 234 | } 235 | 236 | - (void)invalidate 237 | { 238 | assertMainThread(); 239 | if (!self.valid) { 240 | return; 241 | } 242 | 243 | self.valid = NO; 244 | _target = nil; 245 | _userInfo = nil; 246 | 247 | if (!_runLoopModes) { 248 | return; // never scheduled 249 | } 250 | 251 | [_nextCheckTimer invalidate]; 252 | _nextCheckTimer = nil; 253 | 254 | [[NSNotificationCenter defaultCenter] removeObserver:self name:MyLilTimerHostCalendarChangedNotification object:nil]; 255 | _notifier = nil; 256 | } 257 | 258 | 259 | #pragma mark MyLilTimer: Private 260 | 261 | /// Sender is notification, timer, or self 262 | - (void)checkExpirationAndRescheduleIfNeeded:(id)sender 263 | { 264 | assertMainThread(); 265 | if (!self.valid || !_runLoopModes.count) { 266 | return; 267 | } 268 | 269 | // _nextCheckTimer may have the only strong reference to us; keep ourselves alive while it's invalidated 270 | __typeof(self) __attribute__((objc_precise_lifetime, unused)) strongSelf = self; 271 | 272 | [_nextCheckTimer invalidate]; 273 | _nextCheckTimer = nil; 274 | 275 | NSDate *fireDate = self.fireDate; 276 | if (fireDate.timeIntervalSinceNow <= 0) { 277 | // Need to fire; do so in its own run loop pass so callback is run in a consistent execution environment, and run loop is in an expected mode. 278 | // No need to keep track of "waiting to fire" state; multiple calls to -fire are harmless. 279 | [self performSelector:@selector(fire) withObject:nil afterDelay:0 inModes:_runLoopModes.allObjects]; 280 | return; 281 | } 282 | 283 | _nextCheckTimer = [[NSTimer alloc] initWithFireDate:fireDate interval:0 target:self selector:_cmd userInfo:nil repeats:NO]; 284 | if ([_nextCheckTimer respondsToSelector:@selector(setTolerance:)]) { // OS X 10.9, iOS 7.0 285 | _nextCheckTimer.tolerance = self.tolerance; 286 | } 287 | 288 | NSAssert([NSRunLoop currentRunLoop] == [NSRunLoop mainRunLoop], @"MyLilTimer only supports the main run loop"); 289 | NSRunLoop *runLoop = [NSRunLoop mainRunLoop]; 290 | for (NSString *mode in _runLoopModes) { 291 | [runLoop addTimer:_nextCheckTimer forMode:mode]; 292 | } 293 | } 294 | 295 | @end 296 | 297 | 298 | 299 | @implementation MyLilTimerHostCalendarChangedNotifier { 300 | CFRunLoopSourceRef _cfRunLoopSource; 301 | CFRunLoopRef _cfRunLoop; 302 | } 303 | 304 | #pragma mark NSObject 305 | 306 | - (instancetype)init 307 | { 308 | if (!(self = [super init])) { 309 | return nil; 310 | } 311 | 312 | _cfRunLoop = CFRunLoopGetCurrent(); 313 | NSAssert(_cfRunLoop, @"Need a current run loop"); 314 | 315 | // Implementation inspired by 316 | CFMachPortRef cfMachPort = CFMachPortCreate(kCFAllocatorDefault, handleHostCalendarChangeMessage, NULL, NULL); 317 | NSAssert(cfMachPort, nil); 318 | 319 | _cfRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort, 0); 320 | NSAssert(_cfRunLoopSource, nil); 321 | CFRunLoopAddSource(_cfRunLoop, _cfRunLoopSource, kCFRunLoopDefaultMode); 322 | 323 | registerForHostCalendarChangeNotificationOnMachPort(CFMachPortGetPort(cfMachPort)); 324 | 325 | return self; 326 | } 327 | 328 | - (void)dealloc { 329 | CFRunLoopRemoveSource(_cfRunLoop, _cfRunLoopSource, kCFRunLoopDefaultMode); 330 | CFRelease(_cfRunLoopSource); 331 | } 332 | 333 | 334 | #pragma mark MyLilTimerHostCalendarChangedNotifier 335 | 336 | + (instancetype)sharedInstance 337 | { 338 | static __weak MyLilTimerHostCalendarChangedNotifier *weakSharedInstance; 339 | MyLilTimerHostCalendarChangedNotifier *sharedInstance = weakSharedInstance; 340 | if (!sharedInstance) { 341 | sharedInstance = [[self alloc] init]; 342 | weakSharedInstance = sharedInstance; 343 | } 344 | return sharedInstance; 345 | } 346 | 347 | 348 | // These are easier implemented as C functions compared to instance methods to avoid needing a CFMachPortContext struct. 349 | static void registerForHostCalendarChangeNotificationOnMachPort(mach_port_t port) 350 | { 351 | kern_return_t __unused result = host_request_notification(mach_host_self(), HOST_NOTIFY_CALENDAR_CHANGE, port); 352 | NSCAssert(result == KERN_SUCCESS, @"host_request_notification error"); 353 | } 354 | 355 | static void handleHostCalendarChangeMessage(CFMachPortRef port, void *msg, CFIndex size, void *info) 356 | { 357 | const mach_msg_header_t *header = msg; 358 | if (!header || header->msgh_id != HOST_CALENDAR_CHANGED_REPLYID) { 359 | return; 360 | } 361 | 362 | // Register again 363 | registerForHostCalendarChangeNotificationOnMachPort(header->msgh_local_port); 364 | 365 | [[NSNotificationCenter defaultCenter] postNotificationName:MyLilTimerHostCalendarChangedNotification object:nil]; 366 | } 367 | 368 | 369 | @end 370 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /MyLilTimer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "MyLilTimer" 3 | s.version = "0.1.0" 4 | s.summary = "Timer class for iOS and OS X offering a choice of behaviors." 5 | s.homepage = "https://github.com/jmah/MyLilTimer" 6 | s.license = 'Public Domain' 7 | s.author = { "Jonathon Mah" => "me@JonathonMah.com" } 8 | s.source = { 9 | :git => "https://github.com/jmah/MyLilTimer.git", 10 | :tag => "v#{s.version}" 11 | } 12 | 13 | s.ios.deployment_target = '4.0' 14 | s.osx.deployment_target = '10.6' 15 | s.requires_arc = true 16 | 17 | s.source_files = 'Classes/*.{h,m}' 18 | end 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyLilTimer 2 | 3 | ## Know your clocks 4 | 5 | `MyLilTimer` is a Cocoa timer class for iOS and Mac OS X. Its only dependency is the Foundation framework. `MyLilTimer` has an interface similar to `NSTimer`, while also providing a choice of three behaviors (three different clocks). 6 | 7 | Read the accompanying article on dev etc: [Timers, Clocks, and Cocoa](http://devetc.org/code/2014/01/21/timers-clocks-and-cocoa.html). 8 | 9 | ## Behaviors 10 | 11 | The three behaviors are visualized in [this YouTube video](http://www.youtube.com/watch?v=ZRM8mq-ZSO0). 12 | 13 | ### MyLilTimerBehaviorHourglass (MyLilTimerClockBoottime) 14 | 15 | The timer fires after an interval has elapsed, regardless of system clock changes and system sleep. This is the behavior people often expect from `NSTimer`, and it appears to have this behavior **when running under Xcode** (and when an iOS device is plugged in to power). 16 | 17 | ### MyLilTimerBehaviorPauseOnSystemSleep (MyLilTimerClockMonotonic) 18 | 19 | The timer fires after the system has run for some duration; this is paused while the operating system was asleep. This is the **actual** behavior of `NSTimer` — an iOS device will sleep when unplugged from power, the screen is locked, and no apps are keeping it awake. 20 | 21 | This behavior is appropriate for measuring the speed of long-running operations, such as “3 of 7 items processed, 30 seconds remaining”. 22 | 23 | ### MyLilTimerBehaviorObeySystemClockChanges (MyLilTimerClockRealtime) 24 | 25 | The timer fires when the time on the system clock passes the fire date. This behavior is appropriate for an alarm that fires when the clock shows a particular time. 26 | 27 | 28 | ## Usage 29 | 30 | The most common way to set a timer is with the class convenience method: 31 | 32 | +[MyLilTimer scheduledTimerWithBehavior:(MyLilTimerBehavior)behavior 33 | timeInterval:(NSTimeInterval)intervalSinceNow 34 | target:(id)target 35 | selector:(SEL)action 36 | userInfo:(id)userInfo] 37 | 38 | The `TimerTest` iPhone / iPad app demonstrates the use of `MyLilTimer`, along with all three behaviors. 39 | 40 | 41 | ## Requirements 42 | 43 | `MyLilTimer` uses automatic reference counting and the `dispatch_once` GCD function, which are available on Mac OS X 10.6+ and iOS 4+. 44 | 45 | If you so wanted, it would be easy to remove both these dependencies for compatibility to much earlier systems. 46 | 47 | 48 | ## Installation 49 | 50 | Simply add `MyLilTimer.h` and `MyLilTimer.m` to your project file. 51 | 52 | Or, with [CocoaPods](http://cocoapods.org), add to your Podfile: 53 | 54 | pod 'MyLilTimer' 55 | 56 | (Note: I have never used CocoaPods and don't otherwise endorse it,) 57 | 58 | 59 | ## NSTimer features not (yet) supported 60 | 61 | - Repeating timers 62 | - Changing the fire date 63 | - Use on run loops other than the main run loop (including background threads) 64 | 65 | 66 | ## License 67 | 68 | MyLilTimer is released into the public domain, and may be used with or without modification with no restrictions, attribution, or warranty. 69 | Attribution is kindly requested, but not required. 70 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Runs the specs [EMPTY]" 2 | task :spec do 3 | # Provide your own implementation 4 | end 5 | 6 | task :version do 7 | git_remotes = `git remote`.strip.split("\n") 8 | 9 | if git_remotes.count > 0 10 | puts "-- fetching version number from github" 11 | sh 'git fetch' 12 | 13 | remote_version = remote_spec_version 14 | end 15 | 16 | if remote_version.nil? 17 | puts "There is no current released version. You're about to release a new Pod." 18 | version = "0.0.1" 19 | else 20 | puts "The current released version of your pod is " + remote_spec_version.to_s() 21 | version = suggested_version_number 22 | end 23 | 24 | puts "Enter the version you want to release (" + version + ") " 25 | new_version_number = $stdin.gets.strip 26 | if new_version_number == "" 27 | new_version_number = version 28 | end 29 | 30 | replace_version_number(new_version_number) 31 | end 32 | 33 | desc "Release a new version of the Pod" 34 | task :release do 35 | 36 | puts "* Running version" 37 | sh "rake version" 38 | 39 | unless ENV['SKIP_CHECKS'] 40 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' 41 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." 42 | exit 1 43 | end 44 | 45 | if `git tag`.strip.split("\n").include?(spec_version) 46 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" 47 | exit 1 48 | end 49 | 50 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]" 51 | exit if $stdin.gets.strip.downcase != 'y' 52 | end 53 | 54 | puts "* Running specs" 55 | sh "rake spec" 56 | 57 | puts "* Linting the podspec" 58 | sh "pod lib lint" 59 | 60 | # Then release 61 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}'" 62 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" 63 | sh "git push origin master" 64 | sh "git push origin --tags" 65 | sh "pod push master #{podspec_path}" 66 | end 67 | 68 | # @return [Pod::Version] The version as reported by the Podspec. 69 | # 70 | def spec_version 71 | require 'cocoapods' 72 | spec = Pod::Specification.from_file(podspec_path) 73 | spec.version 74 | end 75 | 76 | # @return [Pod::Version] The version as reported by the Podspec from remote. 77 | # 78 | def remote_spec_version 79 | require 'cocoapods-core' 80 | 81 | if spec_file_exist_on_remote? 82 | remote_spec = eval(`git show origin/master:#{podspec_path}`) 83 | remote_spec.version 84 | else 85 | nil 86 | end 87 | end 88 | 89 | # @return [Bool] If the remote repository has a copy of the podpesc file or not. 90 | # 91 | def spec_file_exist_on_remote? 92 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; 93 | then 94 | echo 'true' 95 | else 96 | echo 'false' 97 | fi` 98 | 99 | 'true' == test_condition.strip 100 | end 101 | 102 | # @return [String] The relative path of the Podspec. 103 | # 104 | def podspec_path 105 | podspecs = Dir.glob('*.podspec') 106 | if podspecs.count == 1 107 | podspecs.first 108 | else 109 | raise "Could not select a podspec" 110 | end 111 | end 112 | 113 | # @return [String] The suggested version number based on the local and remote version numbers. 114 | # 115 | def suggested_version_number 116 | if spec_version != remote_spec_version 117 | spec_version.to_s() 118 | else 119 | next_version(spec_version).to_s() 120 | end 121 | end 122 | 123 | # @param [Pod::Version] version 124 | # the version for which you need the next version 125 | # 126 | # @note It is computed by bumping the last component of the versino string by 1. 127 | # 128 | # @return [Pod::Version] The version that comes next after the version supplied. 129 | # 130 | def next_version(version) 131 | version_components = version.to_s().split("."); 132 | last = (version_components.last.to_i() + 1).to_s 133 | version_components[-1] = last 134 | Pod::Version.new(version_components.join(".")) 135 | end 136 | 137 | # @param [String] new_version_number 138 | # the new version number 139 | # 140 | # @note This methods replaces the version number in the podspec file with a new version number. 141 | # 142 | # @return void 143 | # 144 | def replace_version_number(new_version_number) 145 | text = File.read(podspec_path) 146 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/, "\\1#{new_version_number}\\3") 147 | File.open(podspec_path, "w") { |file| file.puts text } 148 | end -------------------------------------------------------------------------------- /TimerTest/TimerTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0BDAF7AD188340DD00574AF9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BDAF7AC188340DD00574AF9 /* Foundation.framework */; }; 11 | 0BDAF7AF188340DD00574AF9 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BDAF7AE188340DD00574AF9 /* CoreGraphics.framework */; }; 12 | 0BDAF7B1188340DD00574AF9 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BDAF7B0188340DD00574AF9 /* UIKit.framework */; }; 13 | 0BDAF7B9188340DD00574AF9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BDAF7B8188340DD00574AF9 /* main.m */; }; 14 | 0BDAF7BD188340DD00574AF9 /* TimerTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BDAF7BC188340DD00574AF9 /* TimerTestAppDelegate.m */; }; 15 | 0BDAF7C6188340DD00574AF9 /* TimerTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BDAF7C5188340DD00574AF9 /* TimerTestViewController.m */; }; 16 | 0BDAF7E9188375E900574AF9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0BDAF7E7188375E900574AF9 /* Main.storyboard */; }; 17 | 0BDAF7EC188377D300574AF9 /* MyLilTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BDAF7EB188377D300574AF9 /* MyLilTimer.m */; }; 18 | 0BDAF7EE188381B000574AF9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BDAF7ED188381B000574AF9 /* Images.xcassets */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 0BDAF7A9188340DD00574AF9 /* TimerTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TimerTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 0BDAF7AC188340DD00574AF9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 24 | 0BDAF7AE188340DD00574AF9 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 25 | 0BDAF7B0188340DD00574AF9 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 26 | 0BDAF7B4188340DD00574AF9 /* TimerTest-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TimerTest-Info.plist"; sourceTree = ""; }; 27 | 0BDAF7B8188340DD00574AF9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; usesTabs = 0; }; 28 | 0BDAF7BB188340DD00574AF9 /* TimerTestAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TimerTestAppDelegate.h; sourceTree = ""; }; 29 | 0BDAF7BC188340DD00574AF9 /* TimerTestAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TimerTestAppDelegate.m; sourceTree = ""; }; 30 | 0BDAF7C4188340DD00574AF9 /* TimerTestViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TimerTestViewController.h; sourceTree = ""; }; 31 | 0BDAF7C5188340DD00574AF9 /* TimerTestViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TimerTestViewController.m; sourceTree = ""; }; 32 | 0BDAF7E8188375E900574AF9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 33 | 0BDAF7EA188377D300574AF9 /* MyLilTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MyLilTimer.h; sourceTree = ""; }; 34 | 0BDAF7EB188377D300574AF9 /* MyLilTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MyLilTimer.m; sourceTree = ""; }; 35 | 0BDAF7ED188381B000574AF9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFrameworksBuildPhase section */ 39 | 0BDAF7A6188340DD00574AF9 /* Frameworks */ = { 40 | isa = PBXFrameworksBuildPhase; 41 | buildActionMask = 2147483647; 42 | files = ( 43 | 0BDAF7AF188340DD00574AF9 /* CoreGraphics.framework in Frameworks */, 44 | 0BDAF7B1188340DD00574AF9 /* UIKit.framework in Frameworks */, 45 | 0BDAF7AD188340DD00574AF9 /* Foundation.framework in Frameworks */, 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 0BC52646188687DF00D08D9F /* MyLilTimer */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 0BDAF7EA188377D300574AF9 /* MyLilTimer.h */, 56 | 0BDAF7EB188377D300574AF9 /* MyLilTimer.m */, 57 | ); 58 | name = MyLilTimer; 59 | path = ../Classes; 60 | sourceTree = ""; 61 | }; 62 | 0BDAF7A0188340DD00574AF9 = { 63 | isa = PBXGroup; 64 | children = ( 65 | 0BC52646188687DF00D08D9F /* MyLilTimer */, 66 | 0BDAF7B2188340DD00574AF9 /* TimerTest */, 67 | 0BDAF7AB188340DD00574AF9 /* Frameworks */, 68 | 0BDAF7AA188340DD00574AF9 /* Products */, 69 | ); 70 | sourceTree = ""; 71 | usesTabs = 0; 72 | }; 73 | 0BDAF7AA188340DD00574AF9 /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 0BDAF7A9188340DD00574AF9 /* TimerTest.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | 0BDAF7AB188340DD00574AF9 /* Frameworks */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 0BDAF7AC188340DD00574AF9 /* Foundation.framework */, 85 | 0BDAF7AE188340DD00574AF9 /* CoreGraphics.framework */, 86 | 0BDAF7B0188340DD00574AF9 /* UIKit.framework */, 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | 0BDAF7B2188340DD00574AF9 /* TimerTest */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 0BDAF7BB188340DD00574AF9 /* TimerTestAppDelegate.h */, 95 | 0BDAF7BC188340DD00574AF9 /* TimerTestAppDelegate.m */, 96 | 0BDAF7C4188340DD00574AF9 /* TimerTestViewController.h */, 97 | 0BDAF7C5188340DD00574AF9 /* TimerTestViewController.m */, 98 | 0BDAF7E7188375E900574AF9 /* Main.storyboard */, 99 | 0BDAF7ED188381B000574AF9 /* Images.xcassets */, 100 | 0BDAF7B3188340DD00574AF9 /* Supporting Files */, 101 | ); 102 | path = TimerTest; 103 | sourceTree = ""; 104 | }; 105 | 0BDAF7B3188340DD00574AF9 /* Supporting Files */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 0BDAF7B4188340DD00574AF9 /* TimerTest-Info.plist */, 109 | 0BDAF7B8188340DD00574AF9 /* main.m */, 110 | ); 111 | name = "Supporting Files"; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | 0BDAF7A8188340DD00574AF9 /* TimerTest */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = 0BDAF7DE188340DD00574AF9 /* Build configuration list for PBXNativeTarget "TimerTest" */; 120 | buildPhases = ( 121 | 0BDAF7A5188340DD00574AF9 /* Sources */, 122 | 0BDAF7A6188340DD00574AF9 /* Frameworks */, 123 | 0BDAF7A7188340DD00574AF9 /* Resources */, 124 | ); 125 | buildRules = ( 126 | ); 127 | dependencies = ( 128 | ); 129 | name = TimerTest; 130 | productName = TimerTest; 131 | productReference = 0BDAF7A9188340DD00574AF9 /* TimerTest.app */; 132 | productType = "com.apple.product-type.application"; 133 | }; 134 | /* End PBXNativeTarget section */ 135 | 136 | /* Begin PBXProject section */ 137 | 0BDAF7A1188340DD00574AF9 /* Project object */ = { 138 | isa = PBXProject; 139 | attributes = { 140 | CLASSPREFIX = TimerTest; 141 | LastUpgradeCheck = 0510; 142 | ORGANIZATIONNAME = "Jonathon Mah"; 143 | }; 144 | buildConfigurationList = 0BDAF7A4188340DD00574AF9 /* Build configuration list for PBXProject "TimerTest" */; 145 | compatibilityVersion = "Xcode 3.2"; 146 | developmentRegion = English; 147 | hasScannedForEncodings = 0; 148 | knownRegions = ( 149 | en, 150 | Base, 151 | ); 152 | mainGroup = 0BDAF7A0188340DD00574AF9; 153 | productRefGroup = 0BDAF7AA188340DD00574AF9 /* Products */; 154 | projectDirPath = ""; 155 | projectRoot = ""; 156 | targets = ( 157 | 0BDAF7A8188340DD00574AF9 /* TimerTest */, 158 | ); 159 | }; 160 | /* End PBXProject section */ 161 | 162 | /* Begin PBXResourcesBuildPhase section */ 163 | 0BDAF7A7188340DD00574AF9 /* Resources */ = { 164 | isa = PBXResourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | 0BDAF7E9188375E900574AF9 /* Main.storyboard in Resources */, 168 | 0BDAF7EE188381B000574AF9 /* Images.xcassets in Resources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXResourcesBuildPhase section */ 173 | 174 | /* Begin PBXSourcesBuildPhase section */ 175 | 0BDAF7A5188340DD00574AF9 /* Sources */ = { 176 | isa = PBXSourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | 0BDAF7BD188340DD00574AF9 /* TimerTestAppDelegate.m in Sources */, 180 | 0BDAF7B9188340DD00574AF9 /* main.m in Sources */, 181 | 0BDAF7C6188340DD00574AF9 /* TimerTestViewController.m in Sources */, 182 | 0BDAF7EC188377D300574AF9 /* MyLilTimer.m in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 0BDAF7E7188375E900574AF9 /* Main.storyboard */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 0BDAF7E8188375E900574AF9 /* Base */, 193 | ); 194 | name = Main.storyboard; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXVariantGroup section */ 198 | 199 | /* Begin XCBuildConfiguration section */ 200 | 0BDAF7DC188340DD00574AF9 /* Debug */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 205 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 206 | CLANG_CXX_LIBRARY = "libc++"; 207 | CLANG_ENABLE_MODULES = YES; 208 | CLANG_ENABLE_OBJC_ARC = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 212 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 213 | CLANG_WARN_EMPTY_BODY = YES; 214 | CLANG_WARN_ENUM_CONVERSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 217 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 218 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 219 | COPY_PHASE_STRIP = NO; 220 | GCC_C_LANGUAGE_STANDARD = gnu99; 221 | GCC_DYNAMIC_NO_PIC = NO; 222 | GCC_OPTIMIZATION_LEVEL = 0; 223 | GCC_PREPROCESSOR_DEFINITIONS = ( 224 | "DEBUG=1", 225 | "$(inherited)", 226 | ); 227 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 228 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 229 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 230 | GCC_WARN_UNDECLARED_SELECTOR = YES; 231 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 232 | GCC_WARN_UNUSED_FUNCTION = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 235 | ONLY_ACTIVE_ARCH = YES; 236 | SDKROOT = iphoneos; 237 | TARGETED_DEVICE_FAMILY = "1,2"; 238 | }; 239 | name = Debug; 240 | }; 241 | 0BDAF7DD188340DD00574AF9 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_SEARCH_USER_PATHS = NO; 245 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 246 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 247 | CLANG_CXX_LIBRARY = "libc++"; 248 | CLANG_ENABLE_MODULES = YES; 249 | CLANG_ENABLE_OBJC_ARC = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 259 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 260 | COPY_PHASE_STRIP = YES; 261 | ENABLE_NS_ASSERTIONS = NO; 262 | GCC_C_LANGUAGE_STANDARD = gnu99; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 270 | SDKROOT = iphoneos; 271 | TARGETED_DEVICE_FAMILY = "1,2"; 272 | VALIDATE_PRODUCT = YES; 273 | }; 274 | name = Release; 275 | }; 276 | 0BDAF7DF188340DD00574AF9 /* Debug */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 280 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 281 | INFOPLIST_FILE = "TimerTest/TimerTest-Info.plist"; 282 | PRODUCT_NAME = "$(TARGET_NAME)"; 283 | WRAPPER_EXTENSION = app; 284 | }; 285 | name = Debug; 286 | }; 287 | 0BDAF7E0188340DD00574AF9 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 292 | INFOPLIST_FILE = "TimerTest/TimerTest-Info.plist"; 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | WRAPPER_EXTENSION = app; 295 | }; 296 | name = Release; 297 | }; 298 | /* End XCBuildConfiguration section */ 299 | 300 | /* Begin XCConfigurationList section */ 301 | 0BDAF7A4188340DD00574AF9 /* Build configuration list for PBXProject "TimerTest" */ = { 302 | isa = XCConfigurationList; 303 | buildConfigurations = ( 304 | 0BDAF7DC188340DD00574AF9 /* Debug */, 305 | 0BDAF7DD188340DD00574AF9 /* Release */, 306 | ); 307 | defaultConfigurationIsVisible = 0; 308 | defaultConfigurationName = Release; 309 | }; 310 | 0BDAF7DE188340DD00574AF9 /* Build configuration list for PBXNativeTarget "TimerTest" */ = { 311 | isa = XCConfigurationList; 312 | buildConfigurations = ( 313 | 0BDAF7DF188340DD00574AF9 /* Debug */, 314 | 0BDAF7E0188340DD00574AF9 /* Release */, 315 | ); 316 | defaultConfigurationIsVisible = 0; 317 | defaultConfigurationName = Release; 318 | }; 319 | /* End XCConfigurationList section */ 320 | }; 321 | rootObject = 0BDAF7A1188340DD00574AF9 /* Project object */; 322 | } 323 | -------------------------------------------------------------------------------- /TimerTest/TimerTest.xcodeproj/xcshareddata/xcschemes/TimerTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 | 37 | 44 | 51 | 58 | 65 | 72 | 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 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/Images.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" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /TimerTest/TimerTest/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /TimerTest/TimerTest/TimerTest-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.jmah.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/TimerTestAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimerTestAppDelegate.h 3 | // MyLilTimer 4 | // 5 | // Created by Jonathon Mah on 2014-01-12. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface TimerTestAppDelegate : UIResponder 13 | 14 | @property (strong, nonatomic) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/TimerTestAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // TimerTestAppDelegate.m 3 | // MyLilTimer 4 | // 5 | // Created by Jonathon Mah on 2014-01-12. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import "TimerTestAppDelegate.h" 10 | 11 | 12 | @implementation TimerTestAppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | return YES; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/TimerTestViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimerTestViewController.h 3 | // MyLilTimer 4 | // 5 | // Created by Jonathon Mah on 2014-01-12. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface TimerTestViewController : UIViewController 13 | 14 | - (IBAction)restartTimers:(id)sender; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/TimerTestViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TimerTestViewController.m 3 | // MyLilTimer 4 | // 5 | // Created by Jonathon Mah on 2014-01-12. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import "TimerTestViewController.h" 10 | 11 | #import "MyLilTimer.h" 12 | 13 | 14 | @interface TimerTestViewController () 15 | 16 | @property (nonatomic) IBOutlet UILabel *hourglassLabel; 17 | @property (nonatomic) IBOutlet UILabel *pauseOnSystemSleepLabel; 18 | @property (nonatomic) IBOutlet UILabel *obeySystemClockChangesLabel; 19 | 20 | @end 21 | 22 | 23 | @implementation TimerTestViewController { 24 | NSMutableArray *_validTimers; 25 | NSTimer *_updateLabelsTimer; 26 | NSNumberFormatter *_numberFormatter; 27 | } 28 | 29 | - (void)viewDidLoad 30 | { 31 | [super viewDidLoad]; 32 | _validTimers = [NSMutableArray array]; 33 | _numberFormatter = [[NSNumberFormatter alloc] init]; 34 | _numberFormatter.numberStyle = NSNumberFormatterDecimalStyle; 35 | _numberFormatter.minimumFractionDigits = _numberFormatter.maximumFractionDigits = 1; 36 | } 37 | 38 | - (IBAction)restartTimers:(id)sender 39 | { 40 | [_updateLabelsTimer invalidate]; 41 | for (MyLilTimer *timer in _validTimers) { 42 | [timer invalidate]; 43 | } 44 | [_validTimers removeAllObjects]; 45 | 46 | static const NSTimeInterval interval = 60; 47 | 48 | [_validTimers addObject:[MyLilTimer scheduledTimerWithBehavior:MyLilTimerBehaviorHourglass timeInterval:interval target:self selector:@selector(timerFired:) userInfo:self.hourglassLabel]]; 49 | [_validTimers addObject:[MyLilTimer scheduledTimerWithBehavior:MyLilTimerBehaviorPauseOnSystemSleep timeInterval:interval target:self selector:@selector(timerFired:) userInfo:self.pauseOnSystemSleepLabel]]; 50 | [_validTimers addObject:[MyLilTimer scheduledTimerWithBehavior:MyLilTimerBehaviorObeySystemClockChanges timeInterval:interval target:self selector:@selector(timerFired:) userInfo:self.obeySystemClockChangesLabel]]; 51 | 52 | _updateLabelsTimer = [NSTimer scheduledTimerWithTimeInterval:(1. / 10.) target:self selector:@selector(updateLabelsTimerFired:) userInfo:nil repeats:YES]; 53 | [_updateLabelsTimer fire]; 54 | } 55 | 56 | - (void)updateLabelsTimerFired:(NSTimer *)timer 57 | { 58 | for (MyLilTimer *timer in _validTimers) { 59 | [self updateLabelForTimer:timer]; 60 | } 61 | } 62 | 63 | - (void)updateLabelForTimer:(MyLilTimer *)timer 64 | { 65 | UILabel *label = timer.userInfo; 66 | NSTimeInterval timeSinceFireDate = timer.timeSinceFireDate; 67 | if (timeSinceFireDate < 0) { 68 | label.backgroundColor = [UIColor whiteColor]; 69 | label.textColor = [UIColor blackColor]; 70 | } else { 71 | label.backgroundColor = [UIColor greenColor]; 72 | label.textColor = [UIColor whiteColor]; 73 | } 74 | 75 | label.text = [_numberFormatter stringFromNumber:@(timeSinceFireDate)]; 76 | } 77 | 78 | - (void)timerFired:(MyLilTimer *)timer 79 | { 80 | [_validTimers removeObject:timer]; 81 | [self updateLabelForTimer:timer]; 82 | 83 | if (!_validTimers.count) { 84 | [_updateLabelsTimer invalidate]; 85 | _updateLabelsTimer = nil; 86 | } 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /TimerTest/TimerTest/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MyLilTimer 4 | // 5 | // Created by Jonathon Mah on 2014-01-12. 6 | // This is free and unencumbered software released into the public domain. 7 | // 8 | 9 | #import 10 | 11 | #import "TimerTestAppDelegate.h" 12 | 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | @autoreleasepool { 17 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([TimerTestAppDelegate class])); 18 | } 19 | } 20 | --------------------------------------------------------------------------------