├── .gitignore ├── DCPullRefresh.podspec.json ├── DCPullRefresh.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── DCPullRefresh.xcscheme ├── Example (PullToRefresh) ├── Example (PullToRefresh).xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── Example (PullToRefresh) │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── DCPullRefresh.swift │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── README.md ├── ScreenShot └── 1.gif └── Source ├── DCPullRefresh.h ├── DCPullRefresh.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /DCPullRefresh.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DCPullRefresh", 3 | "version": "1.0", 4 | "summary": "A pull down refresh tableView animation", 5 | "homepage": "https://github.com/Tangdixi/DCPullRefresh", 6 | "license": { 7 | "type": "MIT", 8 | "text": "The DCExplosion use the MIT license" 9 | }, 10 | "authors": { 11 | "Tangdixi": "Tangdixi@gmail.com" 12 | }, 13 | "platforms": { 14 | "ios": "8.0" 15 | }, 16 | "source": { 17 | "git": "https://github.com/Tangdixi/DCPullRefresh.git", 18 | "tag": "1.0" 19 | }, 20 | "source_files": "Source/*.swift", 21 | "requires_arc": true 22 | } 23 | -------------------------------------------------------------------------------- /DCPullRefresh.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AD498CC71D70379C000A16D6 /* DCPullRefresh.h in Headers */ = {isa = PBXBuildFile; fileRef = AD498CC41D70379C000A16D6 /* DCPullRefresh.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | AD498CC81D70379C000A16D6 /* DCPullRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD498CC51D70379C000A16D6 /* DCPullRefresh.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | AD498CC41D70379C000A16D6 /* DCPullRefresh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DCPullRefresh.h; path = Source/DCPullRefresh.h; sourceTree = SOURCE_ROOT; }; 16 | AD498CC51D70379C000A16D6 /* DCPullRefresh.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DCPullRefresh.swift; path = Source/DCPullRefresh.swift; sourceTree = SOURCE_ROOT; }; 17 | AD498CC61D70379C000A16D6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Source/Info.plist; sourceTree = SOURCE_ROOT; }; 18 | ADA903AD1D7035670029ECC0 /* DCPullRefresh.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DCPullRefresh.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | ADA903A91D7035670029ECC0 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | ADA903A31D7035670029ECC0 = { 33 | isa = PBXGroup; 34 | children = ( 35 | ADA903AF1D7035670029ECC0 /* DCPullRefresh */, 36 | ADA903AE1D7035670029ECC0 /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | ADA903AE1D7035670029ECC0 /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | ADA903AD1D7035670029ECC0 /* DCPullRefresh.framework */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | ADA903AF1D7035670029ECC0 /* DCPullRefresh */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | AD498CC41D70379C000A16D6 /* DCPullRefresh.h */, 52 | AD498CC51D70379C000A16D6 /* DCPullRefresh.swift */, 53 | AD498CC61D70379C000A16D6 /* Info.plist */, 54 | ); 55 | path = DCPullRefresh; 56 | sourceTree = ""; 57 | }; 58 | /* End PBXGroup section */ 59 | 60 | /* Begin PBXHeadersBuildPhase section */ 61 | ADA903AA1D7035670029ECC0 /* Headers */ = { 62 | isa = PBXHeadersBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | AD498CC71D70379C000A16D6 /* DCPullRefresh.h in Headers */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXHeadersBuildPhase section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | ADA903AC1D7035670029ECC0 /* DCPullRefresh */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = ADA903B51D7035670029ECC0 /* Build configuration list for PBXNativeTarget "DCPullRefresh" */; 75 | buildPhases = ( 76 | ADA903A81D7035670029ECC0 /* Sources */, 77 | ADA903A91D7035670029ECC0 /* Frameworks */, 78 | ADA903AA1D7035670029ECC0 /* Headers */, 79 | ADA903AB1D7035670029ECC0 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = DCPullRefresh; 86 | productName = DCPullRefresh; 87 | productReference = ADA903AD1D7035670029ECC0 /* DCPullRefresh.framework */; 88 | productType = "com.apple.product-type.framework"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | ADA903A41D7035670029ECC0 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastUpgradeCheck = 0730; 97 | ORGANIZATIONNAME = "Tangdixi Tangdixi"; 98 | TargetAttributes = { 99 | ADA903AC1D7035670029ECC0 = { 100 | CreatedOnToolsVersion = 7.3.1; 101 | }; 102 | }; 103 | }; 104 | buildConfigurationList = ADA903A71D7035670029ECC0 /* Build configuration list for PBXProject "DCPullRefresh" */; 105 | compatibilityVersion = "Xcode 3.2"; 106 | developmentRegion = English; 107 | hasScannedForEncodings = 0; 108 | knownRegions = ( 109 | en, 110 | ); 111 | mainGroup = ADA903A31D7035670029ECC0; 112 | productRefGroup = ADA903AE1D7035670029ECC0 /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | ADA903AC1D7035670029ECC0 /* DCPullRefresh */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXResourcesBuildPhase section */ 122 | ADA903AB1D7035670029ECC0 /* Resources */ = { 123 | isa = PBXResourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | /* End PBXResourcesBuildPhase section */ 130 | 131 | /* Begin PBXSourcesBuildPhase section */ 132 | ADA903A81D7035670029ECC0 /* Sources */ = { 133 | isa = PBXSourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | AD498CC81D70379C000A16D6 /* DCPullRefresh.swift in Sources */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXSourcesBuildPhase section */ 141 | 142 | /* Begin XCBuildConfiguration section */ 143 | ADA903B31D7035670029ECC0 /* Debug */ = { 144 | isa = XCBuildConfiguration; 145 | buildSettings = { 146 | ALWAYS_SEARCH_USER_PATHS = NO; 147 | CLANG_ANALYZER_NONNULL = YES; 148 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 149 | CLANG_CXX_LIBRARY = "libc++"; 150 | CLANG_ENABLE_MODULES = YES; 151 | CLANG_ENABLE_OBJC_ARC = YES; 152 | CLANG_WARN_BOOL_CONVERSION = YES; 153 | CLANG_WARN_CONSTANT_CONVERSION = YES; 154 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INT_CONVERSION = YES; 158 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 162 | COPY_PHASE_STRIP = NO; 163 | CURRENT_PROJECT_VERSION = 1; 164 | DEBUG_INFORMATION_FORMAT = dwarf; 165 | ENABLE_STRICT_OBJC_MSGSEND = YES; 166 | ENABLE_TESTABILITY = YES; 167 | GCC_C_LANGUAGE_STANDARD = gnu99; 168 | GCC_DYNAMIC_NO_PIC = NO; 169 | GCC_NO_COMMON_BLOCKS = YES; 170 | GCC_OPTIMIZATION_LEVEL = 0; 171 | GCC_PREPROCESSOR_DEFINITIONS = ( 172 | "DEBUG=1", 173 | "$(inherited)", 174 | ); 175 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 176 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 177 | GCC_WARN_UNDECLARED_SELECTOR = YES; 178 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 179 | GCC_WARN_UNUSED_FUNCTION = YES; 180 | GCC_WARN_UNUSED_VARIABLE = YES; 181 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 182 | MTL_ENABLE_DEBUG_INFO = YES; 183 | ONLY_ACTIVE_ARCH = YES; 184 | SDKROOT = iphoneos; 185 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 186 | TARGETED_DEVICE_FAMILY = "1,2"; 187 | VERSIONING_SYSTEM = "apple-generic"; 188 | VERSION_INFO_PREFIX = ""; 189 | }; 190 | name = Debug; 191 | }; 192 | ADA903B41D7035670029ECC0 /* Release */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | CLANG_ANALYZER_NONNULL = YES; 197 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 198 | CLANG_CXX_LIBRARY = "libc++"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 204 | CLANG_WARN_EMPTY_BODY = YES; 205 | CLANG_WARN_ENUM_CONVERSION = YES; 206 | CLANG_WARN_INT_CONVERSION = YES; 207 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 208 | CLANG_WARN_UNREACHABLE_CODE = YES; 209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 210 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 211 | COPY_PHASE_STRIP = NO; 212 | CURRENT_PROJECT_VERSION = 1; 213 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 214 | ENABLE_NS_ASSERTIONS = NO; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | GCC_C_LANGUAGE_STANDARD = gnu99; 217 | GCC_NO_COMMON_BLOCKS = YES; 218 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 219 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 220 | GCC_WARN_UNDECLARED_SELECTOR = YES; 221 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 222 | GCC_WARN_UNUSED_FUNCTION = YES; 223 | GCC_WARN_UNUSED_VARIABLE = YES; 224 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 225 | MTL_ENABLE_DEBUG_INFO = NO; 226 | SDKROOT = iphoneos; 227 | TARGETED_DEVICE_FAMILY = "1,2"; 228 | VALIDATE_PRODUCT = YES; 229 | VERSIONING_SYSTEM = "apple-generic"; 230 | VERSION_INFO_PREFIX = ""; 231 | }; 232 | name = Release; 233 | }; 234 | ADA903B61D7035670029ECC0 /* Debug */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | CLANG_ENABLE_MODULES = YES; 238 | DEFINES_MODULE = YES; 239 | DYLIB_COMPATIBILITY_VERSION = 1; 240 | DYLIB_CURRENT_VERSION = 1; 241 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 242 | INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; 243 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 244 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 245 | PRODUCT_BUNDLE_IDENTIFIER = com.tTangdixi.DCPullRefresh; 246 | PRODUCT_NAME = "$(TARGET_NAME)"; 247 | SKIP_INSTALL = YES; 248 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 249 | }; 250 | name = Debug; 251 | }; 252 | ADA903B71D7035670029ECC0 /* Release */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | CLANG_ENABLE_MODULES = YES; 256 | DEFINES_MODULE = YES; 257 | DYLIB_COMPATIBILITY_VERSION = 1; 258 | DYLIB_CURRENT_VERSION = 1; 259 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 260 | INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; 261 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 262 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 263 | PRODUCT_BUNDLE_IDENTIFIER = com.tTangdixi.DCPullRefresh; 264 | PRODUCT_NAME = "$(TARGET_NAME)"; 265 | SKIP_INSTALL = YES; 266 | }; 267 | name = Release; 268 | }; 269 | /* End XCBuildConfiguration section */ 270 | 271 | /* Begin XCConfigurationList section */ 272 | ADA903A71D7035670029ECC0 /* Build configuration list for PBXProject "DCPullRefresh" */ = { 273 | isa = XCConfigurationList; 274 | buildConfigurations = ( 275 | ADA903B31D7035670029ECC0 /* Debug */, 276 | ADA903B41D7035670029ECC0 /* Release */, 277 | ); 278 | defaultConfigurationIsVisible = 0; 279 | defaultConfigurationName = Release; 280 | }; 281 | ADA903B51D7035670029ECC0 /* Build configuration list for PBXNativeTarget "DCPullRefresh" */ = { 282 | isa = XCConfigurationList; 283 | buildConfigurations = ( 284 | ADA903B61D7035670029ECC0 /* Debug */, 285 | ADA903B71D7035670029ECC0 /* Release */, 286 | ); 287 | defaultConfigurationIsVisible = 0; 288 | defaultConfigurationName = Release; 289 | }; 290 | /* End XCConfigurationList section */ 291 | }; 292 | rootObject = ADA903A41D7035670029ECC0 /* Project object */; 293 | } 294 | -------------------------------------------------------------------------------- /DCPullRefresh.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DCPullRefresh.xcodeproj/xcshareddata/xcschemes/DCPullRefresh.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh).xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8106EF671D30D37D000FA336 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8106EF661D30D37D000FA336 /* AppDelegate.swift */; }; 11 | 8106EF691D30D37D000FA336 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8106EF681D30D37D000FA336 /* ViewController.swift */; }; 12 | 8106EF6C1D30D37D000FA336 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8106EF6A1D30D37D000FA336 /* Main.storyboard */; }; 13 | 8106EF6E1D30D37D000FA336 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8106EF6D1D30D37D000FA336 /* Assets.xcassets */; }; 14 | 8106EF711D30D37D000FA336 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8106EF6F1D30D37D000FA336 /* LaunchScreen.storyboard */; }; 15 | 8106EF791D30DAC5000FA336 /* DCPullRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8106EF781D30DAC5000FA336 /* DCPullRefresh.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 8106EF631D30D37D000FA336 /* Example (PullToRefresh).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example (PullToRefresh).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 8106EF661D30D37D000FA336 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 8106EF681D30D37D000FA336 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 22 | 8106EF6B1D30D37D000FA336 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | 8106EF6D1D30D37D000FA336 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 8106EF701D30D37D000FA336 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | 8106EF721D30D37D000FA336 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 8106EF781D30DAC5000FA336 /* DCPullRefresh.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DCPullRefresh.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 8106EF601D30D37D000FA336 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 8106EF5A1D30D37D000FA336 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 8106EF651D30D37D000FA336 /* Example (PullToRefresh) */, 44 | 8106EF641D30D37D000FA336 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 8106EF641D30D37D000FA336 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8106EF631D30D37D000FA336 /* Example (PullToRefresh).app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 8106EF651D30D37D000FA336 /* Example (PullToRefresh) */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 8106EF661D30D37D000FA336 /* AppDelegate.swift */, 60 | 8106EF681D30D37D000FA336 /* ViewController.swift */, 61 | 8106EF781D30DAC5000FA336 /* DCPullRefresh.swift */, 62 | 8106EF6A1D30D37D000FA336 /* Main.storyboard */, 63 | 8106EF6D1D30D37D000FA336 /* Assets.xcassets */, 64 | 8106EF6F1D30D37D000FA336 /* LaunchScreen.storyboard */, 65 | 8106EF721D30D37D000FA336 /* Info.plist */, 66 | ); 67 | path = "Example (PullToRefresh)"; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 8106EF621D30D37D000FA336 /* Example (PullToRefresh) */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 8106EF751D30D37D000FA336 /* Build configuration list for PBXNativeTarget "Example (PullToRefresh)" */; 76 | buildPhases = ( 77 | 8106EF5F1D30D37D000FA336 /* Sources */, 78 | 8106EF601D30D37D000FA336 /* Frameworks */, 79 | 8106EF611D30D37D000FA336 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = "Example (PullToRefresh)"; 86 | productName = "Example (PullToRefresh)"; 87 | productReference = 8106EF631D30D37D000FA336 /* Example (PullToRefresh).app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 8106EF5B1D30D37D000FA336 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0730; 97 | LastUpgradeCheck = 0730; 98 | ORGANIZATIONNAME = Tangdixi; 99 | TargetAttributes = { 100 | 8106EF621D30D37D000FA336 = { 101 | CreatedOnToolsVersion = 7.3.1; 102 | }; 103 | }; 104 | }; 105 | buildConfigurationList = 8106EF5E1D30D37D000FA336 /* Build configuration list for PBXProject "Example (PullToRefresh)" */; 106 | compatibilityVersion = "Xcode 3.2"; 107 | developmentRegion = English; 108 | hasScannedForEncodings = 0; 109 | knownRegions = ( 110 | en, 111 | Base, 112 | ); 113 | mainGroup = 8106EF5A1D30D37D000FA336; 114 | productRefGroup = 8106EF641D30D37D000FA336 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 8106EF621D30D37D000FA336 /* Example (PullToRefresh) */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXResourcesBuildPhase section */ 124 | 8106EF611D30D37D000FA336 /* Resources */ = { 125 | isa = PBXResourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | 8106EF711D30D37D000FA336 /* LaunchScreen.storyboard in Resources */, 129 | 8106EF6E1D30D37D000FA336 /* Assets.xcassets in Resources */, 130 | 8106EF6C1D30D37D000FA336 /* Main.storyboard in Resources */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | 8106EF5F1D30D37D000FA336 /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 8106EF691D30D37D000FA336 /* ViewController.swift in Sources */, 142 | 8106EF791D30DAC5000FA336 /* DCPullRefresh.swift in Sources */, 143 | 8106EF671D30D37D000FA336 /* AppDelegate.swift in Sources */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXSourcesBuildPhase section */ 148 | 149 | /* Begin PBXVariantGroup section */ 150 | 8106EF6A1D30D37D000FA336 /* Main.storyboard */ = { 151 | isa = PBXVariantGroup; 152 | children = ( 153 | 8106EF6B1D30D37D000FA336 /* Base */, 154 | ); 155 | name = Main.storyboard; 156 | sourceTree = ""; 157 | }; 158 | 8106EF6F1D30D37D000FA336 /* LaunchScreen.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 8106EF701D30D37D000FA336 /* Base */, 162 | ); 163 | name = LaunchScreen.storyboard; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXVariantGroup section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | 8106EF731D30D37D000FA336 /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | CLANG_ANALYZER_NONNULL = YES; 174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 175 | CLANG_CXX_LIBRARY = "libc++"; 176 | CLANG_ENABLE_MODULES = YES; 177 | CLANG_ENABLE_OBJC_ARC = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_CONSTANT_CONVERSION = YES; 180 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 181 | CLANG_WARN_EMPTY_BODY = YES; 182 | CLANG_WARN_ENUM_CONVERSION = YES; 183 | CLANG_WARN_INT_CONVERSION = YES; 184 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 185 | CLANG_WARN_UNREACHABLE_CODE = YES; 186 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 187 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 188 | COPY_PHASE_STRIP = NO; 189 | DEBUG_INFORMATION_FORMAT = dwarf; 190 | ENABLE_STRICT_OBJC_MSGSEND = YES; 191 | ENABLE_TESTABILITY = YES; 192 | GCC_C_LANGUAGE_STANDARD = gnu99; 193 | GCC_DYNAMIC_NO_PIC = NO; 194 | GCC_NO_COMMON_BLOCKS = YES; 195 | GCC_OPTIMIZATION_LEVEL = 0; 196 | GCC_PREPROCESSOR_DEFINITIONS = ( 197 | "DEBUG=1", 198 | "$(inherited)", 199 | ); 200 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 201 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 202 | GCC_WARN_UNDECLARED_SELECTOR = YES; 203 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 204 | GCC_WARN_UNUSED_FUNCTION = YES; 205 | GCC_WARN_UNUSED_VARIABLE = YES; 206 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 207 | MTL_ENABLE_DEBUG_INFO = YES; 208 | ONLY_ACTIVE_ARCH = YES; 209 | SDKROOT = iphoneos; 210 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 211 | }; 212 | name = Debug; 213 | }; 214 | 8106EF741D30D37D000FA336 /* Release */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | ALWAYS_SEARCH_USER_PATHS = NO; 218 | CLANG_ANALYZER_NONNULL = YES; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 220 | CLANG_CXX_LIBRARY = "libc++"; 221 | CLANG_ENABLE_MODULES = YES; 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_CONSTANT_CONVERSION = YES; 225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INT_CONVERSION = YES; 229 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 230 | CLANG_WARN_UNREACHABLE_CODE = YES; 231 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 232 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 235 | ENABLE_NS_ASSERTIONS = NO; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu99; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 246 | MTL_ENABLE_DEBUG_INFO = NO; 247 | SDKROOT = iphoneos; 248 | VALIDATE_PRODUCT = YES; 249 | }; 250 | name = Release; 251 | }; 252 | 8106EF761D30D37D000FA336 /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 256 | INFOPLIST_FILE = "Example (PullToRefresh)/Info.plist"; 257 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 258 | PRODUCT_BUNDLE_IDENTIFIER = "DC.Example--PullToRefresh-"; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | }; 261 | name = Debug; 262 | }; 263 | 8106EF771D30D37D000FA336 /* Release */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 267 | INFOPLIST_FILE = "Example (PullToRefresh)/Info.plist"; 268 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 269 | PRODUCT_BUNDLE_IDENTIFIER = "DC.Example--PullToRefresh-"; 270 | PRODUCT_NAME = "$(TARGET_NAME)"; 271 | }; 272 | name = Release; 273 | }; 274 | /* End XCBuildConfiguration section */ 275 | 276 | /* Begin XCConfigurationList section */ 277 | 8106EF5E1D30D37D000FA336 /* Build configuration list for PBXProject "Example (PullToRefresh)" */ = { 278 | isa = XCConfigurationList; 279 | buildConfigurations = ( 280 | 8106EF731D30D37D000FA336 /* Debug */, 281 | 8106EF741D30D37D000FA336 /* Release */, 282 | ); 283 | defaultConfigurationIsVisible = 0; 284 | defaultConfigurationName = Release; 285 | }; 286 | 8106EF751D30D37D000FA336 /* Build configuration list for PBXNativeTarget "Example (PullToRefresh)" */ = { 287 | isa = XCConfigurationList; 288 | buildConfigurations = ( 289 | 8106EF761D30D37D000FA336 /* Debug */, 290 | 8106EF771D30D37D000FA336 /* Release */, 291 | ); 292 | defaultConfigurationIsVisible = 0; 293 | defaultConfigurationName = Release; 294 | }; 295 | /* End XCConfigurationList section */ 296 | }; 297 | rootObject = 8106EF5B1D30D37D000FA336 /* Project object */; 298 | } 299 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh).xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example (PullToRefresh) 4 | // 5 | // Created by tang dixi on 9/7/2016. 6 | // Copyright © 2016 Tangdixi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // 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. 24 | // 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. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/DCPullRefresh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DCPullRefresh.swift 3 | // Example (PullToRefresh) 4 | // 5 | // Created by tang dixi on 9/7/2016. 6 | // Copyright © 2016 Tangdixi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias DCRefreshControlHander = (()->Void) 12 | 13 | // MARK: - Refresh Status 14 | 15 | enum DCRefreshControlState { 16 | 17 | case Idle 18 | case Charging 19 | case Refreshing 20 | case Dismissing 21 | case End 22 | 23 | } 24 | 25 | // MARK: - Constants 26 | enum DCRefreshControlConstant { 27 | 28 | static let drawPathThreshold = CGFloat(64) 29 | static let beginRefreshingThreshold = CGFloat(116) 30 | static let color = UIColor(red: 140/255, green: 145/255, blue: 176/255, alpha: 1.0) 31 | 32 | static let ballLayerTransformKeyFrame = (11, 16) 33 | static let ballLayerTransformLastKeyFrame = 17 34 | 35 | static let circlePathLayerTransformKeyFrame = (17, 60) 36 | 37 | static let ballLayerDismissKeyFrame = (8, 30) 38 | 39 | static let backgroundWillDismissKeyFrame = (31, 33) 40 | 41 | } 42 | 43 | // MARK: - Constructor 44 | 45 | class DCRefreshControl: UIView { 46 | 47 | // MARK: - Fetch some properties from superView 48 | private weak var mirrorSuperView:UIScrollView! 49 | private var originContentInset:UIEdgeInsets! 50 | private var currentOffsetY = CGFloat(0) 51 | private var panGestureRecognizer:UIPanGestureRecognizer! 52 | 53 | // MARK: - Animations 54 | private var isAnimating = false 55 | private var refreshControlState = DCRefreshControlState.Idle 56 | 57 | // MARK: - Display link 58 | private var frameCount:Int = 0 59 | private var displayLink:CADisplayLink! 60 | 61 | // MARK: - Background path 62 | 63 | /// The background color 64 | var color:UIColor! 65 | private var controlPointAssociateView:UIView! 66 | private var controlPoint:CGPoint! 67 | 68 | // MARK: - Ball layer animation 69 | private var ballLayer:CAShapeLayer! 70 | private var transformPathAssociatePoint:CGPoint! 71 | 72 | // MARK: - Circle layer animation 73 | private var circlePathLayer:CAShapeLayer! 74 | 75 | // MARK: - Dismiss animation 76 | private var dismissAnimationAssociateView:UIView! 77 | private var dismissAnimationAsscciatePoint:CGPoint! 78 | private var fakeBackgroundView:UIView! 79 | 80 | // MARK: - Refresh completion 81 | private var queue:NSOperationQueue! 82 | var refreshHandler:DCRefreshControlHander? = nil 83 | 84 | // MARK: - Initialization 85 | override init(frame: CGRect) { 86 | super.init(frame: frame) 87 | self.opaque = false 88 | self.backgroundColor = UIColor.clearColor() 89 | } 90 | 91 | required init?(coder aDecoder: NSCoder) { 92 | fatalError("init(coder:) has not been implemented") 93 | } 94 | 95 | convenience init(color:UIColor = DCRefreshControlConstant.color, refreshHandler: DCRefreshControlHander) { 96 | self.init(frame: CGRectZero) 97 | self.refreshHandler = refreshHandler 98 | self.color = color 99 | } 100 | 101 | // MARK: - Life cycle 102 | override func willMoveToSuperview(newSuperview: UIView?) { 103 | 104 | self.clipsToBounds = true 105 | 106 | super.willMoveToSuperview(newSuperview) 107 | 108 | // Make sure the superView is a scrollView 109 | // 110 | guard let newSuperview = newSuperview as? UIScrollView else { 111 | 112 | removeObservers() 113 | return 114 | 115 | } 116 | 117 | mirrorSuperView = newSuperview 118 | mirrorSuperView.alwaysBounceVertical = true 119 | 120 | panGestureRecognizer = mirrorSuperView.panGestureRecognizer 121 | 122 | self.frame = CGRect(origin: CGPointZero, size: CGSize(width: mirrorSuperView.frame.width, height: 0)) 123 | 124 | configureObservers() 125 | 126 | } 127 | 128 | // MARK: - Drawing 129 | override func drawRect(rect: CGRect) { 130 | 131 | /* Draw the main background */ 132 | 133 | let path = UIBezierPath() 134 | path.moveToPoint(CGPointZero) 135 | 136 | switch refreshControlState { 137 | case .Idle: 138 | path.addLineToPoint(CGPoint(x: 0, y: self.frame.size.height)) 139 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height)) 140 | case .Charging: 141 | controlPoint = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold) 142 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 143 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 144 | 145 | case .Refreshing: 146 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 147 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 148 | 149 | case .Dismissing: 150 | 151 | let backgroundWillDismiss = (frameCount >= DCRefreshControlConstant.backgroundWillDismissKeyFrame.0) && (frameCount < DCRefreshControlConstant.backgroundWillDismissKeyFrame.1) 152 | 153 | if backgroundWillDismiss == true { 154 | 155 | if frameCount == DCRefreshControlConstant.backgroundWillDismissKeyFrame.0 { 156 | ballLayer.opacity = 0 157 | } 158 | 159 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 160 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: CGPoint(x: controlPoint.x, y: controlPoint.y+CGFloat(frameCount-DCRefreshControlConstant.backgroundWillDismissKeyFrame.0)*6)) 161 | break 162 | } 163 | 164 | let backgroundDismissing = (frameCount > DCRefreshControlConstant.backgroundWillDismissKeyFrame.1) 165 | 166 | if backgroundDismissing == true { 167 | 168 | displayLink.invalidate() 169 | displayLink = nil 170 | 171 | fakeBackgroundView = { 172 | 173 | let view = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: abs(currentOffsetY))) 174 | view.backgroundColor = color 175 | return view 176 | 177 | }() 178 | self.addSubview(fakeBackgroundView) 179 | 180 | UIView.animateWithDuration(0.7, 181 | animations: { 182 | 183 | self.mirrorSuperView.contentInset = self.originContentInset 184 | self.fakeBackgroundView.alpha = 0 185 | 186 | }, 187 | completion: { finished in 188 | 189 | self.dismissAnimationDidEnd() 190 | 191 | }) 192 | 193 | return 194 | } 195 | 196 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 197 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 198 | 199 | default: 200 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height)) 201 | } 202 | 203 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: 0)) 204 | path.closePath() 205 | 206 | let context = UIGraphicsGetCurrentContext() 207 | CGContextAddPath(context, path.CGPath) 208 | self.color.set() 209 | CGContextFillPath(context) 210 | 211 | /* Configure the frame of the refresh animtion */ 212 | 213 | if refreshControlState == .Refreshing { 214 | 215 | /* Ball layer begin transforming */ 216 | let ballLayerTransforming = (frameCount > DCRefreshControlConstant.ballLayerTransformKeyFrame.0) && (frameCount <= DCRefreshControlConstant.ballLayerTransformKeyFrame.1) 217 | 218 | if ballLayerTransforming == true { 219 | 220 | /* Disable the implicit animation in CALayer */ 221 | CATransaction.setDisableActions(true) 222 | 223 | /* There are 12, 9, 6, 3 and finally it stay at abs(currentOffsetY)/2 */ 224 | ballLayer.setCenter(CGPoint(x: controlPoint.x, y: abs(currentOffsetY)*(3/5)-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1)) 225 | 226 | if transformPathAssociatePoint == nil { 227 | transformPathAssociatePoint = ballLayer.center 228 | } 229 | 230 | let topLeftPoint:CGPoint = { 231 | let x = self.frame.size.width/2-sqrt(CGFloat(powf(Float(ballLayer.frame.size.width/2), 2)-powf(Float(transformPathAssociatePoint.y-ballLayer.center.y), 2))) 232 | let y = transformPathAssociatePoint.y 233 | 234 | return CGPoint(x: x, y: y) 235 | }() 236 | 237 | let bottomLeftPoint:CGPoint = { 238 | 239 | let x = min(topLeftPoint.x - (CGFloat(DCRefreshControlConstant.ballLayerTransformKeyFrame.1-frameCount)*3), self.ballLayer.frame.origin.x-5) 240 | 241 | let lowerBounds = Int((abs(currentOffsetY)+controlPoint.y)/2) 242 | let upperBounds = Int(abs(currentOffsetY)) 243 | guard let point = path.crossPointAt(x, range: (lowerBounds, upperBounds)) else { fatalError() } 244 | 245 | return CGPoint(x: point.x, y: point.y+2) 246 | 247 | }() 248 | let bottomRightPoint = CGPoint(x: self.frame.size.width - bottomLeftPoint.x, y: bottomLeftPoint.y) 249 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 250 | 251 | let leftControlPoint = CGPoint(x: topLeftPoint.x+CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*3 , y: bottomLeftPoint.y-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1.8) 252 | let rightControlPoint = CGPoint(x: topRightPoint.x-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*3, y: bottomLeftPoint.y-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1.8) 253 | 254 | // print("\(topLeftPoint)\n ||\(leftControlPoint)\n\(bottomLeftPoint)\n\(topRightPoint)\n ||\(rightControlPoint)\n\(bottomRightPoint)\n") 255 | 256 | let path = UIBezierPath() 257 | path.moveToPoint(topLeftPoint) 258 | path.addQuadCurveToPoint(bottomLeftPoint, controlPoint: leftControlPoint) 259 | path.addLineToPoint(bottomRightPoint) 260 | path.addQuadCurveToPoint(topRightPoint, controlPoint: rightControlPoint) 261 | path.closePath() 262 | 263 | let context = UIGraphicsGetCurrentContext() 264 | CGContextAddPath(context, path.CGPath) 265 | UIColor.whiteColor().set() 266 | CGContextFillPath(context) 267 | 268 | /* 269 | Debug: 270 | UIColor.redColor().setStroke() 271 | path.stroke() 272 | */ 273 | } 274 | 275 | /* Ball layer transform completed */ 276 | let ballLayerDidTransformed = (frameCount == DCRefreshControlConstant.ballLayerTransformLastKeyFrame) 277 | 278 | if ballLayerDidTransformed == true { 279 | 280 | CATransaction.setDisableActions(true) 281 | ballLayer.setCenter(CGPoint(x: controlPoint.x, y: abs(currentOffsetY)*(3/5)-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1)) 282 | 283 | let bottomLeft:CGPoint = { 284 | let x = ballLayer.frame.origin.x-5 285 | let lowerBounds = Int((abs(currentOffsetY)+controlPoint.y)/2) 286 | let upperBounds = Int(abs(currentOffsetY)) 287 | guard let point = path.crossPointAt(x, range: (lowerBounds, upperBounds)) else { fatalError() } 288 | return CGPoint(x: point.x, y: point.y+1) 289 | }() 290 | let bottomRight = CGPoint(x: self.frame.size.width-bottomLeft.x, y: bottomLeft.y) 291 | let topPoint = CGPoint(x: self.frame.size.width/2, y: bottomLeft.y-6) 292 | 293 | let path = UIBezierPath() 294 | path.moveToPoint(topPoint) 295 | path.addLineToPoint(bottomLeft) 296 | path.addLineToPoint(bottomRight) 297 | path.closePath() 298 | 299 | let context = UIGraphicsGetCurrentContext() 300 | CGContextAddPath(context, path.CGPath) 301 | UIColor.whiteColor().set() 302 | CGContextFillPath(context) 303 | 304 | } 305 | 306 | /* Circle layer begin transforming */ 307 | let circleLayerTransforming = (frameCount >= DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0) && (frameCount <= DCRefreshControlConstant.circlePathLayerTransformKeyFrame.1) 308 | 309 | if circleLayerTransforming == true { 310 | 311 | circlePathLayer.setCenter(ballLayer.center) 312 | 313 | let percentage = CGFloat(frameCount-DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0+1) 314 | let total = CGFloat(DCRefreshControlConstant.circlePathLayerTransformKeyFrame.1-DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0) 315 | 316 | CATransaction.setDisableActions(true) 317 | circlePathLayer.strokeEnd = percentage/total 318 | 319 | } 320 | 321 | } 322 | 323 | /* Configure the frame of the dismiss animation */ 324 | 325 | if refreshControlState == .Dismissing { 326 | 327 | CATransaction.setDisableActions(true) 328 | ballLayer.setCenter(dismissAnimationAsscciatePoint) 329 | 330 | /* Ball layer start dismiss */ 331 | 332 | let startTransform = (frameCount == DCRefreshControlConstant.ballLayerDismissKeyFrame.0) 333 | 334 | if startTransform == true { 335 | 336 | let topLeftPoint = CGPoint(x: ballLayer.frame.origin.x, y: ballLayer.center.y+4) 337 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 338 | let topControlPoint = CGPoint(x: ballLayer.center.x, y: ballLayer.center.y+ballLayer.frame.size.height*1.2) 339 | 340 | let bottomLeftPoint = CGPoint(x: ballLayer.frame.origin.x, y: abs(currentOffsetY)) 341 | let bottomRightPoint = CGPoint(x: self.frame.size.width-bottomLeftPoint.x, y: abs(currentOffsetY)) 342 | let bottomTopPoint = CGPoint(x: ballLayer.center.x, y: abs(currentOffsetY)-4) 343 | 344 | let path = UIBezierPath() 345 | path.moveToPoint(topLeftPoint) 346 | path.addQuadCurveToPoint(topRightPoint, controlPoint: topControlPoint) 347 | path.closePath() 348 | 349 | path.moveToPoint(bottomTopPoint) 350 | path.addLineToPoint(bottomLeftPoint) 351 | path.addLineToPoint(bottomRightPoint) 352 | path.closePath() 353 | 354 | let context = UIGraphicsGetCurrentContext() 355 | CGContextAddPath(context, path.CGPath) 356 | UIColor.whiteColor().set() 357 | CGContextFillPath(context) 358 | 359 | } 360 | 361 | /* Ball layer dismissing */ 362 | 363 | let transforming = (frameCount > DCRefreshControlConstant.ballLayerDismissKeyFrame.0) && (frameCount <= DCRefreshControlConstant.ballLayerDismissKeyFrame.1) 364 | 365 | if transforming == true { 366 | 367 | let topLeftPoint:CGPoint = { 368 | 369 | if frameCount >= 16 { 370 | let x = ballLayer.center.x-ballLayer.frame.size.width/2/sqrt(2) 371 | let y = ballLayer.center.y-ballLayer.frame.size.width/2/sqrt(2) 372 | return CGPoint(x: x, y: y) 373 | } 374 | 375 | return CGPoint(x: ballLayer.frame.origin.x, y: ballLayer.center.y) 376 | }() 377 | 378 | let bottomLeftPoint:CGPoint = { 379 | 380 | let x = topLeftPoint.x-CGFloat(frameCount-DCRefreshControlConstant.ballLayerDismissKeyFrame.0)*2-15 381 | let y = abs(currentOffsetY) 382 | 383 | return CGPoint(x: x, y: y) 384 | 385 | }() 386 | 387 | let bottomRightPoint = CGPoint(x: self.frame.size.width-bottomLeftPoint.x, y: bottomLeftPoint.y) 388 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 389 | 390 | let leftControlPoint:CGPoint = { 391 | 392 | if frameCount == DCRefreshControlConstant.ballLayerDismissKeyFrame.0+1 { 393 | return CGPoint(x: topLeftPoint.x+15, y: abs(currentOffsetY+5)) 394 | } 395 | let x = (topLeftPoint.x+bottomLeftPoint.x)/2+8 396 | let y = (topLeftPoint.y+bottomLeftPoint.y)/2+CGFloat(frameCount-DCRefreshControlConstant.ballLayerDismissKeyFrame.0)/1.5 397 | 398 | return CGPoint(x: x, y: y) 399 | 400 | }() 401 | 402 | let rightControlPoint = CGPoint(x: self.frame.size.width-leftControlPoint.x, y: leftControlPoint.y) 403 | 404 | let path = UIBezierPath() 405 | path.moveToPoint(topLeftPoint) 406 | path.addQuadCurveToPoint(bottomLeftPoint, controlPoint: leftControlPoint) 407 | path.addLineToPoint(bottomRightPoint) 408 | path.addQuadCurveToPoint(topRightPoint, controlPoint: rightControlPoint) 409 | path.closePath() 410 | 411 | 412 | let context = UIGraphicsGetCurrentContext() 413 | CGContextAddPath(context, path.CGPath) 414 | UIColor.whiteColor().set() 415 | CGContextFillPath(context) 416 | 417 | // UIColor.redColor().setStroke() 418 | // path.stroke() 419 | 420 | } 421 | 422 | } 423 | 424 | } 425 | 426 | // MARK: - Ball Layer Animation 427 | func performBallLayerAnimation() { 428 | 429 | isAnimating = true 430 | 431 | ballLayerAnimationWillAppear() 432 | 433 | UIView.animateWithDuration(0.7, delay: 0, usingSpringWithDamping: 0.12, initialSpringVelocity: 2, options: .CurveEaseOut, 434 | animations: { 435 | 436 | self.controlPointAssociateView.center = CGPoint(x: self.frame.size.width/2, y: abs(self.currentOffsetY)) 437 | 438 | }, 439 | completion: { finished in 440 | 441 | let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC))) 442 | dispatch_after(delayTime, dispatch_get_main_queue()) { 443 | 444 | self.ballLayerAnimationDidEnd() 445 | 446 | let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC))) 447 | dispatch_after(delayTime, dispatch_get_main_queue()) { 448 | 449 | self.performCircleLayerAnimation() 450 | 451 | } 452 | 453 | } 454 | 455 | }) 456 | 457 | } 458 | 459 | func ballLayerAnimationWillAppear() { 460 | 461 | if displayLink == nil { 462 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler)) 463 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) 464 | 465 | frameCount = 0 466 | } 467 | 468 | controlPointAssociateView = { 469 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 2, height: 2)) 470 | view.center = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height) 471 | view.backgroundColor = UIColor.clearColor() 472 | return view 473 | }() 474 | self.addSubview(controlPointAssociateView) 475 | 476 | ballLayer = { 477 | let layer = CAShapeLayer() 478 | layer.frame = CGRect(x: 0, y: 0, width: 38, height: 38) 479 | layer.setCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2)) 480 | 481 | let path = UIBezierPath() 482 | path.addArcWithCenter(layer.middle, radius: 19, startAngle: 0, endAngle: CGFloat(M_PI)*2, clockwise: true) 483 | 484 | layer.path = path.CGPath 485 | layer.fillColor = UIColor.whiteColor().CGColor 486 | 487 | return layer 488 | }() 489 | self.layer.addSublayer(ballLayer) 490 | 491 | circlePathLayer = { 492 | 493 | let layer = CAShapeLayer() 494 | layer.frame = CGRect(x: 0, y: 0, width: 44, height: 44) 495 | layer.setCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2)) 496 | 497 | let path = UIBezierPath() 498 | path.addArcWithCenter(layer.middle, radius: 22, startAngle: CGFloat(M_PI)/2, endAngle: CGFloat(M_PI)*5/2, clockwise: true) 499 | 500 | layer.path = path.CGPath 501 | layer.lineWidth = 2 502 | layer.fillColor = UIColor.clearColor().CGColor 503 | layer.strokeColor = UIColor.whiteColor().CGColor 504 | 505 | return layer 506 | }() 507 | self.layer.addSublayer(circlePathLayer) 508 | 509 | } 510 | 511 | func ballLayerAnimationDidEnd() { 512 | 513 | self.displayLink.invalidate() 514 | self.displayLink = nil 515 | 516 | 517 | } 518 | 519 | func displayLinkHandler() { 520 | 521 | if refreshControlState == .Refreshing { 522 | guard let controlPointLayer = controlPointAssociateView.layer.presentationLayer() else { return } 523 | controlPoint = controlPointLayer.center 524 | 525 | frameCount += 1 526 | 527 | self.setNeedsDisplay() 528 | 529 | return 530 | } 531 | 532 | if refreshControlState == .Dismissing { 533 | 534 | guard let dismissAnimationAsscciatePointLayer = dismissAnimationAssociateView.layer.presentationLayer() else { return } 535 | dismissAnimationAsscciatePoint = dismissAnimationAsscciatePointLayer.center 536 | 537 | frameCount += 1 538 | 539 | print("\(frameCount): \(dismissAnimationAsscciatePoint)") 540 | 541 | self.setNeedsDisplay() 542 | 543 | return 544 | } 545 | 546 | } 547 | 548 | // MARK: - Circle Layer Animation 549 | func performCircleLayerAnimation() { 550 | 551 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 552 | scaleAnimation.fromValue = 1.0 553 | scaleAnimation.toValue = 1.5 554 | 555 | let alphaAnimation = CABasicAnimation(keyPath: "opacity") 556 | alphaAnimation.fromValue = 1.0 557 | alphaAnimation.toValue = 0 558 | 559 | let animationGroup = CAAnimationGroup() 560 | animationGroup.duration = 0.7 561 | animationGroup.animations = [scaleAnimation, alphaAnimation] 562 | animationGroup.repeatCount = Float.infinity 563 | animationGroup.fillMode = kCAFillModeForwards 564 | 565 | circlePathLayer.addAnimation(animationGroup, forKey: nil) 566 | 567 | guard let refreshHandler = refreshHandler else { fatalError() } 568 | performRefreshHandler(refreshHandler) 569 | 570 | } 571 | 572 | func circleLayerAnimationDidEnd() { 573 | 574 | circlePathLayer.removeAllAnimations() 575 | circlePathLayer.removeFromSuperlayer() 576 | 577 | performDismissAnimation() 578 | 579 | } 580 | 581 | // MARK: - Layer dismiss Animation 582 | 583 | func performDismissAnimation() { 584 | 585 | refreshControlState = .Dismissing 586 | 587 | dismissAnimationWillAppear() 588 | 589 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 590 | scaleAnimation.fromValue = 1.0 591 | scaleAnimation.toValue = 1.5 592 | 593 | let alphaAnimation = CABasicAnimation(keyPath: "opacity") 594 | alphaAnimation.fromValue = 1.0 595 | alphaAnimation.toValue = 0 596 | 597 | let animationGroup = CAAnimationGroup() 598 | animationGroup.duration = 0.7 599 | animationGroup.animations = [scaleAnimation, alphaAnimation] 600 | animationGroup.repeatCount = Float.infinity 601 | animationGroup.fillMode = kCAFillModeForwards 602 | 603 | circlePathLayer.addAnimation(animationGroup, forKey: nil) 604 | 605 | UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .CurveLinear, 606 | animations: { 607 | 608 | self.dismissAnimationAssociateView.center = CGPoint(x:self.frame.size.width/2, y:abs(self.currentOffsetY)+self.ballLayer.frame.size.height/2) 609 | 610 | print(abs(self.currentOffsetY)+self.ballLayer.frame.size.height/2) 611 | 612 | }, 613 | completion: { finished in 614 | self.dismissAnimationDidEnd() 615 | }) 616 | 617 | 618 | } 619 | 620 | func dismissAnimationWillAppear() { 621 | 622 | if displayLink == nil { 623 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler)) 624 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) 625 | 626 | frameCount = 0 627 | } 628 | 629 | dismissAnimationAssociateView = { 630 | 631 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 2, height: 2)) 632 | view.center = ballLayer.center 633 | view.backgroundColor = UIColor.clearColor() 634 | return view 635 | 636 | }() 637 | self.addSubview(dismissAnimationAssociateView) 638 | 639 | } 640 | 641 | func dismissAnimationDidEnd() { 642 | 643 | /* Everything back to normal */ 644 | 645 | isAnimating = false 646 | refreshControlState = .Idle 647 | currentOffsetY = 0 648 | 649 | ballLayer.removeFromSuperlayer() 650 | circlePathLayer.removeFromSuperlayer() 651 | controlPointAssociateView.removeFromSuperview() 652 | dismissAnimationAssociateView.removeFromSuperview() 653 | 654 | mirrorSuperView.userInteractionEnabled = true; 655 | 656 | } 657 | 658 | // MARK: - The User's completion handler 659 | func performRefreshHandler(handler:DCRefreshControlHander) { 660 | 661 | if queue == nil { 662 | queue = NSOperationQueue() 663 | } 664 | 665 | /* Perform the handler in background queue */ 666 | queue.addOperationWithBlock { 667 | handler() 668 | /* Always up date the UI in main queue */ 669 | NSOperationQueue.mainQueue().addOperationWithBlock { 670 | self.circleLayerAnimationDidEnd() 671 | } 672 | } 673 | 674 | } 675 | 676 | // MARK: - KVO 677 | func configureObservers() { 678 | 679 | let options = NSKeyValueObservingOptions([.Old, .New]) 680 | self.mirrorSuperView.addObserver(self, forKeyPath: "contentInset", options: options, context: nil) 681 | self.mirrorSuperView.addObserver(self, forKeyPath: "contentOffset", options: options, context: nil) 682 | self.panGestureRecognizer.addObserver(self, forKeyPath: "state", options: options, context: nil) 683 | 684 | } 685 | 686 | func removeObservers() { 687 | 688 | self.superview?.removeObserver(self, forKeyPath: "contentInset") 689 | self.superview?.removeObserver(self, forKeyPath: "contentOffset") 690 | self.panGestureRecognizer.removeObserver(self, forKeyPath: "state") 691 | 692 | } 693 | 694 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { 695 | 696 | guard let keyPath = keyPath else { return } 697 | guard let change = change else { return } 698 | 699 | if keyPath == "contentInset" { 700 | 701 | guard let oldEdgeInset = change["old"]?.UIEdgeInsetsValue() else { return } 702 | guard let newEdgeInset = change["new"]?.UIEdgeInsetsValue() else { return } 703 | 704 | let condition = (originContentInset == nil) && (oldEdgeInset.top != newEdgeInset.top) 705 | 706 | if condition { 707 | originContentInset = change["new"]?.UIEdgeInsetsValue() 708 | } 709 | 710 | } 711 | 712 | if keyPath == "contentOffset" { 713 | 714 | scrollViewContentOffsetDidChanged(change) 715 | } 716 | 717 | if keyPath == "state" { 718 | 719 | panGestureRecognizerStateDidChanged(change) 720 | 721 | } 722 | 723 | } 724 | 725 | func panGestureRecognizerStateDidChanged(change:[String: AnyObject]) { 726 | 727 | guard let newState = change["new"]?.integerValue else { return } 728 | 729 | let condition = (abs(currentOffsetY) >= DCRefreshControlConstant.beginRefreshingThreshold) && (newState == UIGestureRecognizerState.Ended.rawValue) && (isAnimating == false) 730 | 731 | if condition == true { 732 | 733 | refreshControlState = DCRefreshControlState.Refreshing 734 | 735 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 736 | mirrorSuperView.contentInset = UIEdgeInsets(top: maxOffsetY, left: 0, bottom: 0, right: 0) 737 | 738 | mirrorSuperView.userInteractionEnabled = false; 739 | 740 | performBallLayerAnimation() 741 | } 742 | 743 | } 744 | 745 | func scrollViewContentOffsetDidChanged(change:[String: AnyObject]) { 746 | 747 | guard let originContentInset = originContentInset else { return } 748 | 749 | /* For example, a tableView will make a -64 offset in Y axis if there is a navigation bar */ 750 | 751 | currentOffsetY = originContentInset.top + mirrorSuperView.contentOffset.y 752 | 753 | /* When the tableView scroll to top, return immediately */ 754 | 755 | guard currentOffsetY < 0 else { return } 756 | 757 | /* Prevent some naughty guy */ 758 | 759 | if refreshControlState == .Refreshing { 760 | self.frame = { 761 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 762 | let y = currentOffsetY 763 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 764 | }() 765 | 766 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 767 | mirrorSuperView.setContentOffset(CGPoint(x: 0, y: -maxOffsetY), animated: false) 768 | } 769 | 770 | switch abs(currentOffsetY) { 771 | 772 | case let y where y > 0 && y < DCRefreshControlConstant.drawPathThreshold: 773 | 774 | refreshControlState = DCRefreshControlState.Idle 775 | 776 | self.frame = { 777 | let height = abs(currentOffsetY) 778 | let y = currentOffsetY 779 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 780 | }() 781 | 782 | self.setNeedsDisplay() 783 | 784 | case let y where y >= DCRefreshControlConstant.drawPathThreshold && y < DCRefreshControlConstant.beginRefreshingThreshold: 785 | refreshControlState = DCRefreshControlState.Charging 786 | 787 | self.frame = { 788 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 789 | let y = currentOffsetY 790 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 791 | }() 792 | 793 | self.setNeedsDisplay() 794 | 795 | case let y where y >= DCRefreshControlConstant.beginRefreshingThreshold: 796 | // refreshControlState = DCRefreshControlState.Refreshing 797 | 798 | self.frame = { 799 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 800 | let y = currentOffsetY 801 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 802 | }() 803 | 804 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 805 | mirrorSuperView.setContentOffset(CGPoint(x: 0, y: -maxOffsetY), animated: false) 806 | 807 | default: 808 | return 809 | } 810 | 811 | } 812 | 813 | } 814 | 815 | // MARK: - Convenience 816 | 817 | extension UIBezierPath { 818 | 819 | /** 820 | The cross point at x axis 821 | - parameter x The x axis 822 | - parameter range The range of the y axis 823 | @return The specified cross point 824 | */ 825 | func crossPointAt(x: CGFloat, range:(Int, Int)) -> CGPoint? { 826 | 827 | for y in range.0...range.1 { 828 | let point = CGPoint(x: x, y: CGFloat(y)) 829 | if self.containsPoint(point) { 830 | return point 831 | } 832 | } 833 | return nil 834 | 835 | } 836 | 837 | /** 838 | The cross point at x axis 839 | - parameter x The x axis 840 | @return The specified cross point 841 | */ 842 | func crossPointAt(x: CGFloat) -> CGPoint? { 843 | 844 | let upperBounds = Int(self.bounds.origin.y + self.bounds.size.height) 845 | let lowerBounds = Int(self.topPoint.y) 846 | 847 | return self.crossPointAt(x, range: (lowerBounds, upperBounds)) 848 | } 849 | 850 | /// The top point in a path 851 | var topPoint:CGPoint { 852 | return CGPoint(x: self.bounds.size.width/2, y: self.bounds.origin.y) 853 | } 854 | 855 | } 856 | 857 | extension CALayer { 858 | 859 | /// The middle point in a layer 860 | var middle:CGPoint { 861 | return CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2) 862 | } 863 | 864 | /// The layer's center 865 | var center:CGPoint { 866 | return CGPoint(x: self.frame.origin.x + self.frame.size.width/2, y: self.frame.origin.y + self.frame.size.height/2) 867 | } 868 | 869 | /// Set the layer's center 870 | func setCenter(point: CGPoint) { 871 | self.frame.origin = CGPoint(x: point.x - self.frame.size.width/2, y: point.y - self.frame.size.height/2) 872 | } 873 | 874 | } 875 | 876 | // MARK: - Dynamic property in runtime 877 | 878 | extension UIScrollView { 879 | 880 | private struct AssociatedKey { 881 | static var dcRefreshControlName = "dcRefreshControlName" 882 | } 883 | 884 | var dcRefreshControl:DCRefreshControl? { 885 | set { 886 | if let newValue = newValue { 887 | self.addSubview(newValue) 888 | objc_setAssociatedObject(self, AssociatedKey.dcRefreshControlName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 889 | } 890 | } 891 | get { 892 | guard let refreshControl = objc_getAssociatedObject(self, &AssociatedKey.dcRefreshControlName) as? DCRefreshControl else { return nil } 893 | return refreshControl 894 | } 895 | } 896 | 897 | } 898 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | DCPullRefresh 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example (PullToRefresh)/Example (PullToRefresh)/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example (PullToRefresh) 4 | // 5 | // Created by tang dixi on 9/7/2016. 6 | // Copyright © 2016 Tangdixi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var tableView: UITableView! 14 | var dataSource = [1] 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view, typically from a nib. 19 | 20 | tableView.dcRefreshControl = DCRefreshControl { 21 | // do something... 22 | 23 | sleep(3) 24 | 25 | self.dataSource = [1, 2, 3, 4, 5 ,6, 7] 26 | self.tableView.reloadData() 27 | 28 | } 29 | 30 | 31 | } 32 | 33 | override func viewDidAppear(animated: Bool) { 34 | super.viewDidAppear(animated) 35 | } 36 | 37 | override func didReceiveMemoryWarning() { 38 | super.didReceiveMemoryWarning() 39 | // Dispose of any resources that can be recreated. 40 | } 41 | 42 | } 43 | 44 | extension ViewController: UITableViewDataSource { 45 | 46 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return dataSource.count 48 | } 49 | 50 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) 52 | cell.textLabel?.text = String(dataSource[indexPath.row]) 53 | return cell 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tangdixi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCPullRefresh 2 | 3 | ![Gif](https://raw.githubusercontent.com/Tangdixi/DCPullRefresh/master/ScreenShot/1.gif) 4 | 5 | I saw this amazing design from [Dribble](https://dribbble.com/shots/1797373-Pull-Down-To-Refresh). 6 | 7 | ## Install with CocoaPods 8 | 9 | ```bash 10 | pod 'DCPullRefresh', '~> 1.0' 11 | ``` 12 | 13 | ## How to use 14 | 15 | It's simple, you just need: 16 | 17 | ```Swift 18 | tableView.dcRefreshControl = DCRefreshControl { 19 | 20 | // Updating related code here 21 | // ...... 22 | 23 | self.tableView.reloadData() 24 | 25 | } 26 | ``` 27 | 28 | ### Todo 29 | 30 | * More property 31 | * Add UICollectionView support 32 | * Make animation more smooth 33 | 34 | ## Issues, Bugs, Suggestions 35 | 36 | Open an [issue](https://github.com/Tangdixi/DCPullRefresh/issues) 37 | 38 | ## License 39 | 40 | **DCPullRefresh** is available under the MIT license. See the LICENSE file for more info. 41 | 42 | -------------------------------------------------------------------------------- /ScreenShot/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tangdixi/DCPullRefresh/d5143173e9c700a2397ebede8f137f43c0f78054/ScreenShot/1.gif -------------------------------------------------------------------------------- /Source/DCPullRefresh.h: -------------------------------------------------------------------------------- 1 | // 2 | // DCPullRefresh.h 3 | // DCPullRefresh 4 | // 5 | // Created by Rémy DA COSTA FARO on 26/08/2016. 6 | // Copyright © 2016 Tangdixi Tangdixi. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DCPullRefresh. 12 | FOUNDATION_EXPORT double DCPullRefreshVersionNumber; 13 | 14 | //! Project version string for DCPullRefresh. 15 | FOUNDATION_EXPORT const unsigned char DCPullRefreshVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Source/DCPullRefresh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DCPullRefresh.swift 3 | // Example (PullToRefresh) 4 | // 5 | // Created by tang dixi on 9/7/2016. 6 | // Copyright © 2016 Tangdixi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias DCRefreshControlHander = (()->Void) 12 | 13 | // MARK: - Refresh Status 14 | 15 | enum DCRefreshControlState { 16 | 17 | case Idle 18 | case Charging 19 | case Refreshing 20 | case Dismissing 21 | case End 22 | 23 | } 24 | 25 | // MARK: - Constants 26 | enum DCRefreshControlConstant { 27 | 28 | static let drawPathThreshold = CGFloat(64) 29 | static let beginRefreshingThreshold = CGFloat(116) 30 | static let color = UIColor(red: 140/255, green: 145/255, blue: 176/255, alpha: 1.0) 31 | 32 | static let ballLayerTransformKeyFrame = (11, 16) 33 | static let ballLayerTransformLastKeyFrame = 17 34 | 35 | static let circlePathLayerTransformKeyFrame = (17, 60) 36 | 37 | static let ballLayerDismissKeyFrame = (8, 30) 38 | 39 | static let backgroundWillDismissKeyFrame = (31, 33) 40 | 41 | } 42 | 43 | // MARK: - Constructor 44 | 45 | class DCRefreshControl: UIView { 46 | 47 | // MARK: - Fetch some properties from superView 48 | private weak var mirrorSuperView:UIScrollView! 49 | private var originContentInset:UIEdgeInsets! 50 | private var currentOffsetY = CGFloat(0) 51 | private var panGestureRecognizer:UIPanGestureRecognizer! 52 | 53 | // MARK: - Animations 54 | private var isAnimating = false 55 | private var refreshControlState = DCRefreshControlState.Idle 56 | 57 | // MARK: - Display link 58 | private var frameCount:Int = 0 59 | private var displayLink:CADisplayLink! 60 | 61 | // MARK: - Background path 62 | 63 | /// The background color 64 | var color:UIColor! 65 | private var controlPointAssociateView:UIView! 66 | private var controlPoint:CGPoint! 67 | 68 | // MARK: - Ball layer animation 69 | private var ballLayer:CAShapeLayer! 70 | private var transformPathAssociatePoint:CGPoint! 71 | 72 | // MARK: - Circle layer animation 73 | private var circlePathLayer:CAShapeLayer! 74 | 75 | // MARK: - Dismiss animation 76 | private var dismissAnimationAssociateView:UIView! 77 | private var dismissAnimationAsscciatePoint:CGPoint! 78 | private var fakeBackgroundView:UIView! 79 | 80 | // MARK: - Refresh completion 81 | private var queue:NSOperationQueue! 82 | var refreshHandler:DCRefreshControlHander? = nil 83 | 84 | // MARK: - Initialization 85 | override init(frame: CGRect) { 86 | super.init(frame: frame) 87 | self.opaque = false 88 | self.backgroundColor = UIColor.clearColor() 89 | } 90 | 91 | required init?(coder aDecoder: NSCoder) { 92 | fatalError("init(coder:) has not been implemented") 93 | } 94 | 95 | convenience init(color:UIColor = DCRefreshControlConstant.color, refreshHandler: DCRefreshControlHander) { 96 | self.init(frame: CGRectZero) 97 | self.refreshHandler = refreshHandler 98 | self.color = color 99 | } 100 | 101 | // MARK: - Life cycle 102 | override func willMoveToSuperview(newSuperview: UIView?) { 103 | 104 | self.clipsToBounds = true 105 | 106 | super.willMoveToSuperview(newSuperview) 107 | 108 | // Make sure the superView is a scrollView 109 | // 110 | guard let newSuperview = newSuperview as? UIScrollView else { 111 | 112 | removeObservers() 113 | return 114 | 115 | } 116 | 117 | mirrorSuperView = newSuperview 118 | mirrorSuperView.alwaysBounceVertical = true 119 | 120 | panGestureRecognizer = mirrorSuperView.panGestureRecognizer 121 | 122 | self.frame = CGRect(origin: CGPointZero, size: CGSize(width: mirrorSuperView.frame.width, height: 0)) 123 | 124 | configureObservers() 125 | 126 | } 127 | 128 | // MARK: - Drawing 129 | override func drawRect(rect: CGRect) { 130 | 131 | /* Draw the main background */ 132 | 133 | let path = UIBezierPath() 134 | path.moveToPoint(CGPointZero) 135 | 136 | switch refreshControlState { 137 | case .Idle: 138 | path.addLineToPoint(CGPoint(x: 0, y: self.frame.size.height)) 139 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height)) 140 | case .Charging: 141 | controlPoint = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold) 142 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 143 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 144 | 145 | case .Refreshing: 146 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 147 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 148 | 149 | case .Dismissing: 150 | 151 | let backgroundWillDismiss = (frameCount >= DCRefreshControlConstant.backgroundWillDismissKeyFrame.0) && (frameCount < DCRefreshControlConstant.backgroundWillDismissKeyFrame.1) 152 | 153 | if backgroundWillDismiss == true { 154 | 155 | if frameCount == DCRefreshControlConstant.backgroundWillDismissKeyFrame.0 { 156 | ballLayer.opacity = 0 157 | } 158 | 159 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 160 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: CGPoint(x: controlPoint.x, y: controlPoint.y+CGFloat(frameCount-DCRefreshControlConstant.backgroundWillDismissKeyFrame.0)*6)) 161 | break 162 | } 163 | 164 | let backgroundDismissing = (frameCount > DCRefreshControlConstant.backgroundWillDismissKeyFrame.1) 165 | 166 | if backgroundDismissing == true { 167 | 168 | displayLink.invalidate() 169 | displayLink = nil 170 | 171 | fakeBackgroundView = { 172 | 173 | let view = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: abs(currentOffsetY))) 174 | view.backgroundColor = color 175 | return view 176 | 177 | }() 178 | self.addSubview(fakeBackgroundView) 179 | 180 | UIView.animateWithDuration(0.7, 181 | animations: { 182 | 183 | self.mirrorSuperView.contentInset = self.originContentInset 184 | self.fakeBackgroundView.alpha = 0 185 | 186 | }, 187 | completion: { finished in 188 | 189 | self.dismissAnimationDidEnd() 190 | 191 | }) 192 | 193 | return 194 | } 195 | 196 | path.addLineToPoint(CGPoint(x: 0, y: abs(currentOffsetY))) 197 | path.addQuadCurveToPoint(CGPoint(x: self.frame.size.width, y: abs(currentOffsetY)), controlPoint: controlPoint) 198 | 199 | default: 200 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height)) 201 | } 202 | 203 | path.addLineToPoint(CGPoint(x: self.frame.size.width, y: 0)) 204 | path.closePath() 205 | 206 | let context = UIGraphicsGetCurrentContext() 207 | CGContextAddPath(context, path.CGPath) 208 | self.color.set() 209 | CGContextFillPath(context) 210 | 211 | /* Configure the frame of the refresh animtion */ 212 | 213 | if refreshControlState == .Refreshing { 214 | 215 | /* Ball layer begin transforming */ 216 | let ballLayerTransforming = (frameCount > DCRefreshControlConstant.ballLayerTransformKeyFrame.0) && (frameCount <= DCRefreshControlConstant.ballLayerTransformKeyFrame.1) 217 | 218 | if ballLayerTransforming == true { 219 | 220 | /* Disable the implicit animation in CALayer */ 221 | CATransaction.setDisableActions(true) 222 | 223 | /* There are 12, 9, 6, 3 and finally it stay at abs(currentOffsetY)/2 */ 224 | ballLayer.setCenter(CGPoint(x: controlPoint.x, y: abs(currentOffsetY)*(3/5)-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1)) 225 | 226 | if transformPathAssociatePoint == nil { 227 | transformPathAssociatePoint = ballLayer.center 228 | } 229 | 230 | let topLeftPoint:CGPoint = { 231 | let x = self.frame.size.width/2-sqrt(CGFloat(powf(Float(ballLayer.frame.size.width/2), 2)-powf(Float(transformPathAssociatePoint.y-ballLayer.center.y), 2))) 232 | let y = transformPathAssociatePoint.y 233 | 234 | return CGPoint(x: x, y: y) 235 | }() 236 | 237 | let bottomLeftPoint:CGPoint = { 238 | 239 | let x = min(topLeftPoint.x - (CGFloat(DCRefreshControlConstant.ballLayerTransformKeyFrame.1-frameCount)*3), self.ballLayer.frame.origin.x-5) 240 | 241 | let lowerBounds = Int((abs(currentOffsetY)+controlPoint.y)/2) 242 | let upperBounds = Int(abs(currentOffsetY)) 243 | guard let point = path.crossPointAt(x, range: (lowerBounds, upperBounds)) else { fatalError() } 244 | 245 | return CGPoint(x: point.x, y: point.y+2) 246 | 247 | }() 248 | let bottomRightPoint = CGPoint(x: self.frame.size.width - bottomLeftPoint.x, y: bottomLeftPoint.y) 249 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 250 | 251 | let leftControlPoint = CGPoint(x: topLeftPoint.x+CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*3 , y: bottomLeftPoint.y-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1.8) 252 | let rightControlPoint = CGPoint(x: topRightPoint.x-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*3, y: bottomLeftPoint.y-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1.8) 253 | 254 | // print("\(topLeftPoint)\n ||\(leftControlPoint)\n\(bottomLeftPoint)\n\(topRightPoint)\n ||\(rightControlPoint)\n\(bottomRightPoint)\n") 255 | 256 | let path = UIBezierPath() 257 | path.moveToPoint(topLeftPoint) 258 | path.addQuadCurveToPoint(bottomLeftPoint, controlPoint: leftControlPoint) 259 | path.addLineToPoint(bottomRightPoint) 260 | path.addQuadCurveToPoint(topRightPoint, controlPoint: rightControlPoint) 261 | path.closePath() 262 | 263 | let context = UIGraphicsGetCurrentContext() 264 | CGContextAddPath(context, path.CGPath) 265 | UIColor.whiteColor().set() 266 | CGContextFillPath(context) 267 | 268 | /* 269 | Debug: 270 | UIColor.redColor().setStroke() 271 | path.stroke() 272 | */ 273 | } 274 | 275 | /* Ball layer transform completed */ 276 | let ballLayerDidTransformed = (frameCount == DCRefreshControlConstant.ballLayerTransformLastKeyFrame) 277 | 278 | if ballLayerDidTransformed == true { 279 | 280 | CATransaction.setDisableActions(true) 281 | ballLayer.setCenter(CGPoint(x: controlPoint.x, y: abs(currentOffsetY)*(3/5)-CGFloat(frameCount-DCRefreshControlConstant.ballLayerTransformKeyFrame.0)*1)) 282 | 283 | let bottomLeft:CGPoint = { 284 | let x = ballLayer.frame.origin.x-5 285 | let lowerBounds = Int((abs(currentOffsetY)+controlPoint.y)/2) 286 | let upperBounds = Int(abs(currentOffsetY)) 287 | guard let point = path.crossPointAt(x, range: (lowerBounds, upperBounds)) else { fatalError() } 288 | return CGPoint(x: point.x, y: point.y+1) 289 | }() 290 | let bottomRight = CGPoint(x: self.frame.size.width-bottomLeft.x, y: bottomLeft.y) 291 | let topPoint = CGPoint(x: self.frame.size.width/2, y: bottomLeft.y-6) 292 | 293 | let path = UIBezierPath() 294 | path.moveToPoint(topPoint) 295 | path.addLineToPoint(bottomLeft) 296 | path.addLineToPoint(bottomRight) 297 | path.closePath() 298 | 299 | let context = UIGraphicsGetCurrentContext() 300 | CGContextAddPath(context, path.CGPath) 301 | UIColor.whiteColor().set() 302 | CGContextFillPath(context) 303 | 304 | } 305 | 306 | /* Circle layer begin transforming */ 307 | let circleLayerTransforming = (frameCount >= DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0) && (frameCount <= DCRefreshControlConstant.circlePathLayerTransformKeyFrame.1) 308 | 309 | if circleLayerTransforming == true { 310 | 311 | circlePathLayer.setCenter(ballLayer.center) 312 | 313 | let percentage = CGFloat(frameCount-DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0+1) 314 | let total = CGFloat(DCRefreshControlConstant.circlePathLayerTransformKeyFrame.1-DCRefreshControlConstant.circlePathLayerTransformKeyFrame.0) 315 | 316 | CATransaction.setDisableActions(true) 317 | circlePathLayer.strokeEnd = percentage/total 318 | 319 | } 320 | 321 | } 322 | 323 | /* Configure the frame of the dismiss animation */ 324 | 325 | if refreshControlState == .Dismissing { 326 | 327 | CATransaction.setDisableActions(true) 328 | ballLayer.setCenter(dismissAnimationAsscciatePoint) 329 | 330 | /* Ball layer start dismiss */ 331 | 332 | let startTransform = (frameCount == DCRefreshControlConstant.ballLayerDismissKeyFrame.0) 333 | 334 | if startTransform == true { 335 | 336 | let topLeftPoint = CGPoint(x: ballLayer.frame.origin.x, y: ballLayer.center.y+4) 337 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 338 | let topControlPoint = CGPoint(x: ballLayer.center.x, y: ballLayer.center.y+ballLayer.frame.size.height*1.2) 339 | 340 | let bottomLeftPoint = CGPoint(x: ballLayer.frame.origin.x, y: abs(currentOffsetY)) 341 | let bottomRightPoint = CGPoint(x: self.frame.size.width-bottomLeftPoint.x, y: abs(currentOffsetY)) 342 | let bottomTopPoint = CGPoint(x: ballLayer.center.x, y: abs(currentOffsetY)-4) 343 | 344 | let path = UIBezierPath() 345 | path.moveToPoint(topLeftPoint) 346 | path.addQuadCurveToPoint(topRightPoint, controlPoint: topControlPoint) 347 | path.closePath() 348 | 349 | path.moveToPoint(bottomTopPoint) 350 | path.addLineToPoint(bottomLeftPoint) 351 | path.addLineToPoint(bottomRightPoint) 352 | path.closePath() 353 | 354 | let context = UIGraphicsGetCurrentContext() 355 | CGContextAddPath(context, path.CGPath) 356 | UIColor.whiteColor().set() 357 | CGContextFillPath(context) 358 | 359 | } 360 | 361 | /* Ball layer dismissing */ 362 | 363 | let transforming = (frameCount > DCRefreshControlConstant.ballLayerDismissKeyFrame.0) && (frameCount <= DCRefreshControlConstant.ballLayerDismissKeyFrame.1) 364 | 365 | if transforming == true { 366 | 367 | let topLeftPoint:CGPoint = { 368 | 369 | if frameCount >= 16 { 370 | let x = ballLayer.center.x-ballLayer.frame.size.width/2/sqrt(2) 371 | let y = ballLayer.center.y-ballLayer.frame.size.width/2/sqrt(2) 372 | return CGPoint(x: x, y: y) 373 | } 374 | 375 | return CGPoint(x: ballLayer.frame.origin.x, y: ballLayer.center.y) 376 | }() 377 | 378 | let bottomLeftPoint:CGPoint = { 379 | 380 | let x = topLeftPoint.x-CGFloat(frameCount-DCRefreshControlConstant.ballLayerDismissKeyFrame.0)*2-15 381 | let y = abs(currentOffsetY) 382 | 383 | return CGPoint(x: x, y: y) 384 | 385 | }() 386 | 387 | let bottomRightPoint = CGPoint(x: self.frame.size.width-bottomLeftPoint.x, y: bottomLeftPoint.y) 388 | let topRightPoint = CGPoint(x: self.frame.size.width-topLeftPoint.x, y: topLeftPoint.y) 389 | 390 | let leftControlPoint:CGPoint = { 391 | 392 | if frameCount == DCRefreshControlConstant.ballLayerDismissKeyFrame.0+1 { 393 | return CGPoint(x: topLeftPoint.x+15, y: abs(currentOffsetY+5)) 394 | } 395 | let x = (topLeftPoint.x+bottomLeftPoint.x)/2+8 396 | let y = (topLeftPoint.y+bottomLeftPoint.y)/2+CGFloat(frameCount-DCRefreshControlConstant.ballLayerDismissKeyFrame.0)/1.5 397 | 398 | return CGPoint(x: x, y: y) 399 | 400 | }() 401 | 402 | let rightControlPoint = CGPoint(x: self.frame.size.width-leftControlPoint.x, y: leftControlPoint.y) 403 | 404 | let path = UIBezierPath() 405 | path.moveToPoint(topLeftPoint) 406 | path.addQuadCurveToPoint(bottomLeftPoint, controlPoint: leftControlPoint) 407 | path.addLineToPoint(bottomRightPoint) 408 | path.addQuadCurveToPoint(topRightPoint, controlPoint: rightControlPoint) 409 | path.closePath() 410 | 411 | 412 | let context = UIGraphicsGetCurrentContext() 413 | CGContextAddPath(context, path.CGPath) 414 | UIColor.whiteColor().set() 415 | CGContextFillPath(context) 416 | 417 | // UIColor.redColor().setStroke() 418 | // path.stroke() 419 | 420 | } 421 | 422 | } 423 | 424 | } 425 | 426 | // MARK: - Ball Layer Animation 427 | func performBallLayerAnimation() { 428 | 429 | isAnimating = true 430 | 431 | ballLayerAnimationWillAppear() 432 | 433 | UIView.animateWithDuration(0.7, delay: 0, usingSpringWithDamping: 0.12, initialSpringVelocity: 2, options: .CurveEaseOut, 434 | animations: { 435 | 436 | self.controlPointAssociateView.center = CGPoint(x: self.frame.size.width/2, y: abs(self.currentOffsetY)) 437 | 438 | }, 439 | completion: { finished in 440 | 441 | let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC))) 442 | dispatch_after(delayTime, dispatch_get_main_queue()) { 443 | 444 | self.ballLayerAnimationDidEnd() 445 | 446 | let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC))) 447 | dispatch_after(delayTime, dispatch_get_main_queue()) { 448 | 449 | self.performCircleLayerAnimation() 450 | 451 | } 452 | 453 | } 454 | 455 | }) 456 | 457 | } 458 | 459 | func ballLayerAnimationWillAppear() { 460 | 461 | if displayLink == nil { 462 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler)) 463 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) 464 | 465 | frameCount = 0 466 | } 467 | 468 | controlPointAssociateView = { 469 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 2, height: 2)) 470 | view.center = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height) 471 | view.backgroundColor = UIColor.clearColor() 472 | return view 473 | }() 474 | self.addSubview(controlPointAssociateView) 475 | 476 | ballLayer = { 477 | let layer = CAShapeLayer() 478 | layer.frame = CGRect(x: 0, y: 0, width: 38, height: 38) 479 | layer.setCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2)) 480 | 481 | let path = UIBezierPath() 482 | path.addArcWithCenter(layer.middle, radius: 19, startAngle: 0, endAngle: CGFloat(M_PI)*2, clockwise: true) 483 | 484 | layer.path = path.CGPath 485 | layer.fillColor = UIColor.whiteColor().CGColor 486 | 487 | return layer 488 | }() 489 | self.layer.addSublayer(ballLayer) 490 | 491 | circlePathLayer = { 492 | 493 | let layer = CAShapeLayer() 494 | layer.frame = CGRect(x: 0, y: 0, width: 44, height: 44) 495 | layer.setCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2)) 496 | 497 | let path = UIBezierPath() 498 | path.addArcWithCenter(layer.middle, radius: 22, startAngle: CGFloat(M_PI)/2, endAngle: CGFloat(M_PI)*5/2, clockwise: true) 499 | 500 | layer.path = path.CGPath 501 | layer.lineWidth = 2 502 | layer.fillColor = UIColor.clearColor().CGColor 503 | layer.strokeColor = UIColor.whiteColor().CGColor 504 | 505 | return layer 506 | }() 507 | self.layer.addSublayer(circlePathLayer) 508 | 509 | } 510 | 511 | func ballLayerAnimationDidEnd() { 512 | 513 | self.displayLink.invalidate() 514 | self.displayLink = nil 515 | 516 | 517 | } 518 | 519 | func displayLinkHandler() { 520 | 521 | if refreshControlState == .Refreshing { 522 | guard let controlPointLayer = controlPointAssociateView.layer.presentationLayer() else { return } 523 | controlPoint = controlPointLayer.center 524 | 525 | frameCount += 1 526 | 527 | self.setNeedsDisplay() 528 | 529 | return 530 | } 531 | 532 | if refreshControlState == .Dismissing { 533 | 534 | guard let dismissAnimationAsscciatePointLayer = dismissAnimationAssociateView.layer.presentationLayer() else { return } 535 | dismissAnimationAsscciatePoint = dismissAnimationAsscciatePointLayer.center 536 | 537 | frameCount += 1 538 | 539 | print("\(frameCount): \(dismissAnimationAsscciatePoint)") 540 | 541 | self.setNeedsDisplay() 542 | 543 | return 544 | } 545 | 546 | } 547 | 548 | // MARK: - Circle Layer Animation 549 | func performCircleLayerAnimation() { 550 | 551 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 552 | scaleAnimation.fromValue = 1.0 553 | scaleAnimation.toValue = 1.5 554 | 555 | let alphaAnimation = CABasicAnimation(keyPath: "opacity") 556 | alphaAnimation.fromValue = 1.0 557 | alphaAnimation.toValue = 0 558 | 559 | let animationGroup = CAAnimationGroup() 560 | animationGroup.duration = 0.7 561 | animationGroup.animations = [scaleAnimation, alphaAnimation] 562 | animationGroup.repeatCount = Float.infinity 563 | animationGroup.fillMode = kCAFillModeForwards 564 | 565 | circlePathLayer.addAnimation(animationGroup, forKey: nil) 566 | 567 | guard let refreshHandler = refreshHandler else { fatalError() } 568 | performRefreshHandler(refreshHandler) 569 | 570 | } 571 | 572 | func circleLayerAnimationDidEnd() { 573 | 574 | circlePathLayer.removeAllAnimations() 575 | circlePathLayer.removeFromSuperlayer() 576 | 577 | performDismissAnimation() 578 | 579 | } 580 | 581 | // MARK: - Layer dismiss Animation 582 | 583 | func performDismissAnimation() { 584 | 585 | refreshControlState = .Dismissing 586 | 587 | dismissAnimationWillAppear() 588 | 589 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 590 | scaleAnimation.fromValue = 1.0 591 | scaleAnimation.toValue = 1.5 592 | 593 | let alphaAnimation = CABasicAnimation(keyPath: "opacity") 594 | alphaAnimation.fromValue = 1.0 595 | alphaAnimation.toValue = 0 596 | 597 | let animationGroup = CAAnimationGroup() 598 | animationGroup.duration = 0.7 599 | animationGroup.animations = [scaleAnimation, alphaAnimation] 600 | animationGroup.repeatCount = Float.infinity 601 | animationGroup.fillMode = kCAFillModeForwards 602 | 603 | circlePathLayer.addAnimation(animationGroup, forKey: nil) 604 | 605 | UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .CurveLinear, 606 | animations: { 607 | 608 | self.dismissAnimationAssociateView.center = CGPoint(x:self.frame.size.width/2, y:abs(self.currentOffsetY)+self.ballLayer.frame.size.height/2) 609 | 610 | print(abs(self.currentOffsetY)+self.ballLayer.frame.size.height/2) 611 | 612 | }, 613 | completion: { finished in 614 | self.dismissAnimationDidEnd() 615 | }) 616 | 617 | 618 | } 619 | 620 | func dismissAnimationWillAppear() { 621 | 622 | if displayLink == nil { 623 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler)) 624 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) 625 | 626 | frameCount = 0 627 | } 628 | 629 | dismissAnimationAssociateView = { 630 | 631 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 2, height: 2)) 632 | view.center = ballLayer.center 633 | view.backgroundColor = UIColor.clearColor() 634 | return view 635 | 636 | }() 637 | self.addSubview(dismissAnimationAssociateView) 638 | 639 | } 640 | 641 | func dismissAnimationDidEnd() { 642 | 643 | /* Everything back to normal */ 644 | 645 | isAnimating = false 646 | refreshControlState = .Idle 647 | currentOffsetY = 0 648 | 649 | ballLayer.removeFromSuperlayer() 650 | circlePathLayer.removeFromSuperlayer() 651 | controlPointAssociateView.removeFromSuperview() 652 | dismissAnimationAssociateView.removeFromSuperview() 653 | 654 | } 655 | 656 | // MARK: - The User's completion handler 657 | func performRefreshHandler(handler:DCRefreshControlHander) { 658 | 659 | if queue == nil { 660 | queue = NSOperationQueue() 661 | } 662 | 663 | /* Perform the handler in background queue */ 664 | queue.addOperationWithBlock { 665 | handler() 666 | /* Always up date the UI in main queue */ 667 | NSOperationQueue.mainQueue().addOperationWithBlock { 668 | self.circleLayerAnimationDidEnd() 669 | } 670 | } 671 | 672 | } 673 | 674 | // MARK: - KVO 675 | func configureObservers() { 676 | 677 | let options = NSKeyValueObservingOptions([.Old, .New]) 678 | self.mirrorSuperView.addObserver(self, forKeyPath: "contentInset", options: options, context: nil) 679 | self.mirrorSuperView.addObserver(self, forKeyPath: "contentOffset", options: options, context: nil) 680 | self.panGestureRecognizer.addObserver(self, forKeyPath: "state", options: options, context: nil) 681 | 682 | } 683 | 684 | func removeObservers() { 685 | 686 | self.superview?.removeObserver(self, forKeyPath: "contentInset") 687 | self.superview?.removeObserver(self, forKeyPath: "contentOffset") 688 | self.panGestureRecognizer.removeObserver(self, forKeyPath: "state") 689 | 690 | } 691 | 692 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { 693 | 694 | guard let keyPath = keyPath else { return } 695 | guard let change = change else { return } 696 | 697 | if keyPath == "contentInset" { 698 | 699 | guard let oldEdgeInset = change["old"]?.UIEdgeInsetsValue() else { return } 700 | guard let newEdgeInset = change["new"]?.UIEdgeInsetsValue() else { return } 701 | 702 | let condition = (originContentInset == nil) && (oldEdgeInset.top != newEdgeInset.top) 703 | 704 | if condition { 705 | originContentInset = change["new"]?.UIEdgeInsetsValue() 706 | } 707 | 708 | } 709 | 710 | if keyPath == "contentOffset" { 711 | 712 | scrollViewContentOffsetDidChanged(change) 713 | } 714 | 715 | if keyPath == "state" { 716 | 717 | panGestureRecognizerStateDidChanged(change) 718 | 719 | } 720 | 721 | } 722 | 723 | func panGestureRecognizerStateDidChanged(change:[String: AnyObject]) { 724 | 725 | guard let newState = change["new"]?.integerValue else { return } 726 | 727 | let condition = (self.refreshControlState == .Refreshing) && (newState == UIGestureRecognizerState.Ended.rawValue) && (isAnimating == false) 728 | 729 | if condition == true { 730 | 731 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 732 | mirrorSuperView.contentInset = UIEdgeInsets(top: maxOffsetY, left: 0, bottom: 0, right: 0) 733 | 734 | performBallLayerAnimation() 735 | } 736 | 737 | } 738 | 739 | func scrollViewContentOffsetDidChanged(change:[String: AnyObject]) { 740 | 741 | guard let originContentInset = originContentInset else { return } 742 | 743 | /* For example, a tableView will make a -64 offset in Y axis if there is a navigation bar */ 744 | 745 | currentOffsetY = originContentInset.top + mirrorSuperView.contentOffset.y 746 | 747 | /* When the tableView scroll to top, return immediately */ 748 | 749 | guard currentOffsetY < 0 else { return } 750 | 751 | /* Prevent some naughty guy */ 752 | 753 | if refreshControlState == .Refreshing { 754 | self.frame = { 755 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 756 | let y = currentOffsetY 757 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 758 | }() 759 | 760 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 761 | mirrorSuperView.setContentOffset(CGPoint(x: 0, y: -maxOffsetY), animated: false) 762 | } 763 | 764 | switch abs(currentOffsetY) { 765 | 766 | case let y where y > 0 && y < DCRefreshControlConstant.drawPathThreshold: 767 | 768 | refreshControlState = DCRefreshControlState.Idle 769 | 770 | self.frame = { 771 | let height = abs(currentOffsetY) 772 | let y = currentOffsetY 773 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 774 | }() 775 | 776 | self.setNeedsDisplay() 777 | 778 | case let y where y >= DCRefreshControlConstant.drawPathThreshold && y < DCRefreshControlConstant.beginRefreshingThreshold: 779 | refreshControlState = DCRefreshControlState.Charging 780 | 781 | self.frame = { 782 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 783 | let y = currentOffsetY 784 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 785 | }() 786 | 787 | self.setNeedsDisplay() 788 | 789 | case let y where y >= DCRefreshControlConstant.beginRefreshingThreshold: 790 | refreshControlState = DCRefreshControlState.Refreshing 791 | 792 | self.frame = { 793 | let height = abs(currentOffsetY) + abs(currentOffsetY) - DCRefreshControlConstant.drawPathThreshold 794 | let y = currentOffsetY 795 | return CGRectMake(0, y, mirrorSuperView.frame.width, height) 796 | }() 797 | 798 | let maxOffsetY = DCRefreshControlConstant.beginRefreshingThreshold + originContentInset.top 799 | mirrorSuperView.setContentOffset(CGPoint(x: 0, y: -maxOffsetY), animated: false) 800 | 801 | default: 802 | return 803 | } 804 | 805 | } 806 | 807 | } 808 | 809 | // MARK: - Convenience 810 | 811 | extension UIBezierPath { 812 | 813 | /** 814 | The cross point at x axis 815 | - parameter x The x axis 816 | - parameter range The range of the y axis 817 | @return The specified cross point 818 | */ 819 | func crossPointAt(x: CGFloat, range:(Int, Int)) -> CGPoint? { 820 | 821 | for y in range.0...range.1 { 822 | let point = CGPoint(x: x, y: CGFloat(y)) 823 | if self.containsPoint(point) { 824 | return point 825 | } 826 | } 827 | return nil 828 | 829 | } 830 | 831 | /** 832 | The cross point at x axis 833 | - parameter x The x axis 834 | @return The specified cross point 835 | */ 836 | func crossPointAt(x: CGFloat) -> CGPoint? { 837 | 838 | let upperBounds = Int(self.bounds.origin.y + self.bounds.size.height) 839 | let lowerBounds = Int(self.topPoint.y) 840 | 841 | return self.crossPointAt(x, range: (lowerBounds, upperBounds)) 842 | } 843 | 844 | /// The top point in a path 845 | var topPoint:CGPoint { 846 | return CGPoint(x: self.bounds.size.width/2, y: self.bounds.origin.y) 847 | } 848 | 849 | } 850 | 851 | extension CALayer { 852 | 853 | /// The middle point in a layer 854 | var middle:CGPoint { 855 | return CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2) 856 | } 857 | 858 | /// The layer's center 859 | var center:CGPoint { 860 | return CGPoint(x: self.frame.origin.x + self.frame.size.width/2, y: self.frame.origin.y + self.frame.size.height/2) 861 | } 862 | 863 | /// Set the layer's center 864 | func setCenter(point: CGPoint) { 865 | self.frame.origin = CGPoint(x: point.x - self.frame.size.width/2, y: point.y - self.frame.size.height/2) 866 | } 867 | 868 | } 869 | 870 | // MARK: - Dynamic property in runtime 871 | 872 | extension UIScrollView { 873 | 874 | private struct AssociatedKey { 875 | static var dcRefreshControlName = "dcRefreshControlName" 876 | } 877 | 878 | var dcRefreshControl:DCRefreshControl? { 879 | set { 880 | if let newValue = newValue { 881 | self.addSubview(newValue) 882 | objc_setAssociatedObject(self, AssociatedKey.dcRefreshControlName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 883 | } 884 | } 885 | get { 886 | guard let refreshControl = objc_getAssociatedObject(self, &AssociatedKey.dcRefreshControlName) as? DCRefreshControl else { return nil } 887 | return refreshControl 888 | } 889 | } 890 | 891 | } 892 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------