├── .gitignore ├── Assets └── MRefresh.gif ├── LICENSE ├── MRefresh.podspec ├── MRefresh.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── MRefresh.xcscheme │ └── MRefreshTests.xcscheme ├── MRefresh ├── Extensions │ └── CGPoint.swift ├── Helpers │ └── Stack.swift ├── Info.plist ├── MRefresh.h ├── PullToRefresh │ ├── AnimatableContainerView.swift │ ├── AnimatableView.swift │ ├── AnimationsFactory.swift │ ├── LayerFactory.swift │ ├── PathDrawingAnimatableView+SVGConnectedPath.swift │ ├── PathDrawingAnimatableView.swift │ ├── PullToRefreshViewConfiguration.swift │ └── UIScrollViewExtension.swift └── SVG │ ├── SVGConnectedPathFactory.swift │ ├── SVGInstruction.swift │ ├── SVGNode.swift │ ├── SVGPathFactory.swift │ ├── SVGReader.swift │ ├── SVGResizer.swift │ ├── SVGSimplifier.swift │ ├── SVGSmoother.swift │ └── UIBezierPathExtension.swift ├── MRefreshExamples ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Constraints.swift ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── MRefreshTests ├── Info.plist ├── SVGPathFactoryTests.swift ├── SVGReaderMock.swift ├── SVGReaderTests.swift ├── SVGSimplifierMock.swift └── SVGSmootherMock.swift └── README.md /.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 | -------------------------------------------------------------------------------- /Assets/MRefresh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/MRefresh/4892b4b7224557b19203522361785b7d623711e4/Assets/MRefresh.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mikhail Rakhmanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MRefresh.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | 3 | spec.name = "MRefresh" 4 | spec.version = "0.2.1" 5 | spec.summary = "This pod adds pull to refresh to your views with arbitrary svg animations" 6 | spec.homepage = "https://github.com/strongself/MRefresh" 7 | spec.license = { :type => "MIT", :file => "LICENSE" } 8 | spec.author = { "Mikhail Rakhmanov" => "rakhmanov.m@gmail.com" } 9 | spec.ios.deployment_target = "11.0" 10 | spec.swift_versions = "5.0" 11 | spec.source = { :git => "https://github.com/strongself/MRefresh.git", :tag => "#{spec.version}" } 12 | 13 | spec.source_files = "MRefresh/**/*.{h,m,swift}" 14 | end 15 | -------------------------------------------------------------------------------- /MRefresh.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 961E6D3125BFFD8700D089A1 /* PathDrawingAnimatableView+SVGConnectedPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961E6D3025BFFD8700D089A1 /* PathDrawingAnimatableView+SVGConnectedPath.swift */; }; 11 | 96CA78B125BF72F200218A16 /* MRefresh.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96CA78A725BF72F200218A16 /* MRefresh.framework */; }; 12 | 96CA78B625BF72F200218A16 /* SVGReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA78B525BF72F200218A16 /* SVGReaderTests.swift */; }; 13 | 96CA78B825BF72F200218A16 /* MRefresh.h in Headers */ = {isa = PBXBuildFile; fileRef = 96CA78AA25BF72F200218A16 /* MRefresh.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14 | 96CA78CC25BF73CB00218A16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA78CB25BF73CB00218A16 /* AppDelegate.swift */; }; 15 | 96CA78CE25BF73CB00218A16 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA78CD25BF73CB00218A16 /* SceneDelegate.swift */; }; 16 | 96CA78D025BF73CB00218A16 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA78CF25BF73CB00218A16 /* ViewController.swift */; }; 17 | 96CA78D325BF73CB00218A16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 96CA78D125BF73CB00218A16 /* Main.storyboard */; }; 18 | 96CA78D525BF73CC00218A16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96CA78D425BF73CC00218A16 /* Assets.xcassets */; }; 19 | 96CA78D825BF73CC00218A16 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 96CA78D625BF73CC00218A16 /* LaunchScreen.storyboard */; }; 20 | 96CA791D25BF74B800218A16 /* AnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790825BF74B800218A16 /* AnimatableView.swift */; }; 21 | 96CA791E25BF74B800218A16 /* AnimationsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790925BF74B800218A16 /* AnimationsFactory.swift */; }; 22 | 96CA791F25BF74B800218A16 /* PathDrawingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790A25BF74B800218A16 /* PathDrawingAnimatableView.swift */; }; 23 | 96CA792025BF74B800218A16 /* AnimatableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790B25BF74B800218A16 /* AnimatableContainerView.swift */; }; 24 | 96CA792125BF74B800218A16 /* PullToRefreshViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790C25BF74B800218A16 /* PullToRefreshViewConfiguration.swift */; }; 25 | 96CA792225BF74B800218A16 /* LayerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790D25BF74B800218A16 /* LayerFactory.swift */; }; 26 | 96CA792325BF74B800218A16 /* UIScrollViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA790E25BF74B800218A16 /* UIScrollViewExtension.swift */; }; 27 | 96CA792425BF74B800218A16 /* SVGPathFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791025BF74B800218A16 /* SVGPathFactory.swift */; }; 28 | 96CA792525BF74B800218A16 /* SVGNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791125BF74B800218A16 /* SVGNode.swift */; }; 29 | 96CA792625BF74B800218A16 /* SVGResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791225BF74B800218A16 /* SVGResizer.swift */; }; 30 | 96CA792725BF74B800218A16 /* SVGReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791325BF74B800218A16 /* SVGReader.swift */; }; 31 | 96CA792825BF74B800218A16 /* UIBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791425BF74B800218A16 /* UIBezierPathExtension.swift */; }; 32 | 96CA792925BF74B800218A16 /* SVGInstruction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791525BF74B800218A16 /* SVGInstruction.swift */; }; 33 | 96CA792A25BF74B800218A16 /* SVGConnectedPathFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791625BF74B800218A16 /* SVGConnectedPathFactory.swift */; }; 34 | 96CA792B25BF74B800218A16 /* SVGSimplifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791725BF74B800218A16 /* SVGSimplifier.swift */; }; 35 | 96CA792C25BF74B800218A16 /* SVGSmoother.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791825BF74B800218A16 /* SVGSmoother.swift */; }; 36 | 96CA792D25BF74B800218A16 /* Stack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791A25BF74B800218A16 /* Stack.swift */; }; 37 | 96CA792E25BF74B800218A16 /* CGPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA791C25BF74B800218A16 /* CGPoint.swift */; }; 38 | 96CA793925BF7B8600218A16 /* Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CA793825BF7B8600218A16 /* Constraints.swift */; }; 39 | 96EEB34B25C14D1F004CBFD0 /* SVGReaderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB34A25C14D1F004CBFD0 /* SVGReaderMock.swift */; }; 40 | 96EEB35325C14DA2004CBFD0 /* SVGSimplifierMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB35225C14DA2004CBFD0 /* SVGSimplifierMock.swift */; }; 41 | 96EEB35825C14E25004CBFD0 /* SVGSmootherMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB35725C14E25004CBFD0 /* SVGSmootherMock.swift */; }; 42 | 96EEB35D25C154D4004CBFD0 /* SVGPathFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB35C25C154D4004CBFD0 /* SVGPathFactoryTests.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXContainerItemProxy section */ 46 | 96CA78B225BF72F200218A16 /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 96CA789E25BF72F200218A16 /* Project object */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 96CA78A625BF72F200218A16; 51 | remoteInfo = MRefresh; 52 | }; 53 | /* End PBXContainerItemProxy section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 961E6D3025BFFD8700D089A1 /* PathDrawingAnimatableView+SVGConnectedPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PathDrawingAnimatableView+SVGConnectedPath.swift"; sourceTree = ""; }; 57 | 96CA78A725BF72F200218A16 /* MRefresh.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MRefresh.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 96CA78AA25BF72F200218A16 /* MRefresh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MRefresh.h; sourceTree = ""; }; 59 | 96CA78AB25BF72F200218A16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 96CA78B025BF72F200218A16 /* MRefreshTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MRefreshTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 96CA78B525BF72F200218A16 /* SVGReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGReaderTests.swift; sourceTree = ""; }; 62 | 96CA78B725BF72F200218A16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | 96CA78C925BF73CB00218A16 /* MRefreshExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MRefreshExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 96CA78CB25BF73CB00218A16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 65 | 96CA78CD25BF73CB00218A16 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 66 | 96CA78CF25BF73CB00218A16 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 67 | 96CA78D225BF73CB00218A16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 68 | 96CA78D425BF73CC00218A16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 69 | 96CA78D725BF73CC00218A16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 70 | 96CA78D925BF73CC00218A16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 96CA790825BF74B800218A16 /* AnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatableView.swift; sourceTree = ""; }; 72 | 96CA790925BF74B800218A16 /* AnimationsFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationsFactory.swift; sourceTree = ""; }; 73 | 96CA790A25BF74B800218A16 /* PathDrawingAnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDrawingAnimatableView.swift; sourceTree = ""; }; 74 | 96CA790B25BF74B800218A16 /* AnimatableContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatableContainerView.swift; sourceTree = ""; }; 75 | 96CA790C25BF74B800218A16 /* PullToRefreshViewConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullToRefreshViewConfiguration.swift; sourceTree = ""; }; 76 | 96CA790D25BF74B800218A16 /* LayerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayerFactory.swift; sourceTree = ""; }; 77 | 96CA790E25BF74B800218A16 /* UIScrollViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtension.swift; sourceTree = ""; }; 78 | 96CA791025BF74B800218A16 /* SVGPathFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGPathFactory.swift; sourceTree = ""; }; 79 | 96CA791125BF74B800218A16 /* SVGNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGNode.swift; sourceTree = ""; }; 80 | 96CA791225BF74B800218A16 /* SVGResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGResizer.swift; sourceTree = ""; }; 81 | 96CA791325BF74B800218A16 /* SVGReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGReader.swift; sourceTree = ""; }; 82 | 96CA791425BF74B800218A16 /* UIBezierPathExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIBezierPathExtension.swift; sourceTree = ""; }; 83 | 96CA791525BF74B800218A16 /* SVGInstruction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGInstruction.swift; sourceTree = ""; }; 84 | 96CA791625BF74B800218A16 /* SVGConnectedPathFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGConnectedPathFactory.swift; sourceTree = ""; }; 85 | 96CA791725BF74B800218A16 /* SVGSimplifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGSimplifier.swift; sourceTree = ""; }; 86 | 96CA791825BF74B800218A16 /* SVGSmoother.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGSmoother.swift; sourceTree = ""; }; 87 | 96CA791A25BF74B800218A16 /* Stack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stack.swift; sourceTree = ""; }; 88 | 96CA791C25BF74B800218A16 /* CGPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGPoint.swift; sourceTree = ""; }; 89 | 96CA793825BF7B8600218A16 /* Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constraints.swift; sourceTree = ""; }; 90 | 96EEB34A25C14D1F004CBFD0 /* SVGReaderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGReaderMock.swift; sourceTree = ""; }; 91 | 96EEB35225C14DA2004CBFD0 /* SVGSimplifierMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGSimplifierMock.swift; sourceTree = ""; }; 92 | 96EEB35725C14E25004CBFD0 /* SVGSmootherMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGSmootherMock.swift; sourceTree = ""; }; 93 | 96EEB35C25C154D4004CBFD0 /* SVGPathFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGPathFactoryTests.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | 96CA78A425BF72F200218A16 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | 96CA78AD25BF72F200218A16 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 96CA78B125BF72F200218A16 /* MRefresh.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | 96CA78C625BF73CB00218A16 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXFrameworksBuildPhase section */ 120 | 121 | /* Begin PBXGroup section */ 122 | 96CA789D25BF72F200218A16 = { 123 | isa = PBXGroup; 124 | children = ( 125 | 96CA78A925BF72F200218A16 /* MRefresh */, 126 | 96CA78B425BF72F200218A16 /* MRefreshTests */, 127 | 96CA78CA25BF73CB00218A16 /* MRefreshExamples */, 128 | 96CA78A825BF72F200218A16 /* Products */, 129 | ); 130 | sourceTree = ""; 131 | }; 132 | 96CA78A825BF72F200218A16 /* Products */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 96CA78A725BF72F200218A16 /* MRefresh.framework */, 136 | 96CA78B025BF72F200218A16 /* MRefreshTests.xctest */, 137 | 96CA78C925BF73CB00218A16 /* MRefreshExamples.app */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 96CA78A925BF72F200218A16 /* MRefresh */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 96CA791B25BF74B800218A16 /* Extensions */, 146 | 96CA791925BF74B800218A16 /* Helpers */, 147 | 96CA790725BF74B800218A16 /* PullToRefresh */, 148 | 96CA790F25BF74B800218A16 /* SVG */, 149 | 96CA78AA25BF72F200218A16 /* MRefresh.h */, 150 | 96CA78AB25BF72F200218A16 /* Info.plist */, 151 | ); 152 | path = MRefresh; 153 | sourceTree = ""; 154 | }; 155 | 96CA78B425BF72F200218A16 /* MRefreshTests */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 96CA78B525BF72F200218A16 /* SVGReaderTests.swift */, 159 | 96CA78B725BF72F200218A16 /* Info.plist */, 160 | 96EEB34A25C14D1F004CBFD0 /* SVGReaderMock.swift */, 161 | 96EEB35225C14DA2004CBFD0 /* SVGSimplifierMock.swift */, 162 | 96EEB35725C14E25004CBFD0 /* SVGSmootherMock.swift */, 163 | 96EEB35C25C154D4004CBFD0 /* SVGPathFactoryTests.swift */, 164 | ); 165 | path = MRefreshTests; 166 | sourceTree = ""; 167 | }; 168 | 96CA78CA25BF73CB00218A16 /* MRefreshExamples */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 96CA78CB25BF73CB00218A16 /* AppDelegate.swift */, 172 | 96CA78CD25BF73CB00218A16 /* SceneDelegate.swift */, 173 | 96CA78CF25BF73CB00218A16 /* ViewController.swift */, 174 | 96CA78D125BF73CB00218A16 /* Main.storyboard */, 175 | 96CA78D425BF73CC00218A16 /* Assets.xcassets */, 176 | 96CA78D625BF73CC00218A16 /* LaunchScreen.storyboard */, 177 | 96CA78D925BF73CC00218A16 /* Info.plist */, 178 | 96CA793825BF7B8600218A16 /* Constraints.swift */, 179 | ); 180 | path = MRefreshExamples; 181 | sourceTree = ""; 182 | }; 183 | 96CA790725BF74B800218A16 /* PullToRefresh */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 96CA790825BF74B800218A16 /* AnimatableView.swift */, 187 | 96CA790925BF74B800218A16 /* AnimationsFactory.swift */, 188 | 96CA790A25BF74B800218A16 /* PathDrawingAnimatableView.swift */, 189 | 96CA790B25BF74B800218A16 /* AnimatableContainerView.swift */, 190 | 96CA790C25BF74B800218A16 /* PullToRefreshViewConfiguration.swift */, 191 | 96CA790D25BF74B800218A16 /* LayerFactory.swift */, 192 | 96CA790E25BF74B800218A16 /* UIScrollViewExtension.swift */, 193 | 961E6D3025BFFD8700D089A1 /* PathDrawingAnimatableView+SVGConnectedPath.swift */, 194 | ); 195 | path = PullToRefresh; 196 | sourceTree = ""; 197 | }; 198 | 96CA790F25BF74B800218A16 /* SVG */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 96CA791025BF74B800218A16 /* SVGPathFactory.swift */, 202 | 96CA791125BF74B800218A16 /* SVGNode.swift */, 203 | 96CA791225BF74B800218A16 /* SVGResizer.swift */, 204 | 96CA791325BF74B800218A16 /* SVGReader.swift */, 205 | 96CA791425BF74B800218A16 /* UIBezierPathExtension.swift */, 206 | 96CA791525BF74B800218A16 /* SVGInstruction.swift */, 207 | 96CA791625BF74B800218A16 /* SVGConnectedPathFactory.swift */, 208 | 96CA791725BF74B800218A16 /* SVGSimplifier.swift */, 209 | 96CA791825BF74B800218A16 /* SVGSmoother.swift */, 210 | ); 211 | path = SVG; 212 | sourceTree = ""; 213 | }; 214 | 96CA791925BF74B800218A16 /* Helpers */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 96CA791A25BF74B800218A16 /* Stack.swift */, 218 | ); 219 | path = Helpers; 220 | sourceTree = ""; 221 | }; 222 | 96CA791B25BF74B800218A16 /* Extensions */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 96CA791C25BF74B800218A16 /* CGPoint.swift */, 226 | ); 227 | path = Extensions; 228 | sourceTree = ""; 229 | }; 230 | /* End PBXGroup section */ 231 | 232 | /* Begin PBXHeadersBuildPhase section */ 233 | 96CA78A225BF72F200218A16 /* Headers */ = { 234 | isa = PBXHeadersBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | 96CA78B825BF72F200218A16 /* MRefresh.h in Headers */, 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | /* End PBXHeadersBuildPhase section */ 242 | 243 | /* Begin PBXNativeTarget section */ 244 | 96CA78A625BF72F200218A16 /* MRefresh */ = { 245 | isa = PBXNativeTarget; 246 | buildConfigurationList = 96CA78BB25BF72F200218A16 /* Build configuration list for PBXNativeTarget "MRefresh" */; 247 | buildPhases = ( 248 | 96CA78A225BF72F200218A16 /* Headers */, 249 | 96CA78A325BF72F200218A16 /* Sources */, 250 | 96CA78A425BF72F200218A16 /* Frameworks */, 251 | 96CA78A525BF72F200218A16 /* Resources */, 252 | ); 253 | buildRules = ( 254 | ); 255 | dependencies = ( 256 | ); 257 | name = MRefresh; 258 | productName = MRefresh; 259 | productReference = 96CA78A725BF72F200218A16 /* MRefresh.framework */; 260 | productType = "com.apple.product-type.framework"; 261 | }; 262 | 96CA78AF25BF72F200218A16 /* MRefreshTests */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 96CA78BE25BF72F200218A16 /* Build configuration list for PBXNativeTarget "MRefreshTests" */; 265 | buildPhases = ( 266 | 96CA78AC25BF72F200218A16 /* Sources */, 267 | 96CA78AD25BF72F200218A16 /* Frameworks */, 268 | 96CA78AE25BF72F200218A16 /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | 96CA78B325BF72F200218A16 /* PBXTargetDependency */, 274 | ); 275 | name = MRefreshTests; 276 | productName = MRefreshTests; 277 | productReference = 96CA78B025BF72F200218A16 /* MRefreshTests.xctest */; 278 | productType = "com.apple.product-type.bundle.unit-test"; 279 | }; 280 | 96CA78C825BF73CB00218A16 /* MRefreshExamples */ = { 281 | isa = PBXNativeTarget; 282 | buildConfigurationList = 96CA78F625BF73CC00218A16 /* Build configuration list for PBXNativeTarget "MRefreshExamples" */; 283 | buildPhases = ( 284 | 96CA78C525BF73CB00218A16 /* Sources */, 285 | 96CA78C625BF73CB00218A16 /* Frameworks */, 286 | 96CA78C725BF73CB00218A16 /* Resources */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | ); 292 | name = MRefreshExamples; 293 | productName = MRefreshExamples; 294 | productReference = 96CA78C925BF73CB00218A16 /* MRefreshExamples.app */; 295 | productType = "com.apple.product-type.application"; 296 | }; 297 | /* End PBXNativeTarget section */ 298 | 299 | /* Begin PBXProject section */ 300 | 96CA789E25BF72F200218A16 /* Project object */ = { 301 | isa = PBXProject; 302 | attributes = { 303 | LastSwiftUpdateCheck = 1230; 304 | LastUpgradeCheck = 1230; 305 | TargetAttributes = { 306 | 96CA78A625BF72F200218A16 = { 307 | CreatedOnToolsVersion = 12.3; 308 | }; 309 | 96CA78AF25BF72F200218A16 = { 310 | CreatedOnToolsVersion = 12.3; 311 | }; 312 | 96CA78C825BF73CB00218A16 = { 313 | CreatedOnToolsVersion = 12.3; 314 | }; 315 | }; 316 | }; 317 | buildConfigurationList = 96CA78A125BF72F200218A16 /* Build configuration list for PBXProject "MRefresh" */; 318 | compatibilityVersion = "Xcode 9.3"; 319 | developmentRegion = en; 320 | hasScannedForEncodings = 0; 321 | knownRegions = ( 322 | en, 323 | Base, 324 | ); 325 | mainGroup = 96CA789D25BF72F200218A16; 326 | productRefGroup = 96CA78A825BF72F200218A16 /* Products */; 327 | projectDirPath = ""; 328 | projectRoot = ""; 329 | targets = ( 330 | 96CA78A625BF72F200218A16 /* MRefresh */, 331 | 96CA78AF25BF72F200218A16 /* MRefreshTests */, 332 | 96CA78C825BF73CB00218A16 /* MRefreshExamples */, 333 | ); 334 | }; 335 | /* End PBXProject section */ 336 | 337 | /* Begin PBXResourcesBuildPhase section */ 338 | 96CA78A525BF72F200218A16 /* Resources */ = { 339 | isa = PBXResourcesBuildPhase; 340 | buildActionMask = 2147483647; 341 | files = ( 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | 96CA78AE25BF72F200218A16 /* Resources */ = { 346 | isa = PBXResourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | 96CA78C725BF73CB00218A16 /* Resources */ = { 353 | isa = PBXResourcesBuildPhase; 354 | buildActionMask = 2147483647; 355 | files = ( 356 | 96CA78D825BF73CC00218A16 /* LaunchScreen.storyboard in Resources */, 357 | 96CA78D525BF73CC00218A16 /* Assets.xcassets in Resources */, 358 | 96CA78D325BF73CB00218A16 /* Main.storyboard in Resources */, 359 | ); 360 | runOnlyForDeploymentPostprocessing = 0; 361 | }; 362 | /* End PBXResourcesBuildPhase section */ 363 | 364 | /* Begin PBXSourcesBuildPhase section */ 365 | 96CA78A325BF72F200218A16 /* Sources */ = { 366 | isa = PBXSourcesBuildPhase; 367 | buildActionMask = 2147483647; 368 | files = ( 369 | 96CA792225BF74B800218A16 /* LayerFactory.swift in Sources */, 370 | 96CA792025BF74B800218A16 /* AnimatableContainerView.swift in Sources */, 371 | 96CA792725BF74B800218A16 /* SVGReader.swift in Sources */, 372 | 96CA792D25BF74B800218A16 /* Stack.swift in Sources */, 373 | 96CA792E25BF74B800218A16 /* CGPoint.swift in Sources */, 374 | 96CA792B25BF74B800218A16 /* SVGSimplifier.swift in Sources */, 375 | 96CA792825BF74B800218A16 /* UIBezierPathExtension.swift in Sources */, 376 | 96CA792925BF74B800218A16 /* SVGInstruction.swift in Sources */, 377 | 96CA792525BF74B800218A16 /* SVGNode.swift in Sources */, 378 | 961E6D3125BFFD8700D089A1 /* PathDrawingAnimatableView+SVGConnectedPath.swift in Sources */, 379 | 96CA792625BF74B800218A16 /* SVGResizer.swift in Sources */, 380 | 96CA791D25BF74B800218A16 /* AnimatableView.swift in Sources */, 381 | 96CA792A25BF74B800218A16 /* SVGConnectedPathFactory.swift in Sources */, 382 | 96CA791F25BF74B800218A16 /* PathDrawingAnimatableView.swift in Sources */, 383 | 96CA792425BF74B800218A16 /* SVGPathFactory.swift in Sources */, 384 | 96CA791E25BF74B800218A16 /* AnimationsFactory.swift in Sources */, 385 | 96CA792C25BF74B800218A16 /* SVGSmoother.swift in Sources */, 386 | 96CA792125BF74B800218A16 /* PullToRefreshViewConfiguration.swift in Sources */, 387 | 96CA792325BF74B800218A16 /* UIScrollViewExtension.swift in Sources */, 388 | ); 389 | runOnlyForDeploymentPostprocessing = 0; 390 | }; 391 | 96CA78AC25BF72F200218A16 /* Sources */ = { 392 | isa = PBXSourcesBuildPhase; 393 | buildActionMask = 2147483647; 394 | files = ( 395 | 96EEB35D25C154D4004CBFD0 /* SVGPathFactoryTests.swift in Sources */, 396 | 96CA78B625BF72F200218A16 /* SVGReaderTests.swift in Sources */, 397 | 96EEB34B25C14D1F004CBFD0 /* SVGReaderMock.swift in Sources */, 398 | 96EEB35325C14DA2004CBFD0 /* SVGSimplifierMock.swift in Sources */, 399 | 96EEB35825C14E25004CBFD0 /* SVGSmootherMock.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 96CA78C525BF73CB00218A16 /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 96CA793925BF7B8600218A16 /* Constraints.swift in Sources */, 408 | 96CA78D025BF73CB00218A16 /* ViewController.swift in Sources */, 409 | 96CA78CC25BF73CB00218A16 /* AppDelegate.swift in Sources */, 410 | 96CA78CE25BF73CB00218A16 /* SceneDelegate.swift in Sources */, 411 | ); 412 | runOnlyForDeploymentPostprocessing = 0; 413 | }; 414 | /* End PBXSourcesBuildPhase section */ 415 | 416 | /* Begin PBXTargetDependency section */ 417 | 96CA78B325BF72F200218A16 /* PBXTargetDependency */ = { 418 | isa = PBXTargetDependency; 419 | target = 96CA78A625BF72F200218A16 /* MRefresh */; 420 | targetProxy = 96CA78B225BF72F200218A16 /* PBXContainerItemProxy */; 421 | }; 422 | /* End PBXTargetDependency section */ 423 | 424 | /* Begin PBXVariantGroup section */ 425 | 96CA78D125BF73CB00218A16 /* Main.storyboard */ = { 426 | isa = PBXVariantGroup; 427 | children = ( 428 | 96CA78D225BF73CB00218A16 /* Base */, 429 | ); 430 | name = Main.storyboard; 431 | sourceTree = ""; 432 | }; 433 | 96CA78D625BF73CC00218A16 /* LaunchScreen.storyboard */ = { 434 | isa = PBXVariantGroup; 435 | children = ( 436 | 96CA78D725BF73CC00218A16 /* Base */, 437 | ); 438 | name = LaunchScreen.storyboard; 439 | sourceTree = ""; 440 | }; 441 | /* End PBXVariantGroup section */ 442 | 443 | /* Begin XCBuildConfiguration section */ 444 | 96CA78B925BF72F200218A16 /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ALWAYS_SEARCH_USER_PATHS = NO; 448 | CLANG_ANALYZER_NONNULL = YES; 449 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 450 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 451 | CLANG_CXX_LIBRARY = "libc++"; 452 | CLANG_ENABLE_MODULES = YES; 453 | CLANG_ENABLE_OBJC_ARC = YES; 454 | CLANG_ENABLE_OBJC_WEAK = YES; 455 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 456 | CLANG_WARN_BOOL_CONVERSION = YES; 457 | CLANG_WARN_COMMA = YES; 458 | CLANG_WARN_CONSTANT_CONVERSION = YES; 459 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 460 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 461 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 462 | CLANG_WARN_EMPTY_BODY = YES; 463 | CLANG_WARN_ENUM_CONVERSION = YES; 464 | CLANG_WARN_INFINITE_RECURSION = YES; 465 | CLANG_WARN_INT_CONVERSION = YES; 466 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 467 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 468 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 469 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 470 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 471 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 472 | CLANG_WARN_STRICT_PROTOTYPES = YES; 473 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 474 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 475 | CLANG_WARN_UNREACHABLE_CODE = YES; 476 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 477 | COPY_PHASE_STRIP = NO; 478 | CURRENT_PROJECT_VERSION = 1; 479 | DEBUG_INFORMATION_FORMAT = dwarf; 480 | ENABLE_STRICT_OBJC_MSGSEND = YES; 481 | ENABLE_TESTABILITY = YES; 482 | GCC_C_LANGUAGE_STANDARD = gnu11; 483 | GCC_DYNAMIC_NO_PIC = NO; 484 | GCC_NO_COMMON_BLOCKS = YES; 485 | GCC_OPTIMIZATION_LEVEL = 0; 486 | GCC_PREPROCESSOR_DEFINITIONS = ( 487 | "DEBUG=1", 488 | "$(inherited)", 489 | ); 490 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 491 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 492 | GCC_WARN_UNDECLARED_SELECTOR = YES; 493 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 494 | GCC_WARN_UNUSED_FUNCTION = YES; 495 | GCC_WARN_UNUSED_VARIABLE = YES; 496 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 497 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 498 | MTL_FAST_MATH = YES; 499 | ONLY_ACTIVE_ARCH = YES; 500 | SDKROOT = iphoneos; 501 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 502 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 503 | VERSIONING_SYSTEM = "apple-generic"; 504 | VERSION_INFO_PREFIX = ""; 505 | }; 506 | name = Debug; 507 | }; 508 | 96CA78BA25BF72F200218A16 /* Release */ = { 509 | isa = XCBuildConfiguration; 510 | buildSettings = { 511 | ALWAYS_SEARCH_USER_PATHS = NO; 512 | CLANG_ANALYZER_NONNULL = YES; 513 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 514 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 515 | CLANG_CXX_LIBRARY = "libc++"; 516 | CLANG_ENABLE_MODULES = YES; 517 | CLANG_ENABLE_OBJC_ARC = YES; 518 | CLANG_ENABLE_OBJC_WEAK = YES; 519 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 520 | CLANG_WARN_BOOL_CONVERSION = YES; 521 | CLANG_WARN_COMMA = YES; 522 | CLANG_WARN_CONSTANT_CONVERSION = YES; 523 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 526 | CLANG_WARN_EMPTY_BODY = YES; 527 | CLANG_WARN_ENUM_CONVERSION = YES; 528 | CLANG_WARN_INFINITE_RECURSION = YES; 529 | CLANG_WARN_INT_CONVERSION = YES; 530 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 531 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 532 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 533 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 534 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 535 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 536 | CLANG_WARN_STRICT_PROTOTYPES = YES; 537 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 538 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 539 | CLANG_WARN_UNREACHABLE_CODE = YES; 540 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 541 | COPY_PHASE_STRIP = NO; 542 | CURRENT_PROJECT_VERSION = 1; 543 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 544 | ENABLE_NS_ASSERTIONS = NO; 545 | ENABLE_STRICT_OBJC_MSGSEND = YES; 546 | GCC_C_LANGUAGE_STANDARD = gnu11; 547 | GCC_NO_COMMON_BLOCKS = YES; 548 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 549 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 550 | GCC_WARN_UNDECLARED_SELECTOR = YES; 551 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 552 | GCC_WARN_UNUSED_FUNCTION = YES; 553 | GCC_WARN_UNUSED_VARIABLE = YES; 554 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 555 | MTL_ENABLE_DEBUG_INFO = NO; 556 | MTL_FAST_MATH = YES; 557 | SDKROOT = iphoneos; 558 | SWIFT_COMPILATION_MODE = wholemodule; 559 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 560 | VALIDATE_PRODUCT = YES; 561 | VERSIONING_SYSTEM = "apple-generic"; 562 | VERSION_INFO_PREFIX = ""; 563 | }; 564 | name = Release; 565 | }; 566 | 96CA78BC25BF72F200218A16 /* Debug */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | CODE_SIGN_STYLE = Automatic; 570 | DEFINES_MODULE = YES; 571 | DYLIB_COMPATIBILITY_VERSION = 1; 572 | DYLIB_CURRENT_VERSION = 1; 573 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 574 | INFOPLIST_FILE = MRefresh/Info.plist; 575 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 576 | LD_RUNPATH_SEARCH_PATHS = ( 577 | "$(inherited)", 578 | "@executable_path/Frameworks", 579 | "@loader_path/Frameworks", 580 | ); 581 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefresh; 582 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 583 | SKIP_INSTALL = YES; 584 | SWIFT_VERSION = 5.0; 585 | TARGETED_DEVICE_FAMILY = "1,2"; 586 | }; 587 | name = Debug; 588 | }; 589 | 96CA78BD25BF72F200218A16 /* Release */ = { 590 | isa = XCBuildConfiguration; 591 | buildSettings = { 592 | CODE_SIGN_STYLE = Automatic; 593 | DEFINES_MODULE = YES; 594 | DYLIB_COMPATIBILITY_VERSION = 1; 595 | DYLIB_CURRENT_VERSION = 1; 596 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 597 | INFOPLIST_FILE = MRefresh/Info.plist; 598 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 599 | LD_RUNPATH_SEARCH_PATHS = ( 600 | "$(inherited)", 601 | "@executable_path/Frameworks", 602 | "@loader_path/Frameworks", 603 | ); 604 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefresh; 605 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 606 | SKIP_INSTALL = YES; 607 | SWIFT_VERSION = 5.0; 608 | TARGETED_DEVICE_FAMILY = "1,2"; 609 | }; 610 | name = Release; 611 | }; 612 | 96CA78BF25BF72F200218A16 /* Debug */ = { 613 | isa = XCBuildConfiguration; 614 | buildSettings = { 615 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 616 | CODE_SIGN_STYLE = Automatic; 617 | INFOPLIST_FILE = MRefreshTests/Info.plist; 618 | LD_RUNPATH_SEARCH_PATHS = ( 619 | "$(inherited)", 620 | "@executable_path/Frameworks", 621 | "@loader_path/Frameworks", 622 | ); 623 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefreshTests; 624 | PRODUCT_NAME = "$(TARGET_NAME)"; 625 | SWIFT_VERSION = 5.0; 626 | TARGETED_DEVICE_FAMILY = "1,2"; 627 | }; 628 | name = Debug; 629 | }; 630 | 96CA78C025BF72F200218A16 /* Release */ = { 631 | isa = XCBuildConfiguration; 632 | buildSettings = { 633 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 634 | CODE_SIGN_STYLE = Automatic; 635 | INFOPLIST_FILE = MRefreshTests/Info.plist; 636 | LD_RUNPATH_SEARCH_PATHS = ( 637 | "$(inherited)", 638 | "@executable_path/Frameworks", 639 | "@loader_path/Frameworks", 640 | ); 641 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefreshTests; 642 | PRODUCT_NAME = "$(TARGET_NAME)"; 643 | SWIFT_VERSION = 5.0; 644 | TARGETED_DEVICE_FAMILY = "1,2"; 645 | }; 646 | name = Release; 647 | }; 648 | 96CA78F025BF73CC00218A16 /* Debug */ = { 649 | isa = XCBuildConfiguration; 650 | buildSettings = { 651 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 652 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 653 | CODE_SIGN_STYLE = Automatic; 654 | INFOPLIST_FILE = MRefreshExamples/Info.plist; 655 | LD_RUNPATH_SEARCH_PATHS = ( 656 | "$(inherited)", 657 | "@executable_path/Frameworks", 658 | ); 659 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefreshExamples; 660 | PRODUCT_NAME = "$(TARGET_NAME)"; 661 | SWIFT_VERSION = 5.0; 662 | TARGETED_DEVICE_FAMILY = "1,2"; 663 | }; 664 | name = Debug; 665 | }; 666 | 96CA78F125BF73CC00218A16 /* Release */ = { 667 | isa = XCBuildConfiguration; 668 | buildSettings = { 669 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 670 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 671 | CODE_SIGN_STYLE = Automatic; 672 | INFOPLIST_FILE = MRefreshExamples/Info.plist; 673 | LD_RUNPATH_SEARCH_PATHS = ( 674 | "$(inherited)", 675 | "@executable_path/Frameworks", 676 | ); 677 | PRODUCT_BUNDLE_IDENTIFIER = strongself.MRefreshExamples; 678 | PRODUCT_NAME = "$(TARGET_NAME)"; 679 | SWIFT_VERSION = 5.0; 680 | TARGETED_DEVICE_FAMILY = "1,2"; 681 | }; 682 | name = Release; 683 | }; 684 | /* End XCBuildConfiguration section */ 685 | 686 | /* Begin XCConfigurationList section */ 687 | 96CA78A125BF72F200218A16 /* Build configuration list for PBXProject "MRefresh" */ = { 688 | isa = XCConfigurationList; 689 | buildConfigurations = ( 690 | 96CA78B925BF72F200218A16 /* Debug */, 691 | 96CA78BA25BF72F200218A16 /* Release */, 692 | ); 693 | defaultConfigurationIsVisible = 0; 694 | defaultConfigurationName = Release; 695 | }; 696 | 96CA78BB25BF72F200218A16 /* Build configuration list for PBXNativeTarget "MRefresh" */ = { 697 | isa = XCConfigurationList; 698 | buildConfigurations = ( 699 | 96CA78BC25BF72F200218A16 /* Debug */, 700 | 96CA78BD25BF72F200218A16 /* Release */, 701 | ); 702 | defaultConfigurationIsVisible = 0; 703 | defaultConfigurationName = Release; 704 | }; 705 | 96CA78BE25BF72F200218A16 /* Build configuration list for PBXNativeTarget "MRefreshTests" */ = { 706 | isa = XCConfigurationList; 707 | buildConfigurations = ( 708 | 96CA78BF25BF72F200218A16 /* Debug */, 709 | 96CA78C025BF72F200218A16 /* Release */, 710 | ); 711 | defaultConfigurationIsVisible = 0; 712 | defaultConfigurationName = Release; 713 | }; 714 | 96CA78F625BF73CC00218A16 /* Build configuration list for PBXNativeTarget "MRefreshExamples" */ = { 715 | isa = XCConfigurationList; 716 | buildConfigurations = ( 717 | 96CA78F025BF73CC00218A16 /* Debug */, 718 | 96CA78F125BF73CC00218A16 /* Release */, 719 | ); 720 | defaultConfigurationIsVisible = 0; 721 | defaultConfigurationName = Release; 722 | }; 723 | /* End XCConfigurationList section */ 724 | }; 725 | rootObject = 96CA789E25BF72F200218A16 /* Project object */; 726 | } 727 | -------------------------------------------------------------------------------- /MRefresh.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MRefresh.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MRefresh.xcodeproj/xcshareddata/xcschemes/MRefresh.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /MRefresh.xcodeproj/xcshareddata/xcschemes/MRefreshTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /MRefresh/Extensions/CGPoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension CGPoint { 5 | func offset(_ p: CGPoint) -> CGPoint { 6 | return CGPoint(x: x + p.x, y: y + p.y) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /MRefresh/Helpers/Stack.swift: -------------------------------------------------------------------------------- 1 | // TODO: Just use plain array instead of Stack 2 | struct Stack { 3 | private var array: [T] = [] 4 | 5 | var empty: Bool { 6 | return array.isEmpty 7 | } 8 | 9 | mutating func push(_ element: T) { 10 | array.append(element) 11 | } 12 | 13 | mutating func pop() -> T? { 14 | return array.popLast() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MRefresh/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /MRefresh/MRefresh.h: -------------------------------------------------------------------------------- 1 | // 2 | // MRefresh.h 3 | // MRefresh 4 | // 5 | // Created by MIKHAIL RAKHMANOV on 25.01.21. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for MRefresh. 11 | FOUNDATION_EXPORT double MRefreshVersionNumber; 12 | 13 | //! Project version string for MRefresh. 14 | FOUNDATION_EXPORT const unsigned char MRefreshVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/AnimatableContainerView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | enum RefreshState { 5 | case stopped 6 | case triggered 7 | case loading 8 | } 9 | 10 | struct AnimatableContainerViewConfiguration { 11 | let frame: CGRect 12 | let animatable: AnimatableViewConforming 13 | let scrollView: UIScrollView 14 | let pullToRefreshViewCenterYOffset: CGFloat 15 | let animationDistance: CGFloat 16 | let animationStart: CGFloat 17 | let handler: ActionHandler 18 | let contentInsetChangeAnimationDuration: Double 19 | } 20 | 21 | /// A container view which handles all the logic with respect to positioning the animatable view 22 | /// It manages its own frame inside the scroll view and also the frame of the animatable subview 23 | final class AnimatableContainerView: UIView { 24 | 25 | fileprivate struct Constants { 26 | static let startingThreshold: CGFloat = 20.0 27 | } 28 | 29 | fileprivate let scrollView: UIScrollView 30 | fileprivate let customView: UIView 31 | fileprivate let animatable: AnimatableView 32 | fileprivate let handler: ActionHandler 33 | fileprivate let animationDistance: CGFloat 34 | fileprivate let animationStart: CGFloat 35 | fileprivate let contentInsetChangeAnimationDuration: Double 36 | fileprivate let pullToRefreshViewCenterYOffset: CGFloat 37 | fileprivate var privateState: RefreshState = .triggered 38 | 39 | var originalTopInset: CGFloat 40 | 41 | var isAnimating = false 42 | var isObserving = false 43 | 44 | var stoppedDragging: Bool { 45 | return !scrollView.isDragging || scrollView.isDecelerating 46 | } 47 | 48 | var enteredPullToRefreshZone: Bool { 49 | return scrollView.contentOffset.y + scrollView.contentInset.top <= Constants.startingThreshold 50 | } 51 | 52 | var state: RefreshState { 53 | get { 54 | return privateState 55 | } 56 | 57 | set { 58 | if privateState == newValue { 59 | return 60 | } 61 | privateState = newValue 62 | 63 | switch newValue { 64 | case .triggered: 65 | break 66 | case .stopped: 67 | if self.stoppedDragging { 68 | self.resetScrollViewContentInset() 69 | } 70 | case .loading: 71 | handler() 72 | if !self.isAnimating { 73 | self.willStartAnimation() 74 | } 75 | } 76 | } 77 | } 78 | 79 | init(frame: CGRect, configuration: AnimatableContainerViewConfiguration) { 80 | pullToRefreshViewCenterYOffset = configuration.pullToRefreshViewCenterYOffset 81 | scrollView = configuration.scrollView 82 | customView = configuration.animatable.getView 83 | animatable = configuration.animatable 84 | handler = configuration.handler 85 | animationDistance = configuration.animationDistance 86 | animationStart = configuration.animationStart 87 | originalTopInset = scrollView.contentInset.top 88 | contentInsetChangeAnimationDuration = configuration.contentInsetChangeAnimationDuration 89 | 90 | super.init(frame: frame) 91 | addSubview(customView) 92 | autoresizingMask = .flexibleWidth 93 | } 94 | 95 | required init?(coder aDecoder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | override func layoutSubviews() { 100 | super.layoutSubviews() 101 | 102 | // calculating the new frame of a custom view 103 | let viewBounds = customView.bounds 104 | let customViewWidth = viewBounds.width 105 | let customViewHeight = viewBounds.height 106 | let originX = (bounds.width - customViewWidth) / 2.0 107 | let originY = (bounds.height - customViewHeight - pullToRefreshViewCenterYOffset) / 2.0 108 | 109 | let customViewFrame = CGRect(x: originX, 110 | y: originY, 111 | width: customViewWidth, 112 | height: customViewHeight) 113 | customView.frame = customViewFrame 114 | } 115 | 116 | override func willMove(toSuperview newSuperview: UIView?) { 117 | super.willMove(toSuperview: newSuperview) 118 | 119 | guard let scrollView = superview as? UIScrollView, newSuperview == nil else { 120 | return 121 | } 122 | 123 | if scrollView.showsPullToRefresh && isObserving { 124 | scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset)) 125 | scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.frame)) 126 | scrollView.panGestureRecognizer.removeTarget(self, action: #selector(AnimatableContainerView.gestureRecognizerUpdated(_:))) 127 | } 128 | 129 | isObserving = false 130 | } 131 | 132 | func updatePullToRefreshFramePosition() { 133 | customView.isHidden = false 134 | let height = frame.height 135 | let width = frame.width 136 | 137 | let actualOffset = scrollView.contentOffset.y + originalTopInset 138 | 139 | // positioning the container in the center of the space betwe 140 | let calculatedOriginY = (actualOffset - height) / 2.0 141 | let originY = min(calculatedOriginY, -customView.bounds.height / 2.0 - height / 2.0) 142 | 143 | let newFrame = CGRect(x: frame.origin.x, 144 | y: originY, 145 | width: width, 146 | height: height) 147 | 148 | frame = newFrame 149 | } 150 | 151 | @objc func gestureRecognizerUpdated(_ sender: UIPanGestureRecognizer) { 152 | if !enteredPullToRefreshZone && state == .triggered { 153 | customView.isHidden = true 154 | return 155 | } 156 | updatePullToRefreshFramePosition() 157 | switch sender.state { 158 | case .began: 159 | if state != .loading { 160 | state = .triggered 161 | } 162 | case .ended: 163 | if state == .stopped && isAnimating { 164 | resetScrollViewContentInset() 165 | } else if state == .loading && enteredPullToRefreshZone { 166 | scrollViewContentInsetForLoading() 167 | } 168 | default: 169 | break 170 | } 171 | } 172 | 173 | func scrollViewDidScroll(contentOffset: CGPoint) { 174 | if !enteredPullToRefreshZone && state == .triggered { 175 | customView.isHidden = true 176 | return 177 | } 178 | updatePullToRefreshFramePosition() 179 | let threshold = animationDistance + animationStart 180 | let absoluteOffsetY = abs(contentOffset.y + originalTopInset) 181 | 182 | switch state { 183 | case .triggered: 184 | if absoluteOffsetY > threshold { 185 | state = .loading 186 | } 187 | let proportion = spinnerProportion(absoluteOffsetY: absoluteOffsetY, 188 | threshold: threshold, 189 | start: animationStart) 190 | animatable.drawPullToRefresh(proportion: proportion) 191 | 192 | case .loading: 193 | if stoppedDragging { 194 | scrollViewContentInsetForLoading() 195 | } 196 | default: 197 | break 198 | } 199 | } 200 | 201 | override public func observeValue(forKeyPath keyPath: String?, 202 | of object: Any?, 203 | change: [NSKeyValueChangeKey : Any]?, 204 | context: UnsafeMutableRawPointer?) { 205 | if let keyPath = keyPath { 206 | if keyPath == #keyPath(UIScrollView.contentOffset), 207 | let currentOffset = change?[.newKey] as? NSValue { 208 | scrollViewDidScroll(contentOffset: currentOffset.cgPointValue) 209 | } else if keyPath == #keyPath(UIScrollView.frame) { 210 | layoutSubviews() 211 | } 212 | } 213 | } 214 | 215 | func scrollViewContentInsetForLoading() { 216 | let currentInsets = self.currentInsets(with: originalTopInset + bounds.height) 217 | scrollViewAnimateChange(contentInset: currentInsets, animationBlock: nil) 218 | } 219 | 220 | func currentInsets(with newTopInset: CGFloat) -> UIEdgeInsets { 221 | var currentInsets = scrollView.contentInset 222 | currentInsets.top = newTopInset 223 | return currentInsets 224 | } 225 | 226 | func resetScrollViewContentInset() { 227 | let currentInsets = self.currentInsets(with: originalTopInset) 228 | scrollViewAnimateChange(contentInset: currentInsets) { 229 | if self.state == .stopped { 230 | self.willStopAnimation() 231 | } 232 | } 233 | } 234 | 235 | func stopAnimating() { 236 | DispatchQueue.main.async { 237 | self.state = .stopped 238 | } 239 | } 240 | 241 | private func scrollViewAnimateChange(contentInset: UIEdgeInsets, animationBlock: ActionHandler?) { 242 | UIView.animate(withDuration: contentInsetChangeAnimationDuration) { 243 | animationBlock?() 244 | self.scrollView.contentInset = contentInset 245 | } 246 | } 247 | 248 | private func spinnerProportion(absoluteOffsetY: CGFloat, threshold: CGFloat, start: CGFloat) -> CGFloat { 249 | let deltaOffset = max(absoluteOffsetY - start, 0) 250 | let proportion = min(deltaOffset / (threshold - start), 1.0) 251 | 252 | return proportion 253 | } 254 | 255 | private func willStartAnimation() { 256 | animatable.startAnimation() 257 | isAnimating = true 258 | } 259 | 260 | private func willStopAnimation() { 261 | animatable.stopAnimation() 262 | isAnimating = false 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/AnimatableView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public typealias ActionHandler = () -> Void 4 | 5 | /// Conform your custom view to this protocol to provide your own custom animations 6 | public protocol AnimatableView { 7 | /// When this function is called your view should draw some proportion of the picture/path etc 8 | func drawPullToRefresh(proportion: CGFloat) 9 | /// This function is called when the scroll view has been pulled far enough, so the animation should start 10 | /// Most of the time it is the moment when you want to start loading some data 11 | func startAnimation() 12 | /// This function is called when the user requests the scroll view to stop animating (e.g. when the data is loaded) 13 | func stopAnimation() 14 | } 15 | 16 | public protocol HasView { 17 | var getView: UIView { get } 18 | } 19 | 20 | extension UIView: HasView { 21 | public var getView: UIView { 22 | return self 23 | } 24 | } 25 | 26 | // Using this construction (with ugly HasView) to avoid providing base class for the view 27 | public protocol AnimatableViewConforming: AnimatableView, HasView {} 28 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/AnimationsFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | fileprivate struct Constants { 5 | struct KeyPaths { 6 | static let rotationAnimation = "transform.rotation.z" 7 | static let scaleAnimation = "transform.scale" 8 | static let opacityAnimation = "opacity" 9 | } 10 | 11 | struct Durations { 12 | static let rotationAnimation = 0.8 13 | static let shrinkAnimation = 0.1 14 | static let fadeAnimation = 0.05 15 | } 16 | } 17 | 18 | public final class AnimationsFactory { 19 | 20 | private let constants = Constants.self 21 | 22 | public init() {} 23 | 24 | public func rotationAnimation() -> CABasicAnimation { 25 | let rotationAnimation = CABasicAnimation(keyPath: constants.KeyPaths.rotationAnimation) 26 | 27 | rotationAnimation.toValue = 2.0 * CGFloat.pi 28 | rotationAnimation.duration = constants.Durations.rotationAnimation 29 | rotationAnimation.repeatCount = HUGE 30 | rotationAnimation.isRemovedOnCompletion = false 31 | 32 | return rotationAnimation 33 | } 34 | 35 | public func shrinkAnimation() -> CABasicAnimation { 36 | let shrinkAnimation = CABasicAnimation(keyPath: constants.KeyPaths.scaleAnimation) 37 | 38 | shrinkAnimation.toValue = 0 39 | shrinkAnimation.duration = constants.Durations.shrinkAnimation 40 | shrinkAnimation.fillMode = .forwards 41 | shrinkAnimation.isRemovedOnCompletion = false 42 | 43 | return shrinkAnimation 44 | } 45 | 46 | public func fadeAnimation() -> CABasicAnimation { 47 | let fadeAnimation = CABasicAnimation(keyPath: constants.KeyPaths.opacityAnimation) 48 | 49 | fadeAnimation.toValue = 0 50 | fadeAnimation.duration = constants.Durations.fadeAnimation 51 | fadeAnimation.fillMode = .forwards 52 | fadeAnimation.isRemovedOnCompletion = false 53 | 54 | return fadeAnimation 55 | } 56 | 57 | public func blinkAnimation() -> CABasicAnimation { 58 | let blinkAnimation = CABasicAnimation(keyPath: constants.KeyPaths.opacityAnimation) 59 | 60 | blinkAnimation.toValue = 0.3 61 | blinkAnimation.duration = constants.Durations.rotationAnimation 62 | blinkAnimation.fillMode = .forwards 63 | blinkAnimation.repeatCount = HUGE 64 | blinkAnimation.autoreverses = true 65 | blinkAnimation.isRemovedOnCompletion = false 66 | 67 | return blinkAnimation 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/LayerFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum SpinnerConstants { 4 | enum Circle { 5 | static let borderWidthDefault: CGFloat = 2.0 6 | static let arcStartAngle: CGFloat = -CGFloat.pi / 8.0 7 | static let arcProportionDefault: CGFloat = 0.9 8 | } 9 | } 10 | 11 | public struct LayerConfiguration { 12 | let path: UIBezierPath 13 | let lineWidth: CGFloat 14 | let fillColor: CGColor 15 | let strokeColor: CGColor 16 | 17 | public init(path: UIBezierPath, lineWidth: CGFloat, fillColor: CGColor, strokeColor: CGColor) { 18 | self.path = path 19 | self.lineWidth = lineWidth 20 | self.fillColor = fillColor 21 | self.strokeColor = strokeColor 22 | } 23 | } 24 | 25 | public final class LayerFactory { 26 | 27 | public init() {} 28 | 29 | public func circleLayer(radius: CGFloat, proportion: CGFloat, borderColor: UIColor, backgroundColor: UIColor) -> CAShapeLayer { 30 | let constants = SpinnerConstants.Circle.self 31 | let layer = CAShapeLayer() 32 | let startAngle = constants.arcStartAngle 33 | let endAngle = startAngle + 2 * CGFloat.pi * proportion * constants.arcProportionDefault 34 | 35 | let bezierPath = UIBezierPath(arcCenter: CGPoint(x: radius, y: radius), 36 | radius: radius, 37 | startAngle: startAngle, 38 | endAngle: endAngle, 39 | clockwise: true) 40 | layer.path = bezierPath.cgPath 41 | layer.lineWidth = constants.borderWidthDefault 42 | layer.strokeColor = borderColor.cgColor 43 | layer.fillColor = backgroundColor.cgColor 44 | 45 | return layer 46 | } 47 | 48 | public func layer(from configuration: LayerConfiguration) -> CAShapeLayer { 49 | let layer = CAShapeLayer() 50 | layer.path = configuration.path.cgPath 51 | layer.lineWidth = configuration.lineWidth 52 | layer.strokeColor = configuration.strokeColor 53 | layer.fillColor = configuration.fillColor 54 | 55 | return layer 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/PathDrawingAnimatableView+SVGConnectedPath.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension PathDrawingAnimatableView { 4 | convenience init(path: SVGConnectedPath, frame: CGRect) { 5 | self.init(frame: frame) 6 | makePullToRefreshLayer = makePathLayerClosure(path: path) 7 | } 8 | } 9 | 10 | private func makePathLayerClosure(path: SVGConnectedPath) -> MakeLayerWithProportionClosure { 11 | let factory = LayerFactory() 12 | return { size, proportion in 13 | guard let path = try? UIBezierPath(path: path, proportion: proportion) else { 14 | return CAShapeLayer() 15 | } 16 | let configuration = LayerConfiguration( 17 | path: path, 18 | lineWidth: 1.0, 19 | fillColor: UIColor.clear.cgColor, 20 | strokeColor: UIColor.black.cgColor 21 | ) 22 | let layer = factory.layer(from: configuration) 23 | return layer 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/PathDrawingAnimatableView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public typealias MakeLayerWithProportionClosure = (CGSize, CGFloat) -> CALayer 5 | public typealias ProcessingAnimationClosure = (CALayer) -> () 6 | public typealias EndAnimationClosure = (CALayer, @escaping () -> ()) -> () 7 | 8 | public final class PathDrawingAnimatableView: UIView { 9 | 10 | fileprivate let animationsFactory = AnimationsFactory() 11 | fileprivate let sublayersFactory = LayerFactory() 12 | 13 | fileprivate var pathLayer: CALayer? 14 | 15 | public var makePullToRefreshLayer: MakeLayerWithProportionClosure = makeLayerWithProportionClosure() 16 | public var processingAnimation: ProcessingAnimationClosure = defaultProcessingAnimationClosure() 17 | public var endAnimation: EndAnimationClosure = defaultEndAnimationClosure() 18 | 19 | public override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | backgroundColor = UIColor.clear 22 | } 23 | 24 | required public init?(coder aDecoder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | private func removeAllAnimations() { 29 | pathLayer?.removeFromSuperlayer() 30 | layer.removeAllAnimations() 31 | } 32 | } 33 | 34 | extension PathDrawingAnimatableView: AnimatableViewConforming { 35 | public func drawPullToRefresh(proportion: CGFloat) { 36 | pathLayer?.removeFromSuperlayer() 37 | let newPathLayer = makePullToRefreshLayer(bounds.size, proportion) 38 | layer.addSublayer(newPathLayer) 39 | 40 | pathLayer = newPathLayer 41 | } 42 | 43 | public func stopAnimation() { 44 | endAnimation(layer, removeAllAnimations) 45 | } 46 | 47 | public func startAnimation() { 48 | layer.removeAllAnimations() 49 | processingAnimation(layer) 50 | } 51 | } 52 | 53 | private func makeLayerWithProportionClosure() -> MakeLayerWithProportionClosure { 54 | let factory = LayerFactory() 55 | return { size, proportion in 56 | factory.circleLayer( 57 | radius: size.width / 2.0, 58 | proportion: proportion, 59 | borderColor: .blue, 60 | backgroundColor: UIColor.clear 61 | ) 62 | } 63 | } 64 | 65 | private func defaultProcessingAnimationClosure() -> ProcessingAnimationClosure { 66 | let blinkAnimation = AnimationsFactory().blinkAnimation() 67 | return { layer in 68 | layer.add(blinkAnimation, forKey: "blinkAnimation") 69 | } 70 | } 71 | 72 | private func defaultEndAnimationClosure() -> EndAnimationClosure { 73 | let shrinkAnimation = AnimationsFactory().shrinkAnimation() 74 | let fadeAnimation = AnimationsFactory().fadeAnimation() 75 | 76 | return { layer, completion in 77 | CATransaction.begin() 78 | fadeAnimation.beginTime = layer.convertTime(CACurrentMediaTime(), from: nil) + shrinkAnimation.duration / 2.0 79 | 80 | CATransaction.setCompletionBlock { 81 | completion() 82 | } 83 | layer.add(shrinkAnimation, forKey: "shrinkAnimation") 84 | layer.add(fadeAnimation, forKey: "fadeAnimation") 85 | 86 | CATransaction.commit() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/PullToRefreshViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// The general idea is that the animatable view should be in the 5 | /// center of the area between the first cell of the scroll view 6 | /// and the top of the screen 7 | /// 8 | /// The animatable view is living inside the container which height 9 | /// represents the amount that the user needs to scroll up to 10 | /// start the animation 11 | public struct PullToRefreshConfiguration { 12 | public static let `default` = PullToRefreshConfiguration( 13 | pullToRefreshViewCenterYOffset: 0, 14 | loadingContentInset: 100, 15 | animationDistance: 100, 16 | animationStart: 40, 17 | contentInsetChangeAnimationDuration: 0.25 18 | ) 19 | 20 | /// how much we want the view to be offset against the center of the area described above 21 | let pullToRefreshViewCenterYOffset: CGFloat 22 | /// the inset of the scroll view when pull to refresh is loading 23 | let loadingContentInset: CGFloat 24 | /// the distance the user needs to pull the scroll view 25 | /// after the animation is started to put the view in loading state 26 | let animationDistance: CGFloat 27 | /// the distance the user needs to pull the scroll view 28 | /// to start the animation 29 | let animationStart: CGFloat 30 | /// the duration of the content inset change 31 | let contentInsetChangeAnimationDuration: Double 32 | 33 | public init(pullToRefreshViewCenterYOffset: CGFloat, 34 | loadingContentInset: CGFloat, 35 | animationDistance: CGFloat, 36 | animationStart: CGFloat, 37 | contentInsetChangeAnimationDuration: Double) { 38 | self.pullToRefreshViewCenterYOffset = pullToRefreshViewCenterYOffset 39 | self.loadingContentInset = loadingContentInset 40 | self.animationDistance = animationDistance 41 | self.animationStart = animationStart 42 | self.contentInsetChangeAnimationDuration = contentInsetChangeAnimationDuration 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /MRefresh/PullToRefresh/UIScrollViewExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIScrollView { 5 | 6 | struct AssociatedKeys { 7 | static var pullToRefreshViewKey = "pullToRefreshViewKey" 8 | } 9 | 10 | var pullToRefreshView: AnimatableContainerView? { 11 | get { 12 | return objc_getAssociatedObject(self, &AssociatedKeys.pullToRefreshViewKey) as? AnimatableContainerView 13 | } 14 | 15 | set { 16 | if let newValue = newValue { 17 | objc_setAssociatedObject(self, 18 | &AssociatedKeys.pullToRefreshViewKey, 19 | newValue, 20 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 21 | } 22 | } 23 | } 24 | 25 | open var showsPullToRefresh: Bool { 26 | 27 | get { 28 | if let view = pullToRefreshView { 29 | return !view.isHidden 30 | } 31 | return false 32 | } 33 | set { 34 | guard let pullToRefreshView = pullToRefreshView else { 35 | return 36 | } 37 | 38 | if (!newValue && pullToRefreshView.isObserving) { 39 | removeObserver(pullToRefreshView, forKeyPath: #keyPath(UIScrollView.contentOffset)) 40 | removeObserver(pullToRefreshView, forKeyPath: #keyPath(UIScrollView.frame)) 41 | panGestureRecognizer.removeTarget(pullToRefreshView, 42 | action: #selector(AnimatableContainerView.gestureRecognizerUpdated(_:))) 43 | pullToRefreshView.isObserving = false 44 | } else if (newValue && !pullToRefreshView.isObserving) { 45 | addObserver(pullToRefreshView, 46 | forKeyPath: #keyPath(UIScrollView.contentOffset), 47 | options: NSKeyValueObservingOptions.new, 48 | context: nil) 49 | addObserver(pullToRefreshView, 50 | forKeyPath: #keyPath(UIScrollView.frame), 51 | options: NSKeyValueObservingOptions.new, 52 | context: nil) 53 | panGestureRecognizer.addTarget(pullToRefreshView, 54 | action: #selector(AnimatableContainerView.gestureRecognizerUpdated(_:))) 55 | pullToRefreshView.isObserving = true 56 | } 57 | pullToRefreshView.isHidden = !showsPullToRefresh 58 | } 59 | } 60 | 61 | open func stopAnimating() { 62 | if let animating = pullToRefreshView?.isAnimating, animating == true { 63 | pullToRefreshView?.stopAnimating() 64 | } 65 | } 66 | 67 | open func addPullToRefresh(animatable: AnimatableViewConforming, 68 | configuration: PullToRefreshConfiguration = .default, 69 | handler: @escaping ActionHandler) { 70 | guard pullToRefreshView == nil else { 71 | return 72 | } 73 | 74 | let height = configuration.loadingContentInset 75 | 76 | let pullToRefreshFrame = CGRect(x: 0, 77 | y: -height, 78 | width: bounds.width, 79 | height: height) 80 | 81 | let viewConfiguration = AnimatableContainerViewConfiguration( 82 | frame: pullToRefreshFrame, 83 | animatable: animatable, 84 | scrollView: self, 85 | pullToRefreshViewCenterYOffset: configuration.pullToRefreshViewCenterYOffset, 86 | animationDistance: configuration.animationDistance, 87 | animationStart: configuration.animationStart, 88 | handler: handler, 89 | contentInsetChangeAnimationDuration: configuration.contentInsetChangeAnimationDuration 90 | ) 91 | 92 | let newPullToRefreshView = AnimatableContainerView(frame: pullToRefreshFrame, 93 | configuration: viewConfiguration) 94 | addSubview(newPullToRefreshView) 95 | sendSubviewToBack(newPullToRefreshView) 96 | 97 | pullToRefreshView = newPullToRefreshView 98 | 99 | showsPullToRefresh = true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGConnectedPathFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct SVGConnectedPathConfiguration { 4 | public var elements: [(svg: String, startProportion: CGFloat, depth: Int)] = [] 5 | public let size: CGSize 6 | 7 | public init(size: CGSize) { 8 | self.size = size 9 | } 10 | 11 | public mutating func add(svg: String, startProportion: CGFloat, depth: Int) { 12 | elements.append((svg, startProportion, depth)) 13 | } 14 | } 15 | 16 | public struct SVGConnectedPath { 17 | public let proportionPaths: [(startProportion: CGFloat, path: SVGPath)] 18 | } 19 | 20 | /// Creates one or more connected path from the svgs 21 | public final class SVGConnectedPathFactory { 22 | private let factory: SVGPathFactory 23 | private let resizer: SVGResizer 24 | 25 | public static let `default` = SVGConnectedPathFactory() 26 | 27 | init(factory: SVGPathFactory = SVGPathFactoryImpl(), 28 | resizer: SVGResizer = SVGResizerImpl()) { 29 | self.factory = factory 30 | self.resizer = resizer 31 | } 32 | 33 | public func make(pathConfiguration: SVGConnectedPathConfiguration) throws -> SVGConnectedPath { 34 | var proportionPaths = try pathConfiguration.elements.map { 35 | (startProportion: $0.startProportion, 36 | path: try factory.make(svg: $0.svg, smoothDepth: $0.depth)) 37 | } 38 | 39 | let allNodes = proportionPaths.reduce(into: [], { 40 | $0 += $1.path.nodes 41 | }) 42 | let parameters = resizer.getResizingParameters(allNodes, for: pathConfiguration.size) 43 | proportionPaths = proportionPaths.map { proportion, path in 44 | let rescaled = resizer.rescaled(path.nodes, scale: parameters.scale) 45 | let nodes = resizer.moved(rescaled, offset: parameters.offset) 46 | return (startProportion: proportion, path: SVGPath(nodes: nodes)) 47 | } 48 | 49 | return SVGConnectedPath( 50 | proportionPaths: proportionPaths 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGInstruction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum SVGInstruction: String, Equatable, CaseIterable { 4 | case move = "M" 5 | case moveRelative = "m" 6 | case line = "L" 7 | case lineRelative = "l" 8 | case horizontal = "H" 9 | case horizontalRelative = "h" 10 | case vertical = "V" 11 | case verticalRelative = "v" 12 | case closePath = "Z" 13 | case closePathSmall = "z" 14 | case cubic = "C" 15 | case cubicRelative = "c" 16 | case shorthandCubic = "S" 17 | case shorthandCubicRelative = "s" 18 | case quadratic = "Q" 19 | case quadraticRelative = "q" 20 | case shorthandQuadratic = "T" 21 | case shorthandQuadraticRelative = "t" 22 | 23 | var nonRelative: SVGInstruction { 24 | switch self { 25 | case .horizontalRelative: 26 | return .horizontal 27 | case .verticalRelative: 28 | return .vertical 29 | case .lineRelative: 30 | return .line 31 | case .cubicRelative: 32 | return .cubic 33 | case .shorthandCubicRelative: 34 | return .shorthandCubic 35 | case .moveRelative: 36 | return .move 37 | case .quadraticRelative: 38 | return .quadratic 39 | case .shorthandQuadraticRelative: 40 | return .shorthandQuadratic 41 | 42 | default: 43 | return .line 44 | } 45 | } 46 | 47 | var nonShorthand: SVGInstruction { 48 | switch self { 49 | case .shorthandCubic: 50 | return .cubic 51 | case .shorthandQuadratic: 52 | return .quadratic 53 | default: 54 | return .line 55 | } 56 | } 57 | 58 | var isRelative: Bool { 59 | switch self { 60 | case .horizontalRelative: 61 | return true 62 | case .verticalRelative: 63 | return true 64 | case .lineRelative: 65 | return true 66 | case .cubicRelative: 67 | return true 68 | case .shorthandCubicRelative: 69 | return true 70 | case .moveRelative: 71 | return true 72 | case .quadraticRelative: 73 | return true 74 | case .shorthandQuadraticRelative: 75 | return true 76 | 77 | default: 78 | return false 79 | } 80 | } 81 | 82 | var valuesCount: Int { 83 | switch self { 84 | case .cubic, .cubicRelative: 85 | return 6 86 | case .quadratic, .quadraticRelative, .shorthandCubic, .shorthandCubicRelative: 87 | return 4 88 | case .line, .lineRelative, .shorthandQuadratic, .shorthandQuadraticRelative, .move, .moveRelative: 89 | return 2 90 | case .vertical, .verticalRelative, .horizontal, .horizontalRelative: 91 | return 1 92 | case .closePath, .closePathSmall: 93 | return 0 94 | } 95 | } 96 | 97 | var isShorthand: Bool { 98 | switch self { 99 | case .shorthandCubicRelative: 100 | return true 101 | case .shorthandCubic: 102 | return true 103 | case .shorthandQuadratic: 104 | return true 105 | case .shorthandQuadraticRelative: 106 | return true 107 | 108 | default: 109 | return false 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct SVGNode { 5 | public var instruction: SVGInstruction 6 | public var points: [CGPoint] 7 | } 8 | 9 | extension CGPoint { 10 | static func isEqual( 11 | _ lhs: CGPoint, 12 | _ rhs: CGPoint 13 | ) -> Bool { 14 | let isEqual: (CGFloat, CGFloat) -> Bool = { 15 | return abs($0 - $1) < CGFloat.ulpOfOne 16 | } 17 | return isEqual(lhs.x, rhs.x) && isEqual(lhs.y, rhs.y) 18 | } 19 | } 20 | 21 | extension SVGNode: Equatable { 22 | public static func ==( 23 | lhs: SVGNode, 24 | rhs: SVGNode 25 | ) -> Bool { 26 | if lhs.instruction != rhs.instruction { 27 | return false 28 | } 29 | if lhs.points.count != rhs.points.count { 30 | return false 31 | } 32 | for (first, second) in zip(lhs.points, rhs.points) { 33 | // default CGPoint comparison doesn't work good because of how float comparison works 34 | if !CGPoint.isEqual(first, second) { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGPathFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct SVGPath { 5 | public let nodes: [SVGNode] 6 | } 7 | 8 | /// Converts one path to nodes without resizing it 9 | protocol SVGPathFactory { 10 | func make(svg: String, smoothDepth: Int) throws -> SVGPath 11 | } 12 | 13 | final class SVGPathFactoryImpl: SVGPathFactory { 14 | private let reader: SVGReader 15 | private let simplifier: SVGSimplifier 16 | private let smoother: SVGSmoother 17 | 18 | init(reader: SVGReader = SVGReaderImpl(), 19 | simplifier: SVGSimplifier = SVGSimplifierImpl(), 20 | smoother: SVGSmoother = SVGSmootherImpl()) { 21 | self.reader = reader 22 | self.simplifier = simplifier 23 | self.smoother = smoother 24 | } 25 | 26 | func make(svg: String, smoothDepth: Int) throws -> SVGPath { 27 | let readResult = try reader.read(svg) 28 | let simplified = simplifier.simplify(readResult) 29 | let smoothed = smoother.smooth(times: smoothDepth, nodes: simplified) 30 | 31 | return SVGPath(nodes: smoothed) 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum SVGReaderError: Error { 5 | case cannotRead 6 | case invalidPoints 7 | } 8 | 9 | /// Reads SVG strings and converts them to nodes 10 | protocol SVGReader { 11 | /// Converts the string to nodes 12 | func read(_ svg: String) throws -> [SVGNode] 13 | } 14 | 15 | final class SVGReaderImpl: SVGReader { 16 | func read(_ svg: String) throws -> [SVGNode] { 17 | let scanner = Scanner(string: svg) 18 | 19 | var skippedCharacters = CharacterSet(charactersIn: ",") 20 | skippedCharacters.formUnion(CharacterSet.whitespacesAndNewlines) 21 | 22 | scanner.charactersToBeSkipped = skippedCharacters 23 | var nsStringInstruction: NSString? 24 | var nodes: [SVGNode] = [] 25 | // this looks ugly but we can scan by character only after ios 13.0 26 | while scanner.scanCharacters(from: CharacterSet.letters, into: &nsStringInstruction) { 27 | // TODO: make better errors and simplify the parsing 28 | guard let stringInstructions = nsStringInstruction as String? else { 29 | throw SVGReaderError.cannotRead 30 | } 31 | let instructions = stringInstructions.compactMap { SVGInstruction(rawValue: String($0)) } 32 | // we have unknown symbols 33 | if instructions.count != stringInstructions.count { 34 | throw SVGReaderError.cannotRead 35 | } 36 | switch instructions.count { 37 | case 0: 38 | throw SVGReaderError.cannotRead 39 | case 1: 40 | let instruction = instructions.first! 41 | if instruction == .closePath || instruction == .closePathSmall { 42 | nodes.append(SVGNode(instruction: instruction, points: [])) 43 | continue 44 | } 45 | nodes += try scanValues(scanner: scanner, instruction: instruction) 46 | default: 47 | let allFirstIsClosePaths = instructions.dropLast().filter { 48 | $0 != .closePath && $0 != .closePathSmall 49 | }.count == 0 50 | if !allFirstIsClosePaths { 51 | throw SVGReaderError.cannotRead 52 | } 53 | instructions.dropLast().forEach { 54 | nodes.append(SVGNode(instruction: $0, points: [])) 55 | } 56 | let lastInstruction = instructions.last! 57 | if lastInstruction == .closePath || lastInstruction == .closePathSmall { 58 | nodes.append(SVGNode(instruction: lastInstruction, points: [])) 59 | continue 60 | } 61 | nodes += try scanValues(scanner: scanner, instruction: lastInstruction) 62 | } 63 | } 64 | if !scanner.isAtEnd { 65 | throw SVGReaderError.cannotRead 66 | } 67 | return nodes 68 | } 69 | 70 | private func scanValues(scanner: Scanner, instruction: SVGInstruction) throws -> [SVGNode] { 71 | var value: Double = 0.0 72 | var values: [CGFloat] = [] 73 | 74 | while scanner.scanDouble(&value) { 75 | values.append(CGFloat(value)) 76 | } 77 | 78 | if values.count % instruction.valuesCount != 0 { 79 | throw SVGReaderError.invalidPoints 80 | } 81 | 82 | // different commands provide for different points count 83 | // so we chunk them 84 | // also some commands can be repeated (e.g. we can have hundreds of values which we need to split) 85 | let chunkedValues = values.chunked(into: instruction.valuesCount) 86 | return chunkedValues.map { group in 87 | switch instruction { 88 | case .horizontal, .horizontalRelative: 89 | return SVGNode(instruction: instruction, points: [CGPoint(x: group[0], y: 0.0)]) 90 | case .vertical, .verticalRelative: 91 | return SVGNode(instruction: instruction, points: [CGPoint(x: 0.0, y: group[0])]) 92 | default: 93 | let points = group.chunked(into: 2).map { 94 | CGPoint(x: $0[0], y: $0[1]) 95 | } 96 | return SVGNode(instruction: instruction, points: points) 97 | } 98 | } 99 | } 100 | } 101 | 102 | fileprivate extension Array { 103 | func chunked(into size: Int) -> [[Element]] { 104 | return stride(from: 0, to: count, by: size).map { 105 | Array(self[$0 ..< Swift.min($0 + size, count)]) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGResizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | struct ResizingParameters { 5 | let scale: CGFloat 6 | let offset: CGPoint 7 | } 8 | 9 | /// Converts the original svg points into points inside specific rectangle 10 | protocol SVGResizer { 11 | /// resizing svg to fit the new frame 12 | func getResizingParameters(_ nodes: [SVGNode], for newSize: CGSize) -> ResizingParameters 13 | /// rescaling svg nodes 14 | func rescaled(_ nodes: [SVGNode], scale: CGFloat) -> [SVGNode] 15 | /// moving svg nodes along the offset 16 | func moved(_ nodes: [SVGNode], offset: CGPoint) -> [SVGNode] 17 | } 18 | 19 | final class SVGResizerImpl: SVGResizer { 20 | func getResizingParameters(_ nodes: [SVGNode], for newSize: CGSize) -> ResizingParameters { 21 | let minCoordinate = calculateLimitPoint( 22 | nodes, 23 | limit: min, 24 | start: .greatestFiniteMagnitude 25 | ) 26 | 27 | // shifting all points using minCoordinate 28 | let normalized = normalize(nodes, offset: minCoordinate) 29 | 30 | // calculating max coordinate to get the size of the path 31 | let maxCoordinate = calculateLimitPoint( 32 | normalized, 33 | limit: max, 34 | start: 0.0 35 | ) 36 | let size = CGSize(width: maxCoordinate.x, height: maxCoordinate.y) 37 | // calculating the scale between svg points and the frame 38 | let scale = calcualteScale(first: newSize, second: size) 39 | // getting the offset to center the path inside size 40 | let offset = CGPoint(x: abs(newSize.width - size.width * scale) / 2.0, y: abs(newSize.height - size.height * scale) / 2.0) 41 | return ResizingParameters( 42 | scale: scale, 43 | offset: offset 44 | ) 45 | } 46 | 47 | func rescaled(_ nodes: [SVGNode], scale: CGFloat) -> [SVGNode] { 48 | nodes.map { node in 49 | SVGNode(instruction: node.instruction, 50 | points: node.points.map { CGPoint(x: $0.x * scale, y: $0.y * scale) }) 51 | } 52 | } 53 | 54 | func moved(_ nodes: [SVGNode], offset: CGPoint) -> [SVGNode] { 55 | nodes.map { node in 56 | SVGNode(instruction: node.instruction, 57 | points: node.points.map { $0.offset(offset) }) 58 | } 59 | } 60 | 61 | private func normalize(_ nodes: [SVGNode], offset: CGPoint) -> [SVGNode] { 62 | return nodes.map { node in 63 | return SVGNode(instruction: node.instruction, 64 | points: node.points.map { CGPoint(x: $0.x - offset.x, y: $0.y - offset.y) }) 65 | } 66 | } 67 | 68 | private func calcualteScale(first: CGSize, second: CGSize) -> CGFloat { 69 | return min(first.width / second.width, first.height / second.height) 70 | } 71 | 72 | private func calculateLimitPoint(_ nodes: [SVGNode], limit: (CGFloat, CGFloat) -> CGFloat, start: CGFloat) -> CGPoint { 73 | var x = start 74 | var y = x 75 | 76 | for node in nodes { 77 | for point in node.points { 78 | x = limit(point.x, x) 79 | y = limit(point.y, y) 80 | } 81 | } 82 | 83 | return CGPoint(x: x, y: y) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGSimplifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// Removes relative and shorthand nodes from the path and converts them to representation needed 5 | /// for other components 6 | protocol SVGSimplifier { 7 | func simplify(_ nodes: [SVGNode]) -> [SVGNode] 8 | } 9 | 10 | final class SVGSimplifierImpl: SVGSimplifier { 11 | func simplify(_ nodes: [SVGNode]) -> [SVGNode] { 12 | guard nodes.count > 1 else { 13 | return nodes 14 | } 15 | 16 | var subpathPointsStack = Stack() 17 | var convertedNodes: [SVGNode] = [] 18 | let firstNode = nodes[0] 19 | 20 | convertedNodes.append(firstNode) 21 | if firstNode.instruction == .moveRelative || firstNode.instruction == .move { 22 | subpathPointsStack.push(firstNode) 23 | } 24 | 25 | // this could have been rewritten with zip but I am too lazy :-( 26 | for index in 1 ..< nodes.count { 27 | convertNode(nodes: nodes, convertedNodes: &convertedNodes, pointsStack: &subpathPointsStack, index: index) 28 | } 29 | 30 | return convertedNodes 31 | } 32 | 33 | private func convertNode( 34 | nodes: [SVGNode], 35 | convertedNodes: inout [SVGNode], 36 | pointsStack: inout Stack, 37 | index: Int) { 38 | var offsetNode = convertedNodes[index - 1] 39 | if nodes[index - 1].instruction == .closePath || nodes[index - 1].instruction == .closePathSmall { 40 | if let previousNode = pointsStack.pop() { 41 | offsetNode = previousNode 42 | } 43 | } 44 | 45 | // relative instructions are offset by the previous node 46 | if nodes[index].instruction.isRelative { 47 | let convertedNode = convertRelativeNode(nodes[index], lastNode: offsetNode) 48 | convertedNodes.append(convertedNode) 49 | } else { 50 | var newNode = nodes[index] 51 | if newNode.instruction == .vertical { 52 | newNode.points[0].x = offsetNode.points.last?.x ?? 0.0 53 | } else if newNode.instruction == .horizontal { 54 | newNode.points[0].y = offsetNode.points.last?.y ?? 0.0 55 | } 56 | 57 | convertedNodes.append(newNode) 58 | } 59 | 60 | // the instruction can be relative and shorthand at the same time 61 | if convertedNodes[index].instruction.isShorthand { 62 | let lastPointsCount = convertedNodes[index - 1].points.count 63 | 64 | let updatedPoint: CGPoint 65 | if lastPointsCount > 1 { 66 | let previousPoint = convertedNodes[index - 1].points[lastPointsCount - 1] 67 | let previousControlPoint = convertedNodes[index - 1].points[lastPointsCount - 2] 68 | 69 | // reflecting the previous control point 70 | // see example here https://svgwg.org/specs/paths/images/cubic02.svg 71 | updatedPoint = CGPoint(x: 2.0 * previousPoint.x - previousControlPoint.x, 72 | y: 2.0 * previousPoint.y - previousControlPoint.y) 73 | } else { 74 | updatedPoint = convertedNodes[index - 1].points[0] 75 | } 76 | 77 | convertedNodes[index].points.insert(updatedPoint, at: 0) 78 | convertedNodes[index].instruction = convertedNodes[index].instruction.nonShorthand 79 | } 80 | 81 | if nodes[index].instruction == .moveRelative || nodes[index].instruction == .move { 82 | pointsStack.push(convertedNodes[index]) 83 | } 84 | } 85 | 86 | private func convertRelativeNode(_ currentNode: SVGNode, lastNode: SVGNode) -> SVGNode { 87 | return SVGNode(instruction: currentNode.instruction.nonRelative, 88 | points: nonRelativePoints(from: lastNode.points.last ?? CGPoint.zero, 89 | with: currentNode.points)) 90 | } 91 | 92 | private func nonRelativePoints(from point: CGPoint, with points: [CGPoint]) -> [CGPoint] { 93 | return points.map { point.offset($0) } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MRefresh/SVG/SVGSmoother.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// Adds more intermediate points to bezier curves for smooth drawing 5 | protocol SVGSmoother { 6 | func smooth(times: Int, nodes: [SVGNode]) -> [SVGNode] 7 | } 8 | 9 | final class SVGSmootherImpl: SVGSmoother { 10 | func smooth(times: Int, nodes: [SVGNode]) -> [SVGNode] { 11 | guard !nodes.isEmpty && times >= 1 else { 12 | return nodes 13 | } 14 | var newNodes = [nodes[0]] 15 | 16 | for index in 1 ... nodes.count - 1 { 17 | let previousNode = nodes[index - 1] 18 | let currentNode = nodes[index] 19 | 20 | switch currentNode.instruction { 21 | case .quadratic, .cubic, .line, .horizontal, .vertical: 22 | let currentNodes = splitCurveHalf(firstNode: previousNode, secondNode: currentNode, times: times) 23 | newNodes += currentNodes 24 | default: 25 | newNodes.append(currentNode) 26 | } 27 | } 28 | 29 | return newNodes 30 | } 31 | 32 | private func splitCurveHalf(firstNode: SVGNode, secondNode: SVGNode, times: Int) -> [SVGNode] { 33 | // we split only the nodes that have more than one point 34 | let lastPointOfFirstNode = firstNode.points.last! 35 | let lastPointOfSecondNode = secondNode.points.last! 36 | let points: [CGPoint] = [lastPointOfFirstNode] + secondNode.points 37 | 38 | var firstControlPoints: [CGPoint] = [] 39 | var secondControlPoints: [CGPoint] = [lastPointOfSecondNode] 40 | 41 | splitCurveHelper(points: points, firstControlPoints: &firstControlPoints, secondControlPoints: &secondControlPoints) 42 | let newFirstNode = SVGNode(instruction: secondNode.instruction, 43 | points: firstControlPoints) 44 | 45 | let newSecondNode = SVGNode(instruction: secondNode.instruction, 46 | points: secondControlPoints.reversed()) 47 | 48 | if times > 0 { 49 | let firstHalf = splitCurveHalf(firstNode: firstNode, secondNode: newFirstNode, times: times - 1) 50 | let secondHalf = splitCurveHalf(firstNode: newFirstNode, secondNode: newSecondNode, times: times - 1) 51 | 52 | return firstHalf + secondHalf 53 | } else { 54 | return [newFirstNode, newSecondNode] 55 | } 56 | } 57 | 58 | private func splitCurveHelper(points: [CGPoint], firstControlPoints: inout [CGPoint], secondControlPoints: inout [CGPoint]) { 59 | guard points.count > 1 else { return } 60 | 61 | var newPoints: [CGPoint] = [] 62 | let firstNewPoint = midpoint(points[0], points[1]) 63 | 64 | newPoints.append(firstNewPoint) 65 | firstControlPoints.append(firstNewPoint) 66 | 67 | for index in 1 ..< points.count - 1 { 68 | let newPoint = midpoint(points[index], points[index + 1]) 69 | newPoints.append(newPoint) 70 | 71 | if index == points.count - 2 { 72 | secondControlPoints.append(newPoint) 73 | } 74 | } 75 | 76 | splitCurveHelper(points: newPoints, 77 | firstControlPoints: &firstControlPoints, 78 | secondControlPoints: &secondControlPoints) 79 | } 80 | 81 | private func midpoint(_ firstPoint: CGPoint, _ secondPoint: CGPoint) -> CGPoint { 82 | let x = firstPoint.x * 0.5 + secondPoint.x * 0.5 83 | let y = firstPoint.y * 0.5 + secondPoint.y * 0.5 84 | return CGPoint(x: x, y: y) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MRefresh/SVG/UIBezierPathExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum ConvertErrors: Error { 4 | case invalidArgs 5 | case noStartingPoint 6 | } 7 | 8 | public extension UIBezierPath { 9 | convenience init(path: SVGConnectedPath, proportion: CGFloat? = nil) throws { 10 | self.init() 11 | let currentProportion = proportion ?? 1.0 12 | let currentPaths = path.proportionPaths.filter { $0.startProportion < currentProportion } 13 | 14 | guard !currentPaths.isEmpty else { 15 | return 16 | } 17 | // the worst case about all these things is that when proportion changes we recalculate everything again 18 | // one of the ideas how we can improve performance in the future is by adding caching of the UIBezierPath for different proportions 19 | try currentPaths.map { proportionPath in 20 | let proportion = calculateRelativeProportion(currentProportion: currentProportion, startProportion: proportionPath.startProportion) 21 | return try UIBezierPath(path: proportionPath.path, proportion: proportion) 22 | }.forEach { append($0) } 23 | } 24 | 25 | private convenience init(path: SVGPath, proportion: CGFloat? = nil) throws { 26 | self.init() 27 | let count = Int(CGFloat(path.nodes.count) * (proportion ?? 1.0)) 28 | 29 | guard count >= 2 else { 30 | return 31 | } 32 | 33 | try convert(path.nodes, count: count) 34 | } 35 | 36 | private func convert(_ nodes: [SVGNode], count: Int) throws { 37 | guard nodes.count > 1 else { 38 | throw ConvertErrors.invalidArgs 39 | } 40 | 41 | var subpathPointsStack = Stack() 42 | 43 | if nodes[0].instruction != .move { 44 | throw ConvertErrors.noStartingPoint 45 | } 46 | 47 | move(to: nodes[0].points[0]) 48 | 49 | for index in 1 ..< count { 50 | addNode(nodes[index], stack: &subpathPointsStack) 51 | } 52 | } 53 | 54 | private func addNode(_ currentNode: SVGNode, stack: inout Stack) { 55 | switch currentNode.instruction { 56 | case .line: 57 | addLine(to: currentNode.points[0]) 58 | 59 | case .cubic: 60 | addCurve(to: currentNode.points[2], controlPoint1: currentNode.points[0], controlPoint2: currentNode.points[1]) 61 | 62 | case .quadratic: 63 | addQuadCurve(to: currentNode.points[1], controlPoint: currentNode.points[0]) 64 | 65 | case .horizontal: 66 | addLine(to: currentNode.points[0]) 67 | 68 | case .vertical: 69 | addLine(to: currentNode.points[0]) 70 | 71 | case .move: 72 | move(to: currentNode.points[0]) 73 | stack.push(currentNode.points[0]) 74 | 75 | case .closePathSmall, .closePath: 76 | if let point = stack.pop() { 77 | move(to: point) 78 | } 79 | 80 | default: 81 | break 82 | } 83 | } 84 | 85 | private func calculateRelativeProportion(currentProportion: CGFloat, startProportion: CGFloat) -> CGFloat { 86 | return max((currentProportion - startProportion) / (1.0 - startProportion), 0) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MRefreshExamples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MRefreshExamples 4 | // 5 | // Created by MIKHAIL RAKHMANOV on 25.01.21. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MRefreshExamples/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MRefreshExamples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MRefreshExamples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MRefreshExamples/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 | -------------------------------------------------------------------------------- /MRefreshExamples/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MRefreshExamples/Constraints.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | typealias Constraint = (_ view: UIView) -> NSLayoutConstraint 5 | 6 | protocol AnchorLayoutable { 7 | var leadingAnchor: NSLayoutXAxisAnchor { get } 8 | var trailingAnchor: NSLayoutXAxisAnchor { get } 9 | var leftAnchor: NSLayoutXAxisAnchor { get } 10 | var rightAnchor: NSLayoutXAxisAnchor { get } 11 | var topAnchor: NSLayoutYAxisAnchor { get } 12 | var bottomAnchor: NSLayoutYAxisAnchor { get } 13 | var widthAnchor: NSLayoutDimension { get } 14 | var heightAnchor: NSLayoutDimension { get } 15 | var centerXAnchor: NSLayoutXAxisAnchor { get } 16 | var centerYAnchor: NSLayoutYAxisAnchor { get } 17 | } 18 | 19 | extension UIView: AnchorLayoutable {} 20 | extension UILayoutGuide: AnchorLayoutable {} 21 | 22 | // MARK: == 23 | 24 | func equal(_ keyPath: KeyPath, 25 | _ to: KeyPath, 26 | of secondView: UIView, 27 | constant: CGFloat = 0, 28 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 29 | return { firstView in 30 | let constraint = firstView[keyPath: keyPath].constraint(equalTo: secondView[keyPath: to], 31 | constant: constant) 32 | constraint.priority = priority 33 | return constraint 34 | } 35 | } 36 | 37 | func equalSafeArea(_ keyPath: KeyPath, 38 | to: KeyPath, 39 | of secondView: UIView, 40 | constant: CGFloat = 0, 41 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 42 | return { firstView in 43 | let constraint: NSLayoutConstraint 44 | if #available(iOS 11.0, *) { 45 | constraint = firstView[keyPath: keyPath].constraint( 46 | equalTo: secondView.safeAreaLayoutGuide[keyPath: to], 47 | constant: constant 48 | ) 49 | } else { 50 | constraint = firstView[keyPath: keyPath].constraint( 51 | equalTo: secondView[keyPath: to], 52 | constant: constant 53 | ) 54 | } 55 | 56 | constraint.priority = priority 57 | return constraint 58 | } 59 | } 60 | 61 | func equalSafeArea(_ keyPath: KeyPath, 62 | of secondView: UIView, 63 | constant: CGFloat = 0, 64 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 65 | return equalSafeArea(keyPath, to: keyPath, of: secondView, constant: constant, priority: priority) 66 | } 67 | 68 | func equalLayoutGuide(_ keyPath: KeyPath, 69 | to: KeyPath, 70 | orLayoutGuide layoutGuide: KeyPath, 71 | of secondView: UIView, 72 | constant: CGFloat = 0, 73 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 74 | return { firstView in 75 | let constraint: NSLayoutConstraint 76 | if #available(iOS 11.0, *) { 77 | constraint = firstView[keyPath: keyPath].constraint( 78 | equalTo: secondView.safeAreaLayoutGuide[keyPath: layoutGuide], 79 | constant: constant 80 | ) 81 | } else { 82 | constraint = firstView[keyPath: keyPath].constraint( 83 | equalTo: secondView[keyPath: to], 84 | constant: constant 85 | ) 86 | } 87 | 88 | constraint.priority = priority 89 | return constraint 90 | } 91 | } 92 | 93 | func equal(_ keyPath: KeyPath, 94 | of secondView: UIView, 95 | constant: CGFloat = 0, 96 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 97 | return equal(keyPath, keyPath, of: secondView, constant: constant, priority: priority) 98 | } 99 | 100 | func equal(_ keyPath: KeyPath, 101 | _ to: KeyPath, 102 | of secondView: UILayoutGuide, 103 | constant: CGFloat = 0, 104 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutAnchor { 105 | return { firstView in 106 | let constraint = firstView[keyPath: keyPath].constraint(equalTo: secondView[keyPath: to], 107 | constant: constant) 108 | constraint.priority = priority 109 | return constraint 110 | } 111 | } 112 | 113 | func equal(_ keyPath: KeyPath, 114 | constant: CGFloat = 0, 115 | priority: UILayoutPriority = .required) -> Constraint where Anchor: NSLayoutDimension { 116 | return { view in 117 | let constraint = view[keyPath: keyPath].constraint(equalToConstant: constant) 118 | constraint.priority = priority 119 | return constraint 120 | } 121 | } 122 | 123 | func equal(_ keyPath: KeyPath, 124 | _ to: KeyPath, 125 | of secondView: UIView, 126 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutDimension { 127 | return { view in 128 | view[keyPath: keyPath].constraint( 129 | equalTo: secondView[keyPath: to], 130 | multiplier: 1.0, 131 | constant: constant 132 | ) 133 | } 134 | } 135 | 136 | func lessThan(_ keyPath: KeyPath, 137 | _ to: KeyPath, 138 | of secondView: UIView, 139 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutDimension { 140 | return { view in 141 | view[keyPath: keyPath].constraint( 142 | lessThanOrEqualTo: secondView[keyPath: to], 143 | multiplier: 1.0, 144 | constant: constant 145 | ) 146 | } 147 | } 148 | 149 | // MARK: >= 150 | 151 | func greaterThan(_ keyPath: KeyPath, 152 | _ to: KeyPath, 153 | of secondView: UIView, 154 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutAnchor { 155 | return { firstView in 156 | firstView[keyPath: keyPath].constraint(greaterThanOrEqualTo: secondView[keyPath: to], 157 | constant: constant) 158 | } 159 | } 160 | 161 | func greaterThan(_ keyPath: KeyPath, 162 | of secondView: UIView, 163 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutAnchor { 164 | return greaterThan(keyPath, keyPath, of: secondView, constant: constant) 165 | } 166 | 167 | func greaterThan(_ keyPath: KeyPath, 168 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutDimension { 169 | return { view in 170 | view[keyPath: keyPath].constraint(greaterThanOrEqualToConstant: constant) 171 | } 172 | } 173 | 174 | // MARK: <= 175 | 176 | func lessThan(_ keyPath: KeyPath, 177 | _ to: KeyPath, 178 | of secondView: UIView, 179 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutAnchor { 180 | return { firstView in 181 | firstView[keyPath: keyPath].constraint(lessThanOrEqualTo: secondView[keyPath: to], 182 | constant: constant) 183 | } 184 | } 185 | 186 | func lessThan(_ keyPath: KeyPath, 187 | of secondView: UIView, 188 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutAnchor { 189 | return lessThan(keyPath, keyPath, of: secondView, constant: constant) 190 | } 191 | 192 | func lessThan(_ keyPath: KeyPath, 193 | constant: CGFloat = 0) -> Constraint where Anchor: NSLayoutDimension { 194 | return { view in 195 | view[keyPath: keyPath].constraint(lessThanOrEqualToConstant: constant) 196 | } 197 | } 198 | 199 | // MARK: UIView helpers 200 | 201 | extension UIView { 202 | 203 | func addSubview(_ child: UIView, constraints: [Constraint]) { 204 | addSubview(child) 205 | child.translatesAutoresizingMaskIntoConstraints = false 206 | NSLayoutConstraint.activate(constraints.map { $0(child) }) 207 | } 208 | 209 | func addSubview(_ child: UIView, insets: UIEdgeInsets) { 210 | let constraints = [ 211 | equal(\.topAnchor, of: self, constant: insets.top), 212 | equal(\.bottomAnchor, of: self, constant: -insets.bottom), 213 | equal(\.leadingAnchor, of: self, constant: insets.left), 214 | equal(\.trailingAnchor, of: self, constant: -insets.right) 215 | ] 216 | addSubview(child, constraints: constraints) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /MRefreshExamples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /MRefreshExamples/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MRefreshExamples 4 | // 5 | // Created by MIKHAIL RAKHMANOV on 25.01.21. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /MRefreshExamples/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MRefresh 3 | 4 | class MyCell: UITableViewCell { 5 | 6 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 7 | super.init(style: style, reuseIdentifier: reuseIdentifier) 8 | 9 | contentView.backgroundColor = .orange 10 | } 11 | 12 | required init?(coder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | } 16 | 17 | 18 | class ViewController: UIViewController { 19 | 20 | lazy var tableView: UITableView = { 21 | let table = UITableView() 22 | self.view.addSubview(table, constraints: [ 23 | equal(\.leadingAnchor, of: view), 24 | equal(\.trailingAnchor, of: view), 25 | equal(\.bottomAnchor, of: view), 26 | equalSafeArea(\.topAnchor, of: view), 27 | ]) 28 | table.register(MyCell.self, forCellReuseIdentifier: "MyCell") 29 | return table 30 | }() 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | tableView.dataSource = self 35 | tableView.reloadData() 36 | let connectedPath = makeConnectedPath(size: CGSize(width: 50.0, height: 50.0)) 37 | let frame = CGRect(origin: CGPoint.zero, 38 | size: CGSize(width: 50.0, height: 50.0)) 39 | let animatableView = PathDrawingAnimatableView(path: connectedPath, frame: frame) 40 | tableView.addPullToRefresh(animatable: animatableView, handler: { 41 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 42 | self.tableView.stopAnimating() 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func makeConnectedPath(size: CGSize) -> SVGConnectedPath { 49 | let svg1 = """ 50 | M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 51 | 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 52 | 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 53 | 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 54 | 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2z 55 | """ 56 | // Another svg examples: 57 | // let svg1 = """ 58 | // M12.971 352h32.394C67.172 454.735 181.944 512 288 512c106.229 0 220.853-57.38 242.635-160h32.394c10.691 0 16.045-12.926 59 | // 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0l-67.029 67.029c-7.56 7.56-2.206 20.485 8.485 20.485h35.146c-20.29 60 | // 54.317-84.963 86.588-144.117 94.015V256h52c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12h-52v-5.47c37.281-13.178 63.995-48.725 61 | // 64-90.518C384.005 43.772 341.605.738 289.37.01 235.723-.739 192 42.525 192 96c0 41.798 26.716 77.35 64 90.53V192h-52 62 | // c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h52v190.015c-58.936-7.399-123.82-39.679-144.117-94.015 63 | // h35.146c10.691 0 16.045-12.926 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0 64 | // L4.485 331.515C-3.074 339.074 2.28 352 12.971 352zM288 64c17.645 0 32 14.355 32 32s-14.355 32-32 32-32-14.355-32-32 14.355-32 32-32z 65 | // """ 66 | // let svg1 = """ 67 | // M257.2 162.7c-48.7 1.8-169.5 15.5-169.5 117.5 0 109.5 138.3 114 183.5 43.2 6.5 10.2 35.4 37.5 45.3 46.8l56.8-56S341 288.9 341 261.4V114.3 68 | // C341 89 316.5 32 228.7 32 140.7 32 94 87 94 136.3l73.5 6.8c16.3-49.5 54.2-49.5 54.2-49.5 40.7-.1 35.5 29.8 35.5 69.1zm0 86.8 69 | // c0 80-84.2 68-84.2 17.2 0-47.2 50.5-56.7 84.2-57.8v40.6zm136 163.5c-7.7 10-70 67-174.5 67S34.2 408.5 9.7 379c-6.8-7.7 1-11.3 5.5-8.3 70 | // C88.5 415.2 203 488.5 387.7 401c7.5-3.7 13.3 2 5.5 12zm39.8 2.2c-6.5 15.8-16 26.8-21.2 31-5.5 4.5-9.5 2.7-6.5-3.8 71 | // s19.3-46.5 12.7-55c-6.5-8.3-37-4.3-48-3.2-10.8 1-13 2-14-.3-2.3-5.7 21.7-15.5 37.5-17.5 15.7-1.8 41-.8 46 5.7 3.7 5.1 0 27.1-6.5 43.1z 72 | // """ 73 | let svg2 = """ 74 | M213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2 75 | L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z 76 | """ 77 | let svg3 = "M393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4z" 78 | var configuration = SVGConnectedPathConfiguration(size: size) 79 | configuration.add(svg: svg1, startProportion: 0.0, depth: 2) 80 | configuration.add(svg: svg2, startProportion: 0.3, depth: 2) 81 | configuration.add(svg: svg3, startProportion: 0.4, depth: 2) 82 | 83 | return try! SVGConnectedPathFactory.default.make(pathConfiguration: configuration) 84 | } 85 | 86 | extension ViewController: UITableViewDataSource { 87 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 88 | return 60.0 89 | } 90 | 91 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 92 | let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell 93 | 94 | return cell 95 | } 96 | 97 | func numberOfSections(in tableView: UITableView) -> Int { 98 | return 1 99 | } 100 | 101 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 102 | return 30 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MRefreshTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MRefreshTests/SVGPathFactoryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MRefresh 3 | 4 | class SVGPathFactoryTests: XCTestCase { 5 | 6 | private var reader: SVGReaderMock! 7 | private var simplifier: SVGSimplifierMock! 8 | private var smoother: SVGSmootherMock! 9 | private var factory: SVGPathFactoryImpl! 10 | 11 | override func setUp() { 12 | reader = SVGReaderMock() 13 | simplifier = SVGSimplifierMock() 14 | smoother = SVGSmootherMock() 15 | factory = SVGPathFactoryImpl(reader: reader, simplifier: simplifier, smoother: smoother) 16 | } 17 | 18 | func testFactoryMakesSVGPathCorrectly() throws { 19 | // given 20 | let expectedSvg = "m100 100" 21 | let readNodes = [SVGNode(instruction: .move, points: [p(100, 100)])] 22 | let simplifiedNodes = [SVGNode(instruction: .move, points: [p(200, 200)])] 23 | let smoothedNodes = [SVGNode(instruction: .move, points: [p(300, 300)])] 24 | let expectedDepth = 2 25 | reader.returnValue = readNodes 26 | simplifier.returnValue = simplifiedNodes 27 | smoother.returnValue = smoothedNodes 28 | 29 | // when 30 | let path = try factory.make(svg: expectedSvg, smoothDepth: expectedDepth) 31 | 32 | // then 33 | XCTAssertEqual(path.nodes, smoothedNodes) 34 | XCTAssertEqual(reader.svg, expectedSvg) 35 | XCTAssertEqual(simplifier.nodes, readNodes) 36 | XCTAssertEqual(smoother.parameters?.times, expectedDepth) 37 | XCTAssertEqual(smoother.parameters?.nodes, simplifiedNodes) 38 | } 39 | 40 | func testFactoryFailsWithError() { 41 | // given 42 | let expectedSvg = "m100 100" 43 | let simplifiedNodes = [SVGNode(instruction: .move, points: [p(200, 200)])] 44 | let smoothedNodes = [SVGNode(instruction: .move, points: [p(300, 300)])] 45 | let expectedDepth = 2 46 | reader.returnValue = nil 47 | simplifier.returnValue = simplifiedNodes 48 | smoother.returnValue = smoothedNodes 49 | 50 | // when 51 | var returnedError: Error? 52 | do { 53 | _ = try factory.make(svg: expectedSvg, smoothDepth: expectedDepth) 54 | } catch { 55 | returnedError = error 56 | } 57 | 58 | // then 59 | XCTAssertEqual(reader.svg, expectedSvg) 60 | XCTAssertNotNil(returnedError) 61 | XCTAssertNil(simplifier.nodes) 62 | XCTAssertNil(smoother.parameters) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MRefreshTests/SVGReaderMock.swift: -------------------------------------------------------------------------------- 1 | @testable import MRefresh 2 | 3 | enum MockError: Error { 4 | case somethingHappened 5 | } 6 | 7 | class SVGReaderMock: SVGReader { 8 | var returnValue: [SVGNode]? = [] 9 | var svg: String? 10 | 11 | func read(_ svg: String) throws -> [SVGNode] { 12 | self.svg = svg 13 | 14 | if let returnValue = returnValue { 15 | return returnValue 16 | } 17 | throw MockError.somethingHappened 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MRefreshTests/SVGReaderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MRefresh 3 | 4 | func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { 5 | return CGPoint(x: x, y: y) 6 | } 7 | 8 | class SVGReaderTests: XCTestCase { 9 | 10 | private let reader = MRefresh.SVGReaderImpl() 11 | 12 | func testReadNonRelativeSVG() throws { 13 | // given 14 | let svg = "M100 100L200 200Q300 300 400 400C500 500 600 600 700 700V800H800Z" 15 | let expectedNodes = [ 16 | SVGNode(instruction: .move, points: [p(100, 100)]), 17 | SVGNode(instruction: .line, points: [p(200, 200)]), 18 | SVGNode(instruction: .quadratic, points: [p(300, 300), p(400, 400)]), 19 | SVGNode(instruction: .cubic, points: [p(500, 500), p(600, 600), p(700, 700)]), 20 | SVGNode(instruction: .vertical, points: [p(0, 800)]), 21 | SVGNode(instruction: .horizontal, points: [p(800, 0)]), 22 | SVGNode(instruction: .closePath, points: []) 23 | ] 24 | 25 | // when 26 | let nodes = try reader.read(svg) 27 | 28 | // then 29 | XCTAssertEqual(nodes, expectedNodes) 30 | } 31 | 32 | func testReadRelativeSVG() throws { 33 | // given 34 | let svg = "m100 100l20 20q50 50 50 50c100 100 100 100 100 100v100h100z" 35 | let expectedNodes = [ 36 | SVGNode(instruction: .moveRelative, points: [p(100, 100)]), 37 | SVGNode(instruction: .lineRelative, points: [p(20, 20)]), 38 | SVGNode(instruction: .quadraticRelative, points: [p(50, 50), p(50, 50)]), 39 | SVGNode(instruction: .cubicRelative, points: [p(100, 100), p(100, 100), p(100, 100)]), 40 | SVGNode(instruction: .verticalRelative, points: [p(0, 100)]), 41 | SVGNode(instruction: .horizontalRelative, points: [p(100, 0)]), 42 | SVGNode(instruction: .closePathSmall, points: []) 43 | ] 44 | 45 | // when 46 | let nodes = try reader.read(svg) 47 | 48 | // then 49 | XCTAssertEqual(nodes, expectedNodes) 50 | } 51 | 52 | func testShorthandSVG() throws { 53 | // given 54 | let svg = "M100 100L200 200T400 400S500 500 600 600Z" 55 | let expectedNodes = [ 56 | SVGNode(instruction: .move, points: [p(100, 100)]), 57 | SVGNode(instruction: .line, points: [p(200, 200)]), 58 | SVGNode(instruction: .shorthandQuadratic, points: [p(400, 400)]), 59 | SVGNode(instruction: .shorthandCubic, points: [p(500, 500), p(600, 600)]), 60 | SVGNode(instruction: .closePath, points: []) 61 | ] 62 | 63 | // when 64 | let nodes = try reader.read(svg) 65 | 66 | // then 67 | XCTAssertEqual(nodes, expectedNodes) 68 | } 69 | 70 | func testShorthandRelativeSVG() throws { 71 | // given 72 | let svg = "M100 100L200 200t100 100s50 50 60 60Z" 73 | let expectedNodes = [ 74 | SVGNode(instruction: .move, points: [p(100, 100)]), 75 | SVGNode(instruction: .line, points: [p(200, 200)]), 76 | SVGNode(instruction: .shorthandQuadraticRelative, points: [p(100, 100)]), 77 | SVGNode(instruction: .shorthandCubicRelative, points: [p(50, 50), p(60, 60)]), 78 | SVGNode(instruction: .closePath, points: []) 79 | ] 80 | 81 | // when 82 | let nodes = try reader.read(svg) 83 | 84 | // then 85 | XCTAssertEqual(nodes, expectedNodes) 86 | } 87 | 88 | func testSVGWithUnknownSymbols() { 89 | // given 90 | let svg = "M100 100S100 100 100 100R100 100Z" 91 | 92 | // when 93 | var returnedError: Error? 94 | do { 95 | _ = try reader.read(svg) 96 | } catch { 97 | returnedError = error 98 | } 99 | 100 | // then 101 | XCTAssertNotNil(returnedError) 102 | } 103 | 104 | func testSVGWithIncorrectPoints() { 105 | for instruction in SVGInstruction.allCases { 106 | // given 107 | let values: String 108 | // generating different number of points than expected 109 | switch instruction.valuesCount { 110 | case 0: 111 | values = "100" 112 | case 1: 113 | values = "" 114 | default: 115 | values = (0 ... instruction.valuesCount).map { _ in "100" }.joined(separator: " ") 116 | } 117 | let testedSvg = "M100 100S100 100 100 100\(instruction.rawValue + values)Z" 118 | 119 | // when 120 | var returnedError: Error? 121 | do { 122 | _ = try reader.read(testedSvg) 123 | } catch { 124 | returnedError = error 125 | } 126 | 127 | // then 128 | XCTAssertNotNil(returnedError) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /MRefreshTests/SVGSimplifierMock.swift: -------------------------------------------------------------------------------- 1 | @testable import MRefresh 2 | 3 | class SVGSimplifierMock: SVGSimplifier { 4 | var returnValue: [SVGNode] = [] 5 | var nodes: [SVGNode]? 6 | 7 | func simplify(_ nodes: [SVGNode]) -> [SVGNode] { 8 | self.nodes = nodes 9 | 10 | return returnValue 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MRefreshTests/SVGSmootherMock.swift: -------------------------------------------------------------------------------- 1 | @testable import MRefresh 2 | 3 | class SVGSmootherMock: SVGSmoother { 4 | var returnValue: [SVGNode] = [] 5 | var parameters: (times: Int, nodes: [SVGNode])? 6 | 7 | func smooth(times: Int, nodes: [SVGNode]) -> [SVGNode] { 8 | parameters = (times: times, nodes: nodes) 9 | 10 | return returnValue 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MRefresh 2 | 3 | [![CI Status](http://img.shields.io/travis/Mikhail Rakhmanov/MRefresh.svg?style=flat)](https://travis-ci.org/Mikhail Rakhmanov/MRefresh) 4 | [![Version](https://img.shields.io/cocoapods/v/MRefresh.svg?style=flat)](http://cocoapods.org/pods/MRefresh) 5 | [![License](https://img.shields.io/cocoapods/l/MRefresh.svg?style=flat)](http://cocoapods.org/pods/MRefresh) 6 | [![Platform](https://img.shields.io/cocoapods/p/MRefresh.svg?style=flat)](http://cocoapods.org/pods/MRefresh) 7 | 8 | ## What is MRefresh 9 | 10 | So basically MRefresh is a pull-to-refresh with a clear separation of concerns which consits of several independent components: 11 | - a pull-to-refresh mechanism which adds a container view to a scrollview. This container view uses an animatable view conforming to *AnimatableViewConforming* protocol. The view receives messages during each of the pull-to-refresh stages (see description below), 12 | - a path drawing mechanism which can read multiple SVG paths which are parts of one picture (*SVGConnectedPathFactory*), convert them to UIBezierPath objects, add additional points to such paths, so the drawing would be more smooth (using De Castelaju's Algorithm - https://en.wikipedia.org/wiki/De_Casteljau's_algorithm). 13 | 14 | To sum up, you can: 15 | - take *some* SVG paths (though as of today arc command has not been implemented, this requires some approximation of arcs using qubic curves and is trickier) and draw them in any combination when user pulls the scrollview, 16 | - provide your own custom animations to the pull-to-refresh view 17 | 18 | So here's a quick demo of what this library can do (we are drawing one of the FontAwesome SVG paths): 19 | 20 | ![MRefresh demo](Assets/MRefresh.gif) 21 | 22 | ### Example 23 | 24 | Below see the steps needed to configure the pull-to-refresh view. Of course, if you don't want to read the long description, you can download the example and see everything for yourself. 25 | 26 | #### SVGConnectedPathFactory 27 | 28 | The library was made in a way that enables you to configure all parameters of the pull-to-refresh process. 29 | 30 | Firstly, you need SVG path, let it be something like this (it is a path that was taken from one of the FontAwesome icons, no copyrights infringed I hope): 31 | 32 | ```swift 33 | let path = "M1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23" 34 | ``` 35 | Secondly, you need to create a svg path configuration, which provides additional parameters about how the path should be scaled. 36 | 37 | 38 | ```swift 39 | var configuration = SVGConnectedPathConfiguration(size: size) 40 | ``` 41 | Here you provide a *size* in which the path should be placed, basically the algorithm tries to center the path in the size so the path would take as much place as possible. 42 | 43 | ```swift 44 | configuration.add(svg: path, startProportion: 0.0, depth: 2) 45 | ``` 46 | Here you add the part of the svg path to configuration. 47 | 48 | *startProportion* says when during the pull to refresh process this path should start drawing. This is useful when you have multiple parts of the path to be able to start drawing them simultaneously or with some delay. 49 | 50 | *depth* - how much you want your path to be smoothed (i.e. how many new points you want to be generated amount of points = initialSvgPoints * 2 ^ 3). 51 | 52 | Then it is time to create the *SVGNode*'s through the *SVGConnectedPathFactory* which will do all the hard work converting and resizing your svg's. 53 | 54 | ```swift 55 | let nodes = try SVGConnectedPathFactory.default.make(pathConfiguration: configuration) 56 | ``` 57 | 58 | #### PathDrawingAnimatableView 59 | 60 | When you've done creating the path manager it is time to create the animatable view. You can do it like so: 61 | ```swift 62 | let frame = CGRect(origin: CGPoint.zero, 63 | size: size) 64 | let animatableView = PathDrawingAnimatableView(path: connectedPath, frame: frame) 65 | ``` 66 | So *frame* is obviously the frame in which the view is drawn (actually the origin here doesn't matter because it is calculated under the hood). 67 | 68 | #### Adding handler to a scroll view 69 | 70 | The very last thing is to add the animatable view, the configuration and the action handler (i.e. the closure which is called when the content offset reaches certain value) to a scroll view. 71 | 72 | ```swift 73 | tableView.addPullToRefresh(animatable: animatableView, handler: { [weak self] in 74 | self?.somePresenter.didAskToRefreshAView() 75 | }) 76 | ... 77 | // when the data was loaded 78 | tableView.stopAnimating() 79 | ``` 80 | 81 | #### PullToRefreshConfiguration 82 | 83 | You can specify additional parameters when adding view to scroll view as pull to refresh, see *PullToRefreshConfiguration*. 84 | 85 | ### Pull-to-refresh mechanism 86 | 87 | Below see brief description of the pull-to-refresh mechanism. First of all, there is an extension to UIScrollView which enables you to add a view which conforms to a specific protocol *AnimatableViewConforming*. This view will receive certain messages from the UIScrollView when users pulls it and releases it. 88 | 89 | We can think of a pull-to-refresh as a 4 stage process. 90 | 91 | #### First stage 92 | 93 | The content offset of the scrollview hasn't reached some starting value (*startValue*) when the animatable view becomes visible. 94 | 95 | #### Second stage 96 | 97 | The content offset of the scrollview has reached the starting value, and the *AnimatableContainerView* (which is a container view used under the hood) tells your view to *drawPullToRefresh(proportion: CGFloat)*. The proportion will be a CGFloat value from 0 to 1 depending on whether the contentoffset has reached some other value, let's call it the *endValue*. 98 | 99 | In case of *PathDrawingAnimatableView* which is a view conforming to *AnimatableViewConforming* and is provided in the library, the proportion value will tell your view how many points in a path should be displayed on screen. 100 | 101 | #### Third stage 102 | 103 | The content offset of the scrollview has reached the *endValue*. Now: 104 | - the view receives the *startAnimation* message, 105 | - the scrollview's inset is increased to fit the animatable view with some additional space (== frame of the *AnimatableContainerView*), 106 | - the actionHandler closure is called (e.g. some services shall start downloading something etc) 107 | 108 | #### Fourth stage 109 | 110 | The scrollview receives *stopAnimating* message (you should send the message when e.g. the data/error is received upon loading). After that if user is not holding the view with his finger, the view will receive *stopAnimation* message. 111 | 112 | Please bear in mind the respective timing, because after user releases its finger the scrollview changes its insets to initial value. 113 | 114 | ## Installation 115 | 116 | MRefresh is available through [CocoaPods](http://cocoapods.org). To install 117 | it, simply add the following line to your Podfile: 118 | 119 | ```ruby 120 | pod 'MRefresh', '~> 0.2.1' 121 | ``` 122 | ## Author 123 | 124 | Mikhail Rakhmanov, rakhmanov.m@gmail.com 125 | 126 | ## License 127 | 128 | MRefresh is available under the MIT license. See the LICENSE file for more info. 129 | --------------------------------------------------------------------------------