├── .gitignore ├── JYRefreshController.podspec ├── JYRefreshController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── alvin.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── JYRefreshController.xcscheme │ └── xcschememanagement.plist ├── JYRefreshController ├── AppDelegate.h ├── AppDelegate.m ├── DemoViewController.h ├── DemoViewController.m ├── DemoViewController.xib ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── JYRefreshController-Info.plist ├── JYRefreshController-Prefix.pch ├── Launch Screen.storyboard ├── Source │ ├── JYPullToLoadMoreController.h │ ├── JYPullToLoadMoreController.m │ ├── JYPullToRefreshController.h │ ├── JYPullToRefreshController.m │ ├── JYRefreshController.h │ ├── JYRefreshIndicator.h │ ├── JYRefreshIndicator.m │ ├── JYRefreshView.h │ └── JYRefreshView.m ├── en.lproj │ └── InfoPlist.strings └── main.m ├── JYRefreshControllerTests ├── JYRefreshControllerTests-Info.plist ├── JYRefreshControllerTests.m └── en.lproj │ └── InfoPlist.strings ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | Pods/ 21 | 22 | -------------------------------------------------------------------------------- /JYRefreshController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'JYRefreshController' 3 | s.version = '0.7.1' 4 | s.license = 'MIT' 5 | s.summary = 'An easy way to add pull-to-refresh or pull-to-loadMore to any kind of UIScrollView.' 6 | s.homepage = 'https://github.com/goldfish0506/JYRefreshController' 7 | s.authors = { 'Alvin' => 'goldfish.jinyu@gmail.com' } 8 | s.source = { :git => 'https://github.com/goldfish0506/JYRefreshController.git', 9 | :tag => s.version.to_s 10 | } 11 | s.requires_arc = true 12 | s.source_files = 'JYRefreshController/Source/**/*.{h,m}' 13 | s.platform = :ios, '8.0' 14 | end 15 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7180A8C71C32B95700CFB61B /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7180A8C61C32B95700CFB61B /* Launch Screen.storyboard */; }; 11 | E16D57501A1469100012031B /* JYPullToRefreshController.m in Sources */ = {isa = PBXBuildFile; fileRef = E16D574F1A1469100012031B /* JYPullToRefreshController.m */; }; 12 | E1B044391952BEB00038569E /* JYRefreshIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = E1B044351952BEB00038569E /* JYRefreshIndicator.m */; }; 13 | E1B0443A1952BEB00038569E /* JYRefreshView.m in Sources */ = {isa = PBXBuildFile; fileRef = E1B044371952BEB00038569E /* JYRefreshView.m */; }; 14 | E1D3B1AF1A1993E4007D1B9B /* JYPullToLoadMoreController.m in Sources */ = {isa = PBXBuildFile; fileRef = E1D3B1AE1A1993E4007D1B9B /* JYPullToLoadMoreController.m */; }; 15 | E1D3B1B61A19F583007D1B9B /* DemoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1D3B1B51A19F583007D1B9B /* DemoViewController.xib */; }; 16 | E1FB5834194E8C550061511C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB5833194E8C550061511C /* Foundation.framework */; }; 17 | E1FB5836194E8C550061511C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB5835194E8C550061511C /* CoreGraphics.framework */; }; 18 | E1FB5838194E8C550061511C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB5837194E8C550061511C /* UIKit.framework */; }; 19 | E1FB583E194E8C550061511C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E1FB583C194E8C550061511C /* InfoPlist.strings */; }; 20 | E1FB5840194E8C550061511C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E1FB583F194E8C550061511C /* main.m */; }; 21 | E1FB5844194E8C550061511C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E1FB5843194E8C550061511C /* AppDelegate.m */; }; 22 | E1FB5846194E8C550061511C /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E1FB5845194E8C550061511C /* Images.xcassets */; }; 23 | E1FB584D194E8C550061511C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB584C194E8C550061511C /* XCTest.framework */; }; 24 | E1FB584E194E8C550061511C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB5833194E8C550061511C /* Foundation.framework */; }; 25 | E1FB584F194E8C550061511C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1FB5837194E8C550061511C /* UIKit.framework */; }; 26 | E1FB5857194E8C550061511C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E1FB5855194E8C550061511C /* InfoPlist.strings */; }; 27 | E1FB5859194E8C550061511C /* JYRefreshControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E1FB5858194E8C550061511C /* JYRefreshControllerTests.m */; }; 28 | E1FB5864194E8C6C0061511C /* DemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E1FB5863194E8C6C0061511C /* DemoViewController.m */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | E1FB5850194E8C550061511C /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = E1FB5828194E8C550061511C /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = E1FB582F194E8C550061511C; 37 | remoteInfo = JYRefreshController; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 7180A8C61C32B95700CFB61B /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 43 | E16D574E1A1469100012031B /* JYPullToRefreshController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JYPullToRefreshController.h; sourceTree = ""; }; 44 | E16D574F1A1469100012031B /* JYPullToRefreshController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = JYPullToRefreshController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 45 | E1B044341952BEB00038569E /* JYRefreshIndicator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JYRefreshIndicator.h; sourceTree = ""; }; 46 | E1B044351952BEB00038569E /* JYRefreshIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JYRefreshIndicator.m; sourceTree = ""; }; 47 | E1B044361952BEB00038569E /* JYRefreshView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JYRefreshView.h; sourceTree = ""; }; 48 | E1B044371952BEB00038569E /* JYRefreshView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = JYRefreshView.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 49 | E1D3B1AD1A1993E4007D1B9B /* JYPullToLoadMoreController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JYPullToLoadMoreController.h; sourceTree = ""; }; 50 | E1D3B1AE1A1993E4007D1B9B /* JYPullToLoadMoreController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = JYPullToLoadMoreController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 51 | E1D3B1B01A19DA53007D1B9B /* JYRefreshController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JYRefreshController.h; sourceTree = ""; }; 52 | E1D3B1B51A19F583007D1B9B /* DemoViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DemoViewController.xib; sourceTree = ""; }; 53 | E1FB5830194E8C550061511C /* JYRefreshController.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JYRefreshController.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | E1FB5833194E8C550061511C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 55 | E1FB5835194E8C550061511C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 56 | E1FB5837194E8C550061511C /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 57 | E1FB583B194E8C550061511C /* JYRefreshController-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "JYRefreshController-Info.plist"; sourceTree = ""; }; 58 | E1FB583D194E8C550061511C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 59 | E1FB583F194E8C550061511C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 60 | E1FB5841194E8C550061511C /* JYRefreshController-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JYRefreshController-Prefix.pch"; sourceTree = ""; }; 61 | E1FB5842194E8C550061511C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 62 | E1FB5843194E8C550061511C /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 63 | E1FB5845194E8C550061511C /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 64 | E1FB584B194E8C550061511C /* JYRefreshControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JYRefreshControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | E1FB584C194E8C550061511C /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 66 | E1FB5854194E8C550061511C /* JYRefreshControllerTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "JYRefreshControllerTests-Info.plist"; sourceTree = ""; }; 67 | E1FB5856194E8C550061511C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 68 | E1FB5858194E8C550061511C /* JYRefreshControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JYRefreshControllerTests.m; sourceTree = ""; }; 69 | E1FB5862194E8C6C0061511C /* DemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DemoViewController.h; sourceTree = ""; }; 70 | E1FB5863194E8C6C0061511C /* DemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DemoViewController.m; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | E1FB582D194E8C550061511C /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | E1FB5836194E8C550061511C /* CoreGraphics.framework in Frameworks */, 79 | E1FB5838194E8C550061511C /* UIKit.framework in Frameworks */, 80 | E1FB5834194E8C550061511C /* Foundation.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | E1FB5848194E8C550061511C /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | E1FB584D194E8C550061511C /* XCTest.framework in Frameworks */, 89 | E1FB584F194E8C550061511C /* UIKit.framework in Frameworks */, 90 | E1FB584E194E8C550061511C /* Foundation.framework in Frameworks */, 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | E1B044301952BEB00038569E /* Source */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | E1D3B1B01A19DA53007D1B9B /* JYRefreshController.h */, 101 | E16D574E1A1469100012031B /* JYPullToRefreshController.h */, 102 | E16D574F1A1469100012031B /* JYPullToRefreshController.m */, 103 | E1D3B1AD1A1993E4007D1B9B /* JYPullToLoadMoreController.h */, 104 | E1D3B1AE1A1993E4007D1B9B /* JYPullToLoadMoreController.m */, 105 | E1B044341952BEB00038569E /* JYRefreshIndicator.h */, 106 | E1B044351952BEB00038569E /* JYRefreshIndicator.m */, 107 | E1B044361952BEB00038569E /* JYRefreshView.h */, 108 | E1B044371952BEB00038569E /* JYRefreshView.m */, 109 | ); 110 | path = Source; 111 | sourceTree = ""; 112 | }; 113 | E1FB5827194E8C550061511C = { 114 | isa = PBXGroup; 115 | children = ( 116 | E1FB5839194E8C550061511C /* JYRefreshController */, 117 | E1FB5852194E8C550061511C /* JYRefreshControllerTests */, 118 | E1FB5832194E8C550061511C /* Frameworks */, 119 | E1FB5831194E8C550061511C /* Products */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | E1FB5831194E8C550061511C /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | E1FB5830194E8C550061511C /* JYRefreshController.app */, 127 | E1FB584B194E8C550061511C /* JYRefreshControllerTests.xctest */, 128 | ); 129 | name = Products; 130 | sourceTree = ""; 131 | }; 132 | E1FB5832194E8C550061511C /* Frameworks */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | E1FB5833194E8C550061511C /* Foundation.framework */, 136 | E1FB5835194E8C550061511C /* CoreGraphics.framework */, 137 | E1FB5837194E8C550061511C /* UIKit.framework */, 138 | E1FB584C194E8C550061511C /* XCTest.framework */, 139 | ); 140 | name = Frameworks; 141 | sourceTree = ""; 142 | }; 143 | E1FB5839194E8C550061511C /* JYRefreshController */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | E1B044301952BEB00038569E /* Source */, 147 | E1FB5842194E8C550061511C /* AppDelegate.h */, 148 | E1FB5843194E8C550061511C /* AppDelegate.m */, 149 | E1FB5862194E8C6C0061511C /* DemoViewController.h */, 150 | E1FB5863194E8C6C0061511C /* DemoViewController.m */, 151 | E1D3B1B51A19F583007D1B9B /* DemoViewController.xib */, 152 | 7180A8C61C32B95700CFB61B /* Launch Screen.storyboard */, 153 | E1FB5845194E8C550061511C /* Images.xcassets */, 154 | E1FB583A194E8C550061511C /* Supporting Files */, 155 | ); 156 | path = JYRefreshController; 157 | sourceTree = ""; 158 | }; 159 | E1FB583A194E8C550061511C /* Supporting Files */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | E1FB583B194E8C550061511C /* JYRefreshController-Info.plist */, 163 | E1FB583C194E8C550061511C /* InfoPlist.strings */, 164 | E1FB583F194E8C550061511C /* main.m */, 165 | E1FB5841194E8C550061511C /* JYRefreshController-Prefix.pch */, 166 | ); 167 | name = "Supporting Files"; 168 | sourceTree = ""; 169 | }; 170 | E1FB5852194E8C550061511C /* JYRefreshControllerTests */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | E1FB5858194E8C550061511C /* JYRefreshControllerTests.m */, 174 | E1FB5853194E8C550061511C /* Supporting Files */, 175 | ); 176 | path = JYRefreshControllerTests; 177 | sourceTree = ""; 178 | }; 179 | E1FB5853194E8C550061511C /* Supporting Files */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | E1FB5854194E8C550061511C /* JYRefreshControllerTests-Info.plist */, 183 | E1FB5855194E8C550061511C /* InfoPlist.strings */, 184 | ); 185 | name = "Supporting Files"; 186 | sourceTree = ""; 187 | }; 188 | /* End PBXGroup section */ 189 | 190 | /* Begin PBXNativeTarget section */ 191 | E1FB582F194E8C550061511C /* JYRefreshController */ = { 192 | isa = PBXNativeTarget; 193 | buildConfigurationList = E1FB585C194E8C550061511C /* Build configuration list for PBXNativeTarget "JYRefreshController" */; 194 | buildPhases = ( 195 | E1FB582C194E8C550061511C /* Sources */, 196 | E1FB582D194E8C550061511C /* Frameworks */, 197 | E1FB582E194E8C550061511C /* Resources */, 198 | ); 199 | buildRules = ( 200 | ); 201 | dependencies = ( 202 | ); 203 | name = JYRefreshController; 204 | productName = JYRefreshController; 205 | productReference = E1FB5830194E8C550061511C /* JYRefreshController.app */; 206 | productType = "com.apple.product-type.application"; 207 | }; 208 | E1FB584A194E8C550061511C /* JYRefreshControllerTests */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = E1FB585F194E8C550061511C /* Build configuration list for PBXNativeTarget "JYRefreshControllerTests" */; 211 | buildPhases = ( 212 | E1FB5847194E8C550061511C /* Sources */, 213 | E1FB5848194E8C550061511C /* Frameworks */, 214 | E1FB5849194E8C550061511C /* Resources */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | E1FB5851194E8C550061511C /* PBXTargetDependency */, 220 | ); 221 | name = JYRefreshControllerTests; 222 | productName = JYRefreshControllerTests; 223 | productReference = E1FB584B194E8C550061511C /* JYRefreshControllerTests.xctest */; 224 | productType = "com.apple.product-type.bundle.unit-test"; 225 | }; 226 | /* End PBXNativeTarget section */ 227 | 228 | /* Begin PBXProject section */ 229 | E1FB5828194E8C550061511C /* Project object */ = { 230 | isa = PBXProject; 231 | attributes = { 232 | LastUpgradeCheck = 1010; 233 | TargetAttributes = { 234 | E1FB584A194E8C550061511C = { 235 | TestTargetID = E1FB582F194E8C550061511C; 236 | }; 237 | }; 238 | }; 239 | buildConfigurationList = E1FB582B194E8C550061511C /* Build configuration list for PBXProject "JYRefreshController" */; 240 | compatibilityVersion = "Xcode 3.2"; 241 | developmentRegion = English; 242 | hasScannedForEncodings = 0; 243 | knownRegions = ( 244 | en, 245 | ); 246 | mainGroup = E1FB5827194E8C550061511C; 247 | productRefGroup = E1FB5831194E8C550061511C /* Products */; 248 | projectDirPath = ""; 249 | projectRoot = ""; 250 | targets = ( 251 | E1FB582F194E8C550061511C /* JYRefreshController */, 252 | E1FB584A194E8C550061511C /* JYRefreshControllerTests */, 253 | ); 254 | }; 255 | /* End PBXProject section */ 256 | 257 | /* Begin PBXResourcesBuildPhase section */ 258 | E1FB582E194E8C550061511C /* Resources */ = { 259 | isa = PBXResourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | E1FB583E194E8C550061511C /* InfoPlist.strings in Resources */, 263 | E1D3B1B61A19F583007D1B9B /* DemoViewController.xib in Resources */, 264 | 7180A8C71C32B95700CFB61B /* Launch Screen.storyboard in Resources */, 265 | E1FB5846194E8C550061511C /* Images.xcassets in Resources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | E1FB5849194E8C550061511C /* Resources */ = { 270 | isa = PBXResourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | E1FB5857194E8C550061511C /* InfoPlist.strings in Resources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXResourcesBuildPhase section */ 278 | 279 | /* Begin PBXSourcesBuildPhase section */ 280 | E1FB582C194E8C550061511C /* Sources */ = { 281 | isa = PBXSourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | E16D57501A1469100012031B /* JYPullToRefreshController.m in Sources */, 285 | E1FB5864194E8C6C0061511C /* DemoViewController.m in Sources */, 286 | E1B0443A1952BEB00038569E /* JYRefreshView.m in Sources */, 287 | E1D3B1AF1A1993E4007D1B9B /* JYPullToLoadMoreController.m in Sources */, 288 | E1FB5844194E8C550061511C /* AppDelegate.m in Sources */, 289 | E1B044391952BEB00038569E /* JYRefreshIndicator.m in Sources */, 290 | E1FB5840194E8C550061511C /* main.m in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | E1FB5847194E8C550061511C /* Sources */ = { 295 | isa = PBXSourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | E1FB5859194E8C550061511C /* JYRefreshControllerTests.m in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXTargetDependency section */ 305 | E1FB5851194E8C550061511C /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = E1FB582F194E8C550061511C /* JYRefreshController */; 308 | targetProxy = E1FB5850194E8C550061511C /* PBXContainerItemProxy */; 309 | }; 310 | /* End PBXTargetDependency section */ 311 | 312 | /* Begin PBXVariantGroup section */ 313 | E1FB583C194E8C550061511C /* InfoPlist.strings */ = { 314 | isa = PBXVariantGroup; 315 | children = ( 316 | E1FB583D194E8C550061511C /* en */, 317 | ); 318 | name = InfoPlist.strings; 319 | sourceTree = ""; 320 | }; 321 | E1FB5855194E8C550061511C /* InfoPlist.strings */ = { 322 | isa = PBXVariantGroup; 323 | children = ( 324 | E1FB5856194E8C550061511C /* en */, 325 | ); 326 | name = InfoPlist.strings; 327 | sourceTree = ""; 328 | }; 329 | /* End PBXVariantGroup section */ 330 | 331 | /* Begin XCBuildConfiguration section */ 332 | E1FB585A194E8C550061511C /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_EMPTY_BODY = YES; 347 | CLANG_WARN_ENUM_CONVERSION = YES; 348 | CLANG_WARN_INFINITE_RECURSION = YES; 349 | CLANG_WARN_INT_CONVERSION = YES; 350 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 352 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 354 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 355 | CLANG_WARN_STRICT_PROTOTYPES = YES; 356 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 357 | CLANG_WARN_UNREACHABLE_CODE = YES; 358 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 359 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 360 | COPY_PHASE_STRIP = NO; 361 | ENABLE_STRICT_OBJC_MSGSEND = YES; 362 | ENABLE_TESTABILITY = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu99; 364 | GCC_DYNAMIC_NO_PIC = NO; 365 | GCC_NO_COMMON_BLOCKS = YES; 366 | GCC_OPTIMIZATION_LEVEL = 0; 367 | GCC_PREPROCESSOR_DEFINITIONS = ( 368 | "DEBUG=1", 369 | "$(inherited)", 370 | ); 371 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 372 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 373 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 374 | GCC_WARN_UNDECLARED_SELECTOR = YES; 375 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 376 | GCC_WARN_UNUSED_FUNCTION = YES; 377 | GCC_WARN_UNUSED_VARIABLE = YES; 378 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 379 | ONLY_ACTIVE_ARCH = YES; 380 | SDKROOT = iphoneos; 381 | }; 382 | name = Debug; 383 | }; 384 | E1FB585B194E8C550061511C /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ALWAYS_SEARCH_USER_PATHS = NO; 388 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 389 | CLANG_CXX_LIBRARY = "libc++"; 390 | CLANG_ENABLE_MODULES = YES; 391 | CLANG_ENABLE_OBJC_ARC = YES; 392 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 393 | CLANG_WARN_BOOL_CONVERSION = YES; 394 | CLANG_WARN_COMMA = YES; 395 | CLANG_WARN_CONSTANT_CONVERSION = YES; 396 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 397 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INFINITE_RECURSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNREACHABLE_CODE = YES; 410 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 411 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 412 | COPY_PHASE_STRIP = YES; 413 | ENABLE_NS_ASSERTIONS = NO; 414 | ENABLE_STRICT_OBJC_MSGSEND = YES; 415 | GCC_C_LANGUAGE_STANDARD = gnu99; 416 | GCC_NO_COMMON_BLOCKS = YES; 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 424 | SDKROOT = iphoneos; 425 | VALIDATE_PRODUCT = YES; 426 | }; 427 | name = Release; 428 | }; 429 | E1FB585D194E8C550061511C /* Debug */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 433 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 434 | FRAMEWORK_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "$(PROJECT_DIR)/JYRefreshController", 437 | ); 438 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 439 | GCC_PREFIX_HEADER = "JYRefreshController/JYRefreshController-Prefix.pch"; 440 | INFOPLIST_FILE = "JYRefreshController/JYRefreshController-Info.plist"; 441 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 442 | OTHER_LDFLAGS = "-ObjC"; 443 | PRODUCT_BUNDLE_IDENTIFIER = "com.alvin.$(PRODUCT_NAME:rfc1034identifier)"; 444 | PRODUCT_NAME = "$(TARGET_NAME)"; 445 | WRAPPER_EXTENSION = app; 446 | }; 447 | name = Debug; 448 | }; 449 | E1FB585E194E8C550061511C /* Release */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 453 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 454 | FRAMEWORK_SEARCH_PATHS = ( 455 | "$(inherited)", 456 | "$(PROJECT_DIR)/JYRefreshController", 457 | ); 458 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 459 | GCC_PREFIX_HEADER = "JYRefreshController/JYRefreshController-Prefix.pch"; 460 | INFOPLIST_FILE = "JYRefreshController/JYRefreshController-Info.plist"; 461 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 462 | OTHER_LDFLAGS = "-ObjC"; 463 | PRODUCT_BUNDLE_IDENTIFIER = "com.alvin.$(PRODUCT_NAME:rfc1034identifier)"; 464 | PRODUCT_NAME = "$(TARGET_NAME)"; 465 | WRAPPER_EXTENSION = app; 466 | }; 467 | name = Release; 468 | }; 469 | E1FB5860194E8C550061511C /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/JYRefreshController.app/JYRefreshController"; 473 | FRAMEWORK_SEARCH_PATHS = ( 474 | "$(SDKROOT)/Developer/Library/Frameworks", 475 | "$(inherited)", 476 | "$(DEVELOPER_FRAMEWORKS_DIR)", 477 | ); 478 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 479 | GCC_PREFIX_HEADER = "JYRefreshController/JYRefreshController-Prefix.pch"; 480 | GCC_PREPROCESSOR_DEFINITIONS = ( 481 | "DEBUG=1", 482 | "$(inherited)", 483 | ); 484 | INFOPLIST_FILE = "JYRefreshControllerTests/JYRefreshControllerTests-Info.plist"; 485 | PRODUCT_BUNDLE_IDENTIFIER = "com.jinyu.${PRODUCT_NAME:rfc1034identifier}"; 486 | PRODUCT_NAME = "$(TARGET_NAME)"; 487 | TEST_HOST = "$(BUNDLE_LOADER)"; 488 | WRAPPER_EXTENSION = xctest; 489 | }; 490 | name = Debug; 491 | }; 492 | E1FB5861194E8C550061511C /* Release */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/JYRefreshController.app/JYRefreshController"; 496 | FRAMEWORK_SEARCH_PATHS = ( 497 | "$(SDKROOT)/Developer/Library/Frameworks", 498 | "$(inherited)", 499 | "$(DEVELOPER_FRAMEWORKS_DIR)", 500 | ); 501 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 502 | GCC_PREFIX_HEADER = "JYRefreshController/JYRefreshController-Prefix.pch"; 503 | INFOPLIST_FILE = "JYRefreshControllerTests/JYRefreshControllerTests-Info.plist"; 504 | PRODUCT_BUNDLE_IDENTIFIER = "com.jinyu.${PRODUCT_NAME:rfc1034identifier}"; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | TEST_HOST = "$(BUNDLE_LOADER)"; 507 | WRAPPER_EXTENSION = xctest; 508 | }; 509 | name = Release; 510 | }; 511 | /* End XCBuildConfiguration section */ 512 | 513 | /* Begin XCConfigurationList section */ 514 | E1FB582B194E8C550061511C /* Build configuration list for PBXProject "JYRefreshController" */ = { 515 | isa = XCConfigurationList; 516 | buildConfigurations = ( 517 | E1FB585A194E8C550061511C /* Debug */, 518 | E1FB585B194E8C550061511C /* Release */, 519 | ); 520 | defaultConfigurationIsVisible = 0; 521 | defaultConfigurationName = Release; 522 | }; 523 | E1FB585C194E8C550061511C /* Build configuration list for PBXNativeTarget "JYRefreshController" */ = { 524 | isa = XCConfigurationList; 525 | buildConfigurations = ( 526 | E1FB585D194E8C550061511C /* Debug */, 527 | E1FB585E194E8C550061511C /* Release */, 528 | ); 529 | defaultConfigurationIsVisible = 0; 530 | defaultConfigurationName = Release; 531 | }; 532 | E1FB585F194E8C550061511C /* Build configuration list for PBXNativeTarget "JYRefreshControllerTests" */ = { 533 | isa = XCConfigurationList; 534 | buildConfigurations = ( 535 | E1FB5860194E8C550061511C /* Debug */, 536 | E1FB5861194E8C550061511C /* Release */, 537 | ); 538 | defaultConfigurationIsVisible = 0; 539 | defaultConfigurationName = Release; 540 | }; 541 | /* End XCConfigurationList section */ 542 | }; 543 | rootObject = E1FB5828194E8C550061511C /* Project object */; 544 | } 545 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/xcuserdata/alvin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/xcuserdata/alvin.xcuserdatad/xcschemes/JYRefreshController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /JYRefreshController.xcodeproj/xcuserdata/alvin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JYRefreshController.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E1FB582F194E8C550061511C 16 | 17 | primary 18 | 19 | 20 | E1FB584A194E8C550061511C 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /JYRefreshController/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /JYRefreshController/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "DemoViewController.h" 11 | 12 | @implementation AppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 17 | // Override point for customization after application launch. 18 | self.window.backgroundColor = [UIColor whiteColor]; 19 | 20 | DemoViewController *rootVC = [[DemoViewController alloc] initWithNibName:nil bundle:nil]; 21 | UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:rootVC]; 22 | 23 | self.window.rootViewController = navi; 24 | [self.window makeKeyAndVisible]; 25 | return YES; 26 | } 27 | 28 | - (void)applicationWillResignActive:(UIApplication *)application 29 | { 30 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 31 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 32 | } 33 | 34 | - (void)applicationDidEnterBackground:(UIApplication *)application 35 | { 36 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 37 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 38 | } 39 | 40 | - (void)applicationWillEnterForeground:(UIApplication *)application 41 | { 42 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 43 | } 44 | 45 | - (void)applicationDidBecomeActive:(UIApplication *)application 46 | { 47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | - (void)applicationWillTerminate:(UIApplication *)application 51 | { 52 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /JYRefreshController/DemoViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DemoViewController.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface DemoViewController : UIViewController 12 | 13 | @property (nonatomic, weak) IBOutlet UITableView *tableView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /JYRefreshController/DemoViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DemoViewController.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import "DemoViewController.h" 10 | #import "JYPullToRefreshController.h" 11 | #import "JYPullToLoadMoreController.h" 12 | 13 | @interface DemoViewController () 14 | 15 | @property (nonatomic, strong) NSMutableArray *dataSource; 16 | 17 | @property (nonatomic ,strong) JYPullToRefreshController *refreshController; 18 | @property (nonatomic ,strong) JYPullToLoadMoreController *loadMoreController; 19 | @end 20 | 21 | @implementation DemoViewController 22 | 23 | - (void)viewDidLoad 24 | { 25 | [super viewDidLoad]; 26 | [self configureDataSource]; 27 | [self configureScollView]; 28 | [self configureNavigationItems]; 29 | [self configureRefreshController]; 30 | } 31 | 32 | #pragma mark - Property 33 | - (NSMutableArray *)dataSource 34 | { 35 | if (!_dataSource) { 36 | _dataSource = [NSMutableArray array]; 37 | } 38 | return _dataSource; 39 | } 40 | 41 | #pragma mark - Configure 42 | - (void)configureScollView 43 | { 44 | self.tableView.delegate = self; 45 | self.tableView.dataSource = self; 46 | 47 | /** 48 | * // For testing custom contentInset to uncommon the below line 49 | * self.tableView.contentInset = UIEdgeInsetsMake(100, 0, 100, 0); 50 | */ 51 | } 52 | 53 | - (void)configureRefreshController 54 | { 55 | __weak typeof(self) weakSelf = self; 56 | self.refreshController = [[JYPullToRefreshController alloc] initWithScrollView:self.tableView]; 57 | self.refreshController.pullToRefreshHandleAction = ^{ 58 | [weakSelf insertRowAtTop]; 59 | }; 60 | self.refreshController.showRefreshControllerAboveContent = YES; 61 | 62 | self.loadMoreController = [[JYPullToLoadMoreController alloc] initWithScrollView:self.tableView]; 63 | self.loadMoreController.pullToLoadMoreHandleAction = ^{ 64 | [weakSelf insertRowAtBottom]; 65 | }; 66 | } 67 | 68 | - (void)configureNavigationItems 69 | { 70 | UIBarButtonItem *refreshItem = [[UIBarButtonItem alloc] initWithTitle:@"Refresh" 71 | style:UIBarButtonItemStylePlain 72 | target:self 73 | action:@selector(startRefresh:)]; 74 | 75 | 76 | self.navigationItem.leftBarButtonItem = refreshItem; 77 | 78 | UIBarButtonItem *loadMoreItem = [[UIBarButtonItem alloc] initWithTitle:@"LoadMore" 79 | style:UIBarButtonItemStylePlain 80 | target:self 81 | action:@selector(startLoadMore:)]; 82 | 83 | self.navigationItem.rightBarButtonItem = loadMoreItem; 84 | 85 | /** 86 | * // For testing hidden navigationBar to uncommon the below line 87 | * self.navigationController.navigationBarHidden = YES; 88 | */ 89 | } 90 | 91 | - (void)configureDataSource 92 | { 93 | for(int i = 0; i < 15; i++) { 94 | [self.dataSource addObject:[NSDate dateWithTimeIntervalSinceNow:-(i * 90)]]; 95 | } 96 | } 97 | 98 | #pragma mark - Refresh methods 99 | - (void)startRefresh:(id)sender 100 | { 101 | [self.refreshController triggerRefreshWithAnimated:YES]; 102 | } 103 | 104 | - (void)startLoadMore:(id)sender 105 | { 106 | 107 | [self.loadMoreController triggerLoadMoreWithAnimated:YES]; 108 | } 109 | 110 | #pragma mark - Actions 111 | - (void)insertRowAtTop 112 | { 113 | __weak typeof(self) weakSelf = self; 114 | 115 | int64_t delayInSeconds = 1.0; 116 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 117 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 118 | [weakSelf.tableView beginUpdates]; 119 | [weakSelf.dataSource insertObject:[NSDate date] atIndex:0]; 120 | [weakSelf.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:0]] 121 | withRowAnimation:UITableViewRowAnimationBottom]; 122 | [weakSelf.tableView endUpdates]; 123 | 124 | [weakSelf.refreshController stopRefreshWithAnimated:YES completion:NULL]; 125 | }); 126 | } 127 | 128 | 129 | - (void)insertRowAtBottom { 130 | __weak typeof(self) weakSelf = self; 131 | 132 | int64_t delayInSeconds = 1.0; 133 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 134 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 135 | [weakSelf.tableView beginUpdates]; 136 | [weakSelf.dataSource addObject:[weakSelf.dataSource.lastObject dateByAddingTimeInterval:-90]]; 137 | [weakSelf.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:weakSelf.dataSource.count - 1 inSection:0]] 138 | withRowAnimation:UITableViewRowAnimationTop]; 139 | [weakSelf.tableView endUpdates]; 140 | 141 | [weakSelf.loadMoreController stopLoadMoreCompletion:NULL]; 142 | }); 143 | } 144 | 145 | #pragma mark - UITableViewDataSource 146 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 147 | return 1; 148 | } 149 | 150 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 151 | return self.dataSource.count; 152 | } 153 | 154 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 155 | static NSString *identifier = @"Cell"; 156 | UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; 157 | 158 | if (cell == nil) 159 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; 160 | 161 | NSDate *date = [self.dataSource objectAtIndex:indexPath.row]; 162 | cell.textLabel.text = [NSDateFormatter localizedStringFromDate:date dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterMediumStyle]; 163 | return cell; 164 | } 165 | 166 | @end 167 | -------------------------------------------------------------------------------- /JYRefreshController/DemoViewController.xib: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /JYRefreshController/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /JYRefreshController/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /JYRefreshController/JYRefreshController-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | UILaunchStoryboardName 28 | Launch Screen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /JYRefreshController/JYRefreshController-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_3_0 10 | #warning "This project uses features only available in iOS SDK 3.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /JYRefreshController/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYPullToLoadMoreController.h: -------------------------------------------------------------------------------- 1 | // 2 | // JYPullToLoadMoreController.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14/11/17. 6 | // 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, JYLoadMoreState) 12 | { 13 | JYLoadMoreStateStop = 0, 14 | JYLoadMoreStateTrigger = 1, 15 | JYLoadMoreStateLoading = 2, 16 | }; 17 | 18 | typedef NS_ENUM(NSUInteger, JYLoadMoreDirection) 19 | { 20 | JYLoadMoreDirectionBottom = 0, 21 | JYLoadMoreDirectionRight = 1, 22 | }; 23 | 24 | @protocol JYRefreshView; 25 | 26 | @interface JYPullToLoadMoreController : NSObject 27 | 28 | @property (nonatomic, readonly, strong) UIScrollView *scrollView; 29 | 30 | //自定义手势操作刷新动画 31 | @property (nonatomic, strong) UIPanGestureRecognizer *panGesture; 32 | 33 | @property (nonatomic, assign) BOOL enable; 34 | 35 | - (void)setEnable:(BOOL)enable withAnimation:(BOOL)animated; 36 | 37 | @property (nonatomic, readonly, assign) JYLoadMoreDirection direction; 38 | 39 | /** 40 | * Set to NO, if need user dragging to trigger load more action. Default is YES. 41 | */ 42 | @property (nonatomic, assign) BOOL autoLoadMore; 43 | 44 | /** 45 | * 设置为 YES 表示把 `customView` 放在 `scrollView` 内容部分的下方;否则表示把 46 | * `customView` 放在 `scrollView.contentInset.bottom` 的下方。 47 | */ 48 | @property (nonatomic, assign) BOOL showRefreshControllerBelowContent; 49 | 50 | /** 51 | * Set YES to make refresh view attach to the scrollView edge, default is NO 52 | */ 53 | @property (nonatomic, assign) BOOL attachedEdge; 54 | 55 | @property (nonatomic, readonly, assign) JYLoadMoreState loadMoreState; 56 | 57 | @property (nonatomic, copy) void(^pullToLoadMoreHandleAction)(void); 58 | 59 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView; 60 | 61 | /** 62 | * Set dragging direction to trigger load more action. Default is JYLoadMoreDirectionBottom. 63 | */ 64 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView direction:(JYLoadMoreDirection)direction; 65 | 66 | - (void)triggerLoadMoreWithAnimated:(BOOL)animated; 67 | 68 | - (void)stopLoadMoreCompletion:(void(^)(void))completion; 69 | 70 | - (void)setCustomView:(UIView *)customView; 71 | 72 | @end 73 | 74 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYPullToLoadMoreController.m: -------------------------------------------------------------------------------- 1 | // 2 | // JYPullToLoadMoreController.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14/11/17. 6 | // 7 | // 8 | 9 | #import "JYPullToLoadMoreController.h" 10 | #import "JYRefreshView.h" 11 | 12 | #define JYLoadMoreViewDefaultHeight 44.0f 13 | #define JYLoadMoreViewAnimationDuration 0.3f 14 | 15 | @interface JYPullToLoadMoreController () 16 | 17 | @property (nonatomic, readwrite, strong) UIScrollView *scrollView; 18 | 19 | @property (nonatomic, readwrite, assign) CGFloat originalContentInset; 20 | 21 | @property (nonatomic, readwrite, strong) UIView *loadMoreView; 22 | 23 | @property (nonatomic, readwrite, assign) JYLoadMoreState loadMoreState; 24 | 25 | 26 | - (void)layoutLoadMoreView; 27 | 28 | - (UIView *)defalutRefreshView; 29 | 30 | - (void)checkOffsetsWithChange:(CGPoint)change; 31 | 32 | @end 33 | 34 | @implementation JYPullToLoadMoreController 35 | @synthesize panGesture = _panGesture; 36 | @synthesize loadMoreView = _loadMoreView; 37 | 38 | #pragma mark - life cycle 39 | 40 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView direction:(JYLoadMoreDirection)direction 41 | { 42 | self = [super init]; 43 | if (self) { 44 | _scrollView = scrollView; 45 | _autoLoadMore = YES; 46 | _direction = direction; 47 | CGFloat inset = 0; 48 | if (direction == JYLoadMoreDirectionBottom) { 49 | inset = scrollView.contentInset.bottom; 50 | } else if (direction == JYLoadMoreDirectionRight) { 51 | inset = scrollView.contentInset.right; 52 | } 53 | _originalContentInset = inset; 54 | [_scrollView addObserver:self 55 | forKeyPath:@"contentOffset" 56 | options:NSKeyValueObservingOptionNew 57 | context:NULL]; 58 | [_scrollView addObserver:self 59 | forKeyPath:@"contentSize" 60 | options:NSKeyValueObservingOptionNew 61 | context:NULL]; 62 | [self.panGesture addObserver:self 63 | forKeyPath:@"state" 64 | options:NSKeyValueObservingOptionNew 65 | context:NULL]; 66 | 67 | [self setCustomView:[self defalutRefreshView]]; 68 | [self setEnable:YES withAnimation:NO]; 69 | } 70 | return self; 71 | } 72 | 73 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView 74 | { 75 | return [self initWithScrollView:scrollView direction:JYLoadMoreDirectionBottom]; 76 | } 77 | 78 | - (void)dealloc 79 | { 80 | [self.scrollView removeObserver:self forKeyPath:@"contentOffset"]; 81 | [self.scrollView removeObserver:self forKeyPath:@"contentSize"]; 82 | [self.panGesture removeObserver:self forKeyPath:@"state"]; 83 | } 84 | 85 | #pragma mark- Property 86 | - (void)setLoadMoreState:(JYLoadMoreState)loadMoreState 87 | { 88 | _loadMoreState = loadMoreState; 89 | if ([self.loadMoreView respondsToSelector:@selector(pullToLoadMoreController:didChangeToState:)]) { 90 | [self.loadMoreView pullToLoadMoreController:self didChangeToState:loadMoreState]; 91 | } 92 | } 93 | 94 | - (void)setEnable:(BOOL)enable 95 | { 96 | [self setEnable:enable withAnimation:YES]; 97 | } 98 | 99 | - (void)setEnable:(BOOL)enable withAnimation:(BOOL)animated 100 | { 101 | if (_enable == enable) { // no change 102 | return; 103 | } 104 | _enable = enable; 105 | if ([self.loadMoreView respondsToSelector:@selector(pullToLoadMoreController:didSetEnable:)]) { 106 | [self.loadMoreView pullToLoadMoreController:self didSetEnable:enable]; 107 | } 108 | [self layoutLoadMoreView]; 109 | 110 | UIEdgeInsets contentInset = [self initalContentInset]; 111 | 112 | if (animated) { 113 | [UIView animateWithDuration:JYLoadMoreViewAnimationDuration 114 | delay:0 115 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 116 | animations:^{ 117 | self.scrollView.contentInset = contentInset; 118 | } 119 | completion:NULL]; 120 | } else { 121 | self.scrollView.contentInset = contentInset; 122 | } 123 | } 124 | 125 | - (void)setAutoLoadMore:(BOOL)autoLoadMore 126 | { 127 | if (_autoLoadMore == autoLoadMore) { 128 | return; 129 | } 130 | _autoLoadMore = autoLoadMore; 131 | UIEdgeInsets contentInset = [self initalContentInset]; 132 | self.scrollView.contentInset = contentInset; 133 | } 134 | 135 | #pragma mark - Action 136 | - (void)triggerLoadMoreWithAnimated:(BOOL)animated 137 | { 138 | if (!self.enable || self.loadMoreState == JYLoadMoreStateLoading) { 139 | return; 140 | } 141 | 142 | if ([self.loadMoreView respondsToSelector:@selector(pullToLoadMoreController:didShowhLoadMoreViewPercentage:)]){ 143 | [self.loadMoreView pullToLoadMoreController:self didShowhLoadMoreViewPercentage:1.0]; 144 | } 145 | self.loadMoreState = JYLoadMoreStateLoading; 146 | CGPoint contentOffset = [self triggeredContentOffset]; 147 | 148 | NSTimeInterval duration = animated ? JYLoadMoreViewAnimationDuration : 0.0f; 149 | [UIView animateWithDuration:duration 150 | delay:0 151 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 152 | animations:^{ 153 | self.scrollView.contentOffset = contentOffset; 154 | if (!self.autoLoadMore) { 155 | UIEdgeInsets contentInset = [self adjustedContentInset]; 156 | self.scrollView.contentInset = contentInset; 157 | } 158 | } completion:^(BOOL finished) { 159 | if (self.pullToLoadMoreHandleAction) { 160 | self.pullToLoadMoreHandleAction(); 161 | } 162 | }]; 163 | } 164 | 165 | - (void)stopLoadMoreCompletion:(void(^)(void))completion 166 | { 167 | if (!self.enable || self.loadMoreState == JYLoadMoreStateStop) { 168 | return; 169 | } 170 | self.loadMoreState = JYLoadMoreStateStop; 171 | 172 | if (self.autoLoadMore) { 173 | if (completion) { 174 | completion(); 175 | } 176 | } 177 | else { 178 | UIEdgeInsets contentInset = [self adjustedContentInset]; 179 | [UIView animateWithDuration:JYLoadMoreViewAnimationDuration 180 | delay:0 181 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 182 | animations:^{ 183 | self.scrollView.contentInset = contentInset; 184 | } completion:^(BOOL finished) { 185 | if (finished) { 186 | if (completion) { 187 | completion(); 188 | } 189 | } 190 | }]; 191 | } 192 | } 193 | 194 | - (void)setCustomView:(UIView *)customView 195 | { 196 | if (_loadMoreView.superview) { 197 | [_loadMoreView removeFromSuperview]; 198 | } 199 | _loadMoreView = customView; 200 | [self.scrollView addSubview:_loadMoreView]; 201 | [self layoutLoadMoreView]; 202 | } 203 | 204 | #pragma mark - KVO 205 | - (void)observeValueForKeyPath:(NSString *)keyPath 206 | ofObject:(id)object 207 | change:(NSDictionary *)change 208 | context:(void *)context 209 | { 210 | if ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"state"]) { 211 | CGPoint contentOffset = self.scrollView.contentOffset; 212 | [self checkOffsetsWithChange:contentOffset]; 213 | if (self.attachedEdge) { 214 | [self layoutLoadMoreView]; 215 | } 216 | } else if ([keyPath isEqualToString:@"contentSize"]) { 217 | [self layoutLoadMoreView]; 218 | } 219 | } 220 | 221 | #pragma mark - Private Methods 222 | - (void)checkOffsetsWithChange:(CGPoint)change { 223 | if (!self.enable) { 224 | return; 225 | } 226 | CGPoint contentOffset = change; 227 | BOOL isTriggered = NO; 228 | CGFloat threshold = 0; 229 | CGFloat checkOffset = 0; 230 | CGFloat refreshViewOffset = 0; 231 | if (_direction == JYLoadMoreDirectionBottom) { 232 | refreshViewOffset = self.loadMoreView.frame.size.height; 233 | threshold = self.scrollView.contentSize.height 234 | + self.scrollView.contentInset.bottom 235 | - self.scrollView.bounds.size.height; 236 | checkOffset = contentOffset.y; 237 | 238 | } else if (_direction == JYLoadMoreDirectionRight) { 239 | refreshViewOffset = self.loadMoreView.frame.size.width; 240 | threshold = self.scrollView.contentSize.width 241 | + self.scrollView.contentInset.right 242 | - self.scrollView.bounds.size.width; 243 | checkOffset = contentOffset.x; 244 | } 245 | if (!self.autoLoadMore) { 246 | threshold += refreshViewOffset; 247 | } 248 | 249 | isTriggered = checkOffset >= threshold; 250 | CGFloat refreshViewVisibleOffset = checkOffset - threshold + refreshViewOffset; 251 | 252 | if ([self.loadMoreView respondsToSelector:@selector(pullToLoadMoreController:didShowhLoadMoreViewPercentage:)] 253 | && self.loadMoreState == JYLoadMoreStateStop) { 254 | CGFloat percentage = refreshViewVisibleOffset / refreshViewOffset; 255 | percentage = percentage <= 0 ? 0 : percentage; 256 | percentage = percentage >= 1 ? 1 : percentage; 257 | [self.loadMoreView pullToLoadMoreController:self didShowhLoadMoreViewPercentage:percentage]; 258 | } 259 | 260 | if ([self.loadMoreView respondsToSelector:@selector(pullToLoadMoreController:didScrolllVisableOffset:)] 261 | ) { 262 | [self.loadMoreView pullToLoadMoreController:self didScrolllVisableOffset:refreshViewVisibleOffset]; 263 | } 264 | 265 | if (self.autoLoadMore) { 266 | if (isTriggered && self.loadMoreState == JYLoadMoreStateStop) { 267 | self.loadMoreState = JYLoadMoreStateTrigger; 268 | } else if (!isTriggered && self.loadMoreState == JYLoadMoreStateTrigger) { 269 | self.loadMoreState = JYLoadMoreStateStop; 270 | } 271 | if (!self.scrollView.dragging && !self.scrollView.tracking) { 272 | if (self.loadMoreState == JYLoadMoreStateTrigger) { 273 | self.loadMoreState = JYLoadMoreStateLoading; 274 | CGPoint contentOffset = [self triggeredContentOffset]; 275 | [UIView animateWithDuration:JYLoadMoreViewAnimationDuration 276 | delay:0 277 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 278 | animations:^{ 279 | self.scrollView.contentOffset = contentOffset; 280 | } completion:^(BOOL finished) { 281 | if (self.pullToLoadMoreHandleAction) { 282 | self.pullToLoadMoreHandleAction(); 283 | } 284 | }]; 285 | } 286 | } 287 | } else { 288 | if (self.panGesture.state == UIGestureRecognizerStateBegan 289 | || self.panGesture.state == UIGestureRecognizerStateChanged) { 290 | if (isTriggered && self.loadMoreState == JYLoadMoreStateStop) { 291 | self.loadMoreState = JYLoadMoreStateTrigger; 292 | } else if (!isTriggered && self.loadMoreState == JYLoadMoreStateTrigger) { 293 | self.loadMoreState = JYLoadMoreStateStop; 294 | } 295 | } 296 | else { 297 | if (self.loadMoreState == JYLoadMoreStateTrigger) { 298 | self.loadMoreState = JYLoadMoreStateLoading; 299 | UIEdgeInsets contentInset = [self adjustedContentInset]; 300 | [UIView animateWithDuration:JYLoadMoreViewAnimationDuration 301 | delay:0 302 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 303 | animations:^{ 304 | self.scrollView.contentInset = contentInset; 305 | } completion:^(BOOL finished) { 306 | if (self.pullToLoadMoreHandleAction) { 307 | self.pullToLoadMoreHandleAction(); 308 | } 309 | }]; 310 | } 311 | } 312 | } 313 | } 314 | 315 | - (UIView *)defalutRefreshView 316 | { 317 | CGRect frame = CGRectZero; 318 | if (_direction == JYLoadMoreDirectionBottom) { 319 | frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollView.bounds), JYLoadMoreViewDefaultHeight); 320 | } else if (_direction == JYLoadMoreDirectionRight) { 321 | frame = CGRectMake(0, 0, JYLoadMoreViewDefaultHeight, CGRectGetHeight(self.scrollView.bounds)); 322 | } 323 | JYRefreshView *refreshView = [[JYRefreshView alloc] initWithFrame:frame]; 324 | refreshView.autoresizingMask = _direction == JYLoadMoreDirectionBottom ? UIViewAutoresizingFlexibleWidth : UIViewAutoresizingFlexibleHeight; 325 | return refreshView; 326 | } 327 | 328 | - (void)layoutLoadMoreView 329 | { 330 | if (self.enable) { 331 | [self.loadMoreView setHidden:NO]; 332 | 333 | CGRect frame = self.loadMoreView.frame; 334 | 335 | CGFloat offset = 0; 336 | if (_direction == JYLoadMoreDirectionBottom) { 337 | offset = self.scrollView.contentSize.height; 338 | if (self.attachedEdge) { 339 | offset += MAX(0, self.scrollView.contentOffset.y + self.scrollView.frame.size.height - self.scrollView.contentSize.height); 340 | offset -= self.loadMoreView.frame.size.height; 341 | } 342 | if (!self.showRefreshControllerBelowContent) { 343 | offset += self.originalContentInset; 344 | } 345 | frame.origin.y = offset; 346 | } else if (_direction == JYLoadMoreDirectionRight) { 347 | offset = self.scrollView.contentSize.width; 348 | if (self.attachedEdge) { 349 | offset += MAX(0, self.scrollView.contentOffset.x + self.scrollView.frame.size.width - self.scrollView.contentSize.width); 350 | offset -= self.loadMoreView.frame.size.width; 351 | } 352 | if (!self.showRefreshControllerBelowContent) { 353 | offset += self.originalContentInset; 354 | } 355 | frame.origin.x = offset; 356 | } 357 | 358 | self.loadMoreView.frame = frame; 359 | } else { 360 | [self.loadMoreView setHidden:YES]; 361 | } 362 | } 363 | 364 | - (UIEdgeInsets)initalContentInset 365 | { 366 | UIEdgeInsets contentInset = self.scrollView.contentInset; 367 | if (_enable && _autoLoadMore) { 368 | if (_direction == JYLoadMoreDirectionBottom) { 369 | contentInset.bottom += self.loadMoreView.frame.size.height; 370 | } else if (_direction == JYLoadMoreDirectionRight) { 371 | contentInset.right += self.loadMoreView.frame.size.width; 372 | } 373 | } else { 374 | if (_direction == JYLoadMoreDirectionBottom) { 375 | contentInset.bottom = self.originalContentInset; 376 | } else if (_direction == JYLoadMoreDirectionRight) { 377 | contentInset.right = self.originalContentInset; 378 | } 379 | } 380 | return contentInset; 381 | } 382 | 383 | - (UIEdgeInsets)adjustedContentInset 384 | { 385 | UIEdgeInsets contentInset = self.scrollView.contentInset; 386 | if (self.loadMoreState == JYLoadMoreStateStop) { 387 | if (_direction == JYLoadMoreDirectionBottom) { 388 | contentInset.bottom -= self.loadMoreView.frame.size.height; 389 | } else if (_direction == JYLoadMoreDirectionRight) { 390 | contentInset.right -= self.loadMoreView.frame.size.width; 391 | } 392 | } else { 393 | if (_direction == JYLoadMoreDirectionBottom) { 394 | contentInset.bottom += self.loadMoreView.frame.size.height; 395 | } else if (_direction == JYLoadMoreDirectionRight) { 396 | contentInset.right += self.loadMoreView.frame.size.width; 397 | } 398 | } 399 | return contentInset; 400 | } 401 | 402 | - (CGFloat)refreshViewOffset 403 | { 404 | CGFloat offset = 0; 405 | if (_direction == JYLoadMoreDirectionBottom) { 406 | offset = self.loadMoreView.frame.size.height; 407 | } else if (_direction == JYLoadMoreDirectionRight) { 408 | offset = self.loadMoreView.frame.size.width; 409 | } 410 | return offset; 411 | } 412 | 413 | - (CGPoint)triggeredContentOffset 414 | { 415 | CGFloat offset = 0; 416 | CGPoint contentOffset = CGPointZero; 417 | if (_direction == JYLoadMoreDirectionBottom) { 418 | offset = self.loadMoreView.frame.size.height; 419 | offset = self.scrollView.contentSize.height 420 | - self.scrollView.bounds.size.height 421 | + offset; 422 | contentOffset.y = offset; 423 | } else if (_direction == JYLoadMoreDirectionRight) { 424 | offset = self.loadMoreView.frame.size.width; 425 | offset = self.scrollView.contentSize.width 426 | - self.scrollView.bounds.size.width 427 | + offset; 428 | contentOffset.x = offset; 429 | } 430 | return contentOffset; 431 | } 432 | 433 | - (UIPanGestureRecognizer *)panGesture 434 | { 435 | if (!_panGesture) { 436 | _panGesture = self.scrollView.panGestureRecognizer; 437 | } 438 | return _panGesture; 439 | } 440 | 441 | - (void)setPanGesture:(UIPanGestureRecognizer *)panGesture 442 | { 443 | if (self.panGesture) { 444 | [self.panGesture removeObserver:self forKeyPath:@"state"]; 445 | } 446 | _panGesture = panGesture; 447 | [self.panGesture addObserver:self 448 | forKeyPath:@"state" 449 | options:NSKeyValueObservingOptionNew 450 | context:NULL]; 451 | } 452 | 453 | @end 454 | 455 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYPullToRefreshController.h: -------------------------------------------------------------------------------- 1 | // 2 | // JYPullToRefreshController.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14/11/13. 6 | // 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, JYRefreshState) 12 | { 13 | JYRefreshStateStop = 0, 14 | JYRefreshStateTrigger = 1, 15 | JYRefreshStateLoading = 2, 16 | }; 17 | 18 | typedef NS_ENUM(NSUInteger, JYRefreshDirection) 19 | { 20 | JYRefreshDirectionTop = 0, 21 | JYRefreshDirectionLeft = 1, 22 | }; 23 | 24 | @protocol JYRefreshView; 25 | 26 | @interface JYPullToRefreshController : NSObject 27 | 28 | @property (nonatomic, readonly, strong) UIScrollView *scrollView; 29 | //自定义手势操作刷新动画 30 | @property (nonatomic, strong) UIPanGestureRecognizer *panGesture; 31 | 32 | @property (nonatomic, assign) BOOL enable; 33 | 34 | @property (nonatomic, readonly, assign) JYRefreshState refreshState; 35 | 36 | @property (nonatomic, readonly, assign) JYRefreshDirection direction; 37 | 38 | @property (nonatomic, copy) void(^pullToRefreshHandleAction)(void); 39 | 40 | // 如果为 YES,则会紧贴 scrollView 正文的上部,否则会在 contentInsets 的上部显示 41 | @property (nonatomic, assign) BOOL showRefreshControllerAboveContent; 42 | 43 | /** 44 | * Set YES to make refresh view attach to the scrollView edge, default is NO 45 | */ 46 | @property (nonatomic, assign) BOOL attachedEdge; 47 | 48 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView; 49 | 50 | /** 51 | * Set dragging direction to trigger refresh action. Default is JYRefreshDirectionTop. 52 | */ 53 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView direction:(JYRefreshDirection)direction; 54 | 55 | - (void)triggerRefreshWithAnimated:(BOOL)animated; 56 | 57 | - (void)stopRefreshWithAnimated:(BOOL)animated completion:(void(^)(BOOL finished))completion; 58 | 59 | - (void)setCustomView:(UIView *)customView; 60 | 61 | @end 62 | 63 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYPullToRefreshController.m: -------------------------------------------------------------------------------- 1 | // 2 | // JYPullToRefreshController.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14/11/13. 6 | // 7 | // 8 | 9 | #import "JYPullToRefreshController.h" 10 | #import "JYRefreshView.h" 11 | 12 | #define JYRefreshViewDefaultHeight 44.0f 13 | #define JYRefreshViewAnimationDuration 0.3f 14 | 15 | @interface JYPullToRefreshController () 16 | 17 | @property (nonatomic, readwrite, strong) UIScrollView *scrollView; 18 | 19 | @property (nonatomic, readwrite, assign) CGFloat originalContentInset; 20 | 21 | @property (nonatomic, readwrite, strong) UIView *refreshView; 22 | 23 | @property (nonatomic, readwrite, assign) JYRefreshState refreshState; 24 | 25 | - (void)layoutRefreshView; 26 | 27 | - (UIView *)defalutRefreshView; 28 | 29 | - (void)checkOffsetsWithChange:(CGPoint)change; 30 | 31 | @end 32 | 33 | @implementation JYPullToRefreshController 34 | @synthesize refreshView = _refreshView; 35 | @synthesize panGesture = _panGesture; 36 | 37 | #pragma mark - life cycle 38 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView direction:(JYRefreshDirection)direction 39 | { 40 | self = [super init]; 41 | if (self) { 42 | _scrollView = scrollView; 43 | _enable = YES; 44 | 45 | if (direction == JYRefreshDirectionTop) { 46 | _originalContentInset = scrollView.contentInset.top; 47 | } else if (direction == JYRefreshDirectionLeft) { 48 | _originalContentInset = scrollView.contentInset.left; 49 | } 50 | _direction = direction; 51 | [_scrollView addObserver:self 52 | forKeyPath:@"contentOffset" 53 | options:NSKeyValueObservingOptionNew 54 | context:NULL]; 55 | [_scrollView addObserver:self 56 | forKeyPath:@"contentInset" 57 | options:NSKeyValueObservingOptionNew 58 | context:NULL]; 59 | [self.panGesture addObserver:self 60 | forKeyPath:@"state" 61 | options:NSKeyValueObservingOptionNew 62 | context:NULL]; 63 | 64 | [self setCustomView:[self defalutRefreshView]]; 65 | } 66 | return self; 67 | } 68 | 69 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView 70 | { 71 | return [self initWithScrollView:scrollView direction:JYRefreshDirectionTop]; 72 | } 73 | 74 | - (void)dealloc 75 | { 76 | [self.scrollView removeObserver:self forKeyPath:@"contentOffset"]; 77 | [self.scrollView removeObserver:self forKeyPath:@"contentInset"]; 78 | [self.panGesture removeObserver:self forKeyPath:@"state"]; 79 | } 80 | 81 | #pragma mark- Property 82 | - (void)setRefreshState:(JYRefreshState)refreshState 83 | { 84 | _refreshState = refreshState; 85 | if ([self.refreshView respondsToSelector:@selector(pullToRefreshController:didChangeToState:)]) { 86 | [self.refreshView pullToRefreshController:self didChangeToState:refreshState]; 87 | } 88 | } 89 | 90 | - (void)setEnable:(BOOL)enable 91 | { 92 | if (_enable == enable) { // no change 93 | return; 94 | } 95 | 96 | // stop refreshing if disabled. 97 | if (!enable && _refreshState != JYRefreshStateStop) { 98 | [self stopRefreshWithAnimated:NO completion:nil]; 99 | } 100 | 101 | _enable = enable; 102 | if ([self.refreshView respondsToSelector:@selector(pullToRefreshController:didSetEnable:)]) { 103 | [self.refreshView pullToRefreshController:self didSetEnable:enable]; 104 | } 105 | [self layoutRefreshView]; 106 | } 107 | 108 | #pragma mark - Action 109 | - (void)triggerRefreshWithAnimated:(BOOL)animated 110 | { 111 | if (!self.enable || self.refreshState == JYRefreshStateLoading) { 112 | return; 113 | } 114 | self.refreshState = JYRefreshStateLoading; 115 | UIEdgeInsets contentInset = [self adjustedContentInset]; 116 | CGPoint contentOffset = [self triggeredContentOffset]; 117 | if ([self.refreshView respondsToSelector:@selector(pullToRefreshController:didShowRefreshViewPercentage:)]) { 118 | [self.refreshView pullToRefreshController:self didShowRefreshViewPercentage:1.0]; 119 | } 120 | 121 | NSTimeInterval duration = animated ? JYRefreshViewAnimationDuration : 0.0f; 122 | [UIView animateWithDuration:duration 123 | delay:0 124 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 125 | animations:^{ 126 | self.scrollView.contentInset = contentInset; 127 | self.scrollView.contentOffset = contentOffset; 128 | 129 | } completion:^(BOOL finished) { 130 | if (self.pullToRefreshHandleAction) { 131 | self.pullToRefreshHandleAction(); 132 | } 133 | }]; 134 | } 135 | 136 | - (void)stopRefreshWithAnimated:(BOOL)animated completion:(void(^)(BOOL finished))completion 137 | { 138 | if (!self.enable || self.refreshState == JYRefreshStateStop) { 139 | return; 140 | } 141 | self.refreshState = JYRefreshStateStop; 142 | UIEdgeInsets contentInset = [self adjustedContentInset]; 143 | 144 | NSTimeInterval duration = animated ? JYRefreshViewAnimationDuration : 0.0f; 145 | [UIView animateWithDuration:duration 146 | delay:0 147 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 148 | animations:^{ 149 | self.scrollView.contentInset = contentInset; 150 | 151 | } completion:^(BOOL finished) { 152 | if (completion) { 153 | completion(finished); 154 | } 155 | }]; 156 | } 157 | 158 | - (void)setCustomView:(UIView *)customView 159 | { 160 | if (_refreshView.superview) { 161 | [_refreshView removeFromSuperview]; 162 | } 163 | _refreshView = customView; 164 | [self.scrollView addSubview:_refreshView]; 165 | [self layoutRefreshView]; 166 | } 167 | 168 | #pragma mark - KVO 169 | - (void)observeValueForKeyPath:(NSString *)keyPath 170 | ofObject:(id)object 171 | change:(NSDictionary *)change 172 | context:(void *)context 173 | { 174 | if ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"state"]) { 175 | CGPoint contentOffset = self.scrollView.contentOffset; 176 | [self checkOffsetsWithChange:contentOffset]; 177 | if (self.attachedEdge) { 178 | [self layoutRefreshView]; 179 | } 180 | } 181 | else if ([keyPath isEqualToString:@"contentInset"]) { 182 | UIEdgeInsets insets = [[change objectForKey:NSKeyValueChangeNewKey] UIEdgeInsetsValue]; 183 | if (_originalContentInset != insets.top) { 184 | _originalContentInset = insets.top; 185 | } 186 | [self layoutRefreshView]; 187 | } 188 | } 189 | 190 | #pragma mark - Private Methods 191 | - (void)checkOffsetsWithChange:(CGPoint)change { 192 | if (!self.enable) { 193 | return; 194 | } 195 | CGPoint contentOffset = change; 196 | BOOL isTriggered = NO; 197 | UIEdgeInsets contentInset = self.scrollView.contentInset; 198 | CGFloat refreshViewOffset = 0; 199 | CGFloat threshold = 0; 200 | CGFloat checkOffset = 0; 201 | 202 | if (_direction == JYRefreshDirectionTop) { 203 | refreshViewOffset = self.refreshView.frame.size.height; 204 | threshold = -contentInset.top - refreshViewOffset; 205 | checkOffset = contentOffset.y; 206 | } else if (_direction == JYRefreshDirectionLeft) { 207 | refreshViewOffset = self.refreshView.frame.size.width; 208 | threshold = -contentInset.left - refreshViewOffset; 209 | checkOffset = contentOffset.x; 210 | } 211 | 212 | isTriggered = checkOffset <= threshold; 213 | 214 | CGFloat refreshViewVisibleOffset = 0; 215 | if (_direction == JYRefreshDirectionTop) { 216 | refreshViewVisibleOffset = -checkOffset - contentInset.top; 217 | } else if (_direction == JYRefreshDirectionLeft) { 218 | refreshViewVisibleOffset = -checkOffset - contentInset.left; 219 | } 220 | if ([self.refreshView respondsToSelector:@selector(pullToRefreshController:didShowRefreshViewPercentage:)] 221 | && self.refreshState == JYRefreshStateStop) { 222 | CGFloat percentage = refreshViewVisibleOffset / refreshViewOffset; 223 | percentage = percentage <= 0 ? 0 : percentage; 224 | percentage = percentage >= 1 ? 1 : percentage; 225 | [self.refreshView pullToRefreshController:self didShowRefreshViewPercentage:percentage]; 226 | } 227 | 228 | if ([self.refreshView respondsToSelector:@selector(pullToRefreshController:didScrolllVisableOffset:)] 229 | ) { 230 | [self.refreshView pullToRefreshController:self didScrolllVisableOffset:refreshViewVisibleOffset]; 231 | } 232 | 233 | if (self.panGesture.state == UIGestureRecognizerStateBegan 234 | || self.panGesture.state == UIGestureRecognizerStateChanged) { 235 | if (isTriggered && self.refreshState == JYRefreshStateStop) { 236 | self.refreshState = JYRefreshStateTrigger; 237 | } else if (!isTriggered && self.refreshState == JYRefreshStateTrigger) { 238 | self.refreshState = JYRefreshStateStop; 239 | } 240 | } 241 | else if (self.panGesture.state == UIGestureRecognizerStateEnded) { 242 | if (self.refreshState == JYRefreshStateTrigger) { 243 | self.refreshState = JYRefreshStateLoading; 244 | UIEdgeInsets contentInset = [self adjustedContentInset]; 245 | [UIView animateWithDuration:JYRefreshViewAnimationDuration 246 | delay:0 247 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState 248 | animations:^{ 249 | self.scrollView.contentInset = contentInset; 250 | } completion:^(BOOL finished) { 251 | if (self.pullToRefreshHandleAction && finished) { 252 | self.pullToRefreshHandleAction(); 253 | } 254 | }]; 255 | } 256 | } 257 | } 258 | 259 | - (UIView *)defalutRefreshView 260 | { 261 | CGRect frame = CGRectZero; 262 | if (_direction == JYRefreshDirectionTop) { 263 | frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollView.bounds), JYRefreshViewDefaultHeight); 264 | } else if (_direction == JYRefreshDirectionLeft) { 265 | frame = CGRectMake(0, 0, JYRefreshViewDefaultHeight, CGRectGetHeight(self.scrollView.bounds)); 266 | } 267 | 268 | JYRefreshView *refreshView = [[JYRefreshView alloc] initWithFrame:frame]; 269 | refreshView.autoresizingMask = _direction == JYRefreshDirectionTop ? UIViewAutoresizingFlexibleWidth : UIViewAutoresizingFlexibleHeight; 270 | return refreshView; 271 | } 272 | 273 | - (void)layoutRefreshView 274 | { 275 | 276 | if (self.enable) { 277 | [self.refreshView setHidden:NO]; 278 | CGFloat offset = 0.0; 279 | CGRect frame = self.refreshView.frame; 280 | 281 | if (_direction == JYRefreshDirectionTop) { 282 | if (self.showRefreshControllerAboveContent) { 283 | offset = -CGRectGetHeight(self.refreshView.frame); 284 | } else { 285 | offset = -CGRectGetHeight(self.refreshView.frame) - self.originalContentInset; 286 | } 287 | offset += self.scrollView.contentInset.top; 288 | if (self.attachedEdge) { 289 | offset += MIN(0, self.scrollView.contentOffset.y + frame.size.height); 290 | } 291 | 292 | frame.origin.y = offset; 293 | } else if (_direction == JYRefreshDirectionLeft) { 294 | if (self.showRefreshControllerAboveContent) { 295 | offset = -CGRectGetWidth(self.refreshView.frame); 296 | } else { 297 | offset = -CGRectGetWidth(self.refreshView.frame) - self.originalContentInset; 298 | } 299 | if (self.attachedEdge) { 300 | offset += MIN(0, self.scrollView.contentOffset.y + frame.size.height); 301 | } 302 | frame.origin.x = offset; 303 | } 304 | self.refreshView.frame = frame; 305 | } else { 306 | [self.refreshView setHidden:YES]; 307 | } 308 | } 309 | 310 | - (UIEdgeInsets)adjustedContentInset 311 | { 312 | UIEdgeInsets contentInset = self.scrollView.contentInset; 313 | CGFloat refreshingOffset = 0; 314 | 315 | if (self.refreshState == JYRefreshStateStop) { 316 | if (_direction == JYRefreshDirectionTop) { 317 | refreshingOffset = self.refreshView.frame.size.height; 318 | contentInset.top -= refreshingOffset; 319 | } else if (_direction == JYRefreshDirectionLeft) { 320 | refreshingOffset = self.refreshView.frame.size.width; 321 | contentInset.left -= refreshingOffset; 322 | } 323 | } else { 324 | if (_direction == JYRefreshDirectionTop) { 325 | refreshingOffset = self.refreshView.frame.size.height; 326 | contentInset.top += refreshingOffset; 327 | } else if (_direction == JYRefreshDirectionLeft) { 328 | refreshingOffset = self.refreshView.frame.size.width; 329 | contentInset.left += refreshingOffset; 330 | } 331 | } 332 | return contentInset; 333 | } 334 | 335 | - (CGPoint)triggeredContentOffset 336 | { 337 | CGPoint contentOffset = CGPointZero; 338 | UIEdgeInsets contentInset = [self adjustedContentInset]; 339 | if (_direction == JYRefreshDirectionTop) { 340 | contentOffset = CGPointMake(0, -contentInset.top); 341 | } else if (_direction == JYRefreshDirectionLeft) { 342 | contentOffset = CGPointMake(-contentInset.left, 0); 343 | } 344 | return contentOffset; 345 | } 346 | 347 | - (UIPanGestureRecognizer *)panGesture 348 | { 349 | if (!_panGesture) { 350 | _panGesture = self.scrollView.panGestureRecognizer; 351 | } 352 | return _panGesture; 353 | } 354 | 355 | - (void)setPanGesture:(UIPanGestureRecognizer *)panGesture 356 | { 357 | if (self.panGesture) { 358 | [self.panGesture removeObserver:self forKeyPath:@"state"]; 359 | } 360 | _panGesture = panGesture; 361 | [self.panGesture addObserver:self 362 | forKeyPath:@"state" 363 | options:NSKeyValueObservingOptionNew 364 | context:NULL]; 365 | } 366 | 367 | @end 368 | 369 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYRefreshController.h: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshController.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14/11/17. 6 | // 7 | // 8 | 9 | 10 | #import "JYPullToRefreshController.h" 11 | #import "JYPullToLoadMoreController.h" -------------------------------------------------------------------------------- /JYRefreshController/Source/JYRefreshIndicator.h: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshIndicator.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface JYRefreshIndicator : UIView 12 | 13 | @property (nonatomic, strong) UIColor *color; 14 | @property (nonatomic, assign) BOOL hidesWhenStopped; 15 | 16 | - (instancetype)initWithColor:(UIColor*)color; 17 | 18 | - (void)startAnimating; 19 | - (void)stopAnimating; 20 | - (BOOL)isAnimating; 21 | - (void)setPercentage:(CGFloat)percentage; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYRefreshIndicator.m: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshIndicator.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import "JYRefreshIndicator.h" 10 | #include 11 | 12 | static NSString *const JY_ANIMATION_KEY = @"spinkit-anim"; 13 | 14 | @interface JYRefreshIndicator () 15 | @property (nonatomic, assign) BOOL stopped; 16 | @property (nonatomic, strong) CALayer *cycleLayer; 17 | @end 18 | 19 | @implementation JYRefreshIndicator 20 | 21 | - (instancetype)initWithColor:(UIColor*)color 22 | { 23 | self = [super init]; 24 | 25 | if (self) { 26 | _color = color; 27 | _hidesWhenStopped = YES; 28 | _stopped = YES; 29 | 30 | [self sizeToFit]; 31 | 32 | [[NSNotificationCenter defaultCenter] addObserver:self 33 | selector:@selector(applicationWillEnterForeground) 34 | name:UIApplicationWillEnterForegroundNotification 35 | object:nil]; 36 | 37 | [[NSNotificationCenter defaultCenter] addObserver:self 38 | selector:@selector(applicationDidEnterBackground) 39 | name:UIApplicationDidEnterBackgroundNotification 40 | object:nil]; 41 | } 42 | return self; 43 | } 44 | 45 | #pragma mark - Property 46 | - (CALayer *)cycleLayer 47 | { 48 | if (!_cycleLayer) { 49 | _cycleLayer= [CALayer layer]; 50 | _cycleLayer.frame = CGRectInset(self.bounds, 2.0, 2.0); 51 | _cycleLayer.backgroundColor = _color.CGColor; 52 | _cycleLayer.anchorPoint = CGPointMake(0.5, 0.5); 53 | _cycleLayer.opacity = 1; 54 | _cycleLayer.cornerRadius = CGRectGetHeight(_cycleLayer.bounds) * 0.5; 55 | _cycleLayer.transform = CATransform3DMakeScale(0.2, 0.2, 0.0); 56 | [self.layer addSublayer:_cycleLayer]; 57 | } 58 | return _cycleLayer; 59 | } 60 | 61 | #pragma mark - Notification 62 | - (void)applicationWillEnterForeground 63 | { 64 | if (self.stopped) { 65 | [self pauseLayers]; 66 | } else { 67 | [self resumeLayers]; 68 | } 69 | } 70 | 71 | - (void)applicationDidEnterBackground 72 | { 73 | [self pauseLayers]; 74 | } 75 | 76 | - (BOOL)isAnimating 77 | { 78 | return !self.stopped; 79 | } 80 | 81 | - (void)startAnimating 82 | { 83 | if (self.stopped) { 84 | self.stopped = NO; 85 | [self resumeLayers]; 86 | } 87 | } 88 | 89 | - (void)stopAnimating 90 | { 91 | if ([self isAnimating]) { 92 | if (self.hidesWhenStopped) { 93 | [UIView animateWithDuration:0.3f animations:^{ 94 | self.cycleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 0.0); 95 | }]; 96 | } 97 | [self pauseLayers]; 98 | self.stopped = YES; 99 | } 100 | } 101 | 102 | - (void)setPercentage:(CGFloat)percentage 103 | { 104 | if (![self isAnimating]) { 105 | self.cycleLayer.transform = CATransform3DMakeScale(percentage, percentage, 0.0); 106 | } 107 | } 108 | 109 | - (void)pauseLayers 110 | { 111 | [self.cycleLayer removeAnimationForKey:JY_ANIMATION_KEY]; 112 | } 113 | 114 | - (void)resumeLayers 115 | { 116 | NSTimeInterval beginTime = CACurrentMediaTime(); 117 | 118 | self.cycleLayer.opacity = 1; 119 | self.cycleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 0.0); 120 | 121 | CAKeyframeAnimation *scaleAnim = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; 122 | scaleAnim.values = @[ 123 | [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 0.0)], 124 | [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.2, 0.2, 0.0)], 125 | [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 0.0)], 126 | ]; 127 | 128 | CAKeyframeAnimation *opacityAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; 129 | opacityAnim.values = @[@(1.0), @(0.0), @(1.0)]; 130 | 131 | CAAnimationGroup *animGroup = [CAAnimationGroup animation]; 132 | animGroup.removedOnCompletion = NO; 133 | animGroup.beginTime = beginTime; 134 | animGroup.repeatCount = HUGE_VALF; 135 | animGroup.duration = 1.0; 136 | animGroup.animations = @[scaleAnim, opacityAnim]; 137 | animGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 138 | 139 | [self.cycleLayer addAnimation:animGroup forKey:JY_ANIMATION_KEY]; 140 | } 141 | 142 | - (CGSize)sizeThatFits:(CGSize)size 143 | { 144 | return CGSizeMake(30, 30); 145 | } 146 | 147 | - (void)setColor:(UIColor *)color 148 | { 149 | _color = color; 150 | for (CALayer *layer in self.layer.sublayers) { 151 | layer.backgroundColor = color.CGColor; 152 | } 153 | } 154 | 155 | - (void)dealloc 156 | { 157 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 158 | } 159 | 160 | @end 161 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYRefreshView.h: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshView.h 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | #import "JYRefreshIndicator.h" 11 | #import "JYRefreshController.h" 12 | 13 | @protocol JYRefreshView 14 | 15 | @optional 16 | 17 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 18 | didChangeToState:(JYRefreshState)refreshState; 19 | 20 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 21 | didShowRefreshViewPercentage:(CGFloat)percentage; 22 | 23 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 24 | didScrolllVisableOffset:(CGFloat)visableOffset; 25 | 26 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 27 | didSetEnable:(BOOL)enable; 28 | 29 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 30 | didChangeToState:(JYLoadMoreState)loadMoreState; 31 | 32 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 33 | didShowhLoadMoreViewPercentage:(CGFloat)percentage; 34 | 35 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 36 | didScrolllVisableOffset:(CGFloat)visableOffset; 37 | 38 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 39 | didSetEnable:(BOOL)enable; 40 | 41 | @end 42 | 43 | @interface JYRefreshView : UIView 44 | 45 | @property (nonatomic, weak, readonly) JYRefreshIndicator *refreshIndicator; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /JYRefreshController/Source/JYRefreshView.m: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshView.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import "JYRefreshView.h" 10 | 11 | #define JY_UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ 12 | green:((float)((rgbValue & 0xFF00) >> 8))/255.0 \ 13 | blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] 14 | 15 | 16 | @interface JYRefreshView () 17 | 18 | @property (nonatomic, assign) JYRefreshState refreshState; 19 | @property (nonatomic, assign) JYLoadMoreState loadMoreState; 20 | 21 | @end 22 | 23 | @implementation JYRefreshView 24 | @synthesize refreshIndicator = _refreshIndicator; 25 | 26 | 27 | - (id)initWithFrame:(CGRect)frame 28 | { 29 | self = [super initWithFrame:frame]; 30 | if (self) { 31 | _refreshState = JYRefreshStateStop; 32 | } 33 | return self; 34 | } 35 | 36 | #pragma mark - layout 37 | - (void)layoutSubviews 38 | { 39 | [super layoutSubviews]; 40 | CGPoint boundsCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 41 | [self.refreshIndicator setCenter:boundsCenter]; 42 | } 43 | 44 | #pragma mark - getter 45 | - (JYRefreshIndicator *)refreshIndicator 46 | { 47 | if (!_refreshIndicator) { 48 | JYRefreshIndicator *indicator = [[JYRefreshIndicator alloc] initWithColor:JY_UIColorFromRGB(0x007AFF)]; 49 | indicator.hidden = NO; 50 | [self addSubview:indicator]; 51 | _refreshIndicator = indicator; 52 | } 53 | return _refreshIndicator; 54 | } 55 | 56 | #pragma mark - JYRefreshView Protocol 57 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 58 | didShowRefreshViewPercentage:(CGFloat)percentage 59 | { 60 | [self.refreshIndicator setPercentage:percentage]; 61 | } 62 | 63 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 64 | didSetEnable:(BOOL)enable 65 | { 66 | if (!enable) { 67 | [self.refreshIndicator stopAnimating]; 68 | } 69 | } 70 | 71 | - (void)pullToRefreshController:(JYPullToRefreshController *)refreshController 72 | didChangeToState:(JYRefreshState)refreshState; 73 | { 74 | _refreshState = refreshState; 75 | switch (refreshState) { 76 | case JYRefreshStateStop: 77 | [self.refreshIndicator stopAnimating]; 78 | break; 79 | 80 | case JYRefreshStateLoading: 81 | [self.refreshIndicator startAnimating]; 82 | break; 83 | 84 | default: 85 | break; 86 | } 87 | [self layoutIfNeeded]; 88 | } 89 | 90 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 91 | didChangeToState:(JYLoadMoreState)loadMoreState 92 | { 93 | 94 | _loadMoreState = loadMoreState; 95 | switch (loadMoreState) { 96 | case JYLoadMoreStateStop: 97 | [self.refreshIndicator stopAnimating]; 98 | break; 99 | 100 | case JYLoadMoreStateLoading: 101 | [self.refreshIndicator startAnimating]; 102 | break; 103 | 104 | default: 105 | break; 106 | } 107 | [self layoutIfNeeded]; 108 | } 109 | 110 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 111 | didShowhLoadMoreViewPercentage:(CGFloat)percentage 112 | { 113 | [self.refreshIndicator setPercentage:percentage]; 114 | } 115 | 116 | - (void)pullToLoadMoreController:(JYPullToLoadMoreController *)loadMoreController 117 | didSetEnable:(BOOL)enable 118 | { 119 | if (!enable) { 120 | [self.refreshIndicator stopAnimating]; 121 | } 122 | } 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /JYRefreshController/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /JYRefreshController/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // JYRefreshController 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JYRefreshControllerTests/JYRefreshControllerTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /JYRefreshControllerTests/JYRefreshControllerTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // JYRefreshControllerTests.m 3 | // JYRefreshControllerTests 4 | // 5 | // Created by Alvin on 14-6-16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface JYRefreshControllerTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation JYRefreshControllerTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /JYRefreshControllerTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jinyu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JYRefreshController 2 | =================== 3 | 4 | An easy way to add pull-to-refresh or pull-to-loadMore to any kind of UIScrollView. 5 | 6 | ## Demo 7 | 8 | 9 | 10 | ## Podfile 11 | 12 | ```ruby 13 | pod "JYRefreshController", `~> 0.5.0` 14 | ``` 15 | ## License 16 | 17 | JYRefreshController is available under the MIT license. See the LICENSE file for more info. 18 | 19 | ## ChangeLog 20 | 21 | 0.7.0 22 | 23 | - add panGesture 24 | 25 | 0.5.2 26 | 27 | - add protocal method pullToRefreshController:didScrolllVisableOffset: pullToLoadMoreController:didScrolllVisableOffset: 28 | 29 | 0.5.0 30 | 31 | - Add attachedEdge property to make refresh view attach to the scrollView edge 32 | 33 | 0.4.0 34 | 35 | - Add refresh and load more diection 36 | 37 | --------------------------------------------------------------------------------