├── .gitignore ├── Demo ├── Default-568h@2x.png ├── SVPullToRefreshDemo.xcodeproj │ └── project.pbxproj └── SVPullToRefreshDemo │ ├── SVAppDelegate.h │ ├── SVAppDelegate.m │ ├── SVPullToRefreshDemo-Info.plist │ ├── SVPullToRefreshDemo-Prefix.pch │ ├── SVViewController.h │ ├── SVViewController.m │ ├── en.lproj │ ├── InfoPlist.strings │ └── SVViewController.xib │ └── main.m ├── LICENSE.txt ├── README.md ├── SVPullToRefresh.podspec └── SVPullToRefresh ├── SVPullToRefresh.h ├── UIScrollView+SVInfiniteScrolling.h ├── UIScrollView+SVInfiniteScrolling.m ├── UIScrollView+SVPullToRefresh.h └── UIScrollView+SVPullToRefresh.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | *.DS_Store 3 | *.psd 4 | 5 | # Xcode 6 | *.pbxuser 7 | *.mode1v3 8 | *.mode2v3 9 | *.perspectivev3 10 | *.xcuserstate 11 | project.xcworkspace/ 12 | xcuserdata/ 13 | 14 | # Generated files 15 | build/ 16 | *.[oa] 17 | *.pyc 18 | 19 | # Backup files 20 | *~.nib -------------------------------------------------------------------------------- /Demo/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samvermette/SVPullToRefresh/a5f9dfee86a27c4e994d7edf93d0768c881d58bb/Demo/Default-568h@2x.png -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2214365616B48FD70057C96E /* SVPullToRefresh.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 2214365516B48FD70057C96E /* SVPullToRefresh.podspec */; }; 11 | 22E0D9191545EE5B00BB6BB5 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22E0D9181545EE5B00BB6BB5 /* UIKit.framework */; }; 12 | 22E0D91B1545EE5B00BB6BB5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22E0D91A1545EE5B00BB6BB5 /* Foundation.framework */; }; 13 | 22E0D91D1545EE5B00BB6BB5 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22E0D91C1545EE5B00BB6BB5 /* CoreGraphics.framework */; }; 14 | 22E0D9231545EE5B00BB6BB5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 22E0D9211545EE5B00BB6BB5 /* InfoPlist.strings */; }; 15 | 22E0D9251545EE5B00BB6BB5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 22E0D9241545EE5B00BB6BB5 /* main.m */; }; 16 | 22E0D9291545EE5B00BB6BB5 /* SVAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 22E0D9281545EE5B00BB6BB5 /* SVAppDelegate.m */; }; 17 | 22E0D92C1545EE5B00BB6BB5 /* SVViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22E0D92B1545EE5B00BB6BB5 /* SVViewController.m */; }; 18 | 22E0D92F1545EE5B00BB6BB5 /* SVViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22E0D92D1545EE5B00BB6BB5 /* SVViewController.xib */; }; 19 | 22E0D93D1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m in Sources */ = {isa = PBXBuildFile; fileRef = 22E0D93B1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m */; }; 20 | 22E0D9411545EE9000BB6BB5 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22E0D9401545EE9000BB6BB5 /* QuartzCore.framework */; }; 21 | 22E0D94B1545F63300BB6BB5 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 22E0D94A1545F63300BB6BB5 /* README.md */; }; 22 | 22FDEC971639082E00DB53A8 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 22FDEC961639082E00DB53A8 /* Default-568h@2x.png */; }; 23 | 22FDEC9D16390CC800DB53A8 /* UIScrollView+SVInfiniteScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = 2288146016047C06005C6461 /* UIScrollView+SVInfiniteScrolling.m */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 2214365516B48FD70057C96E /* SVPullToRefresh.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = SVPullToRefresh.podspec; path = ../SVPullToRefresh.podspec; sourceTree = ""; }; 28 | 226989041639F55E00FCBB3B /* SVPullToRefresh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SVPullToRefresh.h; sourceTree = ""; }; 29 | 2288145F16047C06005C6461 /* UIScrollView+SVInfiniteScrolling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+SVInfiniteScrolling.h"; sourceTree = ""; }; 30 | 2288146016047C06005C6461 /* UIScrollView+SVInfiniteScrolling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+SVInfiniteScrolling.m"; sourceTree = ""; }; 31 | 22E0D9141545EE5B00BB6BB5 /* SVPullToRefreshDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SVPullToRefreshDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 22E0D9181545EE5B00BB6BB5 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 33 | 22E0D91A1545EE5B00BB6BB5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 34 | 22E0D91C1545EE5B00BB6BB5 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 35 | 22E0D9201545EE5B00BB6BB5 /* SVPullToRefreshDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SVPullToRefreshDemo-Info.plist"; sourceTree = ""; }; 36 | 22E0D9221545EE5B00BB6BB5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 37 | 22E0D9241545EE5B00BB6BB5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 38 | 22E0D9261545EE5B00BB6BB5 /* SVPullToRefreshDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SVPullToRefreshDemo-Prefix.pch"; sourceTree = ""; }; 39 | 22E0D9271545EE5B00BB6BB5 /* SVAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SVAppDelegate.h; sourceTree = ""; }; 40 | 22E0D9281545EE5B00BB6BB5 /* SVAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SVAppDelegate.m; sourceTree = ""; }; 41 | 22E0D92A1545EE5B00BB6BB5 /* SVViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SVViewController.h; sourceTree = ""; }; 42 | 22E0D92B1545EE5B00BB6BB5 /* SVViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SVViewController.m; sourceTree = ""; }; 43 | 22E0D92E1545EE5B00BB6BB5 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/SVViewController.xib; sourceTree = ""; }; 44 | 22E0D93A1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+SVPullToRefresh.h"; sourceTree = ""; }; 45 | 22E0D93B1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+SVPullToRefresh.m"; sourceTree = ""; }; 46 | 22E0D9401545EE9000BB6BB5 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 47 | 22E0D94A1545F63300BB6BB5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = README.md; path = ../README.md; sourceTree = ""; }; 48 | 22FDEC961639082E00DB53A8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 22E0D9111545EE5B00BB6BB5 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | 22E0D9411545EE9000BB6BB5 /* QuartzCore.framework in Frameworks */, 57 | 22E0D9191545EE5B00BB6BB5 /* UIKit.framework in Frameworks */, 58 | 22E0D91B1545EE5B00BB6BB5 /* Foundation.framework in Frameworks */, 59 | 22E0D91D1545EE5B00BB6BB5 /* CoreGraphics.framework in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 22E0D9091545EE5B00BB6BB5 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 22E0D94A1545F63300BB6BB5 /* README.md */, 70 | 2214365516B48FD70057C96E /* SVPullToRefresh.podspec */, 71 | 22E0D91E1545EE5B00BB6BB5 /* Demo */, 72 | 22E0D9391545EE7600BB6BB5 /* SVPullToRefresh */, 73 | 22E0D9171545EE5B00BB6BB5 /* Frameworks */, 74 | 22E0D9151545EE5B00BB6BB5 /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 22E0D9151545EE5B00BB6BB5 /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 22E0D9141545EE5B00BB6BB5 /* SVPullToRefreshDemo.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 22E0D9171545EE5B00BB6BB5 /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 22E0D9401545EE9000BB6BB5 /* QuartzCore.framework */, 90 | 22E0D9181545EE5B00BB6BB5 /* UIKit.framework */, 91 | 22E0D91A1545EE5B00BB6BB5 /* Foundation.framework */, 92 | 22E0D91C1545EE5B00BB6BB5 /* CoreGraphics.framework */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | 22E0D91E1545EE5B00BB6BB5 /* Demo */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 22E0D9271545EE5B00BB6BB5 /* SVAppDelegate.h */, 101 | 22E0D9281545EE5B00BB6BB5 /* SVAppDelegate.m */, 102 | 22E0D92A1545EE5B00BB6BB5 /* SVViewController.h */, 103 | 22E0D92B1545EE5B00BB6BB5 /* SVViewController.m */, 104 | 22E0D92D1545EE5B00BB6BB5 /* SVViewController.xib */, 105 | 22E0D91F1545EE5B00BB6BB5 /* Supporting Files */, 106 | ); 107 | name = Demo; 108 | path = SVPullToRefreshDemo; 109 | sourceTree = ""; 110 | }; 111 | 22E0D91F1545EE5B00BB6BB5 /* Supporting Files */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 22FDEC961639082E00DB53A8 /* Default-568h@2x.png */, 115 | 22E0D9201545EE5B00BB6BB5 /* SVPullToRefreshDemo-Info.plist */, 116 | 22E0D9211545EE5B00BB6BB5 /* InfoPlist.strings */, 117 | 22E0D9241545EE5B00BB6BB5 /* main.m */, 118 | 22E0D9261545EE5B00BB6BB5 /* SVPullToRefreshDemo-Prefix.pch */, 119 | ); 120 | name = "Supporting Files"; 121 | sourceTree = ""; 122 | }; 123 | 22E0D9391545EE7600BB6BB5 /* SVPullToRefresh */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 226989041639F55E00FCBB3B /* SVPullToRefresh.h */, 127 | 22E0D93A1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.h */, 128 | 22E0D93B1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m */, 129 | 2288145F16047C06005C6461 /* UIScrollView+SVInfiniteScrolling.h */, 130 | 2288146016047C06005C6461 /* UIScrollView+SVInfiniteScrolling.m */, 131 | ); 132 | name = SVPullToRefresh; 133 | path = ../SVPullToRefresh; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | 22E0D9131545EE5B00BB6BB5 /* SVPullToRefreshDemo */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 22E0D9321545EE5B00BB6BB5 /* Build configuration list for PBXNativeTarget "SVPullToRefreshDemo" */; 142 | buildPhases = ( 143 | 22E0D9101545EE5B00BB6BB5 /* Sources */, 144 | 22E0D9111545EE5B00BB6BB5 /* Frameworks */, 145 | 22E0D9121545EE5B00BB6BB5 /* Resources */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | name = SVPullToRefreshDemo; 152 | productName = SVPullToRefreshDemo; 153 | productReference = 22E0D9141545EE5B00BB6BB5 /* SVPullToRefreshDemo.app */; 154 | productType = "com.apple.product-type.application"; 155 | }; 156 | /* End PBXNativeTarget section */ 157 | 158 | /* Begin PBXProject section */ 159 | 22E0D90B1545EE5B00BB6BB5 /* Project object */ = { 160 | isa = PBXProject; 161 | attributes = { 162 | CLASSPREFIX = SV; 163 | LastUpgradeCheck = 0500; 164 | ORGANIZATIONNAME = Home; 165 | }; 166 | buildConfigurationList = 22E0D90E1545EE5B00BB6BB5 /* Build configuration list for PBXProject "SVPullToRefreshDemo" */; 167 | compatibilityVersion = "Xcode 3.2"; 168 | developmentRegion = English; 169 | hasScannedForEncodings = 0; 170 | knownRegions = ( 171 | en, 172 | ); 173 | mainGroup = 22E0D9091545EE5B00BB6BB5; 174 | productRefGroup = 22E0D9151545EE5B00BB6BB5 /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | 22E0D9131545EE5B00BB6BB5 /* SVPullToRefreshDemo */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | 22E0D9121545EE5B00BB6BB5 /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 22E0D9231545EE5B00BB6BB5 /* InfoPlist.strings in Resources */, 189 | 22E0D92F1545EE5B00BB6BB5 /* SVViewController.xib in Resources */, 190 | 22E0D94B1545F63300BB6BB5 /* README.md in Resources */, 191 | 22FDEC971639082E00DB53A8 /* Default-568h@2x.png in Resources */, 192 | 2214365616B48FD70057C96E /* SVPullToRefresh.podspec in Resources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXResourcesBuildPhase section */ 197 | 198 | /* Begin PBXSourcesBuildPhase section */ 199 | 22E0D9101545EE5B00BB6BB5 /* Sources */ = { 200 | isa = PBXSourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 22E0D9251545EE5B00BB6BB5 /* main.m in Sources */, 204 | 22E0D9291545EE5B00BB6BB5 /* SVAppDelegate.m in Sources */, 205 | 22E0D92C1545EE5B00BB6BB5 /* SVViewController.m in Sources */, 206 | 22E0D93D1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m in Sources */, 207 | 22FDEC9D16390CC800DB53A8 /* UIScrollView+SVInfiniteScrolling.m in Sources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXSourcesBuildPhase section */ 212 | 213 | /* Begin PBXVariantGroup section */ 214 | 22E0D9211545EE5B00BB6BB5 /* InfoPlist.strings */ = { 215 | isa = PBXVariantGroup; 216 | children = ( 217 | 22E0D9221545EE5B00BB6BB5 /* en */, 218 | ); 219 | name = InfoPlist.strings; 220 | sourceTree = ""; 221 | }; 222 | 22E0D92D1545EE5B00BB6BB5 /* SVViewController.xib */ = { 223 | isa = PBXVariantGroup; 224 | children = ( 225 | 22E0D92E1545EE5B00BB6BB5 /* en */, 226 | ); 227 | name = SVViewController.xib; 228 | sourceTree = ""; 229 | }; 230 | /* End PBXVariantGroup section */ 231 | 232 | /* Begin XCBuildConfiguration section */ 233 | 22E0D9301545EE5B00BB6BB5 /* Debug */ = { 234 | isa = XCBuildConfiguration; 235 | buildSettings = { 236 | ALWAYS_SEARCH_USER_PATHS = NO; 237 | CLANG_ENABLE_OBJC_ARC = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_EMPTY_BODY = YES; 241 | CLANG_WARN_ENUM_CONVERSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | GCC_C_LANGUAGE_STANDARD = gnu99; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 254 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 255 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 256 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 257 | GCC_WARN_UNDECLARED_SELECTOR = YES; 258 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 259 | GCC_WARN_UNUSED_FUNCTION = YES; 260 | GCC_WARN_UNUSED_VARIABLE = YES; 261 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 262 | ONLY_ACTIVE_ARCH = YES; 263 | RUN_CLANG_STATIC_ANALYZER = YES; 264 | SDKROOT = iphoneos; 265 | }; 266 | name = Debug; 267 | }; 268 | 22E0D9311545EE5B00BB6BB5 /* Release */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ALWAYS_SEARCH_USER_PATHS = NO; 272 | CLANG_ENABLE_OBJC_ARC = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_CONSTANT_CONVERSION = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 279 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 280 | COPY_PHASE_STRIP = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu99; 282 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 290 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 291 | RUN_CLANG_STATIC_ANALYZER = YES; 292 | SDKROOT = iphoneos; 293 | VALIDATE_PRODUCT = YES; 294 | }; 295 | name = Release; 296 | }; 297 | 22E0D9331545EE5B00BB6BB5 /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 301 | GCC_PREFIX_HEADER = "SVPullToRefreshDemo/SVPullToRefreshDemo-Prefix.pch"; 302 | INFOPLIST_FILE = "SVPullToRefreshDemo/SVPullToRefreshDemo-Info.plist"; 303 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 304 | PRODUCT_NAME = "$(TARGET_NAME)"; 305 | WRAPPER_EXTENSION = app; 306 | }; 307 | name = Debug; 308 | }; 309 | 22E0D9341545EE5B00BB6BB5 /* Release */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 313 | GCC_PREFIX_HEADER = "SVPullToRefreshDemo/SVPullToRefreshDemo-Prefix.pch"; 314 | INFOPLIST_FILE = "SVPullToRefreshDemo/SVPullToRefreshDemo-Info.plist"; 315 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 316 | PRODUCT_NAME = "$(TARGET_NAME)"; 317 | WRAPPER_EXTENSION = app; 318 | }; 319 | name = Release; 320 | }; 321 | /* End XCBuildConfiguration section */ 322 | 323 | /* Begin XCConfigurationList section */ 324 | 22E0D90E1545EE5B00BB6BB5 /* Build configuration list for PBXProject "SVPullToRefreshDemo" */ = { 325 | isa = XCConfigurationList; 326 | buildConfigurations = ( 327 | 22E0D9301545EE5B00BB6BB5 /* Debug */, 328 | 22E0D9311545EE5B00BB6BB5 /* Release */, 329 | ); 330 | defaultConfigurationIsVisible = 0; 331 | defaultConfigurationName = Release; 332 | }; 333 | 22E0D9321545EE5B00BB6BB5 /* Build configuration list for PBXNativeTarget "SVPullToRefreshDemo" */ = { 334 | isa = XCConfigurationList; 335 | buildConfigurations = ( 336 | 22E0D9331545EE5B00BB6BB5 /* Debug */, 337 | 22E0D9341545EE5B00BB6BB5 /* Release */, 338 | ); 339 | defaultConfigurationIsVisible = 0; 340 | defaultConfigurationName = Release; 341 | }; 342 | /* End XCConfigurationList section */ 343 | }; 344 | rootObject = 22E0D90B1545EE5B00BB6BB5 /* Project object */; 345 | } 346 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SVAppDelegate.h 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 samvermette.com. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class SVViewController; 12 | 13 | @interface SVAppDelegate : UIResponder 14 | 15 | @property (strong, nonatomic) UIWindow *window; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SVAppDelegate.m 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 samvermette.com. All rights reserved. 7 | // 8 | 9 | #import "SVAppDelegate.h" 10 | 11 | #import "SVViewController.h" 12 | 13 | @implementation SVAppDelegate 14 | 15 | @synthesize window = _window; 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 18 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 19 | self.window.rootViewController = [[SVViewController alloc] initWithNibName:@"SVViewController" bundle:nil]; 20 | [self.window makeKeyAndVisible]; 21 | return YES; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVPullToRefreshDemo-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.samvermette.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVPullToRefreshDemo-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'SVPullToRefreshDemo' target in the 'SVPullToRefreshDemo' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_4_0 8 | #warning "This project uses features only available in iOS SDK 4.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | 16 | #ifdef DEBUG 17 | #define SV_DEBUG_MEMORY_LEAK 18 | #endif 19 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SVViewController.h 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 samvermette.com. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SVViewController : UIViewController 12 | 13 | @property (nonatomic, strong) IBOutlet UITableView *tableView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/SVViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SVViewController.m 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 Home. All rights reserved. 7 | // 8 | 9 | #import "SVViewController.h" 10 | #import "SVPullToRefresh.h" 11 | 12 | @interface SVViewController () 13 | 14 | @property (nonatomic, strong) NSMutableArray *dataSource; 15 | 16 | @end 17 | 18 | @implementation SVViewController 19 | @synthesize tableView = tableView; 20 | 21 | - (void)viewDidLoad { 22 | [super viewDidLoad]; 23 | [self setupDataSource]; 24 | 25 | __weak SVViewController *weakSelf = self; 26 | 27 | // setup pull-to-refresh 28 | [self.tableView addPullToRefreshWithActionHandler:^{ 29 | [weakSelf insertRowAtTop]; 30 | }]; 31 | 32 | // setup infinite scrolling 33 | [self.tableView addInfiniteScrollingWithActionHandler:^{ 34 | [weakSelf insertRowAtBottom]; 35 | }]; 36 | } 37 | 38 | - (void)viewDidAppear:(BOOL)animated { 39 | [tableView triggerPullToRefresh]; 40 | } 41 | 42 | #pragma mark - Actions 43 | 44 | - (void)setupDataSource { 45 | self.dataSource = [NSMutableArray array]; 46 | for(int i=0; i<15; i++) 47 | [self.dataSource addObject:[NSDate dateWithTimeIntervalSinceNow:-(i*90)]]; 48 | } 49 | 50 | - (void)insertRowAtTop { 51 | __weak SVViewController *weakSelf = self; 52 | 53 | int64_t delayInSeconds = 2.0; 54 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 55 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 56 | [weakSelf.tableView beginUpdates]; 57 | [weakSelf.dataSource insertObject:[NSDate date] atIndex:0]; 58 | [weakSelf.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationBottom]; 59 | [weakSelf.tableView endUpdates]; 60 | 61 | [weakSelf.tableView.pullToRefreshView stopAnimating]; 62 | }); 63 | } 64 | 65 | 66 | - (void)insertRowAtBottom { 67 | __weak SVViewController *weakSelf = self; 68 | 69 | int64_t delayInSeconds = 2.0; 70 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 71 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 72 | [weakSelf.tableView beginUpdates]; 73 | [weakSelf.dataSource addObject:[weakSelf.dataSource.lastObject dateByAddingTimeInterval:-90]]; 74 | [weakSelf.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:weakSelf.dataSource.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop]; 75 | [weakSelf.tableView endUpdates]; 76 | 77 | [weakSelf.tableView.infiniteScrollingView stopAnimating]; 78 | }); 79 | } 80 | #pragma mark - 81 | #pragma mark UITableViewDataSource 82 | 83 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 84 | return 1; 85 | } 86 | 87 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 88 | return self.dataSource.count; 89 | } 90 | 91 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 92 | static NSString *identifier = @"Cell"; 93 | UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; 94 | 95 | if (cell == nil) 96 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; 97 | 98 | NSDate *date = [self.dataSource objectAtIndex:indexPath.row]; 99 | cell.textLabel.text = [NSDateFormatter localizedStringFromDate:date dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterMediumStyle]; 100 | return cell; 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/en.lproj/SVViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1536 5 | 11E53 6 | 2809 7 | 1138.47 8 | 569.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 1884 12 | 13 | 14 | IBProxyObject 15 | IBUITableView 16 | IBUIView 17 | 18 | 19 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 20 | 21 | 22 | PluginDependencyRecalculationVersion 23 | 24 | 25 | 26 | 27 | IBFilesOwner 28 | IBCocoaTouchFramework 29 | 30 | 31 | IBFirstResponder 32 | IBCocoaTouchFramework 33 | 34 | 35 | 36 | 274 37 | 38 | 39 | 40 | 274 41 | {320, 460} 42 | 43 | 44 | 45 | 46 | 3 47 | MQA 48 | 49 | YES 50 | IBCocoaTouchFramework 51 | YES 52 | 1 53 | 0 54 | YES 55 | 44 56 | 22 57 | 22 58 | 59 | 60 | {{0, 20}, {320, 460}} 61 | 62 | 63 | 64 | 65 | 3 66 | MC43NQA 67 | 68 | 2 69 | 70 | 71 | NO 72 | 73 | IBCocoaTouchFramework 74 | 75 | 76 | 77 | 78 | 79 | 80 | view 81 | 82 | 83 | 84 | 7 85 | 86 | 87 | 88 | tableView 89 | 90 | 91 | 92 | 12 93 | 94 | 95 | 96 | dataSource 97 | 98 | 99 | 100 | 9 101 | 102 | 103 | 104 | delegate 105 | 106 | 107 | 108 | 10 109 | 110 | 111 | 112 | 113 | 114 | 0 115 | 116 | 117 | 118 | 119 | 120 | -1 121 | 122 | 123 | File's Owner 124 | 125 | 126 | -2 127 | 128 | 129 | 130 | 131 | 6 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 8 140 | 141 | 142 | 143 | 144 | 145 | 146 | SVViewController 147 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 148 | UIResponder 149 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 150 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 151 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 152 | 153 | 154 | 155 | 156 | 157 | 14 158 | 159 | 160 | 161 | 162 | SVViewController 163 | UIViewController 164 | 165 | triggerRefresh: 166 | id 167 | 168 | 169 | triggerRefresh: 170 | 171 | triggerRefresh: 172 | id 173 | 174 | 175 | 176 | tableView 177 | UITableView 178 | 179 | 180 | tableView 181 | 182 | tableView 183 | UITableView 184 | 185 | 186 | 187 | IBProjectSource 188 | ./Classes/SVViewController.h 189 | 190 | 191 | 192 | 193 | 0 194 | IBCocoaTouchFramework 195 | 196 | com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS 197 | 198 | 199 | YES 200 | 3 201 | 1884 202 | 203 | 204 | -------------------------------------------------------------------------------- /Demo/SVPullToRefreshDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 samvermette.com. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "SVAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([SVAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Sam Vermette 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVPullToRefresh + SVInfiniteScrolling 2 | 3 | These UIScrollView categories makes it super easy to add pull-to-refresh and infinite scrolling fonctionalities to any UIScrollView (or any of its subclass). Instead of relying on delegates and/or subclassing `UIViewController`, SVPullToRefresh uses the Objective-C runtime to add the following 3 methods to `UIScrollView`: 4 | 5 | ```objective-c 6 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler; 7 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position; 8 | - (void)addInfiniteScrollingWithActionHandler:(void (^)(void))actionHandler; 9 | ``` 10 | 11 | ## Installation 12 | 13 | ### From CocoaPods 14 | 15 | Add `pod 'SVPullToRefresh'` to your Podfile or `pod 'SVPullToRefresh', :head` if you're feeling adventurous. 16 | 17 | ### Manually 18 | 19 | _**Important note if your project doesn't use ARC**: you must add the `-fobjc-arc` compiler flag to `UIScrollView+SVPullToRefresh.m` and `UIScrollView+SVInfiniteScrolling.m` in Target Settings > Build Phases > Compile Sources._ 20 | 21 | * Drag the `SVPullToRefresh/SVPullToRefresh` folder into your project. 22 | * Add the **QuartzCore** framework to your project. 23 | * Import `UIScrollView+SVPullToRefresh.h` and/or `UIScrollView+SVInfiniteScrolling.h` 24 | 25 | ## Usage 26 | 27 | (see sample Xcode project in `/Demo`) 28 | 29 | ### Adding Pull to Refresh 30 | 31 | ```objective-c 32 | [tableView addPullToRefreshWithActionHandler:^{ 33 | // prepend data to dataSource, insert cells at top of table view 34 | // call [tableView.pullToRefreshView stopAnimating] when done 35 | }]; 36 | ``` 37 | or if you want pull to refresh from the bottom 38 | 39 | ```objective-c 40 | [tableView addPullToRefreshWithActionHandler:^{ 41 | // prepend data to dataSource, insert cells at top of table view 42 | // call [tableView.pullToRefreshView stopAnimating] when done 43 | } position:SVPullToRefreshPositionBottom]; 44 | ``` 45 | 46 | If you’d like to programmatically trigger the refresh (for instance in `viewDidAppear:`), you can do so with: 47 | 48 | ```objective-c 49 | [tableView triggerPullToRefresh]; 50 | ``` 51 | 52 | You can temporarily hide the pull to refresh view by setting the `showsPullToRefresh` property: 53 | 54 | ```objective-c 55 | tableView.showsPullToRefresh = NO; 56 | ``` 57 | 58 | #### Customization 59 | 60 | The pull to refresh view can be customized using the following properties/methods: 61 | 62 | ```objective-c 63 | @property (nonatomic, strong) UIColor *arrowColor; 64 | @property (nonatomic, strong) UIColor *textColor; 65 | @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; 66 | 67 | - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state; 68 | - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state; 69 | - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state; 70 | ``` 71 | 72 | You can access these properties through your scroll view's `pullToRefreshView` property. 73 | 74 | For instance, you would set the `arrowColor` property using: 75 | 76 | ```objective-c 77 | tableView.pullToRefreshView.arrowColor = [UIColor whiteColor]; 78 | ``` 79 | 80 | ### Adding Infinite Scrolling 81 | 82 | ```objective-c 83 | [tableView addInfiniteScrollingWithActionHandler:^{ 84 | // append data to data source, insert new cells at the end of table view 85 | // call [tableView.infiniteScrollingView stopAnimating] when done 86 | }]; 87 | ``` 88 | 89 | If you’d like to programmatically trigger the loading (for instance in `viewDidAppear:`), you can do so with: 90 | 91 | ```objective-c 92 | [tableView triggerInfiniteScrolling]; 93 | ``` 94 | 95 | You can temporarily hide the infinite scrolling view by setting the `showsInfiniteScrolling` property: 96 | 97 | ```objective-c 98 | tableView.showsInfiniteScrolling = NO; 99 | ``` 100 | 101 | #### Customization 102 | 103 | The infinite scrolling view can be customized using the following methods: 104 | 105 | ```objective-c 106 | - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle; 107 | - (void)setCustomView:(UIView *)view forState:(SVInfiniteScrollingState)state; 108 | ``` 109 | 110 | You can access these properties through your scroll view's `infiniteScrollingView` property. 111 | 112 | ## Under the hood 113 | 114 | SVPullToRefresh extends `UIScrollView` by adding new public methods as well as a dynamic properties. 115 | 116 | It uses key-value observing to track the scrollView's `contentOffset`. 117 | 118 | ## Credits 119 | 120 | SVPullToRefresh is brought to you by [Sam Vermette](http://samvermette.com) and [contributors to the project](https://github.com/samvermette/SVPullToRefresh/contributors). If you have feature suggestions or bug reports, feel free to help out by sending pull requests or by [creating new issues](https://github.com/samvermette/SVPullToRefresh/issues/new). If you're using SVPullToRefresh in your project, attribution would be nice. 121 | 122 | Big thanks to [@seb_morel](http://twitter.com/seb_morel) for his [Demistifying the Objective-C runtime](http://cocoaheadsmtl.s3.amazonaws.com/demistifying-runtime.pdf) talk which really helped for this project. 123 | 124 | Hat tip to [Loren Brichter](http://twitter.com/lorenb) for inventing pull-to-refresh. -------------------------------------------------------------------------------- /SVPullToRefresh.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SVPullToRefresh' 3 | s.version = '0.4.1' 4 | s.platform = :ios, '5.0' 5 | s.license = 'MIT' 6 | s.summary = 'Give pull-to-refresh to any UIScrollView with 1 line of code.' 7 | s.homepage = 'https://github.com/samvermette/SVPullToRefresh' 8 | s.author = { 'Sam Vermette' => 'hello@samvermette.com' } 9 | s.source = { :git => 'https://github.com/samvermette/SVPullToRefresh.git', :tag => s.version.to_s } 10 | 11 | s.description = 'SVPullToRefresh allows you to easily add pull-to-refresh ' \ 12 | 'functionality to any UIScrollView subclass with only 1 ' \ 13 | 'line of code. Instead of depending on delegates and/or ' \ 14 | 'subclassing UIViewController, SVPullToRefresh extends ' \ 15 | 'UIScrollView with a addPullToRefreshWithActionHandler: ' \ 16 | 'method as well as a pullToRefreshView property.' 17 | 18 | s.frameworks = 'QuartzCore' 19 | s.source_files = 'SVPullToRefresh/*.{h,m}' 20 | s.preserve_paths = 'Demo' 21 | s.requires_arc = true 22 | end 23 | -------------------------------------------------------------------------------- /SVPullToRefresh/SVPullToRefresh.h: -------------------------------------------------------------------------------- 1 | // 2 | // SVPullToRefresh.h 3 | // SVPullToRefreshDemo 4 | // 5 | // Created by Sam Vermette on 23.04.12. 6 | // Copyright (c) 2012 samvermette.com. All rights reserved. 7 | // 8 | // https://github.com/samvermette/SVPullToRefresh 9 | // 10 | 11 | // this header file is provided for backwards compatibility and will be removed in the future 12 | // here's how you should import SVPullToRefresh now: 13 | 14 | #import "UIScrollView+SVPullToRefresh.h" 15 | #import "UIScrollView+SVInfiniteScrolling.h" 16 | -------------------------------------------------------------------------------- /SVPullToRefresh/UIScrollView+SVInfiniteScrolling.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+SVInfiniteScrolling.h 3 | // 4 | // Created by Sam Vermette on 23.04.12. 5 | // Copyright (c) 2012 samvermette.com. All rights reserved. 6 | // 7 | // https://github.com/samvermette/SVPullToRefresh 8 | // 9 | 10 | #import 11 | 12 | @class SVInfiniteScrollingView; 13 | 14 | @interface UIScrollView (SVInfiniteScrolling) 15 | 16 | - (void)addInfiniteScrollingWithActionHandler:(void (^)(void))actionHandler; 17 | - (void)triggerInfiniteScrolling; 18 | 19 | @property (nonatomic, strong, readonly) SVInfiniteScrollingView *infiniteScrollingView; 20 | @property (nonatomic, assign) BOOL showsInfiniteScrolling; 21 | 22 | @end 23 | 24 | 25 | enum { 26 | SVInfiniteScrollingStateStopped = 0, 27 | SVInfiniteScrollingStateTriggered, 28 | SVInfiniteScrollingStateLoading, 29 | SVInfiniteScrollingStateAll = 10 30 | }; 31 | 32 | typedef NSUInteger SVInfiniteScrollingState; 33 | 34 | @interface SVInfiniteScrollingView : UIView 35 | 36 | @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; 37 | @property (nonatomic, readonly) SVInfiniteScrollingState state; 38 | @property (nonatomic, readwrite) BOOL enabled; 39 | 40 | - (void)setCustomView:(UIView *)view forState:(SVInfiniteScrollingState)state; 41 | 42 | - (void)startAnimating; 43 | - (void)stopAnimating; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /SVPullToRefresh/UIScrollView+SVInfiniteScrolling.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+SVInfiniteScrolling.m 3 | // 4 | // Created by Sam Vermette on 23.04.12. 5 | // Copyright (c) 2012 samvermette.com. All rights reserved. 6 | // 7 | // https://github.com/samvermette/SVPullToRefresh 8 | // 9 | 10 | #import 11 | #import "UIScrollView+SVInfiniteScrolling.h" 12 | 13 | 14 | static CGFloat const SVInfiniteScrollingViewHeight = 60; 15 | 16 | @interface SVInfiniteScrollingDotView : UIView 17 | 18 | @property (nonatomic, strong) UIColor *arrowColor; 19 | 20 | @end 21 | 22 | 23 | 24 | @interface SVInfiniteScrollingView () 25 | 26 | @property (nonatomic, copy) void (^infiniteScrollingHandler)(void); 27 | 28 | @property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; 29 | @property (nonatomic, readwrite) SVInfiniteScrollingState state; 30 | @property (nonatomic, strong) NSMutableArray *viewForState; 31 | @property (nonatomic, weak) UIScrollView *scrollView; 32 | @property (nonatomic, readwrite) CGFloat originalBottomInset; 33 | @property (nonatomic, assign) BOOL wasTriggeredByUser; 34 | @property (nonatomic, assign) BOOL isObserving; 35 | 36 | - (void)resetScrollViewContentInset; 37 | - (void)setScrollViewContentInsetForInfiniteScrolling; 38 | - (void)setScrollViewContentInset:(UIEdgeInsets)insets; 39 | 40 | @end 41 | 42 | 43 | 44 | #pragma mark - UIScrollView (SVInfiniteScrollingView) 45 | #import 46 | 47 | static char UIScrollViewInfiniteScrollingView; 48 | UIEdgeInsets scrollViewOriginalContentInsets; 49 | 50 | @implementation UIScrollView (SVInfiniteScrolling) 51 | 52 | @dynamic infiniteScrollingView; 53 | 54 | - (void)addInfiniteScrollingWithActionHandler:(void (^)(void))actionHandler { 55 | 56 | if(!self.infiniteScrollingView) { 57 | SVInfiniteScrollingView *view = [[SVInfiniteScrollingView alloc] initWithFrame:CGRectMake(0, self.contentSize.height, self.bounds.size.width, SVInfiniteScrollingViewHeight)]; 58 | view.infiniteScrollingHandler = actionHandler; 59 | view.scrollView = self; 60 | [self addSubview:view]; 61 | 62 | view.originalBottomInset = self.contentInset.bottom; 63 | self.infiniteScrollingView = view; 64 | self.showsInfiniteScrolling = YES; 65 | } 66 | } 67 | 68 | - (void)triggerInfiniteScrolling { 69 | self.infiniteScrollingView.state = SVInfiniteScrollingStateTriggered; 70 | [self.infiniteScrollingView startAnimating]; 71 | } 72 | 73 | - (void)setInfiniteScrollingView:(SVInfiniteScrollingView *)infiniteScrollingView { 74 | [self willChangeValueForKey:@"UIScrollViewInfiniteScrollingView"]; 75 | objc_setAssociatedObject(self, &UIScrollViewInfiniteScrollingView, 76 | infiniteScrollingView, 77 | OBJC_ASSOCIATION_ASSIGN); 78 | [self didChangeValueForKey:@"UIScrollViewInfiniteScrollingView"]; 79 | } 80 | 81 | - (SVInfiniteScrollingView *)infiniteScrollingView { 82 | return objc_getAssociatedObject(self, &UIScrollViewInfiniteScrollingView); 83 | } 84 | 85 | - (void)setShowsInfiniteScrolling:(BOOL)showsInfiniteScrolling { 86 | self.infiniteScrollingView.hidden = !showsInfiniteScrolling; 87 | 88 | if(!showsInfiniteScrolling) { 89 | if (self.infiniteScrollingView.isObserving) { 90 | [self removeObserver:self.infiniteScrollingView forKeyPath:@"contentOffset"]; 91 | [self removeObserver:self.infiniteScrollingView forKeyPath:@"contentSize"]; 92 | [self.infiniteScrollingView resetScrollViewContentInset]; 93 | self.infiniteScrollingView.isObserving = NO; 94 | } 95 | } 96 | else { 97 | if (!self.infiniteScrollingView.isObserving) { 98 | [self addObserver:self.infiniteScrollingView forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil]; 99 | [self addObserver:self.infiniteScrollingView forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; 100 | [self.infiniteScrollingView setScrollViewContentInsetForInfiniteScrolling]; 101 | self.infiniteScrollingView.isObserving = YES; 102 | 103 | [self.infiniteScrollingView setNeedsLayout]; 104 | self.infiniteScrollingView.frame = CGRectMake(0, self.contentSize.height, self.infiniteScrollingView.bounds.size.width, SVInfiniteScrollingViewHeight); 105 | } 106 | } 107 | } 108 | 109 | - (BOOL)showsInfiniteScrolling { 110 | return !self.infiniteScrollingView.hidden; 111 | } 112 | 113 | @end 114 | 115 | 116 | #pragma mark - SVInfiniteScrollingView 117 | @implementation SVInfiniteScrollingView 118 | 119 | // public properties 120 | @synthesize infiniteScrollingHandler, activityIndicatorViewStyle; 121 | 122 | @synthesize state = _state; 123 | @synthesize scrollView = _scrollView; 124 | @synthesize activityIndicatorView = _activityIndicatorView; 125 | 126 | 127 | - (id)initWithFrame:(CGRect)frame { 128 | if(self = [super initWithFrame:frame]) { 129 | 130 | // default styling values 131 | self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; 132 | self.autoresizingMask = UIViewAutoresizingFlexibleWidth; 133 | self.state = SVInfiniteScrollingStateStopped; 134 | self.enabled = YES; 135 | 136 | self.viewForState = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; 137 | } 138 | 139 | return self; 140 | } 141 | 142 | - (void)willMoveToSuperview:(UIView *)newSuperview { 143 | if (self.superview && newSuperview == nil) { 144 | UIScrollView *scrollView = (UIScrollView *)self.superview; 145 | if (scrollView.showsInfiniteScrolling) { 146 | if (self.isObserving) { 147 | [scrollView removeObserver:self forKeyPath:@"contentOffset"]; 148 | [scrollView removeObserver:self forKeyPath:@"contentSize"]; 149 | self.isObserving = NO; 150 | } 151 | } 152 | } 153 | } 154 | 155 | - (void)layoutSubviews { 156 | self.activityIndicatorView.center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); 157 | } 158 | 159 | #pragma mark - Scroll View 160 | 161 | - (void)resetScrollViewContentInset { 162 | UIEdgeInsets currentInsets = self.scrollView.contentInset; 163 | currentInsets.bottom = self.originalBottomInset; 164 | [self setScrollViewContentInset:currentInsets]; 165 | } 166 | 167 | - (void)setScrollViewContentInsetForInfiniteScrolling { 168 | UIEdgeInsets currentInsets = self.scrollView.contentInset; 169 | currentInsets.bottom = self.originalBottomInset + SVInfiniteScrollingViewHeight; 170 | [self setScrollViewContentInset:currentInsets]; 171 | } 172 | 173 | - (void)setScrollViewContentInset:(UIEdgeInsets)contentInset { 174 | [UIView animateWithDuration:0.3 175 | delay:0 176 | options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState 177 | animations:^{ 178 | self.scrollView.contentInset = contentInset; 179 | } 180 | completion:NULL]; 181 | } 182 | 183 | #pragma mark - Observing 184 | 185 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 186 | if([keyPath isEqualToString:@"contentOffset"]) 187 | [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; 188 | else if([keyPath isEqualToString:@"contentSize"]) { 189 | [self layoutSubviews]; 190 | self.frame = CGRectMake(0, self.scrollView.contentSize.height, self.bounds.size.width, SVInfiniteScrollingViewHeight); 191 | } 192 | } 193 | 194 | - (void)scrollViewDidScroll:(CGPoint)contentOffset { 195 | if(self.state != SVInfiniteScrollingStateLoading && self.enabled) { 196 | CGFloat scrollViewContentHeight = self.scrollView.contentSize.height; 197 | CGFloat scrollOffsetThreshold = scrollViewContentHeight-self.scrollView.bounds.size.height; 198 | 199 | if(!self.scrollView.isDragging && self.state == SVInfiniteScrollingStateTriggered) 200 | self.state = SVInfiniteScrollingStateLoading; 201 | else if(contentOffset.y > scrollOffsetThreshold && self.state == SVInfiniteScrollingStateStopped && self.scrollView.isDragging) 202 | self.state = SVInfiniteScrollingStateTriggered; 203 | else if(contentOffset.y < scrollOffsetThreshold && self.state != SVInfiniteScrollingStateStopped) 204 | self.state = SVInfiniteScrollingStateStopped; 205 | } 206 | } 207 | 208 | #pragma mark - Getters 209 | 210 | - (UIActivityIndicatorView *)activityIndicatorView { 211 | if(!_activityIndicatorView) { 212 | _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; 213 | _activityIndicatorView.hidesWhenStopped = YES; 214 | [self addSubview:_activityIndicatorView]; 215 | } 216 | return _activityIndicatorView; 217 | } 218 | 219 | - (UIActivityIndicatorViewStyle)activityIndicatorViewStyle { 220 | return self.activityIndicatorView.activityIndicatorViewStyle; 221 | } 222 | 223 | #pragma mark - Setters 224 | 225 | - (void)setCustomView:(UIView *)view forState:(SVInfiniteScrollingState)state { 226 | id viewPlaceholder = view; 227 | 228 | if(!viewPlaceholder) 229 | viewPlaceholder = @""; 230 | 231 | if(state == SVInfiniteScrollingStateAll) 232 | [self.viewForState replaceObjectsInRange:NSMakeRange(0, 3) withObjectsFromArray:@[viewPlaceholder, viewPlaceholder, viewPlaceholder]]; 233 | else 234 | [self.viewForState replaceObjectAtIndex:state withObject:viewPlaceholder]; 235 | 236 | self.state = self.state; 237 | } 238 | 239 | - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)viewStyle { 240 | self.activityIndicatorView.activityIndicatorViewStyle = viewStyle; 241 | } 242 | 243 | #pragma mark - 244 | 245 | - (void)triggerRefresh { 246 | self.state = SVInfiniteScrollingStateTriggered; 247 | self.state = SVInfiniteScrollingStateLoading; 248 | } 249 | 250 | - (void)startAnimating{ 251 | self.state = SVInfiniteScrollingStateLoading; 252 | } 253 | 254 | - (void)stopAnimating { 255 | self.state = SVInfiniteScrollingStateStopped; 256 | } 257 | 258 | - (void)setState:(SVInfiniteScrollingState)newState { 259 | 260 | if(_state == newState) 261 | return; 262 | 263 | SVInfiniteScrollingState previousState = _state; 264 | _state = newState; 265 | 266 | for(id otherView in self.viewForState) { 267 | if([otherView isKindOfClass:[UIView class]]) 268 | [otherView removeFromSuperview]; 269 | } 270 | 271 | id customView = [self.viewForState objectAtIndex:newState]; 272 | BOOL hasCustomView = [customView isKindOfClass:[UIView class]]; 273 | 274 | if(hasCustomView) { 275 | [self addSubview:customView]; 276 | CGRect viewBounds = [customView bounds]; 277 | CGPoint origin = CGPointMake(roundf((self.bounds.size.width-viewBounds.size.width)/2), roundf((self.bounds.size.height-viewBounds.size.height)/2)); 278 | [customView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; 279 | } 280 | else { 281 | CGRect viewBounds = [self.activityIndicatorView bounds]; 282 | CGPoint origin = CGPointMake(roundf((self.bounds.size.width-viewBounds.size.width)/2), roundf((self.bounds.size.height-viewBounds.size.height)/2)); 283 | [self.activityIndicatorView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; 284 | 285 | switch (newState) { 286 | case SVInfiniteScrollingStateStopped: 287 | [self.activityIndicatorView stopAnimating]; 288 | break; 289 | 290 | case SVInfiniteScrollingStateTriggered: 291 | [self.activityIndicatorView startAnimating]; 292 | break; 293 | 294 | case SVInfiniteScrollingStateLoading: 295 | [self.activityIndicatorView startAnimating]; 296 | break; 297 | } 298 | } 299 | 300 | if(previousState == SVInfiniteScrollingStateTriggered && newState == SVInfiniteScrollingStateLoading && self.infiniteScrollingHandler && self.enabled) 301 | self.infiniteScrollingHandler(); 302 | } 303 | 304 | @end 305 | -------------------------------------------------------------------------------- /SVPullToRefresh/UIScrollView+SVPullToRefresh.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+SVPullToRefresh.h 3 | // 4 | // Created by Sam Vermette on 23.04.12. 5 | // Copyright (c) 2012 samvermette.com. All rights reserved. 6 | // 7 | // https://github.com/samvermette/SVPullToRefresh 8 | // 9 | 10 | #import 11 | #import 12 | 13 | 14 | @class SVPullToRefreshView; 15 | 16 | @interface UIScrollView (SVPullToRefresh) 17 | 18 | typedef NS_ENUM(NSUInteger, SVPullToRefreshPosition) { 19 | SVPullToRefreshPositionTop = 0, 20 | SVPullToRefreshPositionBottom, 21 | }; 22 | 23 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler; 24 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position; 25 | - (void)triggerPullToRefresh; 26 | 27 | @property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView; 28 | @property (nonatomic, assign) BOOL showsPullToRefresh; 29 | 30 | @end 31 | 32 | 33 | typedef NS_ENUM(NSUInteger, SVPullToRefreshState) { 34 | SVPullToRefreshStateStopped = 0, 35 | SVPullToRefreshStateTriggered, 36 | SVPullToRefreshStateLoading, 37 | SVPullToRefreshStateAll = 10 38 | }; 39 | 40 | @interface SVPullToRefreshView : UIView 41 | 42 | @property (nonatomic, strong) UIColor *arrowColor; 43 | @property (nonatomic, strong) UIColor *textColor; 44 | @property (nonatomic, strong, readonly) UILabel *titleLabel; 45 | @property (nonatomic, strong, readonly) UILabel *subtitleLabel; 46 | @property (nonatomic, strong, readwrite) UIColor *activityIndicatorViewColor NS_AVAILABLE_IOS(5_0); 47 | @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; 48 | 49 | @property (nonatomic, readonly) SVPullToRefreshState state; 50 | @property (nonatomic, readonly) SVPullToRefreshPosition position; 51 | 52 | - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state; 53 | - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state; 54 | - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state; 55 | 56 | - (void)startAnimating; 57 | - (void)stopAnimating; 58 | 59 | // deprecated; use setSubtitle:forState: instead 60 | @property (nonatomic, strong, readonly) UILabel *dateLabel DEPRECATED_ATTRIBUTE; 61 | @property (nonatomic, strong) NSDate *lastUpdatedDate DEPRECATED_ATTRIBUTE; 62 | @property (nonatomic, strong) NSDateFormatter *dateFormatter DEPRECATED_ATTRIBUTE; 63 | 64 | // deprecated; use [self.scrollView triggerPullToRefresh] instead 65 | - (void)triggerRefresh DEPRECATED_ATTRIBUTE; 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /SVPullToRefresh/UIScrollView+SVPullToRefresh.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+SVPullToRefresh.m 3 | // 4 | // Created by Sam Vermette on 23.04.12. 5 | // Copyright (c) 2012 samvermette.com. All rights reserved. 6 | // 7 | // https://github.com/samvermette/SVPullToRefresh 8 | // 9 | 10 | #import 11 | #import "UIScrollView+SVPullToRefresh.h" 12 | 13 | //fequal() and fequalzro() from http://stackoverflow.com/a/1614761/184130 14 | #define fequal(a,b) (fabs((a) - (b)) < FLT_EPSILON) 15 | #define fequalzero(a) (fabs(a) < FLT_EPSILON) 16 | 17 | static CGFloat const SVPullToRefreshViewHeight = 60; 18 | 19 | @interface SVPullToRefreshArrow : UIView 20 | 21 | @property (nonatomic, strong) UIColor *arrowColor; 22 | 23 | @end 24 | 25 | 26 | @interface SVPullToRefreshView () 27 | 28 | @property (nonatomic, copy) void (^pullToRefreshActionHandler)(void); 29 | 30 | @property (nonatomic, strong) SVPullToRefreshArrow *arrow; 31 | @property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; 32 | @property (nonatomic, strong, readwrite) UILabel *titleLabel; 33 | @property (nonatomic, strong, readwrite) UILabel *subtitleLabel; 34 | @property (nonatomic, readwrite) SVPullToRefreshState state; 35 | @property (nonatomic, readwrite) SVPullToRefreshPosition position; 36 | 37 | @property (nonatomic, strong) NSMutableArray *titles; 38 | @property (nonatomic, strong) NSMutableArray *subtitles; 39 | @property (nonatomic, strong) NSMutableArray *viewForState; 40 | 41 | @property (nonatomic, weak) UIScrollView *scrollView; 42 | @property (nonatomic, readwrite) CGFloat originalTopInset; 43 | @property (nonatomic, readwrite) CGFloat originalBottomInset; 44 | 45 | @property (nonatomic, assign) BOOL wasTriggeredByUser; 46 | @property (nonatomic, assign) BOOL showsPullToRefresh; 47 | @property (nonatomic, assign) BOOL showsDateLabel; 48 | @property(nonatomic, assign) BOOL isObserving; 49 | 50 | - (void)resetScrollViewContentInset; 51 | - (void)setScrollViewContentInsetForLoading; 52 | - (void)setScrollViewContentInset:(UIEdgeInsets)insets; 53 | - (void)rotateArrow:(float)degrees hide:(BOOL)hide; 54 | 55 | @end 56 | 57 | 58 | 59 | #pragma mark - UIScrollView (SVPullToRefresh) 60 | #import 61 | 62 | static char UIScrollViewPullToRefreshView; 63 | 64 | @implementation UIScrollView (SVPullToRefresh) 65 | 66 | @dynamic pullToRefreshView, showsPullToRefresh; 67 | 68 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position { 69 | 70 | if(!self.pullToRefreshView) { 71 | CGFloat yOrigin; 72 | switch (position) { 73 | case SVPullToRefreshPositionTop: 74 | yOrigin = -SVPullToRefreshViewHeight; 75 | break; 76 | case SVPullToRefreshPositionBottom: 77 | yOrigin = self.contentSize.height; 78 | break; 79 | default: 80 | return; 81 | } 82 | SVPullToRefreshView *view = [[SVPullToRefreshView alloc] initWithFrame:CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight)]; 83 | view.pullToRefreshActionHandler = actionHandler; 84 | view.scrollView = self; 85 | [self addSubview:view]; 86 | 87 | view.originalTopInset = self.contentInset.top; 88 | view.originalBottomInset = self.contentInset.bottom; 89 | view.position = position; 90 | self.pullToRefreshView = view; 91 | self.showsPullToRefresh = YES; 92 | } 93 | 94 | } 95 | 96 | - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler { 97 | [self addPullToRefreshWithActionHandler:actionHandler position:SVPullToRefreshPositionTop]; 98 | } 99 | 100 | - (void)triggerPullToRefresh { 101 | self.pullToRefreshView.state = SVPullToRefreshStateTriggered; 102 | [self.pullToRefreshView startAnimating]; 103 | } 104 | 105 | - (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView { 106 | [self willChangeValueForKey:@"SVPullToRefreshView"]; 107 | objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView, 108 | pullToRefreshView, 109 | OBJC_ASSOCIATION_ASSIGN); 110 | [self didChangeValueForKey:@"SVPullToRefreshView"]; 111 | } 112 | 113 | - (SVPullToRefreshView *)pullToRefreshView { 114 | return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView); 115 | } 116 | 117 | - (void)setShowsPullToRefresh:(BOOL)showsPullToRefresh { 118 | self.pullToRefreshView.hidden = !showsPullToRefresh; 119 | 120 | if(!showsPullToRefresh) { 121 | if (self.pullToRefreshView.isObserving) { 122 | [self removeObserver:self.pullToRefreshView forKeyPath:@"contentOffset"]; 123 | [self removeObserver:self.pullToRefreshView forKeyPath:@"contentSize"]; 124 | [self removeObserver:self.pullToRefreshView forKeyPath:@"frame"]; 125 | [self.pullToRefreshView resetScrollViewContentInset]; 126 | self.pullToRefreshView.isObserving = NO; 127 | } 128 | } 129 | else { 130 | if (!self.pullToRefreshView.isObserving) { 131 | [self addObserver:self.pullToRefreshView forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil]; 132 | [self addObserver:self.pullToRefreshView forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; 133 | [self addObserver:self.pullToRefreshView forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil]; 134 | self.pullToRefreshView.isObserving = YES; 135 | 136 | CGFloat yOrigin = 0; 137 | switch (self.pullToRefreshView.position) { 138 | case SVPullToRefreshPositionTop: 139 | yOrigin = -SVPullToRefreshViewHeight; 140 | break; 141 | case SVPullToRefreshPositionBottom: 142 | yOrigin = self.contentSize.height; 143 | break; 144 | } 145 | 146 | self.pullToRefreshView.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight); 147 | } 148 | } 149 | } 150 | 151 | - (BOOL)showsPullToRefresh { 152 | return !self.pullToRefreshView.hidden; 153 | } 154 | 155 | @end 156 | 157 | #pragma mark - SVPullToRefresh 158 | @implementation SVPullToRefreshView 159 | 160 | // public properties 161 | @synthesize pullToRefreshActionHandler, arrowColor, textColor, activityIndicatorViewColor, activityIndicatorViewStyle, lastUpdatedDate, dateFormatter; 162 | 163 | @synthesize state = _state; 164 | @synthesize scrollView = _scrollView; 165 | @synthesize showsPullToRefresh = _showsPullToRefresh; 166 | @synthesize arrow = _arrow; 167 | @synthesize activityIndicatorView = _activityIndicatorView; 168 | 169 | @synthesize titleLabel = _titleLabel; 170 | @synthesize dateLabel = _dateLabel; 171 | 172 | 173 | - (id)initWithFrame:(CGRect)frame { 174 | if(self = [super initWithFrame:frame]) { 175 | 176 | // default styling values 177 | self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; 178 | self.textColor = [UIColor darkGrayColor]; 179 | self.autoresizingMask = UIViewAutoresizingFlexibleWidth; 180 | self.state = SVPullToRefreshStateStopped; 181 | self.showsDateLabel = NO; 182 | 183 | self.titles = [NSMutableArray arrayWithObjects:NSLocalizedString(@"Pull to refresh...",), 184 | NSLocalizedString(@"Release to refresh...",), 185 | NSLocalizedString(@"Loading...",), 186 | nil]; 187 | 188 | self.subtitles = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; 189 | self.viewForState = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; 190 | self.wasTriggeredByUser = YES; 191 | } 192 | 193 | return self; 194 | } 195 | 196 | - (void)willMoveToSuperview:(UIView *)newSuperview { 197 | if (self.superview && newSuperview == nil) { 198 | //use self.superview, not self.scrollView. Why self.scrollView == nil here? 199 | UIScrollView *scrollView = (UIScrollView *)self.superview; 200 | if (scrollView.showsPullToRefresh) { 201 | if (self.isObserving) { 202 | //If enter this branch, it is the moment just before "SVPullToRefreshView's dealloc", so remove observer here 203 | [scrollView removeObserver:self forKeyPath:@"contentOffset"]; 204 | [scrollView removeObserver:self forKeyPath:@"contentSize"]; 205 | [scrollView removeObserver:self forKeyPath:@"frame"]; 206 | self.isObserving = NO; 207 | } 208 | } 209 | } 210 | } 211 | 212 | - (void)layoutSubviews { 213 | 214 | for(id otherView in self.viewForState) { 215 | if([otherView isKindOfClass:[UIView class]]) 216 | [otherView removeFromSuperview]; 217 | } 218 | 219 | id customView = [self.viewForState objectAtIndex:self.state]; 220 | BOOL hasCustomView = [customView isKindOfClass:[UIView class]]; 221 | 222 | self.titleLabel.hidden = hasCustomView; 223 | self.subtitleLabel.hidden = hasCustomView; 224 | self.arrow.hidden = hasCustomView; 225 | 226 | if(hasCustomView) { 227 | [self addSubview:customView]; 228 | CGRect viewBounds = [customView bounds]; 229 | CGPoint origin = CGPointMake(roundf((self.bounds.size.width-viewBounds.size.width)/2), roundf((self.bounds.size.height-viewBounds.size.height)/2)); 230 | [customView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; 231 | } 232 | else { 233 | switch (self.state) { 234 | case SVPullToRefreshStateAll: 235 | case SVPullToRefreshStateStopped: 236 | self.arrow.alpha = 1; 237 | [self.activityIndicatorView stopAnimating]; 238 | switch (self.position) { 239 | case SVPullToRefreshPositionTop: 240 | [self rotateArrow:0 hide:NO]; 241 | break; 242 | case SVPullToRefreshPositionBottom: 243 | [self rotateArrow:(float)M_PI hide:NO]; 244 | break; 245 | } 246 | break; 247 | 248 | case SVPullToRefreshStateTriggered: 249 | switch (self.position) { 250 | case SVPullToRefreshPositionTop: 251 | [self rotateArrow:(float)M_PI hide:NO]; 252 | break; 253 | case SVPullToRefreshPositionBottom: 254 | [self rotateArrow:0 hide:NO]; 255 | break; 256 | } 257 | break; 258 | 259 | case SVPullToRefreshStateLoading: 260 | [self.activityIndicatorView startAnimating]; 261 | switch (self.position) { 262 | case SVPullToRefreshPositionTop: 263 | [self rotateArrow:0 hide:YES]; 264 | break; 265 | case SVPullToRefreshPositionBottom: 266 | [self rotateArrow:(float)M_PI hide:YES]; 267 | break; 268 | } 269 | break; 270 | } 271 | 272 | CGFloat leftViewWidth = MAX(self.arrow.bounds.size.width,self.activityIndicatorView.bounds.size.width); 273 | 274 | CGFloat margin = 10; 275 | CGFloat marginY = 2; 276 | CGFloat labelMaxWidth = self.bounds.size.width - margin - leftViewWidth; 277 | 278 | self.titleLabel.text = [self.titles objectAtIndex:self.state]; 279 | 280 | NSString *subtitle = [self.subtitles objectAtIndex:self.state]; 281 | self.subtitleLabel.text = subtitle.length > 0 ? subtitle : nil; 282 | 283 | 284 | CGSize titleSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font 285 | constrainedToSize:CGSizeMake(labelMaxWidth,self.titleLabel.font.lineHeight) 286 | lineBreakMode:self.titleLabel.lineBreakMode]; 287 | 288 | 289 | CGSize subtitleSize = [self.subtitleLabel.text sizeWithFont:self.subtitleLabel.font 290 | constrainedToSize:CGSizeMake(labelMaxWidth,self.subtitleLabel.font.lineHeight) 291 | lineBreakMode:self.subtitleLabel.lineBreakMode]; 292 | 293 | CGFloat maxLabelWidth = MAX(titleSize.width,subtitleSize.width); 294 | 295 | CGFloat totalMaxWidth; 296 | if (maxLabelWidth) { 297 | totalMaxWidth = leftViewWidth + margin + maxLabelWidth; 298 | } else { 299 | totalMaxWidth = leftViewWidth + maxLabelWidth; 300 | } 301 | 302 | CGFloat labelX = (self.bounds.size.width / 2) - (totalMaxWidth / 2) + leftViewWidth + margin; 303 | 304 | if(subtitleSize.height > 0){ 305 | CGFloat totalHeight = titleSize.height + subtitleSize.height + marginY; 306 | CGFloat minY = (self.bounds.size.height / 2) - (totalHeight / 2); 307 | 308 | CGFloat titleY = minY; 309 | self.titleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY, titleSize.width, titleSize.height)); 310 | self.subtitleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY + titleSize.height + marginY, subtitleSize.width, subtitleSize.height)); 311 | }else{ 312 | CGFloat totalHeight = titleSize.height; 313 | CGFloat minY = (self.bounds.size.height / 2) - (totalHeight / 2); 314 | 315 | CGFloat titleY = minY; 316 | self.titleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY, titleSize.width, titleSize.height)); 317 | self.subtitleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY + titleSize.height + marginY, subtitleSize.width, subtitleSize.height)); 318 | } 319 | 320 | CGFloat arrowX = (self.bounds.size.width / 2) - (totalMaxWidth / 2) + (leftViewWidth - self.arrow.bounds.size.width) / 2; 321 | self.arrow.frame = CGRectMake(arrowX, 322 | (self.bounds.size.height / 2) - (self.arrow.bounds.size.height / 2), 323 | self.arrow.bounds.size.width, 324 | self.arrow.bounds.size.height); 325 | self.activityIndicatorView.center = self.arrow.center; 326 | } 327 | } 328 | 329 | #pragma mark - Scroll View 330 | 331 | - (void)resetScrollViewContentInset { 332 | UIEdgeInsets currentInsets = self.scrollView.contentInset; 333 | switch (self.position) { 334 | case SVPullToRefreshPositionTop: 335 | currentInsets.top = self.originalTopInset; 336 | break; 337 | case SVPullToRefreshPositionBottom: 338 | currentInsets.bottom = self.originalBottomInset; 339 | currentInsets.top = self.originalTopInset; 340 | break; 341 | } 342 | [self setScrollViewContentInset:currentInsets]; 343 | } 344 | 345 | - (void)setScrollViewContentInsetForLoading { 346 | CGFloat offset = MAX(self.scrollView.contentOffset.y * -1, 0); 347 | UIEdgeInsets currentInsets = self.scrollView.contentInset; 348 | switch (self.position) { 349 | case SVPullToRefreshPositionTop: 350 | currentInsets.top = MIN(offset, self.originalTopInset + self.bounds.size.height); 351 | break; 352 | case SVPullToRefreshPositionBottom: 353 | currentInsets.bottom = MIN(offset, self.originalBottomInset + self.bounds.size.height); 354 | break; 355 | } 356 | [self setScrollViewContentInset:currentInsets]; 357 | } 358 | 359 | - (void)setScrollViewContentInset:(UIEdgeInsets)contentInset { 360 | [UIView animateWithDuration:0.3 361 | delay:0 362 | options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState 363 | animations:^{ 364 | self.scrollView.contentInset = contentInset; 365 | } 366 | completion:NULL]; 367 | } 368 | 369 | #pragma mark - Observing 370 | 371 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 372 | if([keyPath isEqualToString:@"contentOffset"]) 373 | [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; 374 | else if([keyPath isEqualToString:@"contentSize"]) { 375 | [self layoutSubviews]; 376 | 377 | CGFloat yOrigin; 378 | switch (self.position) { 379 | case SVPullToRefreshPositionTop: 380 | yOrigin = -SVPullToRefreshViewHeight; 381 | break; 382 | case SVPullToRefreshPositionBottom: 383 | yOrigin = MAX(self.scrollView.contentSize.height, self.scrollView.bounds.size.height); 384 | break; 385 | } 386 | self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight); 387 | } 388 | else if([keyPath isEqualToString:@"frame"]) 389 | [self layoutSubviews]; 390 | 391 | } 392 | 393 | - (void)scrollViewDidScroll:(CGPoint)contentOffset { 394 | if(self.state != SVPullToRefreshStateLoading) { 395 | CGFloat scrollOffsetThreshold = 0; 396 | switch (self.position) { 397 | case SVPullToRefreshPositionTop: 398 | scrollOffsetThreshold = self.frame.origin.y - self.originalTopInset; 399 | break; 400 | case SVPullToRefreshPositionBottom: 401 | scrollOffsetThreshold = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.bounds.size.height + self.originalBottomInset; 402 | break; 403 | } 404 | 405 | if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered) 406 | self.state = SVPullToRefreshStateLoading; 407 | else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) 408 | self.state = SVPullToRefreshStateTriggered; 409 | else if(contentOffset.y >= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) 410 | self.state = SVPullToRefreshStateStopped; 411 | else if(contentOffset.y > scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) 412 | self.state = SVPullToRefreshStateTriggered; 413 | else if(contentOffset.y <= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) 414 | self.state = SVPullToRefreshStateStopped; 415 | } else { 416 | CGFloat offset; 417 | UIEdgeInsets contentInset; 418 | switch (self.position) { 419 | case SVPullToRefreshPositionTop: 420 | offset = MAX(self.scrollView.contentOffset.y * -1, 0.0f); 421 | offset = MIN(offset, self.originalTopInset + self.bounds.size.height); 422 | contentInset = self.scrollView.contentInset; 423 | self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right); 424 | break; 425 | case SVPullToRefreshPositionBottom: 426 | if (self.scrollView.contentSize.height >= self.scrollView.bounds.size.height) { 427 | offset = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.bounds.size.height, 0.0f); 428 | offset = MIN(offset, self.originalBottomInset + self.bounds.size.height); 429 | contentInset = self.scrollView.contentInset; 430 | self.scrollView.contentInset = UIEdgeInsetsMake(contentInset.top, contentInset.left, offset, contentInset.right); 431 | } else if (self.wasTriggeredByUser) { 432 | offset = MIN(self.bounds.size.height, self.originalBottomInset + self.bounds.size.height); 433 | contentInset = self.scrollView.contentInset; 434 | self.scrollView.contentInset = UIEdgeInsetsMake(-offset, contentInset.left, contentInset.bottom, contentInset.right); 435 | } 436 | break; 437 | } 438 | } 439 | } 440 | 441 | #pragma mark - Getters 442 | 443 | - (SVPullToRefreshArrow *)arrow { 444 | if(!_arrow) { 445 | _arrow = [[SVPullToRefreshArrow alloc]initWithFrame:CGRectMake(0, self.bounds.size.height-54, 22, 48)]; 446 | _arrow.backgroundColor = [UIColor clearColor]; 447 | [self addSubview:_arrow]; 448 | } 449 | return _arrow; 450 | } 451 | 452 | - (UIActivityIndicatorView *)activityIndicatorView { 453 | if(!_activityIndicatorView) { 454 | _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; 455 | _activityIndicatorView.hidesWhenStopped = YES; 456 | [self addSubview:_activityIndicatorView]; 457 | } 458 | return _activityIndicatorView; 459 | } 460 | 461 | - (UILabel *)titleLabel { 462 | if(!_titleLabel) { 463 | _titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 210, 20)]; 464 | _titleLabel.text = NSLocalizedString(@"Pull to refresh...",); 465 | _titleLabel.font = [UIFont boldSystemFontOfSize:14]; 466 | _titleLabel.backgroundColor = [UIColor clearColor]; 467 | _titleLabel.textColor = textColor; 468 | [self addSubview:_titleLabel]; 469 | } 470 | return _titleLabel; 471 | } 472 | 473 | - (UILabel *)subtitleLabel { 474 | if(!_subtitleLabel) { 475 | _subtitleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 210, 20)]; 476 | _subtitleLabel.font = [UIFont systemFontOfSize:12]; 477 | _subtitleLabel.backgroundColor = [UIColor clearColor]; 478 | _subtitleLabel.textColor = textColor; 479 | [self addSubview:_subtitleLabel]; 480 | } 481 | return _subtitleLabel; 482 | } 483 | 484 | - (UILabel *)dateLabel { 485 | return self.showsDateLabel ? self.subtitleLabel : nil; 486 | } 487 | 488 | - (NSDateFormatter *)dateFormatter { 489 | if(!dateFormatter) { 490 | dateFormatter = [[NSDateFormatter alloc] init]; 491 | [dateFormatter setDateStyle:NSDateFormatterShortStyle]; 492 | [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; 493 | dateFormatter.locale = [NSLocale currentLocale]; 494 | } 495 | return dateFormatter; 496 | } 497 | 498 | - (UIColor *)arrowColor { 499 | return self.arrow.arrowColor; // pass through 500 | } 501 | 502 | - (UIColor *)textColor { 503 | return self.titleLabel.textColor; 504 | } 505 | 506 | - (UIColor *)activityIndicatorViewColor { 507 | return self.activityIndicatorView.color; 508 | } 509 | 510 | - (UIActivityIndicatorViewStyle)activityIndicatorViewStyle { 511 | return self.activityIndicatorView.activityIndicatorViewStyle; 512 | } 513 | 514 | #pragma mark - Setters 515 | 516 | - (void)setArrowColor:(UIColor *)newArrowColor { 517 | self.arrow.arrowColor = newArrowColor; // pass through 518 | [self.arrow setNeedsDisplay]; 519 | } 520 | 521 | - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state { 522 | if(!title) 523 | title = @""; 524 | 525 | if(state == SVPullToRefreshStateAll) 526 | [self.titles replaceObjectsInRange:NSMakeRange(0, 3) withObjectsFromArray:@[title, title, title]]; 527 | else 528 | [self.titles replaceObjectAtIndex:state withObject:title]; 529 | 530 | [self setNeedsLayout]; 531 | } 532 | 533 | - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state { 534 | if(!subtitle) 535 | subtitle = @""; 536 | 537 | if(state == SVPullToRefreshStateAll) 538 | [self.subtitles replaceObjectsInRange:NSMakeRange(0, 3) withObjectsFromArray:@[subtitle, subtitle, subtitle]]; 539 | else 540 | [self.subtitles replaceObjectAtIndex:state withObject:subtitle]; 541 | 542 | [self setNeedsLayout]; 543 | } 544 | 545 | - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state { 546 | id viewPlaceholder = view; 547 | 548 | if(!viewPlaceholder) 549 | viewPlaceholder = @""; 550 | 551 | if(state == SVPullToRefreshStateAll) 552 | [self.viewForState replaceObjectsInRange:NSMakeRange(0, 3) withObjectsFromArray:@[viewPlaceholder, viewPlaceholder, viewPlaceholder]]; 553 | else 554 | [self.viewForState replaceObjectAtIndex:state withObject:viewPlaceholder]; 555 | 556 | [self setNeedsLayout]; 557 | } 558 | 559 | - (void)setTextColor:(UIColor *)newTextColor { 560 | textColor = newTextColor; 561 | self.titleLabel.textColor = newTextColor; 562 | self.subtitleLabel.textColor = newTextColor; 563 | } 564 | 565 | - (void)setActivityIndicatorViewColor:(UIColor *)color { 566 | self.activityIndicatorView.color = color; 567 | } 568 | 569 | - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)viewStyle { 570 | self.activityIndicatorView.activityIndicatorViewStyle = viewStyle; 571 | } 572 | 573 | - (void)setLastUpdatedDate:(NSDate *)newLastUpdatedDate { 574 | self.showsDateLabel = YES; 575 | self.dateLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Last Updated: %@",), newLastUpdatedDate?[self.dateFormatter stringFromDate:newLastUpdatedDate]:NSLocalizedString(@"Never",)]; 576 | } 577 | 578 | - (void)setDateFormatter:(NSDateFormatter *)newDateFormatter { 579 | dateFormatter = newDateFormatter; 580 | self.dateLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Last Updated: %@",), self.lastUpdatedDate?[newDateFormatter stringFromDate:self.lastUpdatedDate]:NSLocalizedString(@"Never",)]; 581 | } 582 | 583 | #pragma mark - 584 | 585 | - (void)triggerRefresh { 586 | [self.scrollView triggerPullToRefresh]; 587 | } 588 | 589 | - (void)startAnimating{ 590 | switch (self.position) { 591 | case SVPullToRefreshPositionTop: 592 | 593 | if(fequalzero(self.scrollView.contentOffset.y)) { 594 | [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.frame.size.height) animated:YES]; 595 | self.wasTriggeredByUser = NO; 596 | } 597 | else 598 | self.wasTriggeredByUser = YES; 599 | 600 | break; 601 | case SVPullToRefreshPositionBottom: 602 | 603 | if((fequalzero(self.scrollView.contentOffset.y) && self.scrollView.contentSize.height < self.scrollView.bounds.size.height) 604 | || fequal(self.scrollView.contentOffset.y, self.scrollView.contentSize.height - self.scrollView.bounds.size.height)) { 605 | [self.scrollView setContentOffset:(CGPoint){.y = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.frame.size.height} animated:YES]; 606 | self.wasTriggeredByUser = NO; 607 | } 608 | else 609 | self.wasTriggeredByUser = YES; 610 | 611 | break; 612 | } 613 | 614 | self.state = SVPullToRefreshStateLoading; 615 | } 616 | 617 | - (void)stopAnimating { 618 | self.state = SVPullToRefreshStateStopped; 619 | 620 | switch (self.position) { 621 | case SVPullToRefreshPositionTop: 622 | if(!self.wasTriggeredByUser) 623 | [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.originalTopInset) animated:YES]; 624 | break; 625 | case SVPullToRefreshPositionBottom: 626 | if(!self.wasTriggeredByUser) 627 | [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.originalBottomInset) animated:YES]; 628 | break; 629 | } 630 | } 631 | 632 | - (void)setState:(SVPullToRefreshState)newState { 633 | 634 | if(_state == newState) 635 | return; 636 | 637 | SVPullToRefreshState previousState = _state; 638 | _state = newState; 639 | 640 | [self setNeedsLayout]; 641 | [self layoutIfNeeded]; 642 | 643 | switch (newState) { 644 | case SVPullToRefreshStateAll: 645 | case SVPullToRefreshStateStopped: 646 | [self resetScrollViewContentInset]; 647 | break; 648 | 649 | case SVPullToRefreshStateTriggered: 650 | break; 651 | 652 | case SVPullToRefreshStateLoading: 653 | [self setScrollViewContentInsetForLoading]; 654 | 655 | if(previousState == SVPullToRefreshStateTriggered && pullToRefreshActionHandler) 656 | pullToRefreshActionHandler(); 657 | 658 | break; 659 | } 660 | } 661 | 662 | - (void)rotateArrow:(float)degrees hide:(BOOL)hide { 663 | [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{ 664 | self.arrow.layer.transform = CATransform3DMakeRotation(degrees, 0, 0, 1); 665 | self.arrow.layer.opacity = !hide; 666 | //[self.arrow setNeedsDisplay];//ios 4 667 | } completion:NULL]; 668 | } 669 | 670 | @end 671 | 672 | 673 | #pragma mark - SVPullToRefreshArrow 674 | 675 | @implementation SVPullToRefreshArrow 676 | @synthesize arrowColor; 677 | 678 | - (UIColor *)arrowColor { 679 | if (arrowColor) return arrowColor; 680 | return [UIColor grayColor]; // default Color 681 | } 682 | 683 | - (void)drawRect:(CGRect)rect { 684 | CGContextRef c = UIGraphicsGetCurrentContext(); 685 | 686 | // the rects above the arrow 687 | CGContextAddRect(c, CGRectMake(5, 0, 12, 4)); // to-do: use dynamic points 688 | CGContextAddRect(c, CGRectMake(5, 6, 12, 4)); // currently fixed size: 22 x 48pt 689 | CGContextAddRect(c, CGRectMake(5, 12, 12, 4)); 690 | CGContextAddRect(c, CGRectMake(5, 18, 12, 4)); 691 | CGContextAddRect(c, CGRectMake(5, 24, 12, 4)); 692 | CGContextAddRect(c, CGRectMake(5, 30, 12, 4)); 693 | 694 | // the arrow 695 | CGContextMoveToPoint(c, 0, 34); 696 | CGContextAddLineToPoint(c, 11, 48); 697 | CGContextAddLineToPoint(c, 22, 34); 698 | CGContextAddLineToPoint(c, 0, 34); 699 | CGContextClosePath(c); 700 | 701 | CGContextSaveGState(c); 702 | CGContextClip(c); 703 | 704 | // Gradient Declaration 705 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 706 | CGFloat alphaGradientLocations[] = {0, 0.8f}; 707 | 708 | CGGradientRef alphaGradient = nil; 709 | if([[[UIDevice currentDevice] systemVersion]floatValue] >= 5){ 710 | NSArray* alphaGradientColors = [NSArray arrayWithObjects: 711 | (id)[self.arrowColor colorWithAlphaComponent:0].CGColor, 712 | (id)[self.arrowColor colorWithAlphaComponent:1].CGColor, 713 | nil]; 714 | alphaGradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)alphaGradientColors, alphaGradientLocations); 715 | }else{ 716 | const CGFloat * components = CGColorGetComponents([self.arrowColor CGColor]); 717 | size_t numComponents = CGColorGetNumberOfComponents([self.arrowColor CGColor]); 718 | CGFloat colors[8]; 719 | switch(numComponents){ 720 | case 2:{ 721 | colors[0] = colors[4] = components[0]; 722 | colors[1] = colors[5] = components[0]; 723 | colors[2] = colors[6] = components[0]; 724 | break; 725 | } 726 | case 4:{ 727 | colors[0] = colors[4] = components[0]; 728 | colors[1] = colors[5] = components[1]; 729 | colors[2] = colors[6] = components[2]; 730 | break; 731 | } 732 | } 733 | colors[3] = 0; 734 | colors[7] = 1; 735 | alphaGradient = CGGradientCreateWithColorComponents(colorSpace,colors,alphaGradientLocations,2); 736 | } 737 | 738 | 739 | CGContextDrawLinearGradient(c, alphaGradient, CGPointZero, CGPointMake(0, rect.size.height), 0); 740 | 741 | CGContextRestoreGState(c); 742 | 743 | CGGradientRelease(alphaGradient); 744 | CGColorSpaceRelease(colorSpace); 745 | } 746 | @end 747 | --------------------------------------------------------------------------------