├── HWThrottle ├── HWThrottle.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── liuhaiwei.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── HWThrottle.xcscheme │ └── xcuserdata │ │ └── liuhaiwei.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── HWThrottle │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Debounce │ ├── HWDebounce.h │ └── HWDebounce.m │ ├── HWDebounceTestVC.h │ ├── HWDebounceTestVC.m │ ├── HWThrottleTestVC.h │ ├── HWThrottleTestVC.m │ ├── Info.plist │ ├── SceneDelegate.h │ ├── SceneDelegate.m │ ├── Throttle │ ├── HWThrottle.h │ └── HWThrottle.m │ ├── UIView+LayoutHelper.h │ ├── UIView+LayoutHelper.m │ ├── ViewController.h │ ├── ViewController.m │ └── main.m ├── LICENSE └── README.md /HWThrottle/HWThrottle.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 640664BE25E0AC9A00D50C24 /* HWThrottleTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 640664BD25E0AC9A00D50C24 /* HWThrottleTestVC.m */; }; 11 | 6406657E25E0F4A600D50C24 /* UIView+LayoutHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6406657C25E0F4A600D50C24 /* UIView+LayoutHelper.m */; }; 12 | 6406658A25E4DB0300D50C24 /* HWThrottle.m in Sources */ = {isa = PBXBuildFile; fileRef = 6406658625E4DB0300D50C24 /* HWThrottle.m */; }; 13 | 6406658B25E4DB0300D50C24 /* HWDebounce.m in Sources */ = {isa = PBXBuildFile; fileRef = 6406658825E4DB0300D50C24 /* HWDebounce.m */; }; 14 | 6406658F25E4FC0200D50C24 /* HWDebounceTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 6406658E25E4FC0200D50C24 /* HWDebounceTestVC.m */; }; 15 | 64B64DCA25DF61C30099640F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B64DC925DF61C30099640F /* AppDelegate.m */; }; 16 | 64B64DCD25DF61C30099640F /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B64DCC25DF61C30099640F /* SceneDelegate.m */; }; 17 | 64B64DD025DF61C30099640F /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B64DCF25DF61C30099640F /* ViewController.m */; }; 18 | 64B64DD325DF61C30099640F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64B64DD125DF61C30099640F /* Main.storyboard */; }; 19 | 64B64DD525DF61C30099640F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 64B64DD425DF61C30099640F /* Assets.xcassets */; }; 20 | 64B64DD825DF61C30099640F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64B64DD625DF61C30099640F /* LaunchScreen.storyboard */; }; 21 | 64B64DDB25DF61C30099640F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B64DDA25DF61C30099640F /* main.m */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 640664BC25E0AC9900D50C24 /* HWThrottleTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HWThrottleTestVC.h; sourceTree = ""; }; 26 | 640664BD25E0AC9A00D50C24 /* HWThrottleTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HWThrottleTestVC.m; sourceTree = ""; }; 27 | 6406657C25E0F4A600D50C24 /* UIView+LayoutHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+LayoutHelper.m"; sourceTree = ""; }; 28 | 6406657D25E0F4A600D50C24 /* UIView+LayoutHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+LayoutHelper.h"; sourceTree = ""; }; 29 | 6406658525E4DB0300D50C24 /* HWThrottle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HWThrottle.h; sourceTree = ""; }; 30 | 6406658625E4DB0300D50C24 /* HWThrottle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HWThrottle.m; sourceTree = ""; }; 31 | 6406658825E4DB0300D50C24 /* HWDebounce.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HWDebounce.m; sourceTree = ""; }; 32 | 6406658925E4DB0300D50C24 /* HWDebounce.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HWDebounce.h; sourceTree = ""; }; 33 | 6406658D25E4FC0200D50C24 /* HWDebounceTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HWDebounceTestVC.h; sourceTree = ""; }; 34 | 6406658E25E4FC0200D50C24 /* HWDebounceTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HWDebounceTestVC.m; sourceTree = ""; }; 35 | 64B64DC525DF61C30099640F /* HWThrottle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HWThrottle.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 64B64DC825DF61C30099640F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 37 | 64B64DC925DF61C30099640F /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 38 | 64B64DCB25DF61C30099640F /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 39 | 64B64DCC25DF61C30099640F /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 40 | 64B64DCE25DF61C30099640F /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 41 | 64B64DCF25DF61C30099640F /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 42 | 64B64DD225DF61C30099640F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 43 | 64B64DD425DF61C30099640F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 64B64DD725DF61C30099640F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 45 | 64B64DD925DF61C30099640F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | 64B64DDA25DF61C30099640F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 64B64DC225DF61C30099640F /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 6406658425E4DB0300D50C24 /* Throttle */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 6406658525E4DB0300D50C24 /* HWThrottle.h */, 64 | 6406658625E4DB0300D50C24 /* HWThrottle.m */, 65 | ); 66 | path = Throttle; 67 | sourceTree = ""; 68 | }; 69 | 6406658725E4DB0300D50C24 /* Debounce */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 6406658925E4DB0300D50C24 /* HWDebounce.h */, 73 | 6406658825E4DB0300D50C24 /* HWDebounce.m */, 74 | ); 75 | path = Debounce; 76 | sourceTree = ""; 77 | }; 78 | 64B64DBC25DF61C30099640F = { 79 | isa = PBXGroup; 80 | children = ( 81 | 64B64DC725DF61C30099640F /* HWThrottle */, 82 | 64B64DC625DF61C30099640F /* Products */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | 64B64DC625DF61C30099640F /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 64B64DC525DF61C30099640F /* HWThrottle.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | 64B64DC725DF61C30099640F /* HWThrottle */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 6406658725E4DB0300D50C24 /* Debounce */, 98 | 6406658425E4DB0300D50C24 /* Throttle */, 99 | 6406657D25E0F4A600D50C24 /* UIView+LayoutHelper.h */, 100 | 6406657C25E0F4A600D50C24 /* UIView+LayoutHelper.m */, 101 | 64B64DC825DF61C30099640F /* AppDelegate.h */, 102 | 64B64DC925DF61C30099640F /* AppDelegate.m */, 103 | 64B64DCB25DF61C30099640F /* SceneDelegate.h */, 104 | 64B64DCC25DF61C30099640F /* SceneDelegate.m */, 105 | 64B64DCE25DF61C30099640F /* ViewController.h */, 106 | 64B64DCF25DF61C30099640F /* ViewController.m */, 107 | 64B64DD125DF61C30099640F /* Main.storyboard */, 108 | 64B64DD425DF61C30099640F /* Assets.xcassets */, 109 | 64B64DD625DF61C30099640F /* LaunchScreen.storyboard */, 110 | 64B64DD925DF61C30099640F /* Info.plist */, 111 | 64B64DDA25DF61C30099640F /* main.m */, 112 | 640664BC25E0AC9900D50C24 /* HWThrottleTestVC.h */, 113 | 640664BD25E0AC9A00D50C24 /* HWThrottleTestVC.m */, 114 | 6406658D25E4FC0200D50C24 /* HWDebounceTestVC.h */, 115 | 6406658E25E4FC0200D50C24 /* HWDebounceTestVC.m */, 116 | ); 117 | path = HWThrottle; 118 | sourceTree = ""; 119 | }; 120 | /* End PBXGroup section */ 121 | 122 | /* Begin PBXNativeTarget section */ 123 | 64B64DC425DF61C30099640F /* HWThrottle */ = { 124 | isa = PBXNativeTarget; 125 | buildConfigurationList = 64B64DDE25DF61C30099640F /* Build configuration list for PBXNativeTarget "HWThrottle" */; 126 | buildPhases = ( 127 | 64B64DC125DF61C30099640F /* Sources */, 128 | 64B64DC225DF61C30099640F /* Frameworks */, 129 | 64B64DC325DF61C30099640F /* Resources */, 130 | ); 131 | buildRules = ( 132 | ); 133 | dependencies = ( 134 | ); 135 | name = HWThrottle; 136 | productName = HWThrottle; 137 | productReference = 64B64DC525DF61C30099640F /* HWThrottle.app */; 138 | productType = "com.apple.product-type.application"; 139 | }; 140 | /* End PBXNativeTarget section */ 141 | 142 | /* Begin PBXProject section */ 143 | 64B64DBD25DF61C30099640F /* Project object */ = { 144 | isa = PBXProject; 145 | attributes = { 146 | LastUpgradeCheck = 1240; 147 | TargetAttributes = { 148 | 64B64DC425DF61C30099640F = { 149 | CreatedOnToolsVersion = 12.4; 150 | }; 151 | }; 152 | }; 153 | buildConfigurationList = 64B64DC025DF61C30099640F /* Build configuration list for PBXProject "HWThrottle" */; 154 | compatibilityVersion = "Xcode 9.3"; 155 | developmentRegion = en; 156 | hasScannedForEncodings = 0; 157 | knownRegions = ( 158 | en, 159 | Base, 160 | ); 161 | mainGroup = 64B64DBC25DF61C30099640F; 162 | productRefGroup = 64B64DC625DF61C30099640F /* Products */; 163 | projectDirPath = ""; 164 | projectRoot = ""; 165 | targets = ( 166 | 64B64DC425DF61C30099640F /* HWThrottle */, 167 | ); 168 | }; 169 | /* End PBXProject section */ 170 | 171 | /* Begin PBXResourcesBuildPhase section */ 172 | 64B64DC325DF61C30099640F /* Resources */ = { 173 | isa = PBXResourcesBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | 64B64DD825DF61C30099640F /* LaunchScreen.storyboard in Resources */, 177 | 64B64DD525DF61C30099640F /* Assets.xcassets in Resources */, 178 | 64B64DD325DF61C30099640F /* Main.storyboard in Resources */, 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | /* End PBXResourcesBuildPhase section */ 183 | 184 | /* Begin PBXSourcesBuildPhase section */ 185 | 64B64DC125DF61C30099640F /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 6406658F25E4FC0200D50C24 /* HWDebounceTestVC.m in Sources */, 190 | 64B64DD025DF61C30099640F /* ViewController.m in Sources */, 191 | 640664BE25E0AC9A00D50C24 /* HWThrottleTestVC.m in Sources */, 192 | 64B64DCA25DF61C30099640F /* AppDelegate.m in Sources */, 193 | 6406658B25E4DB0300D50C24 /* HWDebounce.m in Sources */, 194 | 64B64DDB25DF61C30099640F /* main.m in Sources */, 195 | 6406658A25E4DB0300D50C24 /* HWThrottle.m in Sources */, 196 | 64B64DCD25DF61C30099640F /* SceneDelegate.m in Sources */, 197 | 6406657E25E0F4A600D50C24 /* UIView+LayoutHelper.m in Sources */, 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXSourcesBuildPhase section */ 202 | 203 | /* Begin PBXVariantGroup section */ 204 | 64B64DD125DF61C30099640F /* Main.storyboard */ = { 205 | isa = PBXVariantGroup; 206 | children = ( 207 | 64B64DD225DF61C30099640F /* Base */, 208 | ); 209 | name = Main.storyboard; 210 | sourceTree = ""; 211 | }; 212 | 64B64DD625DF61C30099640F /* LaunchScreen.storyboard */ = { 213 | isa = PBXVariantGroup; 214 | children = ( 215 | 64B64DD725DF61C30099640F /* Base */, 216 | ); 217 | name = LaunchScreen.storyboard; 218 | sourceTree = ""; 219 | }; 220 | /* End PBXVariantGroup section */ 221 | 222 | /* Begin XCBuildConfiguration section */ 223 | 64B64DDC25DF61C30099640F /* Debug */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | CLANG_ANALYZER_NONNULL = YES; 228 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 229 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 230 | CLANG_CXX_LIBRARY = "libc++"; 231 | CLANG_ENABLE_MODULES = YES; 232 | CLANG_ENABLE_OBJC_ARC = YES; 233 | CLANG_ENABLE_OBJC_WEAK = YES; 234 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 235 | CLANG_WARN_BOOL_CONVERSION = YES; 236 | CLANG_WARN_COMMA = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 239 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 240 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 241 | CLANG_WARN_EMPTY_BODY = YES; 242 | CLANG_WARN_ENUM_CONVERSION = YES; 243 | CLANG_WARN_INFINITE_RECURSION = YES; 244 | CLANG_WARN_INT_CONVERSION = YES; 245 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 247 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 249 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | COPY_PHASE_STRIP = NO; 257 | DEBUG_INFORMATION_FORMAT = dwarf; 258 | ENABLE_STRICT_OBJC_MSGSEND = YES; 259 | ENABLE_TESTABILITY = YES; 260 | GCC_C_LANGUAGE_STANDARD = gnu11; 261 | GCC_DYNAMIC_NO_PIC = NO; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_OPTIMIZATION_LEVEL = 0; 264 | GCC_PREPROCESSOR_DEFINITIONS = ( 265 | "DEBUG=1", 266 | "$(inherited)", 267 | ); 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 275 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 276 | MTL_FAST_MATH = YES; 277 | ONLY_ACTIVE_ARCH = YES; 278 | SDKROOT = iphoneos; 279 | }; 280 | name = Debug; 281 | }; 282 | 64B64DDD25DF61C30099640F /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 289 | CLANG_CXX_LIBRARY = "libc++"; 290 | CLANG_ENABLE_MODULES = YES; 291 | CLANG_ENABLE_OBJC_ARC = YES; 292 | CLANG_ENABLE_OBJC_WEAK = YES; 293 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_COMMA = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 298 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 299 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 300 | CLANG_WARN_EMPTY_BODY = YES; 301 | CLANG_WARN_ENUM_CONVERSION = YES; 302 | CLANG_WARN_INFINITE_RECURSION = YES; 303 | CLANG_WARN_INT_CONVERSION = YES; 304 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 306 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 308 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 309 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 310 | CLANG_WARN_STRICT_PROTOTYPES = YES; 311 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 312 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 313 | CLANG_WARN_UNREACHABLE_CODE = YES; 314 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 315 | COPY_PHASE_STRIP = NO; 316 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 317 | ENABLE_NS_ASSERTIONS = NO; 318 | ENABLE_STRICT_OBJC_MSGSEND = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu11; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 323 | GCC_WARN_UNDECLARED_SELECTOR = YES; 324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 325 | GCC_WARN_UNUSED_FUNCTION = YES; 326 | GCC_WARN_UNUSED_VARIABLE = YES; 327 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 328 | MTL_ENABLE_DEBUG_INFO = NO; 329 | MTL_FAST_MATH = YES; 330 | SDKROOT = iphoneos; 331 | VALIDATE_PRODUCT = YES; 332 | }; 333 | name = Release; 334 | }; 335 | 64B64DDF25DF61C30099640F /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CODE_SIGN_STYLE = Automatic; 341 | INFOPLIST_FILE = HWThrottle/Info.plist; 342 | LD_RUNPATH_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "@executable_path/Frameworks", 345 | ); 346 | PRODUCT_BUNDLE_IDENTIFIER = highway.HWThrottle; 347 | PRODUCT_NAME = "$(TARGET_NAME)"; 348 | TARGETED_DEVICE_FAMILY = "1,2"; 349 | }; 350 | name = Debug; 351 | }; 352 | 64B64DE025DF61C30099640F /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 356 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 357 | CODE_SIGN_STYLE = Automatic; 358 | INFOPLIST_FILE = HWThrottle/Info.plist; 359 | LD_RUNPATH_SEARCH_PATHS = ( 360 | "$(inherited)", 361 | "@executable_path/Frameworks", 362 | ); 363 | PRODUCT_BUNDLE_IDENTIFIER = highway.HWThrottle; 364 | PRODUCT_NAME = "$(TARGET_NAME)"; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 64B64DC025DF61C30099640F /* Build configuration list for PBXProject "HWThrottle" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 64B64DDC25DF61C30099640F /* Debug */, 376 | 64B64DDD25DF61C30099640F /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 64B64DDE25DF61C30099640F /* Build configuration list for PBXNativeTarget "HWThrottle" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 64B64DDF25DF61C30099640F /* Debug */, 385 | 64B64DE025DF61C30099640F /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | }; 392 | rootObject = 64B64DBD25DF61C30099640F /* Project object */; 393 | } 394 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/project.xcworkspace/xcuserdata/liuhaiwei.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HighwayLaw/HWThrottle/08756bcca83a690ce4e35b1ee053a99bad5e25d0/HWThrottle/HWThrottle.xcodeproj/project.xcworkspace/xcuserdata/liuhaiwei.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/xcshareddata/xcschemes/HWThrottle.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/xcuserdata/liuhaiwei.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle.xcodeproj/xcuserdata/liuhaiwei.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | HWThrottle.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 64B64DC425DF61C30099640F 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import "AppDelegate.h" 9 | 10 | @interface AppDelegate () 11 | 12 | @end 13 | 14 | @implementation AppDelegate 15 | 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 18 | // Override point for customization after application launch. 19 | return YES; 20 | } 21 | 22 | 23 | #pragma mark - UISceneSession lifecycle 24 | 25 | 26 | - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; 30 | } 31 | 32 | 33 | - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { 34 | // Called when the user discards a scene session. 35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 37 | } 38 | 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/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 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Debounce/HWDebounce.h: -------------------------------------------------------------------------------- 1 | // 2 | // HWDebounce.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | #pragma mark - public class 13 | 14 | typedef NS_ENUM(NSUInteger, HWDebounceMode) { 15 | HWDebounceModeTrailing, //invoking on the trailing edge of the timeout 16 | HWDebounceModeLeading, //invoking on the leading edge of the timeout 17 | }; 18 | 19 | typedef void(^HWDebounceTaskBlock)(void); 20 | 21 | @interface HWDebounce : NSObject 22 | 23 | /// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing, the execution queue defaults to the main queue. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 24 | /// @param interval debounce time interval, unit second 25 | /// @param taskBlock the task to be debounced 26 | - (instancetype)initWithInterval:(NSTimeInterval)interval 27 | taskBlock:(HWDebounceTaskBlock)taskBlock; 28 | 29 | /// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 30 | /// @param interval debounce time interval, unit second 31 | /// @param queue execution queue, defaults the main queue 32 | /// @param taskBlock the task to be debounced 33 | - (instancetype)initWithInterval:(NSTimeInterval)interval 34 | onQueue:(dispatch_queue_t)queue 35 | taskBlock:(HWDebounceTaskBlock)taskBlock; 36 | 37 | /// Initialize a debounce object. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 38 | /// @param debounceMode the debounce mode, defaults HWDebounceModeTrailing 39 | /// @param interval debounce time interval, unit second 40 | /// @param queue execution queue, defaults the main queue 41 | /// @param taskBlock the task to be debounced 42 | - (instancetype)initWithDebounceMode:(HWDebounceMode)debounceMode 43 | interval:(NSTimeInterval)interval 44 | onQueue:(dispatch_queue_t)queue 45 | taskBlock:(HWDebounceTaskBlock)taskBlock; 46 | 47 | 48 | /// debouncing call the task 49 | - (void)call; 50 | 51 | 52 | /// When the owner of the HWDebounce object is about to release, call this method on the HWDebounce object first to prevent circular references 53 | - (void)invalidate; 54 | 55 | @end 56 | 57 | #pragma mark - private classes 58 | 59 | @interface HWDebounceTrailing : HWDebounce 60 | 61 | @end 62 | 63 | @interface HWDebounceLeading : HWDebounce 64 | 65 | @end 66 | 67 | 68 | NS_ASSUME_NONNULL_END 69 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Debounce/HWDebounce.m: -------------------------------------------------------------------------------- 1 | // 2 | // HWDebounce.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/23. 6 | // 7 | 8 | #import "HWDebounce.h" 9 | 10 | #pragma mark - HWDebounce 11 | 12 | @interface HWDebounce() 13 | 14 | @end 15 | 16 | @implementation HWDebounce 17 | 18 | #pragma mark - life cycle 19 | 20 | - (instancetype)initWithInterval:(NSTimeInterval)interval 21 | taskBlock:(HWDebounceTaskBlock)taskBlock { 22 | return [self initWithInterval:interval 23 | onQueue:dispatch_get_main_queue() 24 | taskBlock:taskBlock]; 25 | } 26 | 27 | - (instancetype)initWithInterval:(NSTimeInterval)interval 28 | onQueue:(dispatch_queue_t)queue 29 | taskBlock:(HWDebounceTaskBlock)taskBlock { 30 | return [self initWithDebounceMode:HWDebounceModeTrailing 31 | interval:interval 32 | onQueue:queue 33 | taskBlock:taskBlock]; 34 | } 35 | 36 | - (instancetype)initWithDebounceMode:(HWDebounceMode)debounceMode 37 | interval:(NSTimeInterval)interval 38 | onQueue:(dispatch_queue_t)queue 39 | taskBlock:(HWDebounceTaskBlock)taskBlock { 40 | if (interval < 0) { 41 | interval = 0.1; 42 | } 43 | if (!queue) { 44 | queue = dispatch_get_main_queue(); 45 | } 46 | 47 | switch (debounceMode) { 48 | case HWDebounceModeTrailing: 49 | self = [[HWDebounceTrailing alloc] initWithInterval:interval 50 | onQueue:queue 51 | taskBlock:taskBlock]; 52 | break; 53 | 54 | case HWDebounceModeLeading: 55 | self = [[HWDebounceLeading alloc] initWithInterval:interval 56 | onQueue:queue 57 | taskBlock:taskBlock]; 58 | break; 59 | } 60 | return self; 61 | } 62 | 63 | #pragma mark - public methods 64 | 65 | - (void)call { 66 | NSAssert(1, @"This method should be overrided!"); 67 | } 68 | 69 | - (void)invalidate { 70 | NSAssert(1, @"This method should be overrided!"); 71 | } 72 | 73 | @end 74 | 75 | #pragma mark - HWDebounceTrailing 76 | 77 | @interface HWDebounceTrailing () 78 | 79 | @property (nonatomic, assign) NSTimeInterval interval; 80 | @property (nonatomic, copy) HWDebounceTaskBlock taskBlock; 81 | @property (nonatomic, strong) dispatch_queue_t queue; 82 | @property (nonatomic, strong) dispatch_block_t block; 83 | 84 | @end 85 | 86 | @implementation HWDebounceTrailing 87 | 88 | - (instancetype)initWithInterval:(NSTimeInterval)interval 89 | onQueue:(dispatch_queue_t)queue 90 | taskBlock:(HWDebounceTaskBlock)taskBlock { 91 | self = [super init]; 92 | if (self) { 93 | _interval = interval; 94 | _taskBlock = taskBlock; 95 | _queue = queue; 96 | } 97 | return self; 98 | } 99 | 100 | - (void)call { 101 | if (self.block) { 102 | dispatch_block_cancel(self.block); 103 | } 104 | __weak typeof(self)weakSelf = self; 105 | self.block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{ 106 | if (weakSelf.taskBlock) { 107 | weakSelf.taskBlock(); 108 | } 109 | }); 110 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.interval * NSEC_PER_SEC)), self.queue, self.block); 111 | } 112 | 113 | - (void)invalidate { 114 | self.taskBlock = nil; 115 | self.block = nil; 116 | } 117 | 118 | @end 119 | 120 | #pragma mark - HWDebounceLeading 121 | 122 | @interface HWDebounceLeading () 123 | 124 | @property (nonatomic, assign) NSTimeInterval interval; 125 | @property (nonatomic, copy) HWDebounceTaskBlock taskBlock; 126 | @property (nonatomic, strong) dispatch_queue_t queue; 127 | @property (nonatomic, strong) dispatch_block_t block; 128 | @property (nonatomic, strong) NSDate *lastCallTaskDate; 129 | 130 | @end 131 | 132 | @implementation HWDebounceLeading 133 | 134 | - (instancetype)initWithInterval:(NSTimeInterval)interval 135 | onQueue:(dispatch_queue_t)queue 136 | taskBlock:(HWDebounceTaskBlock)taskBlock { 137 | self = [super init]; 138 | if (self) { 139 | _interval = interval; 140 | _taskBlock = taskBlock; 141 | _queue = queue; 142 | } 143 | return self; 144 | } 145 | 146 | - (void)call { 147 | if (self.lastCallTaskDate) { 148 | if ([[NSDate date] timeIntervalSinceDate:self.lastCallTaskDate] > self.interval) { 149 | [self runTaskDirectly]; 150 | } 151 | } else { 152 | [self runTaskDirectly]; 153 | } 154 | self.lastCallTaskDate = [NSDate date]; 155 | } 156 | 157 | - (void)invalidate { 158 | self.taskBlock = nil; 159 | self.block = nil; 160 | } 161 | 162 | - (void)runTaskDirectly { 163 | dispatch_async(self.queue, ^{ 164 | if (self.taskBlock) { 165 | self.taskBlock(); 166 | } 167 | }); 168 | } 169 | 170 | @end 171 | 172 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/HWDebounceTestVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // HWDebounceTestVC.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface HWDebounceTestVC : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/HWDebounceTestVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // HWDebounceTestVC.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/23. 6 | // 7 | 8 | #import "HWDebounceTestVC.h" 9 | #import "UIView+LayoutHelper.h" 10 | #import "HWDebounce.h" 11 | 12 | @interface HWDebounceTestVC () 13 | 14 | @property (nonatomic, strong) UIButton *testButton; 15 | @property (nonatomic, strong) UIButton *backButton; 16 | @property (nonatomic, strong) UIButton *selectModeButton; 17 | @property (nonatomic, strong) UILabel *selectLabel; 18 | @property (nonatomic, strong) UILabel *showLabel; 19 | @property (nonatomic, strong) HWDebounce *testDebouncer; 20 | @property (nonatomic, assign) HWDebounceMode selectedMode; 21 | @property (nonatomic, assign) NSUInteger clickCount; 22 | @property (nonatomic, assign) NSUInteger callCount; 23 | 24 | @end 25 | 26 | @implementation HWDebounceTestVC 27 | 28 | - (void)viewDidLoad { 29 | [super viewDidLoad]; 30 | self.selectedMode = HWDebounceModeTrailing; 31 | [self.view setBackgroundColor:[UIColor whiteColor]]; 32 | [self.view addSubview:self.testButton]; 33 | [self.view addSubview:self.backButton]; 34 | [self.view addSubview:self.selectModeButton]; 35 | [self.view addSubview:self.selectLabel]; 36 | [self.view addSubview:self.showLabel]; 37 | } 38 | 39 | - (void)viewWillLayoutSubviews { 40 | [super viewWillLayoutSubviews]; 41 | [self.testButton setFrame:CGRectMake(0, 0, 150, 30)]; 42 | self.testButton.center = self.view.center; 43 | [self.backButton setFrame:CGRectMake(self.testButton.x, self.testButton.maxY + 100, 150, 30)]; 44 | [self.selectModeButton setFrame:CGRectMake(self.testButton.x, self.testButton.y - 30 - 20, 150, 30)]; 45 | [self.selectLabel setFrame:CGRectMake(0, self.testButton.y - 30 - 20, (self.view.width - 150) / 2 - 5, 30)]; 46 | [self.showLabel setFrame:CGRectMake(0, self.testButton.y - 100 - 30, self.view.width, 30)]; 47 | } 48 | 49 | - (void)viewWillDisappear:(BOOL)animated { 50 | [super viewWillDisappear:animated]; 51 | [self.testDebouncer invalidate]; 52 | } 53 | 54 | - (void)dealloc { 55 | //put breakpoint here to check whether there's a retain cycle here 56 | NSLog(@""); 57 | } 58 | 59 | #pragma mark - private methods 60 | 61 | - (void)testDebounce { 62 | if (!self.testDebouncer) { 63 | self.testDebouncer = [[HWDebounce alloc] initWithInterval:1 taskBlock:^{ 64 | self.callCount++; 65 | [self refreshCountLabel]; 66 | }]; 67 | } 68 | [self.testDebouncer call]; 69 | 70 | self.clickCount++; 71 | [self refreshCountLabel]; 72 | } 73 | 74 | - (void)dismiss { 75 | [self dismissViewControllerAnimated:YES completion:nil]; 76 | } 77 | 78 | - (void)selectMode { 79 | UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; 80 | __weak UIAlertController *bAlertVC = alertVC; 81 | void (^block)(UIAlertAction *action) = ^void(UIAlertAction *action){ 82 | NSUInteger index = [bAlertVC.actions indexOfObject:action]; 83 | self.selectedMode = index; 84 | [self.selectModeButton setTitle:[self nameForMode:self.selectedMode] forState:UIControlStateNormal]; 85 | self.clickCount = 0; 86 | self.callCount = 0; 87 | [self refreshCountLabel]; 88 | 89 | [self.testDebouncer invalidate]; 90 | self.testDebouncer = [[HWDebounce alloc] initWithInterval:1 taskBlock:^{ 91 | self.callCount++; 92 | [self refreshCountLabel]; 93 | }]; 94 | }; 95 | 96 | [alertVC addAction:[UIAlertAction actionWithTitle:[self nameForMode:HWDebounceModeTrailing] 97 | style:UIAlertActionStyleDefault 98 | handler:block]]; 99 | [alertVC addAction:[UIAlertAction actionWithTitle:[self nameForMode:HWDebounceModeLeading] 100 | style:UIAlertActionStyleDefault 101 | handler:block]]; 102 | [alertVC addAction:[UIAlertAction actionWithTitle:@"Cancel" 103 | style:UIAlertActionStyleCancel 104 | handler:nil]]; 105 | 106 | [self presentViewController:alertVC animated:YES completion:nil]; 107 | } 108 | 109 | - (NSString *)nameForMode:(HWDebounceMode)mode { 110 | NSString *name = nil; 111 | switch (mode) { 112 | case HWDebounceModeTrailing: 113 | name = @"Trailing"; 114 | break; 115 | 116 | case HWDebounceModeLeading: 117 | name = @"Leading"; 118 | break; 119 | } 120 | return name; 121 | } 122 | 123 | - (UIColor *)colorForR:(CGFloat)r G:(CGFloat)g B:(CGFloat)b { 124 | return [UIColor colorWithRed:r / 255.0 green:g / 255.0 blue:b / 255.0 alpha:1]; 125 | } 126 | 127 | - (void)refreshCountLabel { 128 | _showLabel.text = [NSString stringWithFormat:@"click count: %lu, call count: %lu", (unsigned long)self.clickCount, (unsigned long)self.callCount]; 129 | } 130 | 131 | #pragma mark - settters & getters 132 | 133 | - (UIButton *)testButton { 134 | if (!_testButton) { 135 | _testButton = [UIButton buttonWithType:UIButtonTypeCustom]; 136 | _testButton.backgroundColor = [self colorForR:51 G:109 B:204]; 137 | [_testButton setTitle:@"Click Me" forState:UIControlStateNormal]; 138 | [_testButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 139 | [_testButton setTitleColor:[self colorForR:51 G:109 B:204] forState:UIControlStateHighlighted]; 140 | _testButton.titleLabel.font = [UIFont systemFontOfSize:17]; 141 | _testButton.clipsToBounds = YES; 142 | _testButton.layer.cornerRadius = 10; 143 | [_testButton addTarget:self action:@selector(testDebounce) forControlEvents:UIControlEventTouchUpInside]; 144 | } 145 | return _testButton; 146 | } 147 | 148 | - (UIButton *)backButton { 149 | if (!_backButton) { 150 | _backButton = [UIButton buttonWithType:UIButtonTypeCustom]; 151 | _backButton.backgroundColor = [self colorForR:65 G:162 B:192]; 152 | [_backButton setTitle:@"Back" forState:UIControlStateNormal]; 153 | [_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 154 | [_backButton setTitleColor:[self colorForR:65 G:162 B:192] forState:UIControlStateHighlighted]; 155 | _backButton.titleLabel.font = [UIFont systemFontOfSize:17]; 156 | _backButton.clipsToBounds = YES; 157 | _backButton.layer.cornerRadius = 10; 158 | [_backButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; 159 | } 160 | return _backButton; 161 | } 162 | 163 | - (UIButton *)selectModeButton { 164 | if (!_selectModeButton) { 165 | _selectModeButton = [UIButton buttonWithType:UIButtonTypeCustom]; 166 | _selectModeButton.backgroundColor = [self colorForR:65 G:162 B:192]; 167 | [_selectModeButton setTitle:[self nameForMode:self.selectedMode] forState:UIControlStateNormal]; 168 | [_selectModeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 169 | [_selectModeButton setTitleColor:[self colorForR:65 G:162 B:192] forState:UIControlStateHighlighted]; 170 | _selectModeButton.titleLabel.font = [UIFont systemFontOfSize:17]; 171 | _selectModeButton.clipsToBounds = YES; 172 | _selectModeButton.layer.cornerRadius = 10; 173 | [_selectModeButton addTarget:self action:@selector(selectMode) forControlEvents:UIControlEventTouchUpInside]; 174 | } 175 | return _selectModeButton; 176 | } 177 | 178 | - (UILabel *)selectLabel { 179 | if (!_selectLabel) { 180 | _selectLabel = [[UILabel alloc] init]; 181 | _selectLabel.text = @"Select Mode:"; 182 | _selectLabel.textColor = [UIColor grayColor]; 183 | _selectLabel.font = [UIFont systemFontOfSize:17]; 184 | _selectLabel.textAlignment = NSTextAlignmentRight; 185 | } 186 | return _selectLabel; 187 | } 188 | 189 | - (UILabel *)showLabel { 190 | if (!_showLabel) { 191 | _showLabel = [[UILabel alloc] init]; 192 | _showLabel.text = @"click count: 0, call count: 0"; 193 | _showLabel.textColor = [UIColor grayColor]; 194 | _showLabel.font = [UIFont boldSystemFontOfSize:20]; 195 | _showLabel.textAlignment = NSTextAlignmentCenter; 196 | } 197 | return _showLabel; 198 | } 199 | 200 | 201 | @end 202 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/HWThrottleTestVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // HWThrottleTestVC.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/20. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface HWThrottleTestVC : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/HWThrottleTestVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // HWThrottleTestVC.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/20. 6 | // 7 | 8 | #import "HWThrottleTestVC.h" 9 | #import "UIView+LayoutHelper.h" 10 | #import "HWThrottle.h" 11 | 12 | @interface HWThrottleTestVC () 13 | 14 | @property (nonatomic, strong) UIButton *testButton; 15 | @property (nonatomic, strong) UIButton *backButton; 16 | @property (nonatomic, strong) UIButton *selectModeButton; 17 | @property (nonatomic, strong) UILabel *selectLabel; 18 | @property (nonatomic, strong) UILabel *showLabel; 19 | @property (nonatomic, strong) HWThrottle *testThrottler; 20 | @property (nonatomic, assign) HWThrottleMode selectedMode; 21 | @property (nonatomic, assign) NSUInteger clickCount; 22 | @property (nonatomic, assign) NSUInteger callCount; 23 | 24 | @end 25 | 26 | @implementation HWThrottleTestVC 27 | 28 | #pragma mark - life cycle 29 | 30 | - (void)viewDidLoad { 31 | [super viewDidLoad]; 32 | self.selectedMode = HWThrottleModeLeading; 33 | [self.view setBackgroundColor:[UIColor whiteColor]]; 34 | [self.view addSubview:self.testButton]; 35 | [self.view addSubview:self.backButton]; 36 | [self.view addSubview:self.selectModeButton]; 37 | [self.view addSubview:self.selectLabel]; 38 | [self.view addSubview:self.showLabel]; 39 | } 40 | 41 | - (void)viewWillLayoutSubviews { 42 | [super viewWillLayoutSubviews]; 43 | [self.testButton setFrame:CGRectMake(0, 0, 150, 30)]; 44 | self.testButton.center = self.view.center; 45 | [self.backButton setFrame:CGRectMake(self.testButton.x, self.testButton.maxY + 100, 150, 30)]; 46 | [self.selectModeButton setFrame:CGRectMake(self.testButton.x, self.testButton.y - 30 - 20, 150, 30)]; 47 | [self.selectLabel setFrame:CGRectMake(0, self.testButton.y - 30 - 20, (self.view.width - 150) / 2 - 5, 30)]; 48 | [self.showLabel setFrame:CGRectMake(0, self.testButton.y - 100 - 30, self.view.width, 30)]; 49 | } 50 | 51 | - (void)viewWillDisappear:(BOOL)animated { 52 | [super viewWillDisappear:animated]; 53 | [self.testThrottler invalidate]; 54 | } 55 | 56 | - (void)dealloc { 57 | NSLog(@""); 58 | } 59 | 60 | #pragma mark - private methods 61 | 62 | - (void)testThrottle { 63 | if (!self.testThrottler) { 64 | self.testThrottler = [[HWThrottle alloc] initWithInterval:1 taskBlock:^{ 65 | self.callCount++; 66 | [self refreshCountLabel]; 67 | }]; 68 | } 69 | [self.testThrottler call]; 70 | 71 | self.clickCount++; 72 | [self refreshCountLabel]; 73 | } 74 | 75 | - (void)dismiss { 76 | [self dismissViewControllerAnimated:YES completion:nil]; 77 | } 78 | 79 | - (void)selectMode { 80 | UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; 81 | __weak UIAlertController *bAlertVC = alertVC; 82 | void (^block)(UIAlertAction *action) = ^void(UIAlertAction *action){ 83 | NSUInteger index = [bAlertVC.actions indexOfObject:action]; 84 | self.selectedMode = index; 85 | [self.selectModeButton setTitle:[self nameForMode:self.selectedMode] forState:UIControlStateNormal]; 86 | self.clickCount = 0; 87 | self.callCount = 0; 88 | [self refreshCountLabel]; 89 | 90 | [self.testThrottler invalidate]; 91 | self.testThrottler = [[HWThrottle alloc] initWithInterval:1 taskBlock:^{ 92 | self.callCount++; 93 | [self refreshCountLabel]; 94 | }]; 95 | }; 96 | 97 | [alertVC addAction:[UIAlertAction actionWithTitle:[self nameForMode:HWThrottleModeLeading] 98 | style:UIAlertActionStyleDefault 99 | handler:block]]; 100 | [alertVC addAction:[UIAlertAction actionWithTitle:[self nameForMode:HWThrottleModeTrailing] 101 | style:UIAlertActionStyleDefault 102 | handler:block]]; 103 | [alertVC addAction:[UIAlertAction actionWithTitle:@"Cancel" 104 | style:UIAlertActionStyleCancel 105 | handler:nil]]; 106 | 107 | [self presentViewController:alertVC animated:YES completion:nil]; 108 | } 109 | 110 | - (NSString *)nameForMode:(HWThrottleMode)mode { 111 | NSString *name = nil; 112 | switch (mode) { 113 | case HWThrottleModeLeading: 114 | name = @"Leading"; 115 | break; 116 | 117 | case HWThrottleModeTrailing: 118 | name = @"Trailing"; 119 | break; 120 | } 121 | return name; 122 | } 123 | 124 | - (UIColor *)colorForR:(CGFloat)r G:(CGFloat)g B:(CGFloat)b { 125 | return [UIColor colorWithRed:r / 255.0 green:g / 255.0 blue:b / 255.0 alpha:1]; 126 | } 127 | 128 | - (void)refreshCountLabel { 129 | _showLabel.text = [NSString stringWithFormat:@"click count: %lu, call count: %lu", (unsigned long)self.clickCount, (unsigned long)self.callCount]; 130 | } 131 | 132 | #pragma mark - settters & getters 133 | 134 | - (UIButton *)testButton { 135 | if (!_testButton) { 136 | _testButton = [UIButton buttonWithType:UIButtonTypeCustom]; 137 | _testButton.backgroundColor = [self colorForR:51 G:109 B:204]; 138 | [_testButton setTitle:@"Click Me" forState:UIControlStateNormal]; 139 | [_testButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 140 | [_testButton setTitleColor:[self colorForR:51 G:109 B:204] forState:UIControlStateHighlighted]; 141 | _testButton.titleLabel.font = [UIFont systemFontOfSize:17]; 142 | _testButton.clipsToBounds = YES; 143 | _testButton.layer.cornerRadius = 10; 144 | [_testButton addTarget:self action:@selector(testThrottle) forControlEvents:UIControlEventTouchUpInside]; 145 | } 146 | return _testButton; 147 | } 148 | 149 | - (UIButton *)backButton { 150 | if (!_backButton) { 151 | _backButton = [UIButton buttonWithType:UIButtonTypeCustom]; 152 | _backButton.backgroundColor = [self colorForR:65 G:162 B:192]; 153 | [_backButton setTitle:@"Back" forState:UIControlStateNormal]; 154 | [_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 155 | [_backButton setTitleColor:[self colorForR:65 G:162 B:192] forState:UIControlStateHighlighted]; 156 | _backButton.titleLabel.font = [UIFont systemFontOfSize:17]; 157 | _backButton.clipsToBounds = YES; 158 | _backButton.layer.cornerRadius = 10; 159 | [_backButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; 160 | } 161 | return _backButton; 162 | } 163 | 164 | - (UIButton *)selectModeButton { 165 | if (!_selectModeButton) { 166 | _selectModeButton = [UIButton buttonWithType:UIButtonTypeCustom]; 167 | _selectModeButton.backgroundColor = [self colorForR:65 G:162 B:192]; 168 | [_selectModeButton setTitle:[self nameForMode:self.selectedMode] forState:UIControlStateNormal]; 169 | [_selectModeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 170 | [_selectModeButton setTitleColor:[self colorForR:65 G:162 B:192] forState:UIControlStateHighlighted]; 171 | _selectModeButton.titleLabel.font = [UIFont systemFontOfSize:17]; 172 | _selectModeButton.clipsToBounds = YES; 173 | _selectModeButton.layer.cornerRadius = 10; 174 | [_selectModeButton addTarget:self action:@selector(selectMode) forControlEvents:UIControlEventTouchUpInside]; 175 | } 176 | return _selectModeButton; 177 | } 178 | 179 | - (UILabel *)selectLabel { 180 | if (!_selectLabel) { 181 | _selectLabel = [[UILabel alloc] init]; 182 | _selectLabel.text = @"Select Mode:"; 183 | _selectLabel.textColor = [UIColor grayColor]; 184 | _selectLabel.font = [UIFont systemFontOfSize:17]; 185 | _selectLabel.textAlignment = NSTextAlignmentRight; 186 | } 187 | return _selectLabel; 188 | } 189 | 190 | - (UILabel *)showLabel { 191 | if (!_showLabel) { 192 | _showLabel = [[UILabel alloc] init]; 193 | _showLabel.text = @"click count: 0, call count: 0"; 194 | _showLabel.textColor = [UIColor grayColor]; 195 | _showLabel.font = [UIFont boldSystemFontOfSize:20]; 196 | _showLabel.textAlignment = NSTextAlignmentCenter; 197 | } 198 | return _showLabel; 199 | } 200 | 201 | @end 202 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/SceneDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import 9 | 10 | @interface SceneDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow * window; 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/SceneDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import "SceneDelegate.h" 9 | 10 | @interface SceneDelegate () 11 | 12 | @end 13 | 14 | @implementation SceneDelegate 15 | 16 | 17 | - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | } 22 | 23 | 24 | - (void)sceneDidDisconnect:(UIScene *)scene { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | 32 | - (void)sceneDidBecomeActive:(UIScene *)scene { 33 | // Called when the scene has moved from an inactive state to an active state. 34 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 35 | } 36 | 37 | 38 | - (void)sceneWillResignActive:(UIScene *)scene { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | 44 | - (void)sceneWillEnterForeground:(UIScene *)scene { 45 | // Called as the scene transitions from the background to the foreground. 46 | // Use this method to undo the changes made on entering the background. 47 | } 48 | 49 | 50 | - (void)sceneDidEnterBackground:(UIScene *)scene { 51 | // Called as the scene transitions from the foreground to the background. 52 | // Use this method to save data, release shared resources, and store enough scene-specific state information 53 | // to restore the scene back to its current state. 54 | } 55 | 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Throttle/HWThrottle.h: -------------------------------------------------------------------------------- 1 | // 2 | // HWThrottle.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/9. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | #pragma mark - public class 13 | 14 | typedef NS_ENUM(NSUInteger, HWThrottleMode) { 15 | HWThrottleModeLeading, //invoking on the leading edge of the timeout 16 | HWThrottleModeTrailing, //invoking on the trailing edge of the timeout 17 | }; 18 | 19 | typedef void(^HWThrottleTaskBlock)(void); 20 | 21 | @interface HWThrottle : NSObject 22 | 23 | /// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading, the execution queue defaults to the main queue. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 24 | /// @param interval throttle time interval, unit second 25 | /// @param taskBlock the task to be throttled 26 | - (instancetype)initWithInterval:(NSTimeInterval)interval 27 | taskBlock:(HWThrottleTaskBlock)taskBlock; 28 | 29 | /// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 30 | /// @param interval throttle time interval, unit second 31 | /// @param queue execution queue, defaults the main queue 32 | /// @param taskBlock the task to be throttled 33 | - (instancetype)initWithInterval:(NSTimeInterval)interval 34 | onQueue:(dispatch_queue_t)queue 35 | taskBlock:(HWThrottleTaskBlock)taskBlock; 36 | 37 | /// Initialize a debounce object. Note that debounce is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 38 | /// @param throttleMode the throttle mode, defaults HWThrottleModeLeading 39 | /// @param interval throttle time interval, unit second 40 | /// @param queue execution queue, defaults the main queue 41 | /// @param taskBlock the task to be throttled 42 | - (instancetype)initWithThrottleMode:(HWThrottleMode)throttleMode 43 | interval:(NSTimeInterval)interval 44 | onQueue:(dispatch_queue_t)queue 45 | taskBlock:(HWThrottleTaskBlock)taskBlock; 46 | 47 | 48 | /// throttling call the task 49 | - (void)call; 50 | 51 | 52 | /// When the owner of the HWThrottle object is about to release, call this method on the HWThrottle object first to prevent circular references 53 | - (void)invalidate; 54 | 55 | @end 56 | 57 | #pragma mark - private classes 58 | 59 | @interface HWThrottleLeading : HWThrottle 60 | 61 | @end 62 | 63 | @interface HWThrottleTrailing : HWThrottle 64 | 65 | @end 66 | 67 | NS_ASSUME_NONNULL_END 68 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/Throttle/HWThrottle.m: -------------------------------------------------------------------------------- 1 | // 2 | // HWThrottle.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/9. 6 | // 7 | 8 | #import "HWThrottle.h" 9 | 10 | 11 | #pragma mark - HWThrottle 12 | 13 | @interface HWThrottle() 14 | 15 | @end 16 | 17 | @implementation HWThrottle 18 | 19 | #pragma mark - life cycle 20 | 21 | - (instancetype)initWithInterval:(NSTimeInterval)interval 22 | taskBlock:(HWThrottleTaskBlock)taskBlock { 23 | return [self initWithInterval:interval 24 | onQueue:dispatch_get_main_queue() 25 | taskBlock:taskBlock]; 26 | } 27 | 28 | - (instancetype)initWithInterval:(NSTimeInterval)interval 29 | onQueue:(dispatch_queue_t)queue 30 | taskBlock:(HWThrottleTaskBlock)taskBlock { 31 | return [self initWithThrottleMode:HWThrottleModeLeading 32 | interval:interval 33 | onQueue:queue 34 | taskBlock:taskBlock]; 35 | } 36 | 37 | - (instancetype)initWithThrottleMode:(HWThrottleMode)throttleMode 38 | interval:(NSTimeInterval)interval 39 | onQueue:(dispatch_queue_t)queue 40 | taskBlock:(HWThrottleTaskBlock)taskBlock { 41 | if (interval < 0) { 42 | interval = 0.1; 43 | } 44 | if (!queue) { 45 | queue = dispatch_get_main_queue(); 46 | } 47 | 48 | switch (throttleMode) { 49 | case HWThrottleModeLeading: { 50 | self = [[HWThrottleLeading alloc] initWithInterval:interval 51 | onQueue:queue 52 | taskBlock:taskBlock]; 53 | break; 54 | } 55 | case HWThrottleModeTrailing: { 56 | self = [[HWThrottleTrailing alloc] initWithInterval:interval 57 | onQueue:queue 58 | taskBlock:taskBlock]; 59 | break; 60 | } 61 | } 62 | return self; 63 | } 64 | 65 | #pragma mark - public methods 66 | 67 | - (void)call { 68 | NSAssert(1, @"This method should be overrided!"); 69 | } 70 | 71 | - (void)invalidate { 72 | NSAssert(1, @"This method should be overrided!"); 73 | } 74 | 75 | @end 76 | 77 | #pragma mark - HWThrottleLeading 78 | 79 | @interface HWThrottleLeading() 80 | 81 | @property (nonatomic, assign) NSTimeInterval interval; 82 | @property (nonatomic, copy) HWThrottleTaskBlock taskBlock; 83 | @property (nonatomic, strong) dispatch_queue_t queue; 84 | @property (nonatomic, strong) NSDate *lastRunTaskDate; 85 | 86 | @end 87 | 88 | @implementation HWThrottleLeading 89 | 90 | - (instancetype)initWithInterval:(NSTimeInterval)interval 91 | onQueue:(dispatch_queue_t)queue 92 | taskBlock:(HWThrottleTaskBlock)taskBlock { 93 | self = [super init]; 94 | if (self) { 95 | _interval = interval; 96 | _taskBlock = taskBlock; 97 | _queue = queue; 98 | } 99 | return self; 100 | } 101 | 102 | - (void)call { 103 | if (self.lastRunTaskDate) { 104 | if ([[NSDate date] timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) { 105 | [self runTaskDirectly]; 106 | } 107 | } else { 108 | [self runTaskDirectly]; 109 | } 110 | } 111 | 112 | - (void)runTaskDirectly { 113 | dispatch_async(self.queue, ^{ 114 | if (self.taskBlock) { 115 | self.taskBlock(); 116 | } 117 | self.lastRunTaskDate = [NSDate date]; 118 | }); 119 | } 120 | 121 | - (void)invalidate { 122 | self.taskBlock = nil; 123 | } 124 | 125 | @end 126 | 127 | #pragma mark - HWThrottleTrailing 128 | 129 | @interface HWThrottleTrailing() 130 | 131 | @property (nonatomic, assign) NSTimeInterval interval; 132 | @property (nonatomic, copy) HWThrottleTaskBlock taskBlock; 133 | @property (nonatomic, strong) dispatch_queue_t queue; 134 | @property (nonatomic, strong) NSDate *lastRunTaskDate; 135 | @property (nonatomic, strong) NSDate *nextRunTaskDate; 136 | 137 | @end 138 | 139 | @implementation HWThrottleTrailing 140 | 141 | - (instancetype)initWithInterval:(NSTimeInterval)interval 142 | onQueue:(dispatch_queue_t)queue 143 | taskBlock:(HWThrottleTaskBlock)taskBlock { 144 | self = [super init]; 145 | if (self) { 146 | _interval = interval; 147 | _taskBlock = taskBlock; 148 | _queue = queue; 149 | } 150 | return self; 151 | } 152 | 153 | - (void)call { 154 | NSDate *now = [NSDate date]; 155 | if (!self.nextRunTaskDate) { 156 | if (self.lastRunTaskDate) { 157 | if ([now timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) { 158 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now]; 159 | } else { 160 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:self.lastRunTaskDate]; 161 | } 162 | } else { 163 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now]; 164 | } 165 | 166 | 167 | NSTimeInterval nextInterval = [self.nextRunTaskDate timeIntervalSinceDate:now]; 168 | 169 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextInterval * NSEC_PER_SEC)), self.queue, ^{ 170 | if (self.taskBlock) { 171 | self.taskBlock(); 172 | } 173 | self.lastRunTaskDate = [NSDate date]; 174 | self.nextRunTaskDate = nil; 175 | }); 176 | } 177 | } 178 | 179 | - (void)invalidate { 180 | self.taskBlock = nil; 181 | } 182 | 183 | @end 184 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/UIView+LayoutHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+LayoutHelper.h 3 | // gf_iphone 4 | // 5 | // Created by Highway on 2017/8/31. 6 | // Copyright © 2017年 . All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (LayoutHelper) 12 | 13 | /** 14 | * frame.origin.x 15 | */ 16 | @property (nonatomic) CGFloat x; 17 | 18 | /** 19 | * frame.origin.y 20 | */ 21 | @property (nonatomic) CGFloat y; 22 | 23 | /** 24 | * frame.size.width; 25 | */ 26 | @property (nonatomic) CGFloat width; 27 | 28 | /** 29 | * frame.size.height 30 | */ 31 | @property (nonatomic) CGFloat height; 32 | 33 | /** 34 | * center.x 35 | */ 36 | @property (nonatomic) CGFloat xCenter; 37 | 38 | /** 39 | * center.y 40 | */ 41 | @property (nonatomic) CGFloat yCenter; 42 | 43 | /** 44 | * frame.origin 45 | */ 46 | @property (nonatomic) CGPoint origin; 47 | 48 | /** 49 | * frame.size 50 | */ 51 | @property (nonatomic) CGSize size; 52 | 53 | /** 54 | * CGRectGetMaxX 55 | */ 56 | @property (nonatomic, readonly) CGFloat maxX; 57 | 58 | /** 59 | * CGRectGetMaxY 60 | */ 61 | @property (nonatomic, readonly) CGFloat maxY; 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/UIView+LayoutHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+LayoutHelper.m 3 | // gf_iphone 4 | // 5 | // Created by Highway on 2017/8/31. 6 | // Copyright © 2017年 . All rights reserved. 7 | // 8 | 9 | #import "UIView+LayoutHelper.h" 10 | 11 | @implementation UIView (LayoutHelper) 12 | 13 | - (CGFloat)x { 14 | return CGRectGetMinX(self.frame); 15 | } 16 | 17 | - (void)setX:(CGFloat)x { 18 | CGRect newFrame = self.frame; 19 | newFrame.origin.x = x; 20 | self.frame = newFrame; 21 | } 22 | 23 | - (CGFloat)y { 24 | return CGRectGetMinY(self.frame); 25 | } 26 | 27 | - (void)setY:(CGFloat)y { 28 | CGRect newFrame = self.frame; 29 | newFrame.origin.y = y; 30 | self.frame = newFrame; 31 | } 32 | 33 | - (CGFloat)width { 34 | return CGRectGetWidth(self.frame); 35 | } 36 | 37 | - (void)setWidth:(CGFloat)width { 38 | CGRect newFrame = self.frame; 39 | newFrame.size.width = width; 40 | self.frame = newFrame; 41 | } 42 | 43 | - (CGFloat)height { 44 | return CGRectGetHeight(self.frame); 45 | } 46 | 47 | - (void)setHeight:(CGFloat)height { 48 | CGRect newFrame = self.frame; 49 | newFrame.size.height = height; 50 | self.frame = newFrame; 51 | } 52 | 53 | - (CGFloat)xCenter { 54 | return CGRectGetMidX(self.frame); 55 | } 56 | 57 | - (void)setXCenter:(CGFloat)xCenter { 58 | CGPoint newPoint = self.center; 59 | newPoint.x = xCenter; 60 | self.center = newPoint; 61 | } 62 | 63 | - (CGFloat)yCenter { 64 | return CGRectGetMidY(self.frame); 65 | } 66 | 67 | - (void)setYCenter:(CGFloat)yCenter { 68 | CGPoint newPoint = self.center; 69 | newPoint.y = yCenter; 70 | self.center = newPoint; 71 | } 72 | 73 | - (CGPoint)origin { 74 | return self.frame.origin; 75 | } 76 | 77 | - (void)setOrigin:(CGPoint)origin { 78 | CGRect newFrame = self.frame; 79 | newFrame.origin = origin; 80 | self.frame = newFrame; 81 | } 82 | 83 | - (CGSize)size { 84 | return self.frame.size; 85 | } 86 | 87 | - (void)setSize:(CGSize)size { 88 | CGRect newFrame = self.frame; 89 | newFrame.size = size; 90 | self.frame = newFrame; 91 | } 92 | 93 | - (CGFloat)maxX { 94 | return CGRectGetMaxX(self.frame); 95 | } 96 | 97 | - (CGFloat)maxY { 98 | return CGRectGetMaxY(self.frame); 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import 9 | 10 | @interface ViewController : UIViewController 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // Throttle 4 | // 5 | // Created by highwayLiu on 2021/2/9. 6 | // 7 | 8 | #import "ViewController.h" 9 | #import "HWThrottle.h" 10 | #import "HWThrottleTestVC.h" 11 | #import "HWDebounceTestVC.h" 12 | #import "UIView+LayoutHelper.h" 13 | 14 | @interface ViewController () 15 | 16 | @property (nonatomic, strong) UIButton *throttleButton; 17 | @property (nonatomic, strong) UIButton *debounceButton; 18 | 19 | @end 20 | 21 | @implementation ViewController 22 | 23 | - (void)viewDidLoad { 24 | [super viewDidLoad]; 25 | [self.view addSubview:self.throttleButton]; 26 | [self.view addSubview:self.debounceButton]; 27 | } 28 | 29 | - (void)viewWillLayoutSubviews { 30 | [super viewWillLayoutSubviews]; 31 | [self.throttleButton setFrame:CGRectMake(0, 0, 150, 40)]; 32 | self.throttleButton.center = self.view.center; 33 | [self.debounceButton setFrame:CGRectMake(self.throttleButton.x, self.throttleButton.maxY + 30, 150, 40)]; 34 | } 35 | 36 | - (UIButton *)throttleButton { 37 | if (!_throttleButton) { 38 | _throttleButton = [UIButton buttonWithType:UIButtonTypeCustom]; 39 | _throttleButton.backgroundColor = [UIColor colorWithRed:65 / 255.0 green:162 / 255.0 blue:192 / 255.0 alpha:1]; 40 | [_throttleButton setTitle:@"Test Throttle" forState:UIControlStateNormal]; 41 | [_throttleButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 42 | [_throttleButton setTitleColor:[UIColor colorWithRed:65 / 255.0 green:162 / 255.0 blue:192 / 255.0 alpha:1] forState:UIControlStateHighlighted]; 43 | _throttleButton.titleLabel.font = [UIFont systemFontOfSize:17]; 44 | _throttleButton.clipsToBounds = YES; 45 | _throttleButton.layer.cornerRadius = 10; 46 | [_throttleButton addTarget:self action:@selector(goToThrottlePage) forControlEvents:UIControlEventTouchUpInside]; 47 | } 48 | return _throttleButton; 49 | } 50 | 51 | - (UIButton *)debounceButton { 52 | if (!_debounceButton) { 53 | _debounceButton = [UIButton buttonWithType:UIButtonTypeCustom]; 54 | _debounceButton.backgroundColor = [UIColor colorWithRed:65 / 255.0 green:162 / 255.0 blue:192 / 255.0 alpha:1]; 55 | [_debounceButton setTitle:@"Test Debounce" forState:UIControlStateNormal]; 56 | [_debounceButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 57 | [_debounceButton setTitleColor:[UIColor colorWithRed:65 / 255.0 green:162 / 255.0 blue:192 / 255.0 alpha:1] forState:UIControlStateHighlighted]; 58 | _debounceButton.titleLabel.font = [UIFont systemFontOfSize:17]; 59 | _debounceButton.clipsToBounds = YES; 60 | _debounceButton.layer.cornerRadius = 10; 61 | [_debounceButton addTarget:self action:@selector(goToDebouncePage) forControlEvents:UIControlEventTouchUpInside]; 62 | } 63 | return _debounceButton; 64 | } 65 | 66 | - (void)goToThrottlePage { 67 | HWThrottleTestVC *vc = [[HWThrottleTestVC alloc] init]; 68 | vc.modalPresentationStyle = UIModalPresentationFullScreen; 69 | [self presentViewController:vc animated:YES completion:nil]; 70 | } 71 | 72 | - (void)goToDebouncePage { 73 | HWDebounceTestVC *vc = [[HWDebounceTestVC alloc] init]; 74 | vc.modalPresentationStyle = UIModalPresentationFullScreen; 75 | [self presentViewController:vc animated:YES completion:nil]; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /HWThrottle/HWThrottle/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // HWThrottle 4 | // 5 | // Created by highwayLiu on 2021/2/19. 6 | // 7 | 8 | #import 9 | #import "AppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | NSString * appDelegateClassName; 13 | @autoreleasepool { 14 | // Setup code that might create autoreleased objects goes here. 15 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 16 | } 17 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 HighwayLaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HWThrottle 2 | [函数节流(Throttle)和防抖(Debounce)解析及其OC实现](https://juejin.cn/post/6933952291142074376) 3 | 4 | ## 使用方法 5 | 按需直接将 HWThrottle和HWDebounce的.h及.m文件拖入工程即可。 6 | 7 | ## 一、Throttle和Debounce是什么 8 | Throttle本是机械领域的概念,英文解释为: 9 | >A valve that regulates the supply of fuel to the engine. 10 | 11 | 中文翻译成节流器,用以调节发动机燃料供应的阀门。在计算机领域,同样也引入了Throttle和Debounce概念,这两种技术都可用来降低函数调用频率,相似又有区别。对于连续调用的函数,尤其是触发频率密集、目标函数涉及大量计算时,恰当使用Throttle和Debounce可以有效提升性能及系统稳定性。 12 | 13 | 对于JS前端开发人员,由于无法控制DOM事件触发频率,在给DOM绑定事件的时候,常常需要进行Throttle或者Debounce来防止事件调用过于频繁。而对于iOS开发者来说,也许会觉得这两个术语很陌生,不过你很可能在不经意间已经用到了,只是没想过会有专门的抽象概念。举个常见的例子,对于UITableView,频繁触发reloadData函数可能会引起画面闪动、卡顿,数据源动态变化时甚至会导致崩溃,一些开发者可能会想方设法减少对reload函数的调用,不过对于复杂的UITableView视图可能会显得捉襟见肘,因为reloadData很可能“无处不在”,甚至会被跨文件调用,此时就可以考虑对reloadData函数本身做下降频处理。 14 | 15 | 下面通过概念定义及示例来详细解析对比下Throttle和Debounce,先看下二者在JS的Lodash库中的解释: 16 | 17 | ## Throttle 18 | >Throttle enforces a maximum number of times a function can be called over time. For example, "execute this function at most once every 100 ms." 19 | 20 | 即,Throttle使得函数在规定时间间隔内(如100 ms),最多只能调用一次。 21 | 22 | ## Debounce 23 | > Debounce enforces that a function not be called again until a certain amount of time has passed without it being called. For example, "execute this function only if 100 ms have passed without it being called." 24 | 25 | 即,Debounce可以将小于规定时间间隔(如100 ms)内的函数调用,归并成一次函数调用。 26 | 27 | 对于Debounce的理解,可以想象一下电梯的例子。你在电梯中,门快要关了,突然又有人要进来,电梯此时会再次打开门,直到短时间内没有人再进为止。虽然电梯上行下行的时间延迟了,但是优化了整体资源配置。 28 | 29 | 我们再以拖拽手势回调的动图展示为例,来直观感受下Throttle和Debounce的区别。每次“walk me”图标拖拽时,会产生一次回调。在动图的右上角,可以看到回调函数实际调用的次数。 30 | ### 1)正常回调: 31 | ![正常回调](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6c91a25dace2441ea849cb35a2ca8678~tplv-k3u1fbpfcp-zoom-1.image) 32 | 33 | ### 2)Throttle(Leading)模式下的回调: 34 | ![Throttle(Leading)模式下的回调](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e8867954dce40d3b9981079c0bf6749~tplv-k3u1fbpfcp-zoom-1.image) 35 | 36 | ### 3)Debounce(Trailing)模式下的回调: 37 | ![Debounce(Trailing)模式下的回调](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08823bc7040845c09f1c6fbf55f89b17~tplv-k3u1fbpfcp-zoom-1.image) 38 | 39 | 40 | ## 二、应用场景 41 | 以下是几个典型的Throttle和Debounce应用场景。 42 | 43 | ### 1)防止按钮重复点击 44 | 为了防止用户重复快速点击,导致冗余的网络请求、动画跳转等不必要的损耗,可以使用Throttle的Leading模式,只响应指定时间间隔内的第一次点击。 45 | 46 | ### 2)滚动拖拽等密集事件 47 | 可以在UIScrollView的滚动回调didScroll函数里打日志观察下,调用频率相当高,几乎每移动1个像素都可能产生一次回调,如果回调函数的计算量偏大很可能会导致卡顿,此种情况下就可以考虑使用Throttle降频。 48 | 49 | ### 3)文本输入自动完成 50 | 假如想要实现,在用户输入时实时展示搜索结果,常规的做法是用户每改变一个字符,就触发一次搜索,但此时用户很可能还没有输入完成,造成资源浪费。此时就可以使用Debounce的Trailing模式,在字符改变之后的一段时间内,用户没有继续输入时,再触发搜索动作,从而有效节省网络请求次数。 51 | 52 | ### 4)数据同步 53 | 以用户埋点日志上传为例,没必要在用户每操作一次后就触发一次网络请求,此时就可以使用Debounce的Traling模式,在记录用户开始操作之后,且一段时间内不再操作时,再把日志merge之后上传至服务端。其他类似的场景,比如客户端与服务端版本同步,也可以采取这种策略。 54 | 55 | 在系统层面,或者一些知名的开源库里,也经常可以看到Throttle或者Debounce的身影。 56 | 57 | ### 5) GCD Background Queue 58 | >Items dispatched to the queue run at background priority; the queue is scheduled for execution after all high priority queues have been scheduled and the system runs items on a thread whose priority is set for background status. Such a thread has the lowest priority and any disk I/O is throttled to minimize the impact on the system. 59 | 60 | 在dispatch的Background Queue优先级下,系统会自动将磁盘I/O操作进行Throttle,来降低对系统资源的耗费。 61 | 62 | ### 6)ASIHttpRequest及AFNetworking 63 | ``` 64 | - (void)handleNetworkEvent:(CFStreamEventType)type 65 | { 66 | //... 67 | [self performThrottling]; 68 | //... 69 | } 70 | ``` 71 | ``` 72 | - (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes 73 | delay:(NSTimeInterval)delay; 74 | ``` 75 | 76 | 在弱网环境下, 一个Packet一次传输失败的概率会升高,由于TCP是有序且可靠的,前一个Packet不被ack的情况下,后面的Packet就要等待,所以此时如果启用Network Throttle机制,减小写入数据量,反而会提升网络请求的成功率。 77 | 78 | ## 三、iOS实现 79 | 理解了Throttle和Debounce的概念后,在单个业务场景中实现起来是很容易的事情,但是考虑到其应用如此广泛,就应该封装成为业务无关的组件,减小重复劳动,提升开发效率。 80 | 81 | 前文提过,Throttle和Debounce在Web前端已经有相当成熟的实现,Ben Alman之前做过一个JQuery插件(不再维护),一年后Jeremy Ashkenas把它加入了underscore.js,而后又加入了[Lodash](https://lodash.com/docs/)。但是在iOS开发领域,尤其是对于Objective-C语言,尚且没有一个可靠、稳定且全面的第三方库。 82 | 83 | 杨萧玉曾经开源过一个[MessageThrottle](https://github.com/yulingtianxia/MessageThrottle)库,该库使用Objective-C的runtime与消息转发机制,使用非常便捷。但是这个库的缺点也比较明显,使用了大量的底层HOOK方法,系统兼容性方面还需要进一步的验证和测试,如果集成的项目中同时使用了其他使用底层runtime的方法,可能会产生冲突,导致非预期后果。另外该库是完全面向切面的,作用于全局且隐藏较深,增加了一定的调试成本。 84 | 为此笔者封装了一个新的实现[HWThrottle](https://github.com/HighwayLaw/HWThrottle),并借鉴了Lodash的接口及实现方式,该库有以下特点: 85 | 86 | **1)未使用任何runtime API,全部由顶层API实现;** 87 | 88 | **2)每个业务场景需要使用者自己定义一个实例对象,自行管理生命周期,旨在把对项目的影响控制在最小范围;** 89 | 90 | **3)区分Throttle和Debounce,提供Leading和Trailing选项。** 91 | 92 | 93 | ### Demo 94 | 下面展示了对按钮点击事件进行Throttle或Debounce的效果,click count表示点击按钮次数,call count表示实际调用目标事件的次数。 95 | 96 | 在leading模式下,会在指定时间间隔的开始处触发调用;Trailing模式下,会在指定时间间隔的末尾处触发调用。 97 | 98 | #### 1) Throttle Leading 99 | ![Throttle Leading](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/838a98d40420456a8bd2016bf7b6ea60~tplv-k3u1fbpfcp-zoom-1.image) 100 | #### 2) Throttle Trailing 101 | ![Throttle Trailing](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e70d46d892843389bee8fb7366c32de~tplv-k3u1fbpfcp-zoom-1.image) 102 | #### 3) Debounce Trailing 103 | ![Debounce Trailing](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0fb486372794d08b3eaddac992d920c~tplv-k3u1fbpfcp-zoom-1.image) 104 | #### 4) Debounce Leading 105 | ![Debounce Leading](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e114e55651c40dd8e38f0c1fc0f8c2d~tplv-k3u1fbpfcp-zoom-1.image) 106 | 107 | ### 使用示例: 108 | ``` 109 | if (!self.testThrottler) { 110 | self.testThrottler = [[HWThrottle alloc] initWithInterval:1 taskBlock:^{ 111 | //do some heavy tasks 112 | }]; 113 | } 114 | [self.testThrottler call]; 115 | ``` 116 | 由于使用到了block,**注意在Throttle或Debounce对象所有者即将释放时,即不再使用block时调用invalidate**,该方法会将持有的task block置空,防止循环引用。如果是在页面中使用Throttle或Debounce对象,可在disappear回调中调用invalidate方法。 117 | 118 | ``` 119 | - (void)viewDidDisappear:(BOOL)animated { 120 | [super viewDidDisappear:animated]; 121 | [self.testThrottler invalidate]; 122 | } 123 | ``` 124 | 125 | ### 接口API: 126 | 127 | **HWThrottle.h:** 128 | ``` 129 | #pragma mark - public class 130 | 131 | typedef NS_ENUM(NSUInteger, HWThrottleMode) { 132 | HWThrottleModeLeading, //invoking on the leading edge of the timeout 133 | HWThrottleModeTrailing, //invoking on the trailing edge of the timeout 134 | }; 135 | 136 | typedef void(^HWThrottleTaskBlock)(void); 137 | 138 | @interface HWThrottle : NSObject 139 | 140 | /// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading, the execution queue defaults to the main queue. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 141 | /// @param interval Throttle time interval, unit second 142 | /// @param taskBlock The task to be throttled 143 | - (instancetype)initWithInterval:(NSTimeInterval)interval 144 | taskBlock:(HWThrottleTaskBlock)taskBlock; 145 | 146 | /// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 147 | /// @param interval Throttle time interval, unit second 148 | /// @param queue Execution queue, defaults the main queue 149 | /// @param taskBlock The task to be throttled 150 | - (instancetype)initWithInterval:(NSTimeInterval)interval 151 | onQueue:(dispatch_queue_t)queue 152 | taskBlock:(HWThrottleTaskBlock)taskBlock; 153 | 154 | /// Initialize a debounce object. Note that debounce is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other 155 | /// @param throttleMode The throttle mode, defaults HWThrottleModeLeading 156 | /// @param interval Throttle time interval, unit second 157 | /// @param queue Execution queue, defaults the main queue 158 | /// @param taskBlock The task to be throttled 159 | - (instancetype)initWithThrottleMode:(HWThrottleMode)throttleMode 160 | interval:(NSTimeInterval)interval 161 | onQueue:(dispatch_queue_t)queue 162 | taskBlock:(HWThrottleTaskBlock)taskBlock; 163 | 164 | 165 | /// throttling call the task 166 | - (void)call; 167 | 168 | 169 | /// When the owner of the HWThrottle object is about to release, call this method on the HWThrottle object first to prevent circular references 170 | - (void)invalidate; 171 | 172 | @end 173 | ``` 174 | Throttle默认模式为Leading,因为实际使用中,多数的Throttle场景是在指定时间间隔的开始处调用,比如防止按钮重复点击时,一般会响应第一次点击,而忽略之后的点击。 175 | 176 | **HWDebounce.h:** 177 | 178 | ``` 179 | #pragma mark - public class 180 | 181 | typedef NS_ENUM(NSUInteger, HWDebounceMode) { 182 | HWDebounceModeTrailing, //invoking on the trailing edge of the timeout 183 | HWDebounceModeLeading, //invoking on the leading edge of the timeout 184 | }; 185 | 186 | typedef void(^HWDebounceTaskBlock)(void); 187 | 188 | @interface HWDebounce : NSObject 189 | 190 | /// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing, the execution queue defaults to the main queue. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 191 | /// @param interval Debounce time interval, unit second 192 | /// @param taskBlock The task to be debounced 193 | - (instancetype)initWithInterval:(NSTimeInterval)interval 194 | taskBlock:(HWDebounceTaskBlock)taskBlock; 195 | 196 | /// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 197 | /// @param interval Debounce time interval, unit second 198 | /// @param queue Execution queue, defaults the main queue 199 | /// @param taskBlock The task to be debounced 200 | - (instancetype)initWithInterval:(NSTimeInterval)interval 201 | onQueue:(dispatch_queue_t)queue 202 | taskBlock:(HWDebounceTaskBlock)taskBlock; 203 | 204 | /// Initialize a debounce object. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other 205 | /// @param debounceMode The debounce mode, defaults HWDebounceModeTrailing 206 | /// @param interval Debounce time interval, unit second 207 | /// @param queue Execution queue, defaults the main queue 208 | /// @param taskBlock The task to be debounced 209 | - (instancetype)initWithDebounceMode:(HWDebounceMode)debounceMode 210 | interval:(NSTimeInterval)interval 211 | onQueue:(dispatch_queue_t)queue 212 | taskBlock:(HWDebounceTaskBlock)taskBlock; 213 | 214 | 215 | /// debouncing call the task 216 | - (void)call; 217 | 218 | 219 | /// When the owner of the HWDebounce object is about to release, call this method on the HWDebounce object first to prevent circular references 220 | - (void)invalidate; 221 | 222 | @end 223 | ``` 224 | 225 | Debounce默认模式为Trailing,因为实际使用中,多数的Debounce场景是在指定时间间隔的末尾处调用,比如监听用户输入时,一般是在用户停止输入后再触发调用。 226 | 227 | ### 核心代码: 228 | **Throttle leading:** 229 | ``` 230 | - (void)call { 231 | if (self.lastRunTaskDate) { 232 | if ([[NSDate date] timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) { 233 | [self runTaskDirectly]; 234 | } 235 | } else { 236 | [self runTaskDirectly]; 237 | } 238 | } 239 | 240 | - (void)runTaskDirectly { 241 | dispatch_async(self.queue, ^{ 242 | if (self.taskBlock) { 243 | self.taskBlock(); 244 | } 245 | self.lastRunTaskDate = [NSDate date]; 246 | }); 247 | } 248 | 249 | - (void)invalidate { 250 | self.taskBlock = nil; 251 | } 252 | 253 | ``` 254 | 255 | **Throttle trailing:** 256 | ``` 257 | - (void)call { 258 | NSDate *now = [NSDate date]; 259 | if (!self.nextRunTaskDate) { 260 | if (self.lastRunTaskDate) { 261 | if ([now timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) { 262 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now]; 263 | } else { 264 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:self.lastRunTaskDate]; 265 | } 266 | } else { 267 | self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now]; 268 | } 269 | 270 | 271 | NSTimeInterval nextInterval = [self.nextRunTaskDate timeIntervalSinceDate:now]; 272 | 273 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextInterval * NSEC_PER_SEC)), self.queue, ^{ 274 | if (self.taskBlock) { 275 | self.taskBlock(); 276 | } 277 | self.lastRunTaskDate = [NSDate date]; 278 | self.nextRunTaskDate = nil; 279 | }); 280 | } 281 | } 282 | 283 | - (void)invalidate { 284 | self.taskBlock = nil; 285 | } 286 | 287 | ``` 288 | 289 | **Debounce trailing:** 290 | ``` 291 | - (void)call { 292 | if (self.block) { 293 | dispatch_block_cancel(self.block); 294 | } 295 | __weak typeof(self)weakSelf = self; 296 | self.block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{ 297 | if (weakSelf.taskBlock) { 298 | weakSelf.taskBlock(); 299 | } 300 | }); 301 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.interval * NSEC_PER_SEC)), self.queue, self.block); 302 | } 303 | 304 | - (void)invalidate { 305 | self.taskBlock = nil; 306 | self.block = nil; 307 | } 308 | 309 | ``` 310 | 311 | **Debounce leading:** 312 | ``` 313 | - (void)call { 314 | if (self.lastCallTaskDate) { 315 | if ([[NSDate date] timeIntervalSinceDate:self.lastCallTaskDate] > self.interval) { 316 | [self runTaskDirectly]; 317 | } 318 | } else { 319 | [self runTaskDirectly]; 320 | } 321 | self.lastCallTaskDate = [NSDate date]; 322 | } 323 | 324 | - (void)runTaskDirectly { 325 | dispatch_async(self.queue, ^{ 326 | if (self.taskBlock) { 327 | self.taskBlock(); 328 | } 329 | }); 330 | } 331 | 332 | - (void)invalidate { 333 | self.taskBlock = nil; 334 | self.block = nil; 335 | } 336 | 337 | ``` 338 | ## 四、总结 339 | 希望此篇文章能帮助你全面理解Throttle和Debounce的概念,赶快看看项目中有哪些可以用到Throttle或Debounce来提升性能的地方吧。 340 | 341 | 再次附上OC实现[HWThrottle](https://github.com/HighwayLaw/HWThrottle),欢迎issue和讨论。 342 | 343 | ## 五、参考文章 344 | [1][iOS编程中throttle那些事](https://www.jianshu.com/p/d2e1bcee406e) 345 | 346 | [2][Objective-C Message Throttle and Debounce](http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/ "Objective-C Message Throttle and Debounce") 347 | 348 | [3][Lodash Documentation](https://lodash.com/docs/) 349 | 350 | --------------------------------------------------------------------------------