神刀安全网

NSTimer – Repeat

The NSTimer class is a workhorse of iOS applications, but it is also complex, fraught with hidden gotchas and cumbersome to use. For example, when a timer expires, its callback mechanism consists of performing a selector on a target object, which doesn’t work with a pure Swift class (or struct, etc.) and forces us to use NSObject-derived classes. Also, the run loop maintains a strong reference to a timer’s target object, which can be unexpected and problematic. To break the strong reference, we must stop the timer (by calling invalidate()), but only from the same thread on which the timer was installed. And once invalidated, timers cannot be reused.

by a guest blogger

The core Swift team often asks, “if we didn’t already have it, would we add it to Swift 3?” If the NSTimer API didn’t already exist, what kind of API might we like to have? It would probably:

  •  Be simple to use for simple cases, with a clean, Swifty syntax.
  •  Be closure based.
  • Allow us to hold weak references to objects.
  • Be thread-safe for subscribing and cancelling timers.

Let’s write some hypothetical snippets of how we might like to use an API like this in our program. Once we’re happy with the way it looks, we can work backwards and figure out the implementation details.For a timer that’s going to run one time, we would need to provide the time interval and the code that’s going to execute:

Repeat.once(after: 1) {       print("running once after 1 second") }
Repeat.once(after: 1) {       print("running once after 1 second") } 

Of course we should support a repeating timer as well:

Repeat.every(seconds: 5) {       print("another 5 seconds has gone by..") }
Repeat.every(seconds: 5) {       print("another 5 seconds has gone by..") } 

What if we might need to cancel our timer? There should be a way to keep track of a specific closure that we have scheduled, that doesn’t get in our way when we don’t need it, but is there when we do:

let id = Repeat.once(after: 10) {       print("this is never going to run") } id.invalidate()
let id = Repeat.once(after: 10) {       print("this is never going to run") } id.invalidate() 

It might also be convenient to alter the timer from within the closure itself, perhaps in response to some condition. We might want to stop the timer, repeat it with the same interval, or with a different interval (not shown here):

Repeat.after(seconds: 5) {       print("still here...")        if shouldStop() {             return .Stop       } else {             return .Repeat       } }
Repeat.after(seconds: 5) {       print("still here...")         if shouldStop() {             return .Stop       } else {             return .Repeat       } } 

A closure-based API allows us to easily nest timers as well! The below example counts to 10 (after an initial delay), increasing its speed half-way through:

// after 3 seconds, Repeat.once(after: 3) {       var count = 0        // start off refreshing every 1 second       Repeat.after(seconds: 1) {             print("count = /(count)")             count += 1              // if we get to 10, stop             if count == 10 {                   return .Stop             }             // at 5, go faster             else if count == 5 {                   return .RepeatAfter(0.5)             }             // otherwise repeat at the current rate             else {                   return .Repeat                  }             }       } }
// after 3 seconds, Repeat.once(after: 3) {       var count = 0         // start off refreshing every 1 second       Repeat.after(seconds: 1) {             print("count = /(count)")             count += 1               // if we get to 10, stop             if count == 10 {                   return .Stop             }             // at 5, go faster             else if count == 5 {                   return .RepeatAfter(0.5)             }             // otherwise repeat at the current rate             else {                   return .Repeat                 }             }       } } 

Let’s recap where we are so far. Though the specific details of the above API could be altered to suit one’s preferences, we have created an API that:

  • Is clean and simple to use for simple cases, with increased sophistication available when needed.
  • Keeps the scheduling of the timer close to the code that will execute when the timer fires.
  • Uses closures in a way that feels at home in Swift code and allows for the use of capture lists to capture references weakly.

The following public functions are exposed (the source file contains more-detailed comments):

// Execute closure once after given delay (in seconds) Repeat.once(after: NSTimeInterval, closure: () -> ()) -> RepeatSubscriberId  // Execute closure indefinitely with given delay Repeat.every(seconds: NSTimeInterval, closure: () -> ()) -> RepeatSubscriberId  // Execute closure after given delay. Further executions/delays controlled by  // return value from closure, which can be .Stop, .Repeat or .RepeatAfter(NSTimeInterval) Repeat.after(seconds: NSTimeInterval, closure: () -> Repeat.Result) -> RepeatSubscriberId  // Invalidates closure execution for the given subscriber(s) Repeat.invalidate(id: RepeatSubscriberId) -> Bool Repeat.invalidate(ids: [RepeatSubscriberId]) -> [Bool]
// Execute closure once after given delay (in seconds) Repeat.once(after: NSTimeInterval, closure: () -> ()) -> RepeatSubscriberId   // Execute closure indefinitely with given delay Repeat.every(seconds: NSTimeInterval, closure: () -> ()) -> RepeatSubscriberId   // Execute closure after given delay. Further executions/delays controlled by // return value from closure, which can be .Stop, .Repeat or .RepeatAfter(NSTimeInterval) Repeat.after(seconds: NSTimeInterval, closure: () -> Repeat.Result) -> RepeatSubscriberId   // Invalidates closure execution for the given subscriber(s) Repeat.invalidate(id: RepeatSubscriberId) -> Bool Repeat.invalidate(ids: [RepeatSubscriberId]) -> [Bool] 

