├── .gitignore ├── .travis.yml ├── DORateLimit.podspec ├── DORateLimit └── RateLimit.swift ├── Example ├── RateLimitExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── RateLimitExample │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── README.md ├── RateLimit.xcworkspace └── contents.xcworkspacedata └── Tests ├── RateLimit Tests.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── RateLimit Tests.xcscheme └── Tests ├── Info.plist └── RateLimitTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | # Pods/ 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11 2 | language: objective-c 3 | script: 4 | - xcodebuild -workspace RateLimit.xcworkspace -scheme RateLimit\ Tests -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11' test | xcpretty && exit ${PIPESTATUS[0]} 5 | before_script: 6 | - gem install xcpretty 7 | after_success: 8 | # Workaround to random travis CI failure 9 | # See https://github.com/travis-ci/travis-ci/issues/4725 10 | - sleep 5 -------------------------------------------------------------------------------- /DORateLimit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "DORateLimit" 3 | s.version = "0.1.7" 4 | s.summary = "Rate limit your functions with throttling and debouncing" 5 | s.homepage = "https://github.com/danydev/DORateLimit" 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.source = { :git => "https://github.com/danydev/DORateLimit.git", :tag => s.version.to_s } 8 | s.author = { "Daniele Orrù" => "daniele.orru.dev@gmail.com" } 9 | s.swift_version = '5.0' 10 | s.platform = :ios, '8.0' 11 | s.osx.deployment_target = '10.9' 12 | s.ios.deployment_target = '8.0' 13 | s.watchos.deployment_target = '2.0' 14 | 15 | s.source_files = 'DORateLimit/*.swift' 16 | end 17 | -------------------------------------------------------------------------------- /DORateLimit/RateLimit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RateLimit.swift 3 | // RateLimitExample 4 | // 5 | // Created by Daniele Orrù on 28/06/15. 6 | // Copyright (c) 2015 Daniele Orru'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ThrottleInfo { 12 | let key: String 13 | let threshold: TimeInterval 14 | let trailing: Bool 15 | let closure: () -> () 16 | 17 | init(key: String, threshold: TimeInterval, trailing: Bool, closure: @escaping () -> ()) 18 | { 19 | self.key = key 20 | self.threshold = threshold 21 | self.trailing = trailing 22 | self.closure = closure 23 | } 24 | } 25 | 26 | class ThrottleExecutionInfo { 27 | let lastExecutionDate: Date 28 | let timer: Timer? 29 | let throttleInfo: ThrottleInfo 30 | 31 | init(lastExecutionDate: Date, timer: Timer? = nil, throttleInfo: ThrottleInfo) 32 | { 33 | self.lastExecutionDate = lastExecutionDate 34 | self.timer = timer 35 | self.throttleInfo = throttleInfo 36 | } 37 | } 38 | 39 | class DebounceInfo { 40 | let key: String 41 | let threshold: TimeInterval 42 | let atBegin: Bool 43 | let closure: () -> () 44 | 45 | init(key: String, threshold: TimeInterval, atBegin: Bool, closure: @escaping () -> ()) 46 | { 47 | self.key = key 48 | self.threshold = threshold 49 | self.atBegin = atBegin 50 | self.closure = closure 51 | } 52 | } 53 | 54 | class DebounceExecutionInfo { 55 | let timer: Timer? 56 | let debounceInfo: DebounceInfo 57 | 58 | init(timer: Timer? = nil, debounceInfo: DebounceInfo) 59 | { 60 | self.timer = timer 61 | self.debounceInfo = debounceInfo 62 | } 63 | } 64 | 65 | /** 66 | Provide debounce and throttle functionality. 67 | */ 68 | open class RateLimit 69 | { 70 | fileprivate static let queue = DispatchQueue(label: "org.orru.RateLimit", attributes: []) 71 | 72 | fileprivate static var throttleExecutionDictionary = [String : ThrottleExecutionInfo]() 73 | fileprivate static var debounceExecutionDictionary = [String : DebounceExecutionInfo]() 74 | 75 | /** 76 | Throttle call to a closure using a given threshold 77 | 78 | - parameter name: 79 | - parameter threshold: 80 | - parameter trailing: 81 | - parameter closure: 82 | */ 83 | public static func throttle(_ key: String, threshold: TimeInterval, trailing: Bool = false, closure: @escaping ()->()) 84 | { 85 | let now = Date() 86 | var canExecuteClosure = false 87 | if let rateLimitInfo = self.throttleInfoForKey(key) { 88 | let timeDifference = rateLimitInfo.lastExecutionDate.timeIntervalSince(now) 89 | if timeDifference < 0 && fabs(timeDifference) < threshold { 90 | if trailing && rateLimitInfo.timer == nil { 91 | let timer = Timer.scheduledTimer(timeInterval: threshold, target: self, selector: #selector(RateLimit.throttleTimerFired(_:)), userInfo: ["rateLimitInfo" : rateLimitInfo], repeats: false) 92 | let throttleInfo = ThrottleInfo(key: key, threshold: threshold, trailing: trailing, closure: closure) 93 | self.setThrottleInfoForKey(ThrottleExecutionInfo(lastExecutionDate: rateLimitInfo.lastExecutionDate, timer: timer, throttleInfo: throttleInfo), forKey: key) 94 | } 95 | } else { 96 | canExecuteClosure = true 97 | } 98 | } else { 99 | canExecuteClosure = true 100 | } 101 | if canExecuteClosure { 102 | let throttleInfo = ThrottleInfo(key: key, threshold: threshold, trailing: trailing, closure: closure) 103 | self.setThrottleInfoForKey(ThrottleExecutionInfo(lastExecutionDate: now, timer: nil, throttleInfo: throttleInfo), forKey: key) 104 | closure() 105 | } 106 | } 107 | 108 | @objc fileprivate static func throttleTimerFired(_ timer: Timer) 109 | { 110 | if let userInfo = timer.userInfo as? [String : AnyObject], let rateLimitInfo = userInfo["rateLimitInfo"] as? ThrottleExecutionInfo { 111 | self.throttle(rateLimitInfo.throttleInfo.key, threshold: rateLimitInfo.throttleInfo.threshold, trailing: rateLimitInfo.throttleInfo.trailing, closure: rateLimitInfo.throttleInfo.closure) 112 | } 113 | } 114 | 115 | /** 116 | Debounce call to a closure using a given threshold 117 | 118 | - parameter key: 119 | - parameter threshold: 120 | - parameter atBegin: 121 | - parameter closure: 122 | */ 123 | public static func debounce(_ key: String, threshold: TimeInterval, atBegin: Bool = true, closure: @escaping ()->()) 124 | { 125 | var canExecuteClosure = false 126 | if let rateLimitInfo = self.debounceInfoForKey(key) { 127 | if let timer = rateLimitInfo.timer, timer.isValid { 128 | timer.invalidate() 129 | let debounceInfo = DebounceInfo(key: key, threshold: threshold, atBegin: atBegin, closure: closure) 130 | let timer = Timer.scheduledTimer(timeInterval: threshold, target: self, selector: #selector(RateLimit.debounceTimerFired(_:)), userInfo: ["rateLimitInfo" : debounceInfo], repeats: false) 131 | self.setDebounceInfoForKey(DebounceExecutionInfo(timer: timer, debounceInfo: debounceInfo), forKey: key) 132 | 133 | } else { 134 | if (atBegin) { 135 | canExecuteClosure = true 136 | } else { 137 | let debounceInfo = DebounceInfo(key: key, threshold: threshold, atBegin: atBegin, closure: closure) 138 | let timer = Timer.scheduledTimer(timeInterval: threshold, target: self, selector: #selector(RateLimit.debounceTimerFired(_:)), userInfo: ["rateLimitInfo" : debounceInfo], repeats: false) 139 | self.setDebounceInfoForKey(DebounceExecutionInfo(timer: timer, debounceInfo: debounceInfo), forKey: key) 140 | } 141 | } 142 | } else { 143 | if (atBegin) { 144 | canExecuteClosure = true 145 | } else { 146 | let debounceInfo = DebounceInfo(key: key, threshold: threshold, atBegin: atBegin, closure: closure) 147 | let timer = Timer.scheduledTimer(timeInterval: threshold, target: self, selector: #selector(RateLimit.debounceTimerFired(_:)), userInfo: ["rateLimitInfo" : debounceInfo], repeats: false) 148 | self.setDebounceInfoForKey(DebounceExecutionInfo(timer: timer, debounceInfo: debounceInfo), forKey: key) 149 | } 150 | } 151 | if canExecuteClosure { 152 | let debounceInfo = DebounceInfo(key: key, threshold: threshold, atBegin: atBegin, closure: closure) 153 | let timer = Timer.scheduledTimer(timeInterval: threshold, target: self, selector: #selector(RateLimit.debounceTimerFired(_:)), userInfo: ["rateLimitInfo" : debounceInfo], repeats: false) 154 | self.setDebounceInfoForKey(DebounceExecutionInfo(timer: timer, debounceInfo: debounceInfo), forKey: key) 155 | closure() 156 | } 157 | } 158 | 159 | @objc fileprivate static func debounceTimerFired(_ timer: Timer) 160 | { 161 | if let userInfo = timer.userInfo as? [String : AnyObject], let debounceInfo = userInfo["rateLimitInfo"] as? DebounceInfo, !debounceInfo.atBegin { 162 | debounceInfo.closure() 163 | } 164 | } 165 | 166 | /** 167 | Reset rate limit information for both bouncing and throlling 168 | */ 169 | public static func resetAllRateLimit() 170 | { 171 | queue.sync { 172 | for key in self.throttleExecutionDictionary.keys { 173 | if let rateLimitInfo = self.throttleExecutionDictionary[key], let timer = rateLimitInfo.timer, timer.isValid { 174 | timer.invalidate() 175 | } 176 | self.throttleExecutionDictionary[key] = nil 177 | } 178 | for key in self.debounceExecutionDictionary.keys { 179 | if let rateLimitInfo = self.debounceExecutionDictionary[key], let timer = rateLimitInfo.timer, timer.isValid { 180 | timer.invalidate() 181 | } 182 | self.debounceExecutionDictionary[key] = nil 183 | } 184 | } 185 | } 186 | 187 | /** 188 | Reset rate limit information for both bouncing and throlling for a specific key 189 | */ 190 | public static func resetRateLimitForKey(_ key: String) 191 | { 192 | queue.sync { 193 | if let rateLimitInfo = self.throttleExecutionDictionary[key], let timer = rateLimitInfo.timer, timer.isValid { 194 | timer.invalidate() 195 | } 196 | self.throttleExecutionDictionary[key] = nil 197 | if let rateLimitInfo = self.debounceExecutionDictionary[key], let timer = rateLimitInfo.timer, timer.isValid { 198 | timer.invalidate() 199 | } 200 | self.debounceExecutionDictionary[key] = nil 201 | } 202 | } 203 | 204 | fileprivate static func throttleInfoForKey(_ key: String) -> ThrottleExecutionInfo? 205 | { 206 | var rateLimitInfo: ThrottleExecutionInfo? 207 | queue.sync { 208 | rateLimitInfo = self.throttleExecutionDictionary[key] 209 | } 210 | return rateLimitInfo 211 | } 212 | 213 | fileprivate static func setThrottleInfoForKey(_ rateLimitInfo: ThrottleExecutionInfo, forKey key: String) 214 | { 215 | queue.sync { 216 | self.throttleExecutionDictionary[key] = rateLimitInfo 217 | } 218 | } 219 | 220 | fileprivate static func debounceInfoForKey(_ key: String) -> DebounceExecutionInfo? 221 | { 222 | var rateLimitInfo: DebounceExecutionInfo? 223 | queue.sync { 224 | rateLimitInfo = self.debounceExecutionDictionary[key] 225 | } 226 | return rateLimitInfo 227 | } 228 | 229 | fileprivate static func setDebounceInfoForKey(_ rateLimitInfo: DebounceExecutionInfo, forKey key: String) 230 | { 231 | queue.sync { 232 | self.debounceExecutionDictionary[key] = rateLimitInfo 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Example/RateLimitExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7708E1F61B4032B700BAA0C7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708E1F51B4032B700BAA0C7 /* AppDelegate.swift */; }; 11 | 7708E1F81B4032B700BAA0C7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708E1F71B4032B700BAA0C7 /* ViewController.swift */; }; 12 | 7708E1FB1B4032B700BAA0C7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7708E1F91B4032B700BAA0C7 /* Main.storyboard */; }; 13 | 7708E1FD1B4032B700BAA0C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7708E1FC1B4032B700BAA0C7 /* Images.xcassets */; }; 14 | 7708E2001B4032B700BAA0C7 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7708E1FE1B4032B700BAA0C7 /* LaunchScreen.xib */; }; 15 | 7708E21E1B4087BF00BAA0C7 /* RateLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708E21D1B4087BF00BAA0C7 /* RateLimit.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 7708E1F01B4032B700BAA0C7 /* RateLimitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RateLimitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 7708E1F41B4032B700BAA0C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 21 | 7708E1F51B4032B700BAA0C7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 7708E1F71B4032B700BAA0C7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 7708E1FA1B4032B700BAA0C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 7708E1FC1B4032B700BAA0C7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 25 | 7708E1FF1B4032B700BAA0C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 26 | 7708E21D1B4087BF00BAA0C7 /* RateLimit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimit.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 7708E1ED1B4032B700BAA0C7 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 7708E1E71B4032B700BAA0C7 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 7708E1F21B4032B700BAA0C7 /* RateLimitExample */, 44 | 7708E1F11B4032B700BAA0C7 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 7708E1F11B4032B700BAA0C7 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 7708E1F01B4032B700BAA0C7 /* RateLimitExample.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 7708E1F21B4032B700BAA0C7 /* RateLimitExample */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 7708E21C1B4087BF00BAA0C7 /* DORateLimit */, 60 | 7708E1F51B4032B700BAA0C7 /* AppDelegate.swift */, 61 | 7708E1F71B4032B700BAA0C7 /* ViewController.swift */, 62 | 7708E1F91B4032B700BAA0C7 /* Main.storyboard */, 63 | 7708E1FC1B4032B700BAA0C7 /* Images.xcassets */, 64 | 7708E1FE1B4032B700BAA0C7 /* LaunchScreen.xib */, 65 | 7708E1F31B4032B700BAA0C7 /* Supporting Files */, 66 | ); 67 | path = RateLimitExample; 68 | sourceTree = ""; 69 | }; 70 | 7708E1F31B4032B700BAA0C7 /* Supporting Files */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 7708E1F41B4032B700BAA0C7 /* Info.plist */, 74 | ); 75 | name = "Supporting Files"; 76 | sourceTree = ""; 77 | }; 78 | 7708E21C1B4087BF00BAA0C7 /* DORateLimit */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 7708E21D1B4087BF00BAA0C7 /* RateLimit.swift */, 82 | ); 83 | name = DORateLimit; 84 | path = ../../DORateLimit; 85 | sourceTree = ""; 86 | }; 87 | /* End PBXGroup section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | 7708E1EF1B4032B700BAA0C7 /* RateLimitExample */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = 7708E20F1B4032B700BAA0C7 /* Build configuration list for PBXNativeTarget "RateLimitExample" */; 93 | buildPhases = ( 94 | 7708E1EC1B4032B700BAA0C7 /* Sources */, 95 | 7708E1ED1B4032B700BAA0C7 /* Frameworks */, 96 | 7708E1EE1B4032B700BAA0C7 /* Resources */, 97 | ); 98 | buildRules = ( 99 | ); 100 | dependencies = ( 101 | ); 102 | name = RateLimitExample; 103 | productName = RateLimitExample; 104 | productReference = 7708E1F01B4032B700BAA0C7 /* RateLimitExample.app */; 105 | productType = "com.apple.product-type.application"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | 7708E1E81B4032B700BAA0C7 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | LastSwiftMigration = 0700; 114 | LastSwiftUpdateCheck = 0700; 115 | LastUpgradeCheck = 1110; 116 | ORGANIZATIONNAME = "Daniele Orru'"; 117 | TargetAttributes = { 118 | 7708E1EF1B4032B700BAA0C7 = { 119 | CreatedOnToolsVersion = 6.3.2; 120 | }; 121 | }; 122 | }; 123 | buildConfigurationList = 7708E1EB1B4032B700BAA0C7 /* Build configuration list for PBXProject "RateLimitExample" */; 124 | compatibilityVersion = "Xcode 3.2"; 125 | developmentRegion = en; 126 | hasScannedForEncodings = 0; 127 | knownRegions = ( 128 | en, 129 | Base, 130 | ); 131 | mainGroup = 7708E1E71B4032B700BAA0C7; 132 | productRefGroup = 7708E1F11B4032B700BAA0C7 /* Products */; 133 | projectDirPath = ""; 134 | projectRoot = ""; 135 | targets = ( 136 | 7708E1EF1B4032B700BAA0C7 /* RateLimitExample */, 137 | ); 138 | }; 139 | /* End PBXProject section */ 140 | 141 | /* Begin PBXResourcesBuildPhase section */ 142 | 7708E1EE1B4032B700BAA0C7 /* Resources */ = { 143 | isa = PBXResourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | 7708E1FB1B4032B700BAA0C7 /* Main.storyboard in Resources */, 147 | 7708E2001B4032B700BAA0C7 /* LaunchScreen.xib in Resources */, 148 | 7708E1FD1B4032B700BAA0C7 /* Images.xcassets in Resources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXResourcesBuildPhase section */ 153 | 154 | /* Begin PBXSourcesBuildPhase section */ 155 | 7708E1EC1B4032B700BAA0C7 /* Sources */ = { 156 | isa = PBXSourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 7708E21E1B4087BF00BAA0C7 /* RateLimit.swift in Sources */, 160 | 7708E1F81B4032B700BAA0C7 /* ViewController.swift in Sources */, 161 | 7708E1F61B4032B700BAA0C7 /* AppDelegate.swift in Sources */, 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | }; 165 | /* End PBXSourcesBuildPhase section */ 166 | 167 | /* Begin PBXVariantGroup section */ 168 | 7708E1F91B4032B700BAA0C7 /* Main.storyboard */ = { 169 | isa = PBXVariantGroup; 170 | children = ( 171 | 7708E1FA1B4032B700BAA0C7 /* Base */, 172 | ); 173 | name = Main.storyboard; 174 | sourceTree = ""; 175 | }; 176 | 7708E1FE1B4032B700BAA0C7 /* LaunchScreen.xib */ = { 177 | isa = PBXVariantGroup; 178 | children = ( 179 | 7708E1FF1B4032B700BAA0C7 /* Base */, 180 | ); 181 | name = LaunchScreen.xib; 182 | sourceTree = ""; 183 | }; 184 | /* End PBXVariantGroup section */ 185 | 186 | /* Begin XCBuildConfiguration section */ 187 | 7708E20D1B4032B700BAA0C7 /* Debug */ = { 188 | isa = XCBuildConfiguration; 189 | buildSettings = { 190 | ALWAYS_SEARCH_USER_PATHS = NO; 191 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 192 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 193 | CLANG_CXX_LIBRARY = "libc++"; 194 | CLANG_ENABLE_MODULES = YES; 195 | CLANG_ENABLE_OBJC_ARC = YES; 196 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 197 | CLANG_WARN_BOOL_CONVERSION = YES; 198 | CLANG_WARN_COMMA = YES; 199 | CLANG_WARN_CONSTANT_CONVERSION = YES; 200 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 201 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 202 | CLANG_WARN_EMPTY_BODY = YES; 203 | CLANG_WARN_ENUM_CONVERSION = YES; 204 | CLANG_WARN_INFINITE_RECURSION = YES; 205 | CLANG_WARN_INT_CONVERSION = YES; 206 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 207 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 208 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 209 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 210 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 211 | CLANG_WARN_STRICT_PROTOTYPES = YES; 212 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 213 | CLANG_WARN_UNREACHABLE_CODE = YES; 214 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 215 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 216 | COPY_PHASE_STRIP = NO; 217 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 218 | ENABLE_STRICT_OBJC_MSGSEND = YES; 219 | ENABLE_TESTABILITY = YES; 220 | GCC_C_LANGUAGE_STANDARD = gnu99; 221 | GCC_DYNAMIC_NO_PIC = NO; 222 | GCC_NO_COMMON_BLOCKS = YES; 223 | GCC_OPTIMIZATION_LEVEL = 0; 224 | GCC_PREPROCESSOR_DEFINITIONS = ( 225 | "DEBUG=1", 226 | "$(inherited)", 227 | ); 228 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 229 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 230 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 231 | GCC_WARN_UNDECLARED_SELECTOR = YES; 232 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 233 | GCC_WARN_UNUSED_FUNCTION = YES; 234 | GCC_WARN_UNUSED_VARIABLE = YES; 235 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 236 | MTL_ENABLE_DEBUG_INFO = YES; 237 | ONLY_ACTIVE_ARCH = YES; 238 | SDKROOT = iphoneos; 239 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 240 | }; 241 | name = Debug; 242 | }; 243 | 7708E20E1B4032B700BAA0C7 /* Release */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_EMPTY_BODY = YES; 259 | CLANG_WARN_ENUM_CONVERSION = YES; 260 | CLANG_WARN_INFINITE_RECURSION = YES; 261 | CLANG_WARN_INT_CONVERSION = YES; 262 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 264 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | SDKROOT = iphoneos; 287 | SWIFT_COMPILATION_MODE = wholemodule; 288 | VALIDATE_PRODUCT = YES; 289 | }; 290 | name = Release; 291 | }; 292 | 7708E2101B4032B700BAA0C7 /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | INFOPLIST_FILE = RateLimitExample/Info.plist; 297 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 298 | PRODUCT_BUNDLE_IDENTIFIER = "org.orru.$(PRODUCT_NAME:rfc1034identifier)"; 299 | PRODUCT_NAME = "$(TARGET_NAME)"; 300 | SWIFT_VERSION = 5.0; 301 | }; 302 | name = Debug; 303 | }; 304 | 7708E2111B4032B700BAA0C7 /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 308 | INFOPLIST_FILE = RateLimitExample/Info.plist; 309 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 310 | PRODUCT_BUNDLE_IDENTIFIER = "org.orru.$(PRODUCT_NAME:rfc1034identifier)"; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SWIFT_VERSION = 5.0; 313 | }; 314 | name = Release; 315 | }; 316 | /* End XCBuildConfiguration section */ 317 | 318 | /* Begin XCConfigurationList section */ 319 | 7708E1EB1B4032B700BAA0C7 /* Build configuration list for PBXProject "RateLimitExample" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | 7708E20D1B4032B700BAA0C7 /* Debug */, 323 | 7708E20E1B4032B700BAA0C7 /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | 7708E20F1B4032B700BAA0C7 /* Build configuration list for PBXNativeTarget "RateLimitExample" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | 7708E2101B4032B700BAA0C7 /* Debug */, 332 | 7708E2111B4032B700BAA0C7 /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | /* End XCConfigurationList section */ 338 | }; 339 | rootObject = 7708E1E81B4032B700BAA0C7 /* Project object */; 340 | } 341 | -------------------------------------------------------------------------------- /Example/RateLimitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/RateLimitExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RateLimitExample 4 | // 5 | // Created by Daniele Orrù on 28/06/15. 6 | // Copyright (c) 2015 Daniele Orru'. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Example/RateLimitExample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/RateLimitExample/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 | 36 | 46 | 56 | 66 | 76 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Example/RateLimitExample/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" : "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 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Example/RateLimitExample/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/RateLimitExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RateLimitExample 4 | // 5 | // Created by Daniele Orrù on 28/06/15. 6 | // Copyright (c) 2015 Daniele Orru'. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBAction func button1Tapped(sender: AnyObject) 14 | { 15 | RateLimit.debounce("keyButton3", threshold: 3.0, atBegin: false) { 16 | print("triggered debounce with atBegin false") 17 | } 18 | } 19 | 20 | @IBAction func button2Tapped(sender: AnyObject) 21 | { 22 | RateLimit.debounce("keyButton4", threshold: 3.0, atBegin: true) { 23 | print("triggered debounce with atBegin true") 24 | } 25 | } 26 | 27 | @IBAction func button3Tapped(sender: AnyObject) 28 | { 29 | RateLimit.throttle("keyButton1", threshold: 3.0, trailing: false) { 30 | print("triggered throttle with trailing false") 31 | } 32 | } 33 | 34 | @IBAction func button4Tapped(sender: AnyObject) 35 | { 36 | RateLimit.throttle("keyButton2", threshold: 3.0, trailing: true) { 37 | print("triggered throttle with trailing true") 38 | } 39 | } 40 | 41 | @IBAction func button5Tapped(sender: AnyObject) 42 | { 43 | RateLimit.resetAllRateLimit() 44 | } 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Daniele Orrù 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DORateLimit 2 | 3 | [![CI Status](http://img.shields.io/travis/danydev/DORateLimit.svg?style=flat)](https://travis-ci.org/danydev/DORateLimit) 4 | [![Version](https://img.shields.io/cocoapods/v/DORateLimit.svg?style=flat)](http://cocoapods.org/pods/DORateLimit) 5 | [![License](https://img.shields.io/cocoapods/l/DORateLimit.svg?style=flat)](http://cocoapods.org/pods/DORateLimit) 6 | [![Platform](https://img.shields.io/cocoapods/p/DORateLimit.svg?style=flat)](http://cocoapods.org/pods/DORateLimit) 7 | 8 | DORateLimit allows you to rate limit your function calls both by using throttling and debouncing. 9 | A good explanation about the differences between debouncing and throttling can be found [here](http://benalman.com/projects/jquery-throttle-debounce-plugin/). 10 | 11 | ## Usage 12 | 13 | ``` swift 14 | RateLimit.throttle("throttleFunctionKey", threshold: 1.0) { 15 | print("triggering throttled closure") 16 | } 17 | 18 | RateLimit.debounce("debounceFunctionKey", threshold: 1.0) { 19 | print("triggering debounced closure") 20 | } 21 | ``` 22 | 23 | ## Installation 24 | 25 | RateLimit is available through [CocoaPods](http://cocoapods.org). To install 26 | it, simply add the following line to your Podfile: 27 | 28 | ```ruby 29 | pod "DORateLimit" 30 | ``` 31 | 32 | ## License 33 | 34 | RateLimit is available under the MIT license. See the LICENSE file for more info. 35 | -------------------------------------------------------------------------------- /RateLimit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/RateLimit Tests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7724C2161BE524BE00108692 /* RateLimitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7724C2141BE524BE00108692 /* RateLimitTests.swift */; }; 11 | 7724C2191BE627FB00108692 /* RateLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7724C2181BE627FB00108692 /* RateLimit.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 7724C2031BE5242300108692 /* RateLimit Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RateLimit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 7724C2131BE524BE00108692 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17 | 7724C2141BE524BE00108692 /* RateLimitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimitTests.swift; sourceTree = ""; }; 18 | 7724C2181BE627FB00108692 /* RateLimit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimit.swift; sourceTree = ""; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXGroup section */ 22 | 262CA94E9A99368212B9A475 /* Frameworks */ = { 23 | isa = PBXGroup; 24 | children = ( 25 | ); 26 | name = Frameworks; 27 | sourceTree = ""; 28 | }; 29 | 7724C1E61BE5242300108692 = { 30 | isa = PBXGroup; 31 | children = ( 32 | 7724C2171BE627FB00108692 /* DORateLimit */, 33 | 7724C2121BE524BE00108692 /* Tests */, 34 | 7724C1F01BE5242300108692 /* Products */, 35 | 262CA94E9A99368212B9A475 /* Frameworks */, 36 | ); 37 | sourceTree = ""; 38 | }; 39 | 7724C1F01BE5242300108692 /* Products */ = { 40 | isa = PBXGroup; 41 | children = ( 42 | 7724C2031BE5242300108692 /* RateLimit Tests.xctest */, 43 | ); 44 | name = Products; 45 | sourceTree = ""; 46 | }; 47 | 7724C2121BE524BE00108692 /* Tests */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 7724C2131BE524BE00108692 /* Info.plist */, 51 | 7724C2141BE524BE00108692 /* RateLimitTests.swift */, 52 | ); 53 | path = Tests; 54 | sourceTree = ""; 55 | }; 56 | 7724C2171BE627FB00108692 /* DORateLimit */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 7724C2181BE627FB00108692 /* RateLimit.swift */, 60 | ); 61 | name = DORateLimit; 62 | path = ../DORateLimit; 63 | sourceTree = ""; 64 | }; 65 | /* End PBXGroup section */ 66 | 67 | /* Begin PBXNativeTarget section */ 68 | 7724C2021BE5242300108692 /* RateLimit Tests */ = { 69 | isa = PBXNativeTarget; 70 | buildConfigurationList = 7724C20F1BE5242300108692 /* Build configuration list for PBXNativeTarget "RateLimit Tests" */; 71 | buildPhases = ( 72 | 7724C1FF1BE5242300108692 /* Sources */, 73 | 7724C2011BE5242300108692 /* Resources */, 74 | ); 75 | buildRules = ( 76 | ); 77 | dependencies = ( 78 | ); 79 | name = "RateLimit Tests"; 80 | productName = "RateLimit TestsTests"; 81 | productReference = 7724C2031BE5242300108692 /* RateLimit Tests.xctest */; 82 | productType = "com.apple.product-type.bundle.unit-test"; 83 | }; 84 | /* End PBXNativeTarget section */ 85 | 86 | /* Begin PBXProject section */ 87 | 7724C1E71BE5242300108692 /* Project object */ = { 88 | isa = PBXProject; 89 | attributes = { 90 | LastSwiftUpdateCheck = 0710; 91 | LastUpgradeCheck = 1110; 92 | ORGANIZATIONNAME = "Daniele Orru'"; 93 | TargetAttributes = { 94 | 7724C2021BE5242300108692 = { 95 | CreatedOnToolsVersion = 7.1; 96 | LastSwiftMigration = 0910; 97 | }; 98 | }; 99 | }; 100 | buildConfigurationList = 7724C1EA1BE5242300108692 /* Build configuration list for PBXProject "RateLimit Tests" */; 101 | compatibilityVersion = "Xcode 3.2"; 102 | developmentRegion = en; 103 | hasScannedForEncodings = 0; 104 | knownRegions = ( 105 | en, 106 | Base, 107 | ); 108 | mainGroup = 7724C1E61BE5242300108692; 109 | productRefGroup = 7724C1F01BE5242300108692 /* Products */; 110 | projectDirPath = ""; 111 | projectRoot = ""; 112 | targets = ( 113 | 7724C2021BE5242300108692 /* RateLimit Tests */, 114 | ); 115 | }; 116 | /* End PBXProject section */ 117 | 118 | /* Begin PBXResourcesBuildPhase section */ 119 | 7724C2011BE5242300108692 /* Resources */ = { 120 | isa = PBXResourcesBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXResourcesBuildPhase section */ 127 | 128 | /* Begin PBXSourcesBuildPhase section */ 129 | 7724C1FF1BE5242300108692 /* Sources */ = { 130 | isa = PBXSourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | 7724C2161BE524BE00108692 /* RateLimitTests.swift in Sources */, 134 | 7724C2191BE627FB00108692 /* RateLimit.swift in Sources */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXSourcesBuildPhase section */ 139 | 140 | /* Begin XCBuildConfiguration section */ 141 | 7724C20A1BE5242300108692 /* Debug */ = { 142 | isa = XCBuildConfiguration; 143 | buildSettings = { 144 | ALWAYS_SEARCH_USER_PATHS = NO; 145 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 146 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 147 | CLANG_CXX_LIBRARY = "libc++"; 148 | CLANG_ENABLE_MODULES = YES; 149 | CLANG_ENABLE_OBJC_ARC = YES; 150 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 151 | CLANG_WARN_BOOL_CONVERSION = YES; 152 | CLANG_WARN_COMMA = YES; 153 | CLANG_WARN_CONSTANT_CONVERSION = YES; 154 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 155 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 156 | CLANG_WARN_EMPTY_BODY = YES; 157 | CLANG_WARN_ENUM_CONVERSION = YES; 158 | CLANG_WARN_INFINITE_RECURSION = YES; 159 | CLANG_WARN_INT_CONVERSION = YES; 160 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 161 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 162 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 163 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNREACHABLE_CODE = YES; 168 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 169 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | GCC_C_LANGUAGE_STANDARD = gnu99; 175 | GCC_DYNAMIC_NO_PIC = NO; 176 | GCC_NO_COMMON_BLOCKS = YES; 177 | GCC_OPTIMIZATION_LEVEL = 0; 178 | GCC_PREPROCESSOR_DEFINITIONS = ( 179 | "DEBUG=1", 180 | "$(inherited)", 181 | ); 182 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 183 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 184 | GCC_WARN_UNDECLARED_SELECTOR = YES; 185 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 186 | GCC_WARN_UNUSED_FUNCTION = YES; 187 | GCC_WARN_UNUSED_VARIABLE = YES; 188 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 189 | MTL_ENABLE_DEBUG_INFO = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = iphoneos; 192 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 193 | }; 194 | name = Debug; 195 | }; 196 | 7724C20B1BE5242300108692 /* Release */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 227 | ENABLE_NS_ASSERTIONS = NO; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu99; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 238 | MTL_ENABLE_DEBUG_INFO = NO; 239 | SDKROOT = iphoneos; 240 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 241 | VALIDATE_PRODUCT = YES; 242 | }; 243 | name = Release; 244 | }; 245 | 7724C2101BE5242300108692 /* Debug */ = { 246 | isa = XCBuildConfiguration; 247 | buildSettings = { 248 | DEVELOPMENT_TEAM = ""; 249 | INFOPLIST_FILE = Tests/Info.plist; 250 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 251 | PRODUCT_BUNDLE_IDENTIFIER = "org.orru.RateLimit-TestsTests"; 252 | PRODUCT_NAME = "$(TARGET_NAME)"; 253 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 254 | SWIFT_VERSION = 5.0; 255 | }; 256 | name = Debug; 257 | }; 258 | 7724C2111BE5242300108692 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | DEVELOPMENT_TEAM = ""; 262 | INFOPLIST_FILE = Tests/Info.plist; 263 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 264 | PRODUCT_BUNDLE_IDENTIFIER = "org.orru.RateLimit-TestsTests"; 265 | PRODUCT_NAME = "$(TARGET_NAME)"; 266 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 267 | SWIFT_VERSION = 5.0; 268 | }; 269 | name = Release; 270 | }; 271 | /* End XCBuildConfiguration section */ 272 | 273 | /* Begin XCConfigurationList section */ 274 | 7724C1EA1BE5242300108692 /* Build configuration list for PBXProject "RateLimit Tests" */ = { 275 | isa = XCConfigurationList; 276 | buildConfigurations = ( 277 | 7724C20A1BE5242300108692 /* Debug */, 278 | 7724C20B1BE5242300108692 /* Release */, 279 | ); 280 | defaultConfigurationIsVisible = 0; 281 | defaultConfigurationName = Release; 282 | }; 283 | 7724C20F1BE5242300108692 /* Build configuration list for PBXNativeTarget "RateLimit Tests" */ = { 284 | isa = XCConfigurationList; 285 | buildConfigurations = ( 286 | 7724C2101BE5242300108692 /* Debug */, 287 | 7724C2111BE5242300108692 /* Release */, 288 | ); 289 | defaultConfigurationIsVisible = 0; 290 | defaultConfigurationName = Release; 291 | }; 292 | /* End XCConfigurationList section */ 293 | }; 294 | rootObject = 7724C1E71BE5242300108692 /* Project object */; 295 | } 296 | -------------------------------------------------------------------------------- /Tests/RateLimit Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/RateLimit Tests.xcodeproj/xcshareddata/xcschemes/RateLimit Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Tests/Tests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Tests/RateLimitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RateLimitTests.swift 3 | // RateLimit Tests 4 | // 5 | // Created by Daniele Orrù on 31/10/15. 6 | // Copyright © 2015 Daniele Orru'. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class RateLimitTests: XCTestCase { 12 | 13 | func testDebounceTriggersOnceWhenContinuoslyCalledBeforeThreshold() 14 | { 15 | let threshold = 1.0 16 | var closureCallsCount = 0 17 | let readyExpectation = expectation(description: "ready") 18 | 19 | let startTimestamp = Date().timeIntervalSince1970 20 | var currentTimestamp = Date().timeIntervalSince1970 21 | while((currentTimestamp - startTimestamp) < threshold - 0.5) { 22 | // Action: Call debounce multiple times for (threshold - 0.5) seconds 23 | RateLimit.debounce("debounceKey_t1", threshold: threshold) { 24 | closureCallsCount += 1 25 | } 26 | currentTimestamp = Date().timeIntervalSince1970 27 | } 28 | 29 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 30 | // Expectation: The closure has been called just 1 time 31 | XCTAssertEqual(1, closureCallsCount) 32 | readyExpectation.fulfill() 33 | } 34 | 35 | waitForExpectations(timeout: 5, handler: nil) 36 | } 37 | 38 | func testDebounceTriggersOnceWhenContinuoslyCalledAfterThreshold() 39 | { 40 | let threshold = 1.0 41 | var closureCallsCount = 0 42 | let readyExpectation = expectation(description: "ready") 43 | 44 | let startTimestamp = Date().timeIntervalSince1970 45 | var currentTimestamp = Date().timeIntervalSince1970 46 | while((currentTimestamp - startTimestamp) < threshold + 0.5) { 47 | // Action: Call debounce multiple times for (threshold + 0.5) seconds 48 | RateLimit.debounce("debounceKey_t2", threshold: threshold) { 49 | closureCallsCount += 1 50 | } 51 | currentTimestamp = Date().timeIntervalSince1970 52 | } 53 | 54 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 55 | // Expectation: The closure has been called just 1 time 56 | XCTAssertEqual(1, closureCallsCount) 57 | readyExpectation.fulfill() 58 | } 59 | 60 | waitForExpectations(timeout: 5, handler: nil) 61 | } 62 | 63 | func testDebounceTriggersWhenCalledAfterThreshold() 64 | { 65 | let threshold = 1.0 66 | var closureCallsCount = 0 67 | 68 | let readyExpectation = expectation(description: "ready") 69 | 70 | let callThrottle = { 71 | RateLimit.debounce("debounceKey_t3", threshold: threshold) { 72 | closureCallsCount += 1 73 | } 74 | } 75 | 76 | // Action: Call the closure 1 time 77 | callThrottle() 78 | 79 | // Action: Call again the closure after *waiting* (threshold + 0.5) seconds 80 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 81 | callThrottle() 82 | // Expectation: The closure has been called 2 times 83 | XCTAssertEqual(2, closureCallsCount) 84 | readyExpectation.fulfill() 85 | } 86 | 87 | waitForExpectations(timeout: 5, handler: nil) 88 | } 89 | 90 | func testDebounceRespectsTriggerAtBeginDisabled() 91 | { 92 | let threshold = 1.0 93 | var closureCallsCount = 0 94 | 95 | let readyExpectation = expectation(description: "ready") 96 | 97 | // Action: debounce with atBegin false 98 | RateLimit.debounce("debounceKey_t4", threshold: threshold, atBegin: false) { 99 | closureCallsCount += 1 100 | } 101 | 102 | // Expectation: Closure should have NOT been called at this time 103 | XCTAssertEqual(0, closureCallsCount) 104 | 105 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 106 | // Expectation: After (threshold + 0.5) seconds, the closure has been called 107 | XCTAssertEqual(1, closureCallsCount) 108 | readyExpectation.fulfill() 109 | } 110 | 111 | waitForExpectations(timeout: 5, handler: nil) 112 | } 113 | 114 | func testDebounceRespectsTriggerAtBeginEnabled() 115 | { 116 | let threshold = 1.0 117 | var closureCallsCount = 0 118 | 119 | // Action: debounce with atBegin true 120 | RateLimit.debounce("debounceKey_t5", threshold: threshold) { 121 | closureCallsCount += 1 122 | } 123 | 124 | // Expectation: Closure should have been immediately called 125 | XCTAssertEqual(1, closureCallsCount) 126 | } 127 | 128 | func testDebounceRespectsDifferentKeys() 129 | { 130 | let threshold = 1.0 131 | var closureCallsCount = 0 132 | 133 | // Action: call debounce twice with different keys 134 | RateLimit.debounce("debounceKey_t6_1", threshold: threshold) { 135 | closureCallsCount += 1 136 | } 137 | RateLimit.debounce("debounceKey_t6_2", threshold: threshold) { 138 | closureCallsCount += 1 139 | } 140 | 141 | // Expectation: Closure should have been called 1 time each 142 | XCTAssertEqual(2, closureCallsCount) 143 | } 144 | 145 | func testThrottleIgnoresTriggerWhenContinuoslyCalledBeforeThreshold() 146 | { 147 | let threshold = 1.0 148 | var closureCallsCount = 0 149 | let readyExpectation = expectation(description: "ready") 150 | 151 | let startTimestamp = Date().timeIntervalSince1970 152 | var currentTimestamp = Date().timeIntervalSince1970 153 | while((currentTimestamp - startTimestamp) < threshold - 0.5) { 154 | // Action: Call throttle multiple times for (threshold - 0.5) seconds 155 | RateLimit.throttle("throttleKey_t1", threshold: threshold) { 156 | closureCallsCount += 1 157 | } 158 | currentTimestamp = Date().timeIntervalSince1970 159 | } 160 | 161 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 162 | // Expectation: The closure has been called 1 time 163 | XCTAssertEqual(1, closureCallsCount) 164 | readyExpectation.fulfill() 165 | } 166 | 167 | waitForExpectations(timeout: 5, handler: nil) 168 | } 169 | 170 | func testThrottleTriggersWhenContinuoslyCalledAfterThreshold() 171 | { 172 | let threshold = 1.0 173 | var closureCallsCount = 0 174 | let readyExpectation = expectation(description: "ready") 175 | 176 | let startTimestamp = Date().timeIntervalSince1970 177 | var currentTimestamp = Date().timeIntervalSince1970 178 | while((currentTimestamp - startTimestamp) < threshold + 0.5) { 179 | // Action: Call throttle continuosly for (threshold + 0.5) seconds 180 | RateLimit.throttle("throttleKey_t2", threshold: threshold) { 181 | closureCallsCount += 1 182 | } 183 | currentTimestamp = Date().timeIntervalSince1970 184 | } 185 | 186 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 187 | // Expectation: Closure should have been called 2 times 188 | XCTAssertEqual(2, closureCallsCount) 189 | readyExpectation.fulfill() 190 | } 191 | 192 | waitForExpectations(timeout: 5, handler: nil) 193 | } 194 | 195 | func testThrottleAllowsTriggerAfterThreshold() 196 | { 197 | let threshold = 1.0 198 | var closureCallsCount = 0 199 | 200 | let readyExpectation = expectation(description: "ready") 201 | 202 | let callThrottle = { 203 | RateLimit.throttle("throttleKey_t3", threshold: threshold) { 204 | closureCallsCount += 1 205 | } 206 | } 207 | 208 | // Action: Call the closure 1 time 209 | callThrottle() 210 | 211 | // Action: Call again the closure after *waiting* (threshold + 0.5) seconds 212 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 213 | callThrottle() 214 | // Expectation: The closure has been called 2 times 215 | XCTAssertEqual(2, closureCallsCount) 216 | readyExpectation.fulfill() 217 | } 218 | 219 | waitForExpectations(timeout: 5, handler: nil) 220 | } 221 | 222 | func testThrottleRespectsTrailingEnabled() 223 | { 224 | let threshold = 1.0 225 | var closureCallsCount = 0 226 | 227 | let readyExpectation = expectation(description: "ready") 228 | 229 | // Action: Call throttle twice 230 | for _ in 1...2 { 231 | RateLimit.throttle("throttleKey_t4", threshold: threshold, trailing: true) { 232 | closureCallsCount += 1 233 | } 234 | } 235 | 236 | // Expectation: Closure should have been called 1 time at this point 237 | XCTAssertEqual(1, closureCallsCount) 238 | 239 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 240 | // Expectation: Trailing is enabled, that means that another trailing closure trigger should have been performed at this point 241 | XCTAssertEqual(2, closureCallsCount) 242 | readyExpectation.fulfill() 243 | } 244 | 245 | waitForExpectations(timeout: 5, handler: nil) 246 | } 247 | 248 | func testThrottleRespectsTrailingDisabled() 249 | { 250 | let threshold = 1.0 251 | var closureCallsCount = 0 252 | 253 | let readyExpectation = expectation(description: "ready") 254 | 255 | // Action: Call throttle twice 256 | for _ in 1...2 { 257 | RateLimit.throttle("throttleKey_t5", threshold: threshold) { 258 | closureCallsCount += 1 259 | } 260 | } 261 | 262 | // Expectation: The closure has been already called 1 time at this point 263 | XCTAssertEqual(1, closureCallsCount) 264 | 265 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64((threshold + 0.5) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 266 | // Expectation: Trailing is disabled, the closure should haven't been called more than once 267 | XCTAssertEqual(1, closureCallsCount) 268 | readyExpectation.fulfill() 269 | } 270 | 271 | waitForExpectations(timeout: 5, handler: nil) 272 | } 273 | 274 | func testThrottleRespectsDifferentKeys() 275 | { 276 | let threshold = 1.0 277 | var closureCallsCount = 0 278 | 279 | // Action: call debounce twice with different keys 280 | RateLimit.throttle("throttleKey_t6_1", threshold: threshold) { 281 | closureCallsCount += 1 282 | } 283 | RateLimit.throttle("throttleKey_t6_2", threshold: threshold) { 284 | closureCallsCount += 1 285 | } 286 | 287 | // Expectation: Closure should have been called 1 time each 288 | XCTAssertEqual(2, closureCallsCount) 289 | } 290 | } 291 | --------------------------------------------------------------------------------