From c45e93077d19d44658d8b6d5dc9e24c85fe58abb Mon Sep 17 00:00:00 2001 From: ipodishima Date: Wed, 23 Apr 2014 10:31:43 -0400 Subject: [PATCH 1/2] Moved the pull to refresh to a separate subclass so that you can inherit and custom your own pull to refresh view --- .../project.pbxproj | 6 + .../SVPullToRefreshDemo/SVDemoPullToRefresh.h | 13 + .../SVPullToRefreshDemo/SVDemoPullToRefresh.m | 101 ++++ Demo/SVPullToRefreshDemo/SVViewController.m | 3 +- .../UIScrollView+SVPullToRefresh.h | 36 +- .../UIScrollView+SVPullToRefresh.m | 535 ++++++++++-------- 6 files changed, 438 insertions(+), 256 deletions(-) create mode 100644 Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.h create mode 100644 Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.m diff --git a/Demo/SVPullToRefreshDemo.xcodeproj/project.pbxproj b/Demo/SVPullToRefreshDemo.xcodeproj/project.pbxproj index cc837b7a..59a170b6 100644 --- a/Demo/SVPullToRefreshDemo.xcodeproj/project.pbxproj +++ b/Demo/SVPullToRefreshDemo.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 22E0D94B1545F63300BB6BB5 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 22E0D94A1545F63300BB6BB5 /* README.md */; }; 22FDEC971639082E00DB53A8 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 22FDEC961639082E00DB53A8 /* Default-568h@2x.png */; }; 22FDEC9D16390CC800DB53A8 /* UIScrollView+SVInfiniteScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = 2288146016047C06005C6461 /* UIScrollView+SVInfiniteScrolling.m */; }; + E0CC069D190801C700905FFD /* SVDemoPullToRefresh.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CC069C190801C700905FFD /* SVDemoPullToRefresh.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,6 +47,8 @@ 22E0D9401545EE9000BB6BB5 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 22E0D94A1545F63300BB6BB5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = README.md; path = ../README.md; sourceTree = ""; }; 22FDEC961639082E00DB53A8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + E0CC069B190801C700905FFD /* SVDemoPullToRefresh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SVDemoPullToRefresh.h; sourceTree = ""; }; + E0CC069C190801C700905FFD /* SVDemoPullToRefresh.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SVDemoPullToRefresh.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -102,6 +105,8 @@ 22E0D92A1545EE5B00BB6BB5 /* SVViewController.h */, 22E0D92B1545EE5B00BB6BB5 /* SVViewController.m */, 22E0D92D1545EE5B00BB6BB5 /* SVViewController.xib */, + E0CC069B190801C700905FFD /* SVDemoPullToRefresh.h */, + E0CC069C190801C700905FFD /* SVDemoPullToRefresh.m */, 22E0D91F1545EE5B00BB6BB5 /* Supporting Files */, ); name = Demo; @@ -201,6 +206,7 @@ buildActionMask = 2147483647; files = ( 22E0D9251545EE5B00BB6BB5 /* main.m in Sources */, + E0CC069D190801C700905FFD /* SVDemoPullToRefresh.m in Sources */, 22E0D9291545EE5B00BB6BB5 /* SVAppDelegate.m in Sources */, 22E0D92C1545EE5B00BB6BB5 /* SVViewController.m in Sources */, 22E0D93D1545EE7600BB6BB5 /* UIScrollView+SVPullToRefresh.m in Sources */, diff --git a/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.h b/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.h new file mode 100644 index 00000000..aa8e20df --- /dev/null +++ b/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.h @@ -0,0 +1,13 @@ +// +// SVDemoPullToRefresh.h +// SVPullToRefreshDemo +// +// Created by Marian Paul on 23/04/2014. +// Copyright (c) 2014 Home. All rights reserved. +// + +#import "UIScrollView+SVPullToRefresh.h" + +@interface SVDemoPullToRefresh : SVPullToRefreshView + +@end diff --git a/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.m b/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.m new file mode 100644 index 00000000..2cafdfca --- /dev/null +++ b/Demo/SVPullToRefreshDemo/SVDemoPullToRefresh.m @@ -0,0 +1,101 @@ +// +// SVDemoPullToRefresh.m +// SVPullToRefreshDemo +// +// Created by Marian Paul on 23/04/2014. +// Copyright (c) 2014 Home. All rights reserved. +// + +#import "SVDemoPullToRefresh.h" + +const static CGFloat kDeltaScaleForPullToRefresh = 0.2f; + +@implementation SVDemoPullToRefresh +{ + UIView *_circleView; + UIActivityIndicatorView *_activity; + BOOL _triggered; +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + _circleView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 20.0f, 20.0f)]; + + UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect:_circleView.bounds]; + CAShapeLayer *plane = [CAShapeLayer layer]; + plane.fillColor = [UIColor redColor].CGColor; + plane.path = ovalPath.CGPath; + plane.frame = _circleView.bounds; + plane.anchorPoint = CGPointMake(0.5, 0.5); + plane.anchorPointZ = 0.5; + + [_circleView.layer addSublayer:plane]; + + [self addSubview:_circleView]; + + _activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [_activity setHidesWhenStopped:YES]; + [self addSubview:_activity]; + } + return self; +} + +- (void)handleNewState:(SVPullToRefreshState)state +{ + switch (state) { + case SVPullToRefreshStateStopped: + [_activity stopAnimating]; + _circleView.hidden = NO; + break; + case SVPullToRefreshStateTriggered: + + break; + case SVPullToRefreshStateLoading: + { + if (!_triggered) { + _triggered = YES; + CGFloat duration = 0.6f; + + [UIView animateWithDuration:duration + animations:^{ + _circleView.center = CGPointMake(CGRectGetWidth(self.frame)/2.0f, CGRectGetHeight(self.frame) / 2.0f); + _circleView.transform = CGAffineTransformIdentity; + } + completion:^(BOOL finished) { + if (finished && self.state == SVPullToRefreshStateLoading) + { + [_activity setCenter:_circleView.center]; + [_activity startAnimating]; + _circleView.hidden = YES; + } + }]; + } + + } + break; + case SVPullToRefreshStateAll: + + break; + default: + break; + } +} + +- (void) updateForPercentage:(CGFloat)percentage +{ + percentage = MAX(0.0f, percentage); + + if (percentage == 0.0f) _triggered = NO; + + if (_triggered) return; + + CGFloat deltaScale = percentage * kDeltaScaleForPullToRefresh + 1.0f; + + [_circleView setTransform:CGAffineTransformMakeScale(deltaScale, deltaScale)]; + _circleView.center = CGPointMake(CGRectGetWidth(self.frame)/2.0f, CGRectGetHeight(self.frame) * (1.0f - percentage*0.3f) - 13.0f); + _activity.center = _circleView.center; +} + +@end diff --git a/Demo/SVPullToRefreshDemo/SVViewController.m b/Demo/SVPullToRefreshDemo/SVViewController.m index 10c1aaa1..6443737e 100644 --- a/Demo/SVPullToRefreshDemo/SVViewController.m +++ b/Demo/SVPullToRefreshDemo/SVViewController.m @@ -8,6 +8,7 @@ #import "SVViewController.h" #import "SVPullToRefresh.h" +#import "SVDemoPullToRefresh.h" @interface SVViewController () @@ -25,7 +26,7 @@ - (void)viewDidLoad { __weak SVViewController *weakSelf = self; // setup pull-to-refresh - [self.tableView addPullToRefreshWithActionHandler:^{ + [self.tableView addPullToRefresh:[SVDemoPullToRefresh class] withActionHandler:^{ [weakSelf insertRowAtTop]; }]; diff --git a/SVPullToRefresh/UIScrollView+SVPullToRefresh.h b/SVPullToRefresh/UIScrollView+SVPullToRefresh.h index 73c92b7f..7303ace9 100644 --- a/SVPullToRefresh/UIScrollView+SVPullToRefresh.h +++ b/SVPullToRefresh/UIScrollView+SVPullToRefresh.h @@ -20,8 +20,12 @@ typedef NS_ENUM(NSUInteger, SVPullToRefreshPosition) { SVPullToRefreshPositionBottom, }; -- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler; -- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position; +- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler DEPRECATED_MSG_ATTRIBUTE("Use `addPullToRefresh:withActionHandler:` instead"); +- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position DEPRECATED_MSG_ATTRIBUTE("Use `addPullToRefresh:withActionHandler:position:` instead"); + +- (void)addPullToRefresh:(Class)pullToRefreshClass withActionHandler:(void (^)(void))actionHandler; +- (void)addPullToRefresh:(Class)pullToRefreshClass withActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position; + - (void)triggerPullToRefresh; @property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView; @@ -37,8 +41,27 @@ typedef NS_ENUM(NSUInteger, SVPullToRefreshState) { SVPullToRefreshStateAll = 10 }; +#pragma mark - The class to inherit from @interface SVPullToRefreshView : UIView +@property (nonatomic, readonly) SVPullToRefreshState state; +@property (nonatomic, readonly) SVPullToRefreshPosition position; + +- (void)startAnimating; +- (void)stopAnimating; + +// To override if needed +- (void) handleNewState:(SVPullToRefreshState)state; +- (void) updateForPercentage:(CGFloat)percentage; // This is not bounded to 0.0f -> 1.0f by convenience + +// deprecated; use [self.scrollView triggerPullToRefresh] instead +- (void)triggerRefresh DEPRECATED_ATTRIBUTE; + +@end + + +#pragma mark - An example with the classic arrow +@interface SVArrowPullToRefreshView : SVPullToRefreshView @property (nonatomic, strong) UIColor *arrowColor; @property (nonatomic, strong) UIColor *textColor; @property (nonatomic, strong, readonly) UILabel *titleLabel; @@ -46,22 +69,13 @@ typedef NS_ENUM(NSUInteger, SVPullToRefreshState) { @property (nonatomic, strong, readwrite) UIColor *activityIndicatorViewColor NS_AVAILABLE_IOS(5_0); @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; -@property (nonatomic, readonly) SVPullToRefreshState state; -@property (nonatomic, readonly) SVPullToRefreshPosition position; - - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state; - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state; - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state; -- (void)startAnimating; -- (void)stopAnimating; - // deprecated; use setSubtitle:forState: instead @property (nonatomic, strong, readonly) UILabel *dateLabel DEPRECATED_ATTRIBUTE; @property (nonatomic, strong) NSDate *lastUpdatedDate DEPRECATED_ATTRIBUTE; @property (nonatomic, strong) NSDateFormatter *dateFormatter DEPRECATED_ATTRIBUTE; -// deprecated; use [self.scrollView triggerPullToRefresh] instead -- (void)triggerRefresh DEPRECATED_ATTRIBUTE; - @end diff --git a/SVPullToRefresh/UIScrollView+SVPullToRefresh.m b/SVPullToRefresh/UIScrollView+SVPullToRefresh.m index ba19078c..6b478a74 100644 --- a/SVPullToRefresh/UIScrollView+SVPullToRefresh.m +++ b/SVPullToRefresh/UIScrollView+SVPullToRefresh.m @@ -27,34 +27,39 @@ @interface SVPullToRefreshView () @property (nonatomic, copy) void (^pullToRefreshActionHandler)(void); -@property (nonatomic, strong) SVPullToRefreshArrow *arrow; -@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; -@property (nonatomic, strong, readwrite) UILabel *titleLabel; -@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; @property (nonatomic, readwrite) SVPullToRefreshState state; @property (nonatomic, readwrite) SVPullToRefreshPosition position; -@property (nonatomic, strong) NSMutableArray *titles; -@property (nonatomic, strong) NSMutableArray *subtitles; -@property (nonatomic, strong) NSMutableArray *viewForState; - @property (nonatomic, weak) UIScrollView *scrollView; @property (nonatomic, readwrite) CGFloat originalTopInset; @property (nonatomic, readwrite) CGFloat originalBottomInset; @property (nonatomic, assign) BOOL wasTriggeredByUser; @property (nonatomic, assign) BOOL showsPullToRefresh; -@property (nonatomic, assign) BOOL showsDateLabel; @property(nonatomic, assign) BOOL isObserving; - (void)resetScrollViewContentInset; - (void)setScrollViewContentInsetForLoading; - (void)setScrollViewContentInset:(UIEdgeInsets)insets; -- (void)rotateArrow:(float)degrees hide:(BOOL)hide; @end +@interface SVArrowPullToRefreshView () +@property (nonatomic, strong) SVPullToRefreshArrow *arrow; + +@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; +@property (nonatomic, strong, readwrite) UILabel *titleLabel; +@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; + +@property (nonatomic, assign) BOOL showsDateLabel; + +@property (nonatomic, strong) NSMutableArray *titles; +@property (nonatomic, strong) NSMutableArray *subtitles; +@property (nonatomic, strong) NSMutableArray *viewForState; +- (void)rotateArrow:(float)degrees hide:(BOOL)hide; + +@end #pragma mark - UIScrollView (SVPullToRefresh) #import @@ -65,32 +70,43 @@ @implementation UIScrollView (SVPullToRefresh) @dynamic pullToRefreshView, showsPullToRefresh; -- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position { +- (void)addPullToRefresh:(Class)pullToRefreshClass withActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position +{ + NSAssert([pullToRefreshClass isSubclassOfClass:[SVPullToRefreshView class]], @"Your class for pull to refresh needs to inherit from `SVPullToRefreshView`"); - if(!self.pullToRefreshView) { - CGFloat yOrigin; - switch (position) { - case SVPullToRefreshPositionTop: - yOrigin = -SVPullToRefreshViewHeight; - break; - case SVPullToRefreshPositionBottom: - yOrigin = self.contentSize.height; - break; - default: - return; - } - SVPullToRefreshView *view = [[SVPullToRefreshView alloc] initWithFrame:CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight)]; - view.pullToRefreshActionHandler = actionHandler; - view.scrollView = self; - [self addSubview:view]; - - view.originalTopInset = self.contentInset.top; - view.originalBottomInset = self.contentInset.bottom; - view.position = position; - self.pullToRefreshView = view; - self.showsPullToRefresh = YES; + [self.pullToRefreshView removeFromSuperview]; + CGFloat yOrigin; + switch (position) { + case SVPullToRefreshPositionTop: + yOrigin = -SVPullToRefreshViewHeight; + break; + case SVPullToRefreshPositionBottom: + yOrigin = self.contentSize.height; + break; + default: + return; } + SVPullToRefreshView *view = [[pullToRefreshClass alloc] initWithFrame:CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight)]; + view.pullToRefreshActionHandler = actionHandler; + view.scrollView = self; + [self addSubview:view]; + view.originalTopInset = self.contentInset.top; + view.originalBottomInset = self.contentInset.bottom; + view.position = position; + self.pullToRefreshView = view; + self.showsPullToRefresh = YES; +} + +- (void)addPullToRefresh:(Class)pullToRefreshClass withActionHandler:(void (^)(void))actionHandler +{ + [self addPullToRefresh:pullToRefreshClass withActionHandler:actionHandler position:SVPullToRefreshPositionTop]; +} + +- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position { + [self addPullToRefresh:[SVArrowPullToRefreshView class] + withActionHandler:actionHandler + position:position]; } - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler { @@ -158,35 +174,17 @@ - (BOOL)showsPullToRefresh { @implementation SVPullToRefreshView // public properties -@synthesize pullToRefreshActionHandler, arrowColor, textColor, activityIndicatorViewColor, activityIndicatorViewStyle, lastUpdatedDate, dateFormatter; +@synthesize pullToRefreshActionHandler; @synthesize state = _state; @synthesize scrollView = _scrollView; @synthesize showsPullToRefresh = _showsPullToRefresh; -@synthesize arrow = _arrow; -@synthesize activityIndicatorView = _activityIndicatorView; - -@synthesize titleLabel = _titleLabel; -@synthesize dateLabel = _dateLabel; - - (id)initWithFrame:(CGRect)frame { if(self = [super initWithFrame:frame]) { - // default styling values - self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; - self.textColor = [UIColor darkGrayColor]; self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.state = SVPullToRefreshStateStopped; - self.showsDateLabel = NO; - - self.titles = [NSMutableArray arrayWithObjects:NSLocalizedString(@"Pull to refresh...",), - NSLocalizedString(@"Release to refresh...",), - NSLocalizedString(@"Loading...",), - nil]; - - self.subtitles = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; - self.viewForState = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; self.wasTriggeredByUser = YES; } @@ -209,6 +207,247 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { } } +- (void) handleNewState:(SVPullToRefreshState)state +{ + +} + +- (void) updateForPercentage:(CGFloat)percentage +{ + +} + +#pragma mark - Scroll View + +- (void)resetScrollViewContentInset { + UIEdgeInsets currentInsets = self.scrollView.contentInset; + switch (self.position) { + case SVPullToRefreshPositionTop: + currentInsets.top = self.originalTopInset; + break; + case SVPullToRefreshPositionBottom: + currentInsets.bottom = self.originalBottomInset; + currentInsets.top = self.originalTopInset; + break; + } + [self setScrollViewContentInset:currentInsets]; +} + +- (void)setScrollViewContentInsetForLoading { + CGFloat offset = MAX(self.scrollView.contentOffset.y * -1, 0); + UIEdgeInsets currentInsets = self.scrollView.contentInset; + switch (self.position) { + case SVPullToRefreshPositionTop: + currentInsets.top = MIN(offset, self.originalTopInset + self.bounds.size.height); + break; + case SVPullToRefreshPositionBottom: + currentInsets.bottom = MIN(offset, self.originalBottomInset + self.bounds.size.height); + break; + } + [self setScrollViewContentInset:currentInsets]; +} + +- (void)setScrollViewContentInset:(UIEdgeInsets)contentInset { + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState + animations:^{ + self.scrollView.contentInset = contentInset; + } + completion:NULL]; +} + +#pragma mark - Observing + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if([keyPath isEqualToString:@"contentOffset"]) + [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; + else if([keyPath isEqualToString:@"contentSize"]) { + [self layoutSubviews]; + + CGFloat yOrigin; + switch (self.position) { + case SVPullToRefreshPositionTop: + yOrigin = -SVPullToRefreshViewHeight; + break; + case SVPullToRefreshPositionBottom: + yOrigin = MAX(self.scrollView.contentSize.height, self.scrollView.bounds.size.height); + break; + } + self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight); + } + else if([keyPath isEqualToString:@"frame"]) + [self layoutSubviews]; + +} + +- (void)scrollViewDidScroll:(CGPoint)contentOffset { + if(self.state != SVPullToRefreshStateLoading) { + CGFloat scrollOffsetThreshold = 0; + switch (self.position) { + case SVPullToRefreshPositionTop: + scrollOffsetThreshold = self.frame.origin.y - self.originalTopInset; + break; + case SVPullToRefreshPositionBottom: + scrollOffsetThreshold = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.bounds.size.height + self.originalBottomInset; + break; + } + + //[self updateForPercentage:MIN(1.0f, MAX(0.0f, contentOffset.y / scrollOffsetThreshold))]; + // We don't max or min the offset + [self updateForPercentage:contentOffset.y / scrollOffsetThreshold]; + + if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered) + self.state = SVPullToRefreshStateLoading; + else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) + self.state = SVPullToRefreshStateTriggered; + else if(contentOffset.y >= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) + self.state = SVPullToRefreshStateStopped; + else if(contentOffset.y > scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) + self.state = SVPullToRefreshStateTriggered; + else if(contentOffset.y <= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) + self.state = SVPullToRefreshStateStopped; + } else { + CGFloat offset; + UIEdgeInsets contentInset; + switch (self.position) { + case SVPullToRefreshPositionTop: + offset = MAX(self.scrollView.contentOffset.y * -1, 0.0f); + offset = MIN(offset, self.originalTopInset + self.bounds.size.height); + contentInset = self.scrollView.contentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right); + break; + case SVPullToRefreshPositionBottom: + if (self.scrollView.contentSize.height >= self.scrollView.bounds.size.height) { + offset = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.bounds.size.height, 0.0f); + offset = MIN(offset, self.originalBottomInset + self.bounds.size.height); + contentInset = self.scrollView.contentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(contentInset.top, contentInset.left, offset, contentInset.right); + } else if (self.wasTriggeredByUser) { + offset = MIN(self.bounds.size.height, self.originalBottomInset + self.bounds.size.height); + contentInset = self.scrollView.contentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(-offset, contentInset.left, contentInset.bottom, contentInset.right); + } + break; + } + } +} + +#pragma mark - + +- (void)triggerRefresh { + [self.scrollView triggerPullToRefresh]; +} + +- (void)startAnimating{ + switch (self.position) { + case SVPullToRefreshPositionTop: + + if(fequalzero(self.scrollView.contentOffset.y)) { + [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.frame.size.height) animated:YES]; + self.wasTriggeredByUser = NO; + } + else + self.wasTriggeredByUser = YES; + + break; + case SVPullToRefreshPositionBottom: + + if((fequalzero(self.scrollView.contentOffset.y) && self.scrollView.contentSize.height < self.scrollView.bounds.size.height) + || fequal(self.scrollView.contentOffset.y, self.scrollView.contentSize.height - self.scrollView.bounds.size.height)) { + [self.scrollView setContentOffset:(CGPoint){.y = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.frame.size.height} animated:YES]; + self.wasTriggeredByUser = NO; + } + else + self.wasTriggeredByUser = YES; + + break; + } + + self.state = SVPullToRefreshStateLoading; +} + +- (void)stopAnimating { + self.state = SVPullToRefreshStateStopped; + + switch (self.position) { + case SVPullToRefreshPositionTop: + if(!self.wasTriggeredByUser) + [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.originalTopInset) animated:YES]; + break; + case SVPullToRefreshPositionBottom: + if(!self.wasTriggeredByUser) + [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.originalBottomInset) animated:YES]; + break; + } +} + +- (void)setState:(SVPullToRefreshState)newState { + + if(_state == newState) + return; + + SVPullToRefreshState previousState = _state; + _state = newState; + + [self handleNewState:_state]; + + [self setNeedsLayout]; + [self layoutIfNeeded]; + + switch (newState) { + case SVPullToRefreshStateAll: + case SVPullToRefreshStateStopped: + [self resetScrollViewContentInset]; + break; + + case SVPullToRefreshStateTriggered: + break; + + case SVPullToRefreshStateLoading: + [self setScrollViewContentInsetForLoading]; + + if(previousState == SVPullToRefreshStateTriggered && pullToRefreshActionHandler) + pullToRefreshActionHandler(); + + break; + } +} + +@end + +#pragma mark - SVArrowPullToRefreshView + +@implementation SVArrowPullToRefreshView +@synthesize arrowColor, textColor, activityIndicatorViewColor, activityIndicatorViewStyle, lastUpdatedDate, dateFormatter; + +@synthesize arrow = _arrow; +@synthesize activityIndicatorView = _activityIndicatorView; + +@synthesize titleLabel = _titleLabel; +@synthesize dateLabel = _dateLabel; + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // default styling values + self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; + self.textColor = [UIColor darkGrayColor]; + + self.showsDateLabel = NO; + + self.titles = [NSMutableArray arrayWithObjects:NSLocalizedString(@"Pull to refresh...",), + NSLocalizedString(@"Release to refresh...",), + NSLocalizedString(@"Loading...",), + nil]; + + self.subtitles = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; + self.viewForState = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil]; + } + return self; +} + - (void)layoutSubviews { for(id otherView in self.viewForState) { @@ -326,118 +565,6 @@ - (void)layoutSubviews { } } -#pragma mark - Scroll View - -- (void)resetScrollViewContentInset { - UIEdgeInsets currentInsets = self.scrollView.contentInset; - switch (self.position) { - case SVPullToRefreshPositionTop: - currentInsets.top = self.originalTopInset; - break; - case SVPullToRefreshPositionBottom: - currentInsets.bottom = self.originalBottomInset; - currentInsets.top = self.originalTopInset; - break; - } - [self setScrollViewContentInset:currentInsets]; -} - -- (void)setScrollViewContentInsetForLoading { - CGFloat offset = MAX(self.scrollView.contentOffset.y * -1, 0); - UIEdgeInsets currentInsets = self.scrollView.contentInset; - switch (self.position) { - case SVPullToRefreshPositionTop: - currentInsets.top = MIN(offset, self.originalTopInset + self.bounds.size.height); - break; - case SVPullToRefreshPositionBottom: - currentInsets.bottom = MIN(offset, self.originalBottomInset + self.bounds.size.height); - break; - } - [self setScrollViewContentInset:currentInsets]; -} - -- (void)setScrollViewContentInset:(UIEdgeInsets)contentInset { - [UIView animateWithDuration:0.3 - delay:0 - options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState - animations:^{ - self.scrollView.contentInset = contentInset; - } - completion:NULL]; -} - -#pragma mark - Observing - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if([keyPath isEqualToString:@"contentOffset"]) - [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; - else if([keyPath isEqualToString:@"contentSize"]) { - [self layoutSubviews]; - - CGFloat yOrigin; - switch (self.position) { - case SVPullToRefreshPositionTop: - yOrigin = -SVPullToRefreshViewHeight; - break; - case SVPullToRefreshPositionBottom: - yOrigin = MAX(self.scrollView.contentSize.height, self.scrollView.bounds.size.height); - break; - } - self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight); - } - else if([keyPath isEqualToString:@"frame"]) - [self layoutSubviews]; - -} - -- (void)scrollViewDidScroll:(CGPoint)contentOffset { - if(self.state != SVPullToRefreshStateLoading) { - CGFloat scrollOffsetThreshold = 0; - switch (self.position) { - case SVPullToRefreshPositionTop: - scrollOffsetThreshold = self.frame.origin.y - self.originalTopInset; - break; - case SVPullToRefreshPositionBottom: - scrollOffsetThreshold = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.bounds.size.height + self.originalBottomInset; - break; - } - - if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered) - self.state = SVPullToRefreshStateLoading; - else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) - self.state = SVPullToRefreshStateTriggered; - else if(contentOffset.y >= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) - self.state = SVPullToRefreshStateStopped; - else if(contentOffset.y > scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) - self.state = SVPullToRefreshStateTriggered; - else if(contentOffset.y <= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) - self.state = SVPullToRefreshStateStopped; - } else { - CGFloat offset; - UIEdgeInsets contentInset; - switch (self.position) { - case SVPullToRefreshPositionTop: - offset = MAX(self.scrollView.contentOffset.y * -1, 0.0f); - offset = MIN(offset, self.originalTopInset + self.bounds.size.height); - contentInset = self.scrollView.contentInset; - self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right); - break; - case SVPullToRefreshPositionBottom: - if (self.scrollView.contentSize.height >= self.scrollView.bounds.size.height) { - offset = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.bounds.size.height, 0.0f); - offset = MIN(offset, self.originalBottomInset + self.bounds.size.height); - contentInset = self.scrollView.contentInset; - self.scrollView.contentInset = UIEdgeInsetsMake(contentInset.top, contentInset.left, offset, contentInset.right); - } else if (self.wasTriggeredByUser) { - offset = MIN(self.bounds.size.height, self.originalBottomInset + self.bounds.size.height); - contentInset = self.scrollView.contentInset; - self.scrollView.contentInset = UIEdgeInsetsMake(-offset, contentInset.left, contentInset.bottom, contentInset.right); - } - break; - } - } -} - #pragma mark - Getters - (SVPullToRefreshArrow *)arrow { @@ -580,85 +707,6 @@ - (void)setDateFormatter:(NSDateFormatter *)newDateFormatter { self.dateLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Last Updated: %@",), self.lastUpdatedDate?[newDateFormatter stringFromDate:self.lastUpdatedDate]:NSLocalizedString(@"Never",)]; } -#pragma mark - - -- (void)triggerRefresh { - [self.scrollView triggerPullToRefresh]; -} - -- (void)startAnimating{ - switch (self.position) { - case SVPullToRefreshPositionTop: - - if(fequalzero(self.scrollView.contentOffset.y)) { - [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.frame.size.height) animated:YES]; - self.wasTriggeredByUser = NO; - } - else - self.wasTriggeredByUser = YES; - - break; - case SVPullToRefreshPositionBottom: - - if((fequalzero(self.scrollView.contentOffset.y) && self.scrollView.contentSize.height < self.scrollView.bounds.size.height) - || fequal(self.scrollView.contentOffset.y, self.scrollView.contentSize.height - self.scrollView.bounds.size.height)) { - [self.scrollView setContentOffset:(CGPoint){.y = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.frame.size.height} animated:YES]; - self.wasTriggeredByUser = NO; - } - else - self.wasTriggeredByUser = YES; - - break; - } - - self.state = SVPullToRefreshStateLoading; -} - -- (void)stopAnimating { - self.state = SVPullToRefreshStateStopped; - - switch (self.position) { - case SVPullToRefreshPositionTop: - if(!self.wasTriggeredByUser) - [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.originalTopInset) animated:YES]; - break; - case SVPullToRefreshPositionBottom: - if(!self.wasTriggeredByUser) - [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.originalBottomInset) animated:YES]; - break; - } -} - -- (void)setState:(SVPullToRefreshState)newState { - - if(_state == newState) - return; - - SVPullToRefreshState previousState = _state; - _state = newState; - - [self setNeedsLayout]; - [self layoutIfNeeded]; - - switch (newState) { - case SVPullToRefreshStateAll: - case SVPullToRefreshStateStopped: - [self resetScrollViewContentInset]; - break; - - case SVPullToRefreshStateTriggered: - break; - - case SVPullToRefreshStateLoading: - [self setScrollViewContentInsetForLoading]; - - if(previousState == SVPullToRefreshStateTriggered && pullToRefreshActionHandler) - pullToRefreshActionHandler(); - - break; - } -} - - (void)rotateArrow:(float)degrees hide:(BOOL)hide { [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{ self.arrow.layer.transform = CATransform3DMakeRotation(degrees, 0, 0, 1); @@ -669,7 +717,6 @@ - (void)rotateArrow:(float)degrees hide:(BOOL)hide { @end - #pragma mark - SVPullToRefreshArrow @implementation SVPullToRefreshArrow From 7b1854c3471f018d360371c625046a37075b2fd0 Mon Sep 17 00:00:00 2001 From: ipodishima Date: Tue, 4 Aug 2015 16:49:03 -0400 Subject: [PATCH 2/2] Fixed an issue on percentage calculation --- SVPullToRefresh/UIScrollView+SVPullToRefresh.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SVPullToRefresh/UIScrollView+SVPullToRefresh.m b/SVPullToRefresh/UIScrollView+SVPullToRefresh.m index 6b478a74..f5a01aaa 100644 --- a/SVPullToRefresh/UIScrollView+SVPullToRefresh.m +++ b/SVPullToRefresh/UIScrollView+SVPullToRefresh.m @@ -295,7 +295,7 @@ - (void)scrollViewDidScroll:(CGPoint)contentOffset { //[self updateForPercentage:MIN(1.0f, MAX(0.0f, contentOffset.y / scrollOffsetThreshold))]; // We don't max or min the offset - [self updateForPercentage:contentOffset.y / scrollOffsetThreshold]; + [self updateForPercentage:(contentOffset.y + self.originalTopInset) / (scrollOffsetThreshold + self.originalTopInset)]; if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered) self.state = SVPullToRefreshStateLoading;