├── .gitignore ├── README.md ├── UIScrollView+ScrollAnimation.h └── UIScrollView+ScrollAnimation.m /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | UIScrollView-ScrollAnimation 2 | ============================ 3 | 4 | UIScrollView category with custom timing function for animation of setContentOffset 5 | 6 | Background 7 | --- 8 | This work was inspired by this project here: 9 | https://github.com/plancalculus/MOScrollView 10 | 11 | 12 | Features 13 | -------- 14 | 15 | Provides methods 16 | 17 | - (void)setContentOffset:(CGPoint)contentOffset 18 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 19 | 20 | and 21 | 22 | - (void)setContentOffset:(CGPoint)contentOffset 23 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 24 | duration:(CFTimeInterval)duration 25 | 26 | 27 | Usage 28 | ----- 29 | 30 | Import `UIScrollView+ScrollAnimation.h` and `UIScrollView+ScrollAnimation.m` into your 31 | project. The implementation uses a `CADisplayLink`, therefore, you 32 | have to add the `QuartzCore` library to your project. As the class 33 | uses automatic refernce counting either your project has to use 34 | automatic reference counting as well. 35 | 36 | Exmaple: 37 | ``` objc 38 | 39 | #import "UIScrollView+ScrollAnimation.h" 40 | 41 | @interface MyCollectionViewController : UIViewController 42 | @property (nonatomic, strong) UICollectionView* collectionView; 43 | @end 44 | 45 | @implementation MyCollectionViewController 46 | 47 | - (void)viewDidAppear:(BOOL)animated { 48 | [super viewDidAppear:animated]; 49 | 50 | // call method to slowly scroll up or down, left or right 51 | [self.collectionView setContentOffset:offsetPoint 52 | withTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] 53 | duration:animationDuration]; 54 | } 55 | 56 | @end 57 | 58 | ``` 59 | 60 | Requirements 61 | ------------ 62 | 63 | XCode 4.2 or later and iOS 4 or later as the module uses automatic reference counting. 64 | 65 | 66 | 67 | License 68 | --- 69 | 70 | ``` 71 | Permission is hereby granted, free of charge, to any person obtaining a copy of 72 | this software and associated documentation files (the "Software"), to deal in 73 | the Software without restriction, including without limitation the rights to 74 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 75 | of the Software, and to permit persons to whom the Software is furnished to do 76 | so, subject to the following conditions: 77 | 78 | The above copyright notice and this permission notice shall be included in all 79 | copies or substantial portions of the Software. 80 | 81 | If you happen to meet one of the copyright holders in a bar you are obligated 82 | to buy them one pint of beer. 83 | 84 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 85 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 86 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 87 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 88 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 89 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 90 | SOFTWARE. 91 | 92 | 93 | ``` -------------------------------------------------------------------------------- /UIScrollView+ScrollAnimation.h: -------------------------------------------------------------------------------- 1 | /* 2 | UIScrollView+ScrollAnimation.h 3 | 4 | 5 | Created by Jonathan Lott. 6 | Copyright (c) 2014 A Lott Of Ideas. All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | of the Software, and to permit persons to whom the Software is furnished to do 13 | so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | If you happen to meet one of the copyright holders in a bar you are obligated 19 | to buy them one pint of beer. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | This work was inspired by this project here: 30 | https://github.com/plancalculus/MOScrollView 31 | */ 32 | 33 | #import 34 | 35 | @interface UIScrollView (ScrollAnimation) 36 | // all methods were derived from: https://github.com/plancalculus/MOScrollView 37 | /** 38 | * Sets the contentOffset of the ScrollView and animates the transition. The 39 | * animation takes 0.25 seconds. 40 | * 41 | * @param contentOffset A point (expressed in points) that is offset from the 42 | * content view’s origin. 43 | * @param timingFunction A timing function that defines the pacing of the 44 | * animation. 45 | */ 46 | - (void)setContentOffset:(CGPoint)contentOffset 47 | withTimingFunction:(CAMediaTimingFunction *)timingFunction; 48 | 49 | /** 50 | * Sets the contentOffset of the ScrollView and animates the transition. 51 | * 52 | * @param contentOffset A point (expressed in points) that is offset from the 53 | * content view’s origin. 54 | * @param timingFunction A timing function that defines the pacing of the 55 | * animation. 56 | * @param duration Duration of the animation in seconds. 57 | */ 58 | - (void)setContentOffset:(CGPoint)contentOffset 59 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 60 | duration:(CFTimeInterval)duration; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /UIScrollView+ScrollAnimation.m: -------------------------------------------------------------------------------- 1 | /* 2 | UIScrollView+ScrollAnimation.m 3 | 4 | 5 | Created by Jonathan Lott. 6 | Copyright (c) 2014 A Lott Of Ideas. All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | of the Software, and to permit persons to whom the Software is furnished to do 13 | so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | If you happen to meet one of the copyright holders in a bar you are obligated 19 | to buy them one pint of beer. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | This work was inspired by this project here: 30 | https://github.com/plancalculus/MOScrollView/tree/master/MOScrollView 31 | */ 32 | 33 | #import "UIScrollView+ScrollAnimation.h" 34 | #import 35 | #import 36 | 37 | 38 | const static CFTimeInterval kDefaultSetContentOffsetDuration = 0.25; 39 | 40 | /// Constants used for Newton approximation of cubic function root. 41 | const static double kApproximationTolerance = 0.00000001; 42 | const static int kMaximumSteps = 10; 43 | 44 | @interface ScrollViewTimingDelegate : NSObject 45 | 46 | @property (nonatomic, strong) UIScrollView* scrollView; 47 | 48 | /// Display link used to trigger event to scroll the view. 49 | @property(nonatomic) CADisplayLink *displayLink; 50 | 51 | /// Timing function of an scroll animation. 52 | @property(nonatomic) CAMediaTimingFunction *timingFunction; 53 | 54 | /// Duration of an scroll animation. 55 | @property(nonatomic) CFTimeInterval duration; 56 | 57 | /// States whether the animation has started. 58 | @property(nonatomic) BOOL animationStarted; 59 | 60 | /// Time at the begining of an animation. 61 | @property(nonatomic) CFTimeInterval beginTime; 62 | 63 | /// The content offset at the begining of an animation. 64 | @property(nonatomic) CGPoint beginContentOffset; 65 | 66 | /// The delta between the contentOffset at the start of the animation and 67 | /// the contentOffset at the end of the animation. 68 | @property(nonatomic) CGPoint deltaContentOffset; 69 | 70 | @end 71 | 72 | @implementation ScrollViewTimingDelegate 73 | 74 | #pragma mark - Set ContentOffset with Custom Animation 75 | 76 | - (void)setContentOffset:(CGPoint)contentOffset 77 | withTimingFunction:(CAMediaTimingFunction *)timingFunction { 78 | [self setContentOffset:contentOffset 79 | withTimingFunction:timingFunction 80 | duration:kDefaultSetContentOffsetDuration]; 81 | } 82 | 83 | - (void)setContentOffset:(CGPoint)contentOffset 84 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 85 | duration:(CFTimeInterval)duration { 86 | 87 | if(!self.scrollView) 88 | return; 89 | 90 | self.duration = duration; 91 | self.timingFunction = timingFunction; 92 | 93 | self.deltaContentOffset = CGPointMinus(contentOffset, self.scrollView.contentOffset); 94 | 95 | if (!self.displayLink) { 96 | self.displayLink = [CADisplayLink 97 | displayLinkWithTarget:self 98 | selector:@selector(updateContentOffset:)]; 99 | self.displayLink.frameInterval = 1; 100 | [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] 101 | forMode:NSDefaultRunLoopMode]; 102 | } else { 103 | self.displayLink.paused = NO; 104 | } 105 | } 106 | 107 | - (void)updateContentOffset:(CADisplayLink *)displayLink { 108 | if (self.beginTime == 0.0) { 109 | self.beginTime = self.displayLink.timestamp; 110 | self.beginContentOffset = self.scrollView.contentOffset; 111 | } else { 112 | CFTimeInterval deltaTime = displayLink.timestamp - self.beginTime; 113 | 114 | // Ratio of duration that went by 115 | CGFloat progress = (CGFloat)(deltaTime / self.duration); 116 | if (progress < 1.0) { 117 | // Ratio adjusted by timing function 118 | CGFloat adjustedProgress = (CGFloat)timingFunctionValue(self.timingFunction, progress); 119 | if (1 - adjustedProgress < 0.001) { 120 | [self stopAnimation]; 121 | } else { 122 | [self updateProgress:adjustedProgress]; 123 | } 124 | } else { 125 | [self stopAnimation]; 126 | } 127 | } 128 | } 129 | 130 | - (void)updateProgress:(CGFloat)progress { 131 | CGPoint currentDeltaContentOffset = CGPointScalarMult(progress, self.deltaContentOffset); 132 | self.scrollView.contentOffset = CGPointAdd(self.beginContentOffset, currentDeltaContentOffset); 133 | } 134 | 135 | - (void)stopAnimation { 136 | self.displayLink.paused = YES; 137 | self.beginTime = 0.0; 138 | 139 | self.scrollView.contentOffset = CGPointAdd(self.beginContentOffset, self.deltaContentOffset); 140 | 141 | if (self.scrollView.delegate 142 | && [self.scrollView.delegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) { 143 | // inform delegate about end of animation 144 | [self.scrollView.delegate scrollViewDidEndScrollingAnimation:self.scrollView]; 145 | } 146 | } 147 | 148 | 149 | CGPoint CGPointScalarMult(CGFloat s, CGPoint p) { 150 | return CGPointMake(s * p.x, s * p.y); 151 | } 152 | 153 | CGPoint CGPointAdd(CGPoint p, CGPoint q) { 154 | return CGPointMake(p.x + q.x, p.y + q.y); 155 | } 156 | 157 | CGPoint CGPointMinus(CGPoint p, CGPoint q) { 158 | return CGPointMake(p.x - q.x, p.y - q.y); 159 | } 160 | 161 | double cubicFunctionValue(double a, double b, double c, double d, double x) { 162 | return (a*x*x*x)+(b*x*x)+(c*x)+d; 163 | } 164 | 165 | double cubicDerivativeValue(double a, double b, double c, double __unused d, double x) { 166 | /// Derivation of the cubic (a*x*x*x)+(b*x*x)+(c*x)+d 167 | return (3*a*x*x)+(2*b*x)+c; 168 | } 169 | 170 | double rootOfCubic(double a, double b, double c, double d, double startPoint) { 171 | // We use 0 as start point as the root will be in the interval [0,1] 172 | double x = startPoint; 173 | double lastX = 1; 174 | 175 | // Approximate a root by using the Newton-Raphson method 176 | int y = 0; 177 | while (y <= kMaximumSteps && fabs(lastX - x) > kApproximationTolerance) { 178 | lastX = x; 179 | x = x - (cubicFunctionValue(a, b, c, d, x) / cubicDerivativeValue(a, b, c, d, x)); 180 | y++; 181 | } 182 | 183 | return x; 184 | } 185 | 186 | double timingFunctionValue(CAMediaTimingFunction *function, double x) { 187 | float a[2]; 188 | float b[2]; 189 | float c[2]; 190 | float d[2]; 191 | 192 | [function getControlPointAtIndex:0 values:a]; 193 | [function getControlPointAtIndex:1 values:b]; 194 | [function getControlPointAtIndex:2 values:c]; 195 | [function getControlPointAtIndex:3 values:d]; 196 | 197 | // Look for t value that corresponds to provided x 198 | double t = rootOfCubic(-a[0]+3*b[0]-3*c[0]+d[0], 3*a[0]-6*b[0]+3*c[0], -3*a[0]+3*b[0], a[0]-x, x); 199 | 200 | // Return corresponding y value 201 | double y = cubicFunctionValue(-a[1]+3*b[1]-3*c[1]+d[1], 3*a[1]-6*b[1]+3*c[1], -3*a[1]+3*b[1], a[1], t); 202 | 203 | return y; 204 | } 205 | @end 206 | 207 | @implementation UIScrollView (ScrollAnimation) 208 | 209 | - (ScrollViewTimingDelegate*)scrollViewTimingDelegate 210 | { 211 | ScrollViewTimingDelegate* timingDelegate = objc_getAssociatedObject(self, "scrollViewTimingDelegate"); 212 | return timingDelegate; 213 | } 214 | 215 | - (void)setScrollViewTimingDelegate:(ScrollViewTimingDelegate*)timingDelegate 216 | { 217 | objc_setAssociatedObject(self, "scrollViewTimingDelegate", timingDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 218 | } 219 | 220 | - (void)setContentOffset:(CGPoint)contentOffset 221 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 222 | { 223 | if(![self scrollViewTimingDelegate]) 224 | { 225 | ScrollViewTimingDelegate* timingDelegate = [[ScrollViewTimingDelegate alloc] init]; 226 | timingDelegate.scrollView = self; 227 | [self setScrollViewTimingDelegate:timingDelegate]; 228 | } 229 | [[self scrollViewTimingDelegate] setContentOffset:contentOffset withTimingFunction:timingFunction]; 230 | } 231 | 232 | - (void)setContentOffset:(CGPoint)contentOffset 233 | withTimingFunction:(CAMediaTimingFunction *)timingFunction 234 | duration:(CFTimeInterval)duration 235 | { 236 | if(![self scrollViewTimingDelegate]) 237 | { 238 | ScrollViewTimingDelegate* timingDelegate = [[ScrollViewTimingDelegate alloc] init]; 239 | timingDelegate.scrollView = self; 240 | [self setScrollViewTimingDelegate:timingDelegate]; 241 | } 242 | [[self scrollViewTimingDelegate] setContentOffset:contentOffset withTimingFunction:timingFunction duration:duration]; 243 | } 244 | @end 245 | --------------------------------------------------------------------------------