Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: 39bea23317
Fetching contributors…

Cannot retrieve contributors at this time

334 lines (256 sloc) 9.557 kB
//
// SSPullToRefreshView.m
// SSPullToRefresh
//
// Created by Sam Soffes on 4/9/12.
// Copyright (c) 2012 Sam Soffes. All rights reserved.
//
#import "SSPullToRefreshView.h"
#import "SSPullToRefreshDefaultContentView.h"
@interface SSPullToRefreshView ()
@property (nonatomic, assign, readwrite) SSPullToRefreshViewState state;
@property (nonatomic, weak, readwrite) UIScrollView *scrollView;
@property (nonatomic, assign, readwrite, getter = isExpanded) BOOL expanded;
- (void)_setContentInsetTop:(CGFloat)topInset;
- (void)_setState:(SSPullToRefreshViewState)state animated:(BOOL)animated expanded:(BOOL)expanded completion:(void (^)(void))completion;
- (void)_setPullProgress:(CGFloat)pullProgress;
@end
@implementation SSPullToRefreshView {
dispatch_semaphore_t _animationSemaphore;
CGFloat _topInset;
}
@synthesize delegate = _delegate;
@synthesize scrollView = _scrollView;
@synthesize expandedHeight = _expandedHeight;
@synthesize contentView = _contentView;
@synthesize state = _state;
@synthesize expanded = _expanded;
@synthesize defaultContentInset = _defaultContentInset;
#pragma mark - Accessors
- (void)setState:(SSPullToRefreshViewState)state {
BOOL loading = _state == SSPullToRefreshViewStateLoading;
_state = state;
// Forward to content view
[self.contentView setState:_state withPullToRefreshView:self];
// Update delegate
if (loading && _state != SSPullToRefreshViewStateLoading) {
if ([self.delegate respondsToSelector:@selector(pullToRefreshViewDidFinishLoading:)]) {
[self.delegate pullToRefreshViewDidFinishLoading:self];
}
} else if (!loading && _state == SSPullToRefreshViewStateLoading) {
[self _setPullProgress:1.0f];
if ([self.delegate respondsToSelector:@selector(pullToRefreshViewDidStartLoading:)]) {
[self.delegate pullToRefreshViewDidStartLoading:self];
}
}
}
- (void)setExpanded:(BOOL)expanded {
_expanded = expanded;
[self _setContentInsetTop:expanded ? self.expandedHeight : 0.0f];
}
- (void)setScrollView:(UIScrollView *)scrollView {
_scrollView = scrollView;
_defaultContentInset = self.scrollView.contentInset;
}
- (UIView<SSPullToRefreshContentView> *)contentView {
// Use the simple content view as the default
if (!_contentView) {
self.contentView = [[SSPullToRefreshDefaultContentView alloc] initWithFrame:CGRectZero];
}
return _contentView;
}
- (void)setContentView:(UIView<SSPullToRefreshContentView> *)contentView {
[_contentView removeFromSuperview];
_contentView = contentView;
_contentView.autoresizingMask = UIViewAutoresizingNone;
[_contentView setState:_state withPullToRefreshView:self];
[self refreshLastUpdatedAt];
[self addSubview:_contentView];
}
- (void)setDefaultContentInset:(UIEdgeInsets)defaultContentInset {
_defaultContentInset = defaultContentInset;
[self _setContentInsetTop:_topInset];
}
#pragma mark - NSObject
- (void)dealloc {
[self removeObserver:self forKeyPath:@"scrollView.contentOffset" context:(__bridge void *)self];
self.scrollView = nil;
self.delegate = nil;
dispatch_semaphore_wait(_animationSemaphore, DISPATCH_TIME_FOREVER);
dispatch_release(_animationSemaphore);
_animationSemaphore = NULL;
}
#pragma mark - UIView
- (void)removeFromSuperview {
self.scrollView = nil;
[super removeFromSuperview];
}
- (void)layoutSubviews {
CGSize size = self.bounds.size;
CGSize contentSize = [self.contentView sizeThatFits:size];
if (contentSize.width < size.width) {
contentSize.width = size.width;
}
if (contentSize.height < _expandedHeight) {
contentSize.height = _expandedHeight;
}
self.contentView.frame = CGRectMake(roundf((size.width - contentSize.width) / 2.0f), size.height - contentSize.height, contentSize.width, contentSize.height);
}
#pragma mark - Initializer
- (id)initWithScrollView:(UIScrollView *)scrollView delegate:(id<SSPullToRefreshViewDelegate>)delegate {
CGRect frame = CGRectMake(0.0f, 0.0f - scrollView.bounds.size.height, scrollView.bounds.size.width,
scrollView.bounds.size.height);
if ((self = [self initWithFrame:frame])) {
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.scrollView = scrollView;
self.delegate = delegate;
self.state = SSPullToRefreshViewStateNormal;
self.expandedHeight = 70.0f;
// Add to scroll view
[self.scrollView addSubview:self];
[self addObserver:self forKeyPath:@"scrollView.contentOffset" options:NSKeyValueObservingOptionNew context:(__bridge void *)self];
// Semaphore is used to ensure only one animation plays at a time
_animationSemaphore = dispatch_semaphore_create(0);
dispatch_semaphore_signal(_animationSemaphore);
}
return self;
}
#pragma mark - Loading
- (void)startLoading {
[self startLoadingAndExpand:NO];
}
- (void)startLoadingAndExpand:(BOOL)shouldExpand {
// If we're not loading, this method has no effect
if (_state == SSPullToRefreshViewStateLoading) {
return;
}
// Animate back to the loading state
[self _setState:SSPullToRefreshViewStateLoading animated:YES expanded:shouldExpand completion:nil];
}
- (void)finishLoading {
// If we're not loading, this method has no effect
if (_state != SSPullToRefreshViewStateLoading) {
return;
}
// Animate back to the normal state
[self _setState:SSPullToRefreshViewStateClosing animated:YES expanded:NO completion:^{
self.state = SSPullToRefreshViewStateNormal;
}];
}
- (void)refreshLastUpdatedAt {
NSDate *date = nil;
if ([self.delegate respondsToSelector:@selector(pullToRefreshViewLastUpdatedAt:)]) {
date = [self.delegate pullToRefreshViewLastUpdatedAt:self];
} else {
date = [NSDate date];
}
// Forward to content view
if ([self.contentView respondsToSelector:@selector(setLastUpdatedAt:withPullToRefreshView:)]) {
[self.contentView setLastUpdatedAt:date withPullToRefreshView:self];
}
}
#pragma mark - Private
- (void)_setContentInsetTop:(CGFloat)topInset {
_topInset = topInset;
// Default to the scroll view's initial content inset
UIEdgeInsets inset = _defaultContentInset;
// Add the top inset
inset.top += _topInset;
// Don't set it if that is already the current inset
if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.contentInset, inset)) {
return;
}
// Update the content inset
self.scrollView.contentInset = inset;
}
- (void)_setState:(SSPullToRefreshViewState)state animated:(BOOL)animated expanded:(BOOL)expanded completion:(void (^)(void))completion {
if (!animated) {
self.state = state;
self.expanded = expanded;
if (completion) {
completion();
}
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
dispatch_semaphore_wait(_animationSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
self.state = state;
self.expanded = expanded;
} completion:^(BOOL finished) {
dispatch_semaphore_signal(_animationSemaphore);
if (completion) {
completion();
}
}];
});
});
}
- (void)_setPullProgress:(CGFloat)pullProgress {
if ([self.contentView respondsToSelector:@selector(setPullProgress:)]) {
[self.contentView setPullProgress:pullProgress];
}
}
#pragma mark - NSKeyValueObserving
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// Call super if we didn't register for this notification
if (context != (__bridge void *)self) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
// We don't care about this notificaiton
if (object != self || ![keyPath isEqualToString:@"scrollView.contentOffset"]) {
return;
}
// Get the offset out of the change notification
id point = [change objectForKey:NSKeyValueChangeNewKey];
// Ensure the point is valid
if (!point || point == [NSNull null] || ![point respondsToSelector:@selector(CGPointValue)]) {
return;
}
// Retrieve the y offset from the point
CGFloat y = [point CGPointValue].y;
// Scroll view is dragging
if (self.scrollView.isDragging) {
// Scroll view is ready
if (_state == SSPullToRefreshViewStateReady) {
// Dragged enough to refresh
if (y > -_expandedHeight && y < 0.0f) {
self.state = SSPullToRefreshViewStateNormal;
}
// Scroll view is normal
} else if (_state == SSPullToRefreshViewStateNormal) {
// Update the content view's pulling progressing
[self _setPullProgress:-y / _expandedHeight];
// Dragged enough to be ready
if (y < -_expandedHeight) {
self.state = SSPullToRefreshViewStateReady;
}
// Scroll view is loading
} else if (_state == SSPullToRefreshViewStateLoading) {
if (y >= 0.0f) {
[self _setContentInsetTop:0.0f];
} else {
[self _setContentInsetTop:MIN(-y, _expandedHeight)];
}
}
return;
}
// If the scroll view isn't ready, we're not interested
if (_state != SSPullToRefreshViewStateReady) {
return;
}
// We're ready, prepare to switch to loading. Be default, we should refresh.
SSPullToRefreshViewState newState = SSPullToRefreshViewStateLoading;
// Ask the delegate if it's cool to start loading
if ([self.delegate respondsToSelector:@selector(pullToRefreshViewShouldStartLoading:)]) {
if (![self.delegate pullToRefreshViewShouldStartLoading:self]) {
// Animate back to normal since the delegate said no
newState = SSPullToRefreshViewStateNormal;
}
}
// Animate to the new state
[self _setState:newState animated:YES expanded:YES completion:nil];
}
@end
Jump to Line
Something went wrong with that request. Please try again.