A reference implementation, which uses GCD as the underlying timer mechanism, is available to download . A few implementation notes:

  • The public API is thread-safe via a lock on the singleton Repeat instance (equivalent to wrapping the body of each function in Objective-C’s @synchronized {}). Consequently, a reasonable number of timers is expected to exist at any one time – i.e. not hundreds/thousands/etc.
  • The provided closures are executed on the main queue.
  • Like NSTimer and GCD, this is not suitable for realtime needs (i.e. don’t drive your game’s run loop or latency-sensitive audio processing with this). According to Apple’s documentation, “the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds.”

import Foundation

typealias RepeatClosure = () -> () typealias RepeatClosureWithRet = () -> Repeat.Result

typealias RepeatSubscriberId = UInt

class Repeat { enum Result { case Stop case Repeat case RepeatAfter(NSTimeInterval) }

private struct SubscriberInfo {     let closure: RepeatClosureWithRet     var timeInterval: NSTimeInterval }  private static let sharedInstance = Repeat()  private static var subscribers: [RepeatSubscriberId: SubscriberInfo] = [:]  private static var _nextSubscriberId: RepeatSubscriberId = 0 private static var nextSubscriberId: RepeatSubscriberId {     let id = _nextSubscriberId     _nextSubscriberId += 1     return id }   // Execute a closure once  // - Parameters: // - after: The timeInterval in seconds after which the closure is executed // - closure: The closure to execute  // - Returns: Id which can be used to invalidate execution of the closure  static func once(after timeInterval: NSTimeInterval, closure: RepeatClosure) -> RepeatSubscriberId {     let closureWithRet: RepeatClosureWithRet = {         closure()         return .Stop     }     return Repeat.dispatch(timeInterval, closure: closureWithRet) }   // Execute a closure repeatedly  // - Parameters: // - seconds: The timeInterval in seconds after which the closure is executed // - closure: The closure to execute  // - Returns: Id which can be used to invalidate execution of the closure  static func every(seconds timeInterval: NSTimeInterval, closure: RepeatClosure) -> RepeatSubscriberId {     let closureWithRet: RepeatClosureWithRet = {         closure()         return .Repeat     }     return Repeat.dispatch(timeInterval, closure: closureWithRet) }   // Execute a closure after a desired delay. The closure's return param - to be provided by the client - will control whether the closure repeats (with the same or a different delay) or stops.  // - Parameters: // - seconds: The timeInterval in seconds after which the closure is executed // - closure: The closure to execute  // - Returns: Id which can be used to invalidate execution of the closure  static func after(seconds timeInterval: NSTimeInterval, closure: RepeatClosureWithRet) -> RepeatSubscriberId {     return Repeat.dispatch(timeInterval, closure: closure) }   // Internal function which does the subscription and scheduling of the closures  // - Parameters: // - timeInterval: TimeInterval (in seconds) until execution of closure // - closure: Closure to execute, should return RepeatResult  // - Returns: Id which can be used to invalidate execution of the closure  static private func dispatch(timeInterval: NSTimeInterval, closure: RepeatClosureWithRet) -> RepeatSubscriberId {     assert(timeInterval > 0, "Expecting intervalSecs to be > 0, not /(timeInterval)")      // thread safety     objc_sync_enter(Repeat.sharedInstance)     defer { objc_sync_exit(Repeat.sharedInstance) }      // setup info for the repeat request     let id = Repeat.nextSubscriberId     Repeat.subscribers[id] = SubscriberInfo(closure: closure, timeInterval: timeInterval)      // call the actual dispatch     Repeat.dispatch(subscriberId: id, timeInterval: timeInterval)      return id }   // Internal function which does the scheduling of the closures  // - Parameters: // - subscriberId: SubscribedId to dispatch for // - timeInterval: time until the next desired callback. Could be looked up via `subscriberId`, but 'unrolled' to avoid the unnecessary dictionary lookup, as both call sites (of this function) have it readily available.  static private func dispatch(subscriberId subscriberId: RepeatSubscriberId, timeInterval: NSTimeInterval) {     assert(Repeat.subscribers.keys.contains(subscriberId), "Invalid subscriberId /(subscriberId)")     assert(timeInterval > 0, "Expecting intervalSecs to be > 0, not /(timeInterval)")      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSTimeInterval(NSEC_PER_SEC) * timeInterval)), dispatch_get_main_queue()) {         Repeat.sharedInstance.timerCallback(subscriberId)     } }   // Invalidates closure execution for the given subscriberId  // - Parameters: // - id: SubscriberId to cancel execution for  // - Returns: Whether a subscriber was found and invalidated  static func invalidate(id: RepeatSubscriberId) -> Bool {     objc_sync_enter(Repeat.sharedInstance)     defer { objc_sync_exit(Repeat.sharedInstance) }      return Repeat.subscribers.removeValueForKey(id) != nil }   // Invalidates closure execution for the given subscribers  // - Parameters: // - ids: SubscriberIds to invalidate  // - Returns: List of booleans which indicate whether each given subscriber was found and invalidated  static func invalidate(ids: [RepeatSubscriberId]) -> [Bool] {     guard !ids.isEmpty else { return [] }      objc_sync_enter(Repeat.sharedInstance)     defer { objc_sync_exit(Repeat.sharedInstance) }      return ids.map { Repeat.subscribers.removeValueForKey($0) != nil } }   // Internal function which processes the timer callbacks  // - Parameters: // - timer: Timer which triggered  private func timerCallback(subscriberId: RepeatSubscriberId) {     objc_sync_enter(Repeat.sharedInstance)     defer { objc_sync_exit(Repeat.sharedInstance) }      // if we no longer have a record of the subscriber, assume it was invalidated and return (without scheduling any further callbacks for that subscriber)     guard var info = Repeat.subscribers[subscriberId] else { return }      let result = info.closure()      // the client may have just invalidated us in the above closure - if so, don't attempt to dispatch another callback     guard Repeat.subscribers.keys.contains(subscriberId) else { return }      switch result {     case .Stop:         Repeat.subscribers.removeValueForKey(subscriberId)     case .Repeat:         Repeat.dispatch(subscriberId: subscriberId, timeInterval: info.timeInterval)     case .RepeatAfter(let interval):         assert(interval > 0, "Expecting interval to be > 0, not /(interval)")          info.timeInterval = interval         Repeat.dispatch(subscriberId: subscriberId, timeInterval: interval)     } }

}

extension RepeatSubscriberId {

// Invalidates closure execution for this subscriber.  // Instead of calling `Repeat.invalidate(subscriberId)`, this convenience extension lets us call `subscriberId.invalidate()`.  // Note that due to the typealias of `RepeatSubscriberId` to `UInt`, this pollutes UInt's 'namespace' so that you can do `UInt(0).invalidate()`  // and it compiles (though it is obviously nonsensical).  // - Returns: Whether the subscriber was found and invalidated successfully  func invalidate() -> Bool {     return Repeat.invalidate(self) }

}

import Foundation   typealias RepeatClosure        = () -> () typealias RepeatClosureWithRet = () -> Repeat.Result   typealias RepeatSubscriberId  = UInt     class Repeat {     enum Result {         case Stop         case Repeat         case RepeatAfter(NSTimeInterval)     }          private struct SubscriberInfo {         let closure: RepeatClosureWithRet         var timeInterval: NSTimeInterval     }          privatestatic let sharedInstance = Repeat()          privatestatic var subscribers: [RepeatSubscriberId: SubscriberInfo] = [:]          privatestatic var _nextSubscriberId: RepeatSubscriberId = 0     privatestatic var nextSubscriberId:RepeatSubscriberId {         let id = _nextSubscriberId         _nextSubscriberId += 1         return id     }               // Execute a closure once          // - Parameters:     // - after: The timeInterval in seconds after which the closure is executed     // - closure: The closure to execute          // - Returns: Id which can be used to invalidate execution of the closure          static func once(aftertimeInterval: NSTimeInterval, closure: RepeatClosure) -> RepeatSubscriberId {         let closureWithRet: RepeatClosureWithRet = {             closure()             return .Stop         }         return Repeat.dispatch(timeInterval, closure: closureWithRet)     }               // Execute a closure repeatedly          // - Parameters:     // - seconds: The timeInterval in seconds after which the closure is executed     // - closure: The closure to execute          // - Returns: Id which can be used to invalidate execution of the closure       static func every(secondstimeInterval: NSTimeInterval, closure: RepeatClosure) -> RepeatSubscriberId {         let closureWithRet: RepeatClosureWithRet = {             closure()             return .Repeat         }         return Repeat.dispatch(timeInterval, closure: closureWithRet)     }               // Execute a closure after a desired delay. The closure's return param - to be provided by the client - will control whether the closure repeats (with the same or a different delay) or stops.          // - Parameters:     // - seconds: The timeInterval in seconds after which the closure is executed     // - closure: The closure to execute          // - Returns: Id which can be used to invalidate execution of the closure       static func after(secondstimeInterval: NSTimeInterval, closure: RepeatClosureWithRet) -> RepeatSubscriberId {         return Repeat.dispatch(timeInterval, closure: closure)     }               // Internal function which does the subscription and scheduling of the closures          // - Parameters:     // - timeInterval: TimeInterval (in seconds) until execution of closure     // - closure: Closure to execute, should return RepeatResult          // - Returns: Id which can be used to invalidate execution of the closure          static privatefunc dispatch(timeInterval: NSTimeInterval, closure: RepeatClosureWithRet) -> RepeatSubscriberId {         assert(timeInterval > 0, "Expecting intervalSecs to be > 0, not /(timeInterval)")                  // thread safety         objc_sync_enter(Repeat.sharedInstance)         defer { objc_sync_exit(Repeat.sharedInstance) }                  // setup info for the repeat request         let id = Repeat.nextSubscriberId         Repeat.subscribers[id] = SubscriberInfo(closure: closure, timeInterval: timeInterval)                  // call the actual dispatch         Repeat.dispatch(subscriberId: id, timeInterval: timeInterval)                  return id     }               // Internal function which does the scheduling of the closures          // - Parameters:     // - subscriberId: SubscribedId to dispatch for     // - timeInterval: time until the next desired callback. Could be looked up via `subscriberId`, but 'unrolled' to avoid the unnecessary dictionary lookup, as both call sites (of this function) have it readily available.          static privatefunc dispatch(subscriberIdsubscriberId: RepeatSubscriberId, timeInterval: NSTimeInterval) {         assert(Repeat.subscribers.keys.contains(subscriberId), "Invalid subscriberId /(subscriberId)")         assert(timeInterval > 0, "Expecting intervalSecs to be > 0, not /(timeInterval)")                  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSTimeInterval(NSEC_PER_SEC) * timeInterval)), dispatch_get_main_queue()) {             Repeat.sharedInstance.timerCallback(subscriberId)         }     }               // Invalidates closure execution for the given subscriberId          // - Parameters:     // - id: SubscriberId to cancel execution for          // - Returns: Whether a subscriber was found and invalidated          static func invalidate(id: RepeatSubscriberId) -> Bool {         objc_sync_enter(Repeat.sharedInstance)         defer { objc_sync_exit(Repeat.sharedInstance) }                  return Repeat.subscribers.removeValueForKey(id) != nil     }               // Invalidates closure execution for the given subscribers          // - Parameters:     // - ids: SubscriberIds to invalidate          // - Returns: List of booleans which indicate whether each given subscriber was found and invalidated          static func invalidate(ids: [RepeatSubscriberId]) -> [Bool] {         guard !ids.isEmpty else { return [] }                  objc_sync_enter(Repeat.sharedInstance)         defer { objc_sync_exit(Repeat.sharedInstance) }                  return ids.map { Repeat.subscribers.removeValueForKey($0) != nil }     }               // Internal function which processes the timer callbacks          // - Parameters:     // - timer: Timer which triggered          privatefunc timerCallback(subscriberId: RepeatSubscriberId) {         objc_sync_enter(Repeat.sharedInstance)         defer { objc_sync_exit(Repeat.sharedInstance) }                  // if we no longer have a record of the subscriber, assume it was invalidated and return (without scheduling any further callbacks for that subscriber)         guardvar info = Repeat.subscribers[subscriberId] else { return }                  let result = info.closure()                  // the client may have just invalidated us in the above closure - if so, don't attempt to dispatch another callback         guardRepeat.subscribers.keys.contains(subscriberId) else { return }                  switch result {         case .Stop:             Repeat.subscribers.removeValueForKey(subscriberId)         case .Repeat:             Repeat.dispatch(subscriberId: subscriberId, timeInterval: info.timeInterval)         case .RepeatAfter(let interval):             assert(interval > 0, "Expecting interval to be > 0, not /(interval)")                          info.timeInterval = interval             Repeat.dispatch(subscriberId: subscriberId, timeInterval: interval)         }     } }     extension RepeatSubscriberId {          // Invalidates closure execution for this subscriber.          // Instead of calling `Repeat.invalidate(subscriberId)`, this convenience extension lets us call `subscriberId.invalidate()`.     // Note that due to the typealias of `RepeatSubscriberId` to `UInt`, this pollutes UInt's 'namespace' so that you can do `UInt(0).invalidate()`     // and it compiles (though it is obviously nonsensical).          // - Returns: Whether the subscriber was found and invalidated successfully          func invalidate() -> Bool {         return Repeat.invalidate(self)     } } 

References

Class Reference

Title Image: @ sergign / shutterstock.com

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » NSTimer – Repeat

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