├── .gitignore ├── Assets └── .gitkeep ├── BSRefreshableScrollView.podspec ├── CHANGELOG.md ├── Classes ├── ios │ └── .gitkeep └── osx │ ├── .gitkeep │ ├── BSRefreshableClipView.h │ ├── BSRefreshableClipView.m │ ├── BSRefreshableScrollView.h │ ├── BSRefreshableScrollView.m │ └── BSRefreshableScrollView_Private.h ├── Example ├── Podfile ├── RefreshableScrollView.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── adib.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── adib.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints.xcbkptlist │ │ └── xcschemes │ │ ├── RefreshableScrollView 2.xcscheme │ │ ├── RefreshableScrollView.xcscheme │ │ └── xcschememanagement.plist └── RefreshableScrollView │ ├── BSAppDelegate.h │ ├── BSAppDelegate.m │ ├── RefreshableScrollView-Info.plist │ ├── RefreshableScrollView-Prefix.pch │ ├── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.xib │ └── main.m ├── LICENSE ├── README.md ├── Rakefile └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | -------------------------------------------------------------------------------- /Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adib/RefreshableScrollView/715d9152f59d927f36478881eed3ac51dccc5b30/Assets/.gitkeep -------------------------------------------------------------------------------- /BSRefreshableScrollView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint NAME.podspec' to ensure this is a 3 | # valid spec and remove all comments before submitting the spec. 4 | # 5 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 6 | # 7 | Pod::Spec.new do |s| 8 | s.name = "BSRefreshableScrollView" 9 | s.version = "1.0.1" 10 | s.summary = "An `NSScrollView` subclass that supports pull-to-refresh both in the top and bottom edges." 11 | s.description = <<-DESC 12 | The intended usage is for hosting a timeline-like view that shows a list of messages in reverse-chronological order. 13 | 14 | * Get iOS like pull-to-refresh experience on OS X complete with pull indicator. 15 | * The bottom pulling upwards to reveal the bottom side will be good for loading older data. 16 | DESC 17 | s.homepage = "http://cutecoder.org/tag/bsrefreshablescrollview/" 18 | s.screenshots = "http://i0.wp.com/cutecoder.org/wp-content/uploads/2012/11/Bi-Directional-Refreshable-Scroll-View.png" 19 | s.license = 'BSD' 20 | s.author = { "Sasmito Adibowo" => "adib@basil-salad.com" } 21 | s.source = { :git => "https://github.com/adib/RefreshableScrollView.git", :tag => 'release-' + s.version.to_s } 22 | s.social_media_url = 'https://www.facebook.com/cutecoder' 23 | 24 | # s.platform = :osx, '10.8' 25 | # s.ios.deployment_target = '5.0' 26 | s.osx.deployment_target = '10.8' 27 | s.requires_arc = true 28 | 29 | s.source_files = 'Classes/osx/*' 30 | # s.resources = 'Assets/*.png' 31 | 32 | # s.ios.exclude_files = 'Classes/osx' 33 | s.osx.exclude_files = 'Classes/ios' 34 | s.public_header_files = 'Classes/**/BSRefreshableScrollView.h' 35 | # s.frameworks = 'SomeFramework', 'AnotherFramework' 36 | # s.dependency 'JSONKit', '~> 1.4' 37 | end 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # BSRefreshableScrollView CHANGELOG 2 | 3 | ## 0.1.0 4 | 5 | Initial release. 6 | -------------------------------------------------------------------------------- /Classes/ios/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adib/RefreshableScrollView/715d9152f59d927f36478881eed3ac51dccc5b30/Classes/ios/.gitkeep -------------------------------------------------------------------------------- /Classes/osx/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adib/RefreshableScrollView/715d9152f59d927f36478881eed3ac51dccc5b30/Classes/osx/.gitkeep -------------------------------------------------------------------------------- /Classes/osx/BSRefreshableClipView.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSRefreshableClipView.h 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | #import 22 | 23 | @interface BSRefreshableClipView : NSClipView 24 | 25 | -(instancetype) initWithOriginalClipView:(NSClipView*) clipView; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Classes/osx/BSRefreshableClipView.m: -------------------------------------------------------------------------------- 1 | // 2 | // BSRefreshableClipView.m 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | #if !__has_feature(objc_arc) 23 | #error Need automatic reference counting to compile this. 24 | #endif 25 | 26 | #import "BSRefreshableScrollView_Private.h" 27 | #import "BSRefreshableClipView.h" 28 | 29 | @implementation BSRefreshableClipView 30 | 31 | -(instancetype) initWithOriginalClipView:(NSClipView*) clipView 32 | { 33 | if (self = [super initWithFrame:clipView.frame]) { 34 | [self setAutoresizingMask:[clipView autoresizingMask]]; 35 | [self setAutoresizesSubviews:[clipView autoresizesSubviews]]; 36 | [self setBackgroundColor:[clipView backgroundColor]]; 37 | [self setTranslatesAutoresizingMaskIntoConstraints:[clipView translatesAutoresizingMaskIntoConstraints]]; 38 | [self setCopiesOnScroll:[clipView copiesOnScroll]]; 39 | 40 | // 10.9 only 41 | if ([clipView respondsToSelector:@selector(canDrawSubviewsIntoLayer)] && [self respondsToSelector:@selector(setCanDrawSubviewsIntoLayer:)]) { 42 | [self setCanDrawSubviewsIntoLayer:[clipView canDrawSubviewsIntoLayer]]; 43 | } 44 | } 45 | return self; 46 | } 47 | 48 | -(NSView*) headerView 49 | { 50 | return [(BSRefreshableScrollView*) self.superview headerView]; 51 | } 52 | 53 | -(NSView*) footerView 54 | { 55 | return [(BSRefreshableScrollView*) self.superview footerView]; 56 | } 57 | 58 | 59 | -(BSRefreshableScrollViewSide) refreshingSides 60 | { 61 | return [(BSRefreshableScrollView*) self.superview refreshingSides]; 62 | } 63 | 64 | 65 | #pragma mark NSClipView 66 | 67 | - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin 68 | { 69 | NSPoint constrained = [super constrainScrollPoint:proposedNewOrigin]; 70 | const NSRect clipViewBounds = self.bounds; 71 | NSView* const documentView = self.documentView; 72 | const NSRect documentFrame = documentView.frame; 73 | 74 | const BSRefreshableScrollViewSide refreshingSides = [self refreshingSides]; 75 | 76 | if ((refreshingSides & BSRefreshableScrollViewSideTop) && proposedNewOrigin.y <= 0) { 77 | const NSRect headerFrame = [self headerView].frame; 78 | constrained.y = MAX(-headerFrame.size.height, proposedNewOrigin.y); 79 | } 80 | 81 | if((refreshingSides & BSRefreshableScrollViewSideBottom) ) { 82 | const NSRect footerFrame = [self footerView].frame; 83 | if (proposedNewOrigin.y > documentFrame.size.height - clipViewBounds.size.height) { 84 | const CGFloat maxHeight = documentFrame.size.height - clipViewBounds.size.height + footerFrame.size.height + 1; 85 | constrained.y = MIN(maxHeight, proposedNewOrigin.y); 86 | } 87 | } 88 | 89 | return constrained; 90 | } 91 | 92 | 93 | -(NSRect)documentRect 94 | { 95 | NSRect documentRect = [super documentRect]; 96 | const BSRefreshableScrollViewSide refreshingSides = [self refreshingSides]; 97 | if (refreshingSides & BSRefreshableScrollViewSideTop) { 98 | const NSRect headerFrame = [self headerView].frame; 99 | documentRect.size.height += headerFrame.size.height; 100 | documentRect.origin.y -= headerFrame.size.height; 101 | } 102 | 103 | if(refreshingSides & BSRefreshableScrollViewSideBottom) { 104 | const NSRect footerFrame = [self footerView].frame; 105 | documentRect.size.height += footerFrame.size.height ; 106 | } 107 | 108 | return documentRect; 109 | } 110 | 111 | 112 | #pragma mark NSView 113 | 114 | -(BOOL)isFlipped 115 | { 116 | return YES; 117 | } 118 | 119 | 120 | @end 121 | -------------------------------------------------------------------------------- /Classes/osx/BSRefreshableScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSRefreshableScrollView.h 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | #import 23 | 24 | /** 25 | Indicates which sides that are refreshable. 26 | */ 27 | enum { 28 | BSRefreshableScrollViewSideNone = 0, 29 | /** 30 | Pulling downwards will reveal a refresh indicator and when sufficiently pulled should trigger a data load for newer items. 31 | */ 32 | BSRefreshableScrollViewSideTop = 1, 33 | /** 34 | Pulling upwards will reveal a refresh indicator and when sufficiently pulled should trigger a data load for older items. 35 | */ 36 | BSRefreshableScrollViewSideBottom = 1 << 1, 37 | // left & right edges are for future expansion but not currently implemented 38 | /** 39 | Currently unimplemented. 40 | */ 41 | BSRefreshableScrollViewSideLeft = 1 << 2, 42 | /** 43 | Currently unimplemented. 44 | */ 45 | BSRefreshableScrollViewSideRight = 1 << 3 46 | }; 47 | 48 | typedef NSUInteger BSRefreshableScrollViewSide; 49 | 50 | // --- 51 | 52 | @protocol BSRefreshableScrollViewDelegate; 53 | @protocol BSRefreshableScrollViewDataSource; 54 | 55 | // --- 56 | 57 | /** 58 | A scroll view that can be pulled downwards or upwards for the user to trigger refreshing of newer data or loading historical data. 59 | 60 | */ 61 | @interface BSRefreshableScrollView : NSScrollView 62 | 63 | /** 64 | Which sides are refreshable, of type BSRefreshableScrollViewSide 65 | */ 66 | @property (nonatomic) NSUInteger refreshableSides; 67 | 68 | /** 69 | Which sides are currently refreshing, of type BSRefreshableScrollViewSide 70 | */ 71 | @property (nonatomic,readonly) NSUInteger refreshingSides; 72 | 73 | /** 74 | The object that will provide the refreshable data. 75 | */ 76 | @property (nonatomic,weak) IBOutlet id refreshableDataSource; 77 | @property (nonatomic,weak) IBOutlet id refreshableDelegate; 78 | 79 | /** 80 | Call this when you have loaded the data to dismiss the refresh progress indicator. 81 | */ 82 | -(void) stopRefreshingSide:(BSRefreshableScrollViewSide) refreshableSide; 83 | 84 | 85 | @end 86 | 87 | // --- 88 | 89 | /** 90 | The object that will provide the refreshable data. 91 | This protocol is present for future implementation and currently empty 😏 92 | */ 93 | @protocol BSRefreshableScrollViewDataSource 94 | 95 | 96 | @end 97 | 98 | 99 | // --- 100 | /** 101 | The object that will provide the refreshable data. 102 | This protocol is present for future implementation and currently empty 😏 103 | */ 104 | @protocol BSRefreshableScrollViewDelegate 105 | 106 | @optional 107 | 108 | /** 109 | Called by the scroll view to indicate that refresh should start. 110 | */ 111 | -(BOOL) scrollView:(BSRefreshableScrollView*) aScrollView startRefreshSide:(BSRefreshableScrollViewSide) refreshableSide; 112 | 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /Classes/osx/BSRefreshableScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // BSRefreshableScrollView.m 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | #if !__has_feature(objc_arc) 23 | #error Need automatic reference counting to compile this. 24 | #endif 25 | 26 | #import "BSRefreshableClipView.h" 27 | #import "BSRefreshableScrollView_Private.h" 28 | 29 | 30 | @implementation BSRefreshableScrollView 31 | 32 | @synthesize refreshingSides = _refreshingSides; 33 | 34 | -(void) stopRefreshingSide:(BSRefreshableScrollViewSide) refreshableSides 35 | { 36 | NSClipView* const clipView = self.contentView; 37 | const NSRect clipViewBounds = clipView.bounds; 38 | 39 | void (^stopRefresh)(BSRefreshableScrollViewSide side, NSProgressIndicator* progressIndicator, BOOL (^shouldScroll)()) = ^(BSRefreshableScrollViewSide side, NSProgressIndicator* progressIndicator, BOOL (^shouldScroll)()) { 40 | 41 | if ( !(self.refreshingSides & side) ) { 42 | return; 43 | } 44 | 45 | self.refreshingSides &= ~side; 46 | [progressIndicator stopAnimation:self]; 47 | [progressIndicator setDisplayedWhenStopped:NO]; 48 | if (shouldScroll()) { 49 | // fake scrolling 50 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 51 | int scrollAmount = 0; 52 | if (side & BSRefreshableScrollViewSideTop) { 53 | scrollAmount = 1; 54 | } else if(side & BSRefreshableScrollViewSideBottom) { 55 | scrollAmount = -1; 56 | } 57 | CGEventRef cgEvent = CGEventCreateScrollWheelEvent(NULL, 58 | kCGScrollEventUnitLine, 59 | 1, 60 | scrollAmount, 61 | 0); 62 | 63 | NSEvent *scrollEvent = [NSEvent eventWithCGEvent:cgEvent]; 64 | [self scrollWheel:scrollEvent]; 65 | CFRelease(cgEvent); 66 | }]; 67 | } 68 | }; 69 | 70 | if (refreshableSides & BSRefreshableScrollViewSideTop) { 71 | stopRefresh(BSRefreshableScrollViewSideTop,self.topProgressIndicator,^{ 72 | return (BOOL) (clipViewBounds.origin.y < 0); 73 | }); 74 | } 75 | 76 | if (refreshableSides & BSRefreshableScrollViewSideBottom) { 77 | stopRefresh(BSRefreshableScrollViewSideBottom,self.bottomProgressIndicator,^{ 78 | return (BOOL) (clipViewBounds.origin.y > 0); 79 | }); 80 | } 81 | 82 | } 83 | 84 | 85 | -(NSView*) newEdgeViewForSide:(BSRefreshableScrollViewSide) edgeSide progressIndicator:(NSProgressIndicator*) indicatorView 86 | { 87 | NSView* const contentView = self.contentView; 88 | NSView* const documentView = self.documentView; 89 | const NSRect indicatorViewBounds = indicatorView.bounds; 90 | 91 | 92 | NSView* edgeView = [[NSView alloc] initWithFrame:NSZeroRect]; 93 | [edgeView setTranslatesAutoresizingMaskIntoConstraints:NO]; 94 | [edgeView setWantsLayer:YES]; 95 | 96 | [edgeView addSubview:indicatorView]; 97 | 98 | // vertically centered 99 | [edgeView addConstraint:[NSLayoutConstraint constraintWithItem:indicatorView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:edgeView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; 100 | 101 | // horizontally centered 102 | [edgeView addConstraint:[NSLayoutConstraint constraintWithItem:indicatorView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:edgeView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; 103 | 104 | [contentView addSubview:edgeView]; 105 | 106 | 107 | if (edgeSide & (BSRefreshableScrollViewSideTop | BSRefreshableScrollViewSideBottom) ) { 108 | // span horizontally 109 | [contentView addConstraint:[NSLayoutConstraint constraintWithItem:edgeView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]]; 110 | [contentView addConstraint:[NSLayoutConstraint constraintWithItem:edgeView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeRight multiplier:1 constant:0]]; 111 | 112 | 113 | // set height 114 | [contentView addConstraint:[NSLayoutConstraint constraintWithItem:edgeView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:indicatorViewBounds.size.height]]; 115 | 116 | if (edgeSide & BSRefreshableScrollViewSideTop) { 117 | // above the content view top 118 | [contentView addConstraint:[NSLayoutConstraint constraintWithItem:edgeView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:documentView attribute:NSLayoutAttributeTop multiplier:1 constant:0]]; 119 | } else if(edgeSide & BSRefreshableScrollViewSideBottom) { 120 | [contentView addConstraint:[NSLayoutConstraint constraintWithItem:edgeView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:documentView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]]; 121 | } 122 | } 123 | 124 | return edgeView; 125 | } 126 | 127 | 128 | #pragma mark NSObject 129 | 130 | -(void)dealloc 131 | { 132 | 133 | } 134 | 135 | 136 | #pragma mark NSResponder 137 | 138 | -(void)scrollWheel:(NSEvent *)theEvent 139 | { 140 | const NSEventPhase eventPhase = theEvent.phase; 141 | 142 | if (eventPhase & NSEventPhaseChanged) { 143 | NSClipView* const clipView = self.contentView; 144 | const NSRect clipViewBounds = clipView.bounds; 145 | NSView* const documentView = self.documentView; 146 | const NSRect headerFrame = self.headerView.frame; 147 | const NSRect footerFrame = self.footerView.frame; 148 | const NSRect documentFrame = documentView.frame; 149 | 150 | void (^startScrollPhase)(BSRefreshableScrollViewSide refreshSide,NSProgressIndicator* progressIndicator,float progressMaxValue,float progressCurrentValue, BOOL (^shouldTriggerRefresh)(void)) = ^(BSRefreshableScrollViewSide refreshSide,NSProgressIndicator* progressIndicator,float progressMaxValue,float progressCurrentValue, BOOL (^shouldTriggerRefresh)(void)) { 151 | 152 | if(!(self.refreshingSides & refreshSide) && (self.refreshableSides & refreshSide) ) { 153 | // not refreshing top 154 | 155 | if (progressIndicator.isIndeterminate) { 156 | [progressIndicator setIndeterminate:NO]; 157 | [progressIndicator setDisplayedWhenStopped:YES]; 158 | } 159 | [progressIndicator setAlphaValue:1]; 160 | 161 | progressIndicator.minValue = 0; 162 | progressIndicator.maxValue = progressMaxValue; 163 | progressIndicator.doubleValue = progressCurrentValue; 164 | 165 | self.activatedRefreshingSides |= refreshSide; 166 | 167 | if (shouldTriggerRefresh()) { 168 | self.triggeredRefreshingSides |= refreshSide; 169 | } 170 | } 171 | }; 172 | 173 | 174 | if (clipViewBounds.origin.y < 0 ) { 175 | // showing top area 176 | 177 | startScrollPhase(BSRefreshableScrollViewSideTop,self.topProgressIndicator,headerFrame.size.height,-clipViewBounds.origin.y, ^{ 178 | return (BOOL) (clipViewBounds.origin.y < -headerFrame.size.height); 179 | }); 180 | 181 | } else if (clipViewBounds.origin.y > documentFrame.size.height - clipViewBounds.size.height) { 182 | // scrolling to bottom 183 | CGFloat gapHeight = clipViewBounds.origin.y + clipViewBounds.size.height - documentFrame.size.height; 184 | startScrollPhase(BSRefreshableScrollViewSideBottom,self.bottomProgressIndicator,footerFrame.size.height,gapHeight, ^{ 185 | return (BOOL) (gapHeight > footerFrame.size.height); 186 | }); 187 | } 188 | } else if(eventPhase & NSEventPhaseEnded) { 189 | NSClipView* const clipView = self.contentView; 190 | const NSRect clipViewBounds = clipView.bounds; 191 | 192 | 193 | void (^completeScrollPhase)(BSRefreshableScrollViewSide refreshSide,NSProgressIndicator* progressIndicator) = ^(BSRefreshableScrollViewSide refreshSide,NSProgressIndicator* progressIndicator) { 194 | if(!(self.refreshingSides & refreshSide) && (self.activatedRefreshingSides & refreshSide)) { 195 | // not refreshing and OK to refresh 196 | 197 | self.activatedRefreshingSides &= ~refreshSide; 198 | 199 | if (self.triggeredRefreshingSides & refreshSide) { 200 | self.triggeredRefreshingSides &= ~refreshSide; 201 | 202 | [[NSOperationQueue mainQueue ] addOperationWithBlock:^{ 203 | BOOL refreshStarted = NO; 204 | id delegate = self.refreshableDelegate; 205 | if ([delegate respondsToSelector:@selector(scrollView:startRefreshSide:)]) { 206 | refreshStarted = [delegate scrollView:self startRefreshSide:refreshSide]; 207 | } 208 | 209 | if (refreshStarted) { 210 | [progressIndicator setIndeterminate:YES]; 211 | [progressIndicator startAnimation:self]; 212 | self.refreshingSides |= refreshSide; 213 | } 214 | }]; 215 | } else { 216 | // un-triggered 217 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 218 | [progressIndicator.animator setAlphaValue:0]; 219 | } completionHandler:^{ 220 | [progressIndicator stopAnimation:self]; 221 | [progressIndicator setIndeterminate:NO]; 222 | 223 | }]; 224 | 225 | } 226 | } 227 | }; 228 | 229 | if (clipViewBounds.origin.y < 0) { 230 | // showing top area 231 | completeScrollPhase(BSRefreshableScrollViewSideTop,self.topProgressIndicator); 232 | } else if(clipViewBounds.origin.y > 0) { 233 | // showing bottom area 234 | completeScrollPhase(BSRefreshableScrollViewSideBottom,self.bottomProgressIndicator); 235 | } 236 | } 237 | [super scrollWheel:theEvent]; 238 | } 239 | 240 | 241 | #pragma mark NSView 242 | 243 | // Place NSView overrides here – currently empty 😊 244 | 245 | 246 | #pragma mark NSScrollView 247 | 248 | -(NSClipView *)contentView 249 | { 250 | NSClipView* superClipView = [super contentView]; 251 | if (![superClipView isKindOfClass:[BSRefreshableClipView class]]) { 252 | NSView* documentView = superClipView.documentView; 253 | 254 | BSRefreshableClipView* clipView = [[BSRefreshableClipView alloc] initWithOriginalClipView:superClipView]; 255 | NSDictionary* bindings = NSDictionaryOfVariableBindings(documentView); 256 | clipView.documentView = documentView; 257 | [clipView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[documentView]|" options:0 metrics:nil views:bindings]]; 258 | [clipView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[documentView]|" options:0 metrics:nil views:bindings]]; 259 | [self setContentView:clipView]; 260 | 261 | superClipView = clipView; 262 | } 263 | return superClipView; 264 | } 265 | 266 | 267 | #pragma mark Property Access 268 | 269 | @synthesize topProgressIndicator = _topProgressIndicator; 270 | 271 | -(NSProgressIndicator *)topProgressIndicator 272 | { 273 | if (!_topProgressIndicator && (self.refreshableSides & BSRefreshableScrollViewSideTop)) { 274 | _topProgressIndicator = [NSProgressIndicator new]; 275 | [_topProgressIndicator setTranslatesAutoresizingMaskIntoConstraints:NO]; 276 | [_topProgressIndicator setIndeterminate:YES]; 277 | [_topProgressIndicator setStyle:NSProgressIndicatorSpinningStyle]; 278 | [_topProgressIndicator setControlSize: NSRegularControlSize]; 279 | [_topProgressIndicator setDisplayedWhenStopped:YES]; 280 | [_topProgressIndicator setAlphaValue:0]; 281 | [_topProgressIndicator sizeToFit]; 282 | } 283 | return _topProgressIndicator; 284 | } 285 | 286 | 287 | @synthesize bottomProgressIndicator = _bottomProgressIndicator; 288 | 289 | -(NSProgressIndicator *)bottomProgressIndicator 290 | { 291 | if (!_bottomProgressIndicator && (self.refreshableSides & BSRefreshableScrollViewSideBottom)) { 292 | _bottomProgressIndicator = [NSProgressIndicator new]; 293 | [_bottomProgressIndicator setTranslatesAutoresizingMaskIntoConstraints:NO]; 294 | [_bottomProgressIndicator setIndeterminate:YES]; 295 | [_bottomProgressIndicator setStyle:NSProgressIndicatorSpinningStyle]; 296 | [_bottomProgressIndicator setControlSize: NSRegularControlSize]; 297 | [_bottomProgressIndicator setDisplayedWhenStopped:YES]; 298 | [_bottomProgressIndicator setAlphaValue:0]; 299 | [_bottomProgressIndicator sizeToFit]; 300 | } 301 | return _bottomProgressIndicator; 302 | } 303 | 304 | 305 | @synthesize headerView = _headerView; 306 | 307 | -(NSView *)headerView 308 | { 309 | if (!_headerView && (self.refreshableSides & BSRefreshableScrollViewSideTop)) { 310 | _headerView = [self newEdgeViewForSide:BSRefreshableScrollViewSideTop progressIndicator:self.topProgressIndicator]; 311 | } 312 | return _headerView; 313 | } 314 | 315 | 316 | @synthesize footerView = _footerView; 317 | 318 | -(NSView *)footerView 319 | { 320 | if (!_footerView && (self.refreshableSides & BSRefreshableScrollViewSideBottom)) { 321 | _footerView = [self newEdgeViewForSide:BSRefreshableScrollViewSideBottom progressIndicator:self.bottomProgressIndicator]; 322 | } 323 | return _footerView; 324 | } 325 | 326 | 327 | @end 328 | -------------------------------------------------------------------------------- /Classes/osx/BSRefreshableScrollView_Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSRefreshableScrollView_Private.h 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | 23 | #import "BSRefreshableScrollView.h" 24 | 25 | @interface BSRefreshableScrollView () 26 | 27 | @property (nonatomic,strong) NSProgressIndicator* topProgressIndicator; 28 | @property (nonatomic,strong) NSProgressIndicator* bottomProgressIndicator; 29 | 30 | @property (nonatomic,readonly,strong) NSView* headerView; 31 | @property (nonatomic,readonly,strong) NSView* footerView; 32 | 33 | @property (nonatomic,readwrite) BSRefreshableScrollViewSide refreshingSides; 34 | @property (nonatomic,readwrite) BSRefreshableScrollViewSide triggeredRefreshingSides; 35 | 36 | @property (nonatomic,readwrite) BSRefreshableScrollViewSide activatedRefreshingSides; 37 | 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | pod "BSRefreshableScrollView", '~> 0.1' 2 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C00D9552165CC61B009F59FF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C057343D165A23B000087E78 /* Cocoa.framework */; }; 11 | C00D9561165CC663009F59FF /* libRefreshableScrollView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C00D9551165CC61B009F59FF /* libRefreshableScrollView.a */; }; 12 | C02FC955191BBFFD0014FFB8 /* BSRefreshableClipView.m in Sources */ = {isa = PBXBuildFile; fileRef = C02FC94F191BBFC70014FFB8 /* BSRefreshableClipView.m */; }; 13 | C02FC956191BBFFD0014FFB8 /* BSRefreshableScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = C02FC952191BBFC70014FFB8 /* BSRefreshableScrollView.m */; }; 14 | C057343E165A23B000087E78 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C057343D165A23B000087E78 /* Cocoa.framework */; }; 15 | C0573448165A23B000087E78 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C0573446165A23B000087E78 /* InfoPlist.strings */; }; 16 | C057344A165A23B000087E78 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0573449165A23B000087E78 /* main.m */; }; 17 | C057344E165A23B000087E78 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = C057344C165A23B000087E78 /* Credits.rtf */; }; 18 | C0573451165A23B000087E78 /* BSAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C0573450165A23B000087E78 /* BSAppDelegate.m */; }; 19 | C0573454165A23B000087E78 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C0573452165A23B000087E78 /* MainMenu.xib */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | C00D9562165CC68F009F59FF /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = C0573432165A23AF00087E78 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = C00D9550165CC61B009F59FF; 28 | remoteInfo = RefreshableScrollView; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | C00D9551165CC61B009F59FF /* libRefreshableScrollView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRefreshableScrollView.a; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | C00D9555165CC61B009F59FF /* RefreshableScrollView-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RefreshableScrollView-Prefix.pch"; sourceTree = ""; }; 35 | C02FC94E191BBFC70014FFB8 /* BSRefreshableClipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSRefreshableClipView.h; sourceTree = ""; }; 36 | C02FC94F191BBFC70014FFB8 /* BSRefreshableClipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSRefreshableClipView.m; sourceTree = ""; }; 37 | C02FC950191BBFC70014FFB8 /* BSRefreshableScrollView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSRefreshableScrollView_Private.h; sourceTree = ""; }; 38 | C02FC951191BBFC70014FFB8 /* BSRefreshableScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSRefreshableScrollView.h; sourceTree = ""; }; 39 | C02FC952191BBFC70014FFB8 /* BSRefreshableScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSRefreshableScrollView.m; sourceTree = ""; }; 40 | C057343A165A23AF00087E78 /* RefreshableScrollViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RefreshableScrollViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | C057343D165A23B000087E78 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 42 | C0573440165A23B000087E78 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 43 | C0573441165A23B000087E78 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 44 | C0573442165A23B000087E78 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 45 | C0573445165A23B000087E78 /* RefreshableScrollView-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RefreshableScrollView-Info.plist"; sourceTree = ""; }; 46 | C0573447165A23B000087E78 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 47 | C0573449165A23B000087E78 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 48 | C057344B165A23B000087E78 /* RefreshableScrollView-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RefreshableScrollView-Prefix.pch"; sourceTree = ""; }; 49 | C057344D165A23B000087E78 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 50 | C057344F165A23B000087E78 /* BSAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSAppDelegate.h; sourceTree = ""; }; 51 | C0573450165A23B000087E78 /* BSAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSAppDelegate.m; sourceTree = ""; }; 52 | C0573453165A23B000087E78 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | C00D954E165CC61B009F59FF /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | C00D9552165CC61B009F59FF /* Cocoa.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | C0573437165A23AF00087E78 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | C00D9561165CC663009F59FF /* libRefreshableScrollView.a in Frameworks */, 69 | C057343E165A23B000087E78 /* Cocoa.framework in Frameworks */, 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | C00D9553165CC61B009F59FF /* RefreshableScrollView */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | C00D9554165CC61B009F59FF /* Supporting Files */, 80 | ); 81 | path = RefreshableScrollView; 82 | sourceTree = ""; 83 | }; 84 | C00D9554165CC61B009F59FF /* Supporting Files */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | C00D9555165CC61B009F59FF /* RefreshableScrollView-Prefix.pch */, 88 | ); 89 | name = "Supporting Files"; 90 | sourceTree = ""; 91 | }; 92 | C0573431165A23AF00087E78 = { 93 | isa = PBXGroup; 94 | children = ( 95 | C057345A165A23C900087E78 /* Views */, 96 | C0573443165A23B000087E78 /* RefreshableScrollView */, 97 | C00D9553165CC61B009F59FF /* RefreshableScrollView */, 98 | C057343C165A23AF00087E78 /* Frameworks */, 99 | C057343B165A23AF00087E78 /* Products */, 100 | ); 101 | sourceTree = ""; 102 | }; 103 | C057343B165A23AF00087E78 /* Products */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | C057343A165A23AF00087E78 /* RefreshableScrollViewDemo.app */, 107 | C00D9551165CC61B009F59FF /* libRefreshableScrollView.a */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | C057343C165A23AF00087E78 /* Frameworks */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | C057343D165A23B000087E78 /* Cocoa.framework */, 116 | C057343F165A23B000087E78 /* Other Frameworks */, 117 | ); 118 | name = Frameworks; 119 | sourceTree = ""; 120 | }; 121 | C057343F165A23B000087E78 /* Other Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | C0573440165A23B000087E78 /* AppKit.framework */, 125 | C0573441165A23B000087E78 /* CoreData.framework */, 126 | C0573442165A23B000087E78 /* Foundation.framework */, 127 | ); 128 | name = "Other Frameworks"; 129 | sourceTree = ""; 130 | }; 131 | C0573443165A23B000087E78 /* RefreshableScrollView */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | C057344F165A23B000087E78 /* BSAppDelegate.h */, 135 | C0573450165A23B000087E78 /* BSAppDelegate.m */, 136 | C0573452165A23B000087E78 /* MainMenu.xib */, 137 | C0573444165A23B000087E78 /* Supporting Files */, 138 | ); 139 | path = RefreshableScrollView; 140 | sourceTree = ""; 141 | }; 142 | C0573444165A23B000087E78 /* Supporting Files */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | C0573445165A23B000087E78 /* RefreshableScrollView-Info.plist */, 146 | C0573446165A23B000087E78 /* InfoPlist.strings */, 147 | C0573449165A23B000087E78 /* main.m */, 148 | C057344B165A23B000087E78 /* RefreshableScrollView-Prefix.pch */, 149 | C057344C165A23B000087E78 /* Credits.rtf */, 150 | ); 151 | name = "Supporting Files"; 152 | sourceTree = ""; 153 | }; 154 | C057345A165A23C900087E78 /* Views */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | C02FC94E191BBFC70014FFB8 /* BSRefreshableClipView.h */, 158 | C02FC94F191BBFC70014FFB8 /* BSRefreshableClipView.m */, 159 | C02FC950191BBFC70014FFB8 /* BSRefreshableScrollView_Private.h */, 160 | C02FC951191BBFC70014FFB8 /* BSRefreshableScrollView.h */, 161 | C02FC952191BBFC70014FFB8 /* BSRefreshableScrollView.m */, 162 | ); 163 | name = Views; 164 | path = ../Classes/osx; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXGroup section */ 168 | 169 | /* Begin PBXHeadersBuildPhase section */ 170 | C00D954F165CC61B009F59FF /* Headers */ = { 171 | isa = PBXHeadersBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXHeadersBuildPhase section */ 178 | 179 | /* Begin PBXNativeTarget section */ 180 | C00D9550165CC61B009F59FF /* RefreshableScrollView */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = C00D9559165CC61B009F59FF /* Build configuration list for PBXNativeTarget "RefreshableScrollView" */; 183 | buildPhases = ( 184 | C00D954D165CC61B009F59FF /* Sources */, 185 | C00D954E165CC61B009F59FF /* Frameworks */, 186 | C00D954F165CC61B009F59FF /* Headers */, 187 | ); 188 | buildRules = ( 189 | ); 190 | dependencies = ( 191 | ); 192 | name = RefreshableScrollView; 193 | productName = RefreshableScrollView; 194 | productReference = C00D9551165CC61B009F59FF /* libRefreshableScrollView.a */; 195 | productType = "com.apple.product-type.library.static"; 196 | }; 197 | C0573439165A23AF00087E78 /* RefreshableScrollViewDemo */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = C0573457165A23B000087E78 /* Build configuration list for PBXNativeTarget "RefreshableScrollViewDemo" */; 200 | buildPhases = ( 201 | C0573436165A23AF00087E78 /* Sources */, 202 | C0573437165A23AF00087E78 /* Frameworks */, 203 | C0573438165A23AF00087E78 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | C00D9563165CC68F009F59FF /* PBXTargetDependency */, 209 | ); 210 | name = RefreshableScrollViewDemo; 211 | productName = RefreshableScrollView; 212 | productReference = C057343A165A23AF00087E78 /* RefreshableScrollViewDemo.app */; 213 | productType = "com.apple.product-type.application"; 214 | }; 215 | /* End PBXNativeTarget section */ 216 | 217 | /* Begin PBXProject section */ 218 | C0573432165A23AF00087E78 /* Project object */ = { 219 | isa = PBXProject; 220 | attributes = { 221 | CLASSPREFIX = BS; 222 | LastUpgradeCheck = 0510; 223 | ORGANIZATIONNAME = "Basil Salad Software"; 224 | }; 225 | buildConfigurationList = C0573435165A23AF00087E78 /* Build configuration list for PBXProject "RefreshableScrollView" */; 226 | compatibilityVersion = "Xcode 3.2"; 227 | developmentRegion = English; 228 | hasScannedForEncodings = 0; 229 | knownRegions = ( 230 | en, 231 | ); 232 | mainGroup = C0573431165A23AF00087E78; 233 | productRefGroup = C057343B165A23AF00087E78 /* Products */; 234 | projectDirPath = ""; 235 | projectRoot = ""; 236 | targets = ( 237 | C0573439165A23AF00087E78 /* RefreshableScrollViewDemo */, 238 | C00D9550165CC61B009F59FF /* RefreshableScrollView */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | C0573438165A23AF00087E78 /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | C0573448165A23B000087E78 /* InfoPlist.strings in Resources */, 249 | C057344E165A23B000087E78 /* Credits.rtf in Resources */, 250 | C0573454165A23B000087E78 /* MainMenu.xib in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXResourcesBuildPhase section */ 255 | 256 | /* Begin PBXSourcesBuildPhase section */ 257 | C00D954D165CC61B009F59FF /* Sources */ = { 258 | isa = PBXSourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | C02FC956191BBFFD0014FFB8 /* BSRefreshableScrollView.m in Sources */, 262 | C02FC955191BBFFD0014FFB8 /* BSRefreshableClipView.m in Sources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | C0573436165A23AF00087E78 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | C057344A165A23B000087E78 /* main.m in Sources */, 271 | C0573451165A23B000087E78 /* BSAppDelegate.m in Sources */, 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | /* End PBXSourcesBuildPhase section */ 276 | 277 | /* Begin PBXTargetDependency section */ 278 | C00D9563165CC68F009F59FF /* PBXTargetDependency */ = { 279 | isa = PBXTargetDependency; 280 | target = C00D9550165CC61B009F59FF /* RefreshableScrollView */; 281 | targetProxy = C00D9562165CC68F009F59FF /* PBXContainerItemProxy */; 282 | }; 283 | /* End PBXTargetDependency section */ 284 | 285 | /* Begin PBXVariantGroup section */ 286 | C0573446165A23B000087E78 /* InfoPlist.strings */ = { 287 | isa = PBXVariantGroup; 288 | children = ( 289 | C0573447165A23B000087E78 /* en */, 290 | ); 291 | name = InfoPlist.strings; 292 | sourceTree = ""; 293 | }; 294 | C057344C165A23B000087E78 /* Credits.rtf */ = { 295 | isa = PBXVariantGroup; 296 | children = ( 297 | C057344D165A23B000087E78 /* en */, 298 | ); 299 | name = Credits.rtf; 300 | sourceTree = ""; 301 | }; 302 | C0573452165A23B000087E78 /* MainMenu.xib */ = { 303 | isa = PBXVariantGroup; 304 | children = ( 305 | C0573453165A23B000087E78 /* en */, 306 | ); 307 | name = MainMenu.xib; 308 | sourceTree = ""; 309 | }; 310 | /* End PBXVariantGroup section */ 311 | 312 | /* Begin XCBuildConfiguration section */ 313 | C00D955A165CC61B009F59FF /* Debug */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 317 | GCC_PREFIX_HEADER = "RefreshableScrollView/RefreshableScrollView-Prefix.pch"; 318 | OTHER_LDFLAGS = "-ObjC"; 319 | PRODUCT_NAME = "$(TARGET_NAME)"; 320 | }; 321 | name = Debug; 322 | }; 323 | C00D955B165CC61B009F59FF /* Release */ = { 324 | isa = XCBuildConfiguration; 325 | buildSettings = { 326 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 327 | GCC_PREFIX_HEADER = "RefreshableScrollView/RefreshableScrollView-Prefix.pch"; 328 | OTHER_LDFLAGS = "-ObjC"; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | }; 331 | name = Release; 332 | }; 333 | C0573455165A23B000087E78 /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ALWAYS_SEARCH_USER_PATHS = NO; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 338 | CLANG_CXX_LIBRARY = "libc++"; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_INT_CONVERSION = YES; 344 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 345 | COPY_PHASE_STRIP = NO; 346 | GCC_C_LANGUAGE_STANDARD = gnu99; 347 | GCC_DYNAMIC_NO_PIC = NO; 348 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 349 | GCC_OPTIMIZATION_LEVEL = 0; 350 | GCC_PREPROCESSOR_DEFINITIONS = ( 351 | "DEBUG=1", 352 | "$(inherited)", 353 | ); 354 | GCC_STRICT_ALIASING = YES; 355 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 356 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 357 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 358 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | MACOSX_DEPLOYMENT_TARGET = 10.8; 361 | ONLY_ACTIVE_ARCH = YES; 362 | SDKROOT = macosx; 363 | }; 364 | name = Debug; 365 | }; 366 | C0573456165A23B000087E78 /* Release */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ALWAYS_SEARCH_USER_PATHS = NO; 370 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 371 | CLANG_CXX_LIBRARY = "libc++"; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 378 | COPY_PHASE_STRIP = YES; 379 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 380 | GCC_C_LANGUAGE_STANDARD = gnu99; 381 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 382 | GCC_STRICT_ALIASING = YES; 383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 384 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 386 | GCC_WARN_UNUSED_VARIABLE = YES; 387 | MACOSX_DEPLOYMENT_TARGET = 10.8; 388 | SDKROOT = macosx; 389 | }; 390 | name = Release; 391 | }; 392 | C0573458165A23B000087E78 /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | COMBINE_HIDPI_IMAGES = YES; 396 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 397 | GCC_PREFIX_HEADER = "RefreshableScrollView/RefreshableScrollView-Prefix.pch"; 398 | INFOPLIST_FILE = "RefreshableScrollView/RefreshableScrollView-Info.plist"; 399 | OTHER_LDFLAGS = "-ObjC"; 400 | PRODUCT_NAME = "$(TARGET_NAME)"; 401 | WRAPPER_EXTENSION = app; 402 | }; 403 | name = Debug; 404 | }; 405 | C0573459165A23B000087E78 /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | COMBINE_HIDPI_IMAGES = YES; 409 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 410 | GCC_PREFIX_HEADER = "RefreshableScrollView/RefreshableScrollView-Prefix.pch"; 411 | INFOPLIST_FILE = "RefreshableScrollView/RefreshableScrollView-Info.plist"; 412 | OTHER_LDFLAGS = "-ObjC"; 413 | PRODUCT_NAME = "$(TARGET_NAME)"; 414 | WRAPPER_EXTENSION = app; 415 | }; 416 | name = Release; 417 | }; 418 | /* End XCBuildConfiguration section */ 419 | 420 | /* Begin XCConfigurationList section */ 421 | C00D9559165CC61B009F59FF /* Build configuration list for PBXNativeTarget "RefreshableScrollView" */ = { 422 | isa = XCConfigurationList; 423 | buildConfigurations = ( 424 | C00D955A165CC61B009F59FF /* Debug */, 425 | C00D955B165CC61B009F59FF /* Release */, 426 | ); 427 | defaultConfigurationIsVisible = 0; 428 | defaultConfigurationName = Release; 429 | }; 430 | C0573435165A23AF00087E78 /* Build configuration list for PBXProject "RefreshableScrollView" */ = { 431 | isa = XCConfigurationList; 432 | buildConfigurations = ( 433 | C0573455165A23B000087E78 /* Debug */, 434 | C0573456165A23B000087E78 /* Release */, 435 | ); 436 | defaultConfigurationIsVisible = 0; 437 | defaultConfigurationName = Release; 438 | }; 439 | C0573457165A23B000087E78 /* Build configuration list for PBXNativeTarget "RefreshableScrollViewDemo" */ = { 440 | isa = XCConfigurationList; 441 | buildConfigurations = ( 442 | C0573458165A23B000087E78 /* Debug */, 443 | C0573459165A23B000087E78 /* Release */, 444 | ); 445 | defaultConfigurationIsVisible = 0; 446 | defaultConfigurationName = Release; 447 | }; 448 | /* End XCConfigurationList section */ 449 | }; 450 | rootObject = C0573432165A23AF00087E78 /* Project object */; 451 | } 452 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/project.xcworkspace/xcuserdata/adib.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adib/RefreshableScrollView/715d9152f59d927f36478881eed3ac51dccc5b30/Example/RefreshableScrollView.xcodeproj/project.xcworkspace/xcuserdata/adib.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/xcuserdata/adib.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/xcuserdata/adib.xcuserdatad/xcschemes/RefreshableScrollView 2.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 42 | 43 | 44 | 45 | 51 | 52 | 54 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/xcuserdata/adib.xcuserdatad/xcschemes/RefreshableScrollView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView.xcodeproj/xcuserdata/adib.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RefreshableScrollView 2.xcscheme 8 | 9 | orderHint 10 | 2 11 | 12 | RefreshableScrollView.xcscheme 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | C00D9550165CC61B009F59FF 21 | 22 | primary 23 | 24 | 25 | C0573439165A23AF00087E78 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/BSAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSAppDelegate.h 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | #import 23 | 24 | #import "BSRefreshableScrollView.h" 25 | 26 | @interface BSAppDelegate : NSObject 27 | 28 | @property (assign) IBOutlet NSWindow *window; 29 | 30 | @property (nonatomic,weak) IBOutlet BSRefreshableScrollView* refreshableScrollView; 31 | 32 | - (IBAction)stopRefreshTop:(id)sender; 33 | - (IBAction)stopRefreshBottom:(id)sender; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/BSAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // BSAppDelegate.m 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | #import "BSAppDelegate.h" 23 | 24 | 25 | @implementation BSAppDelegate 26 | 27 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 28 | { 29 | // Insert code here to initialize your application 30 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints"]; 31 | } 32 | 33 | 34 | -(void)awakeFromNib 35 | { 36 | [super awakeFromNib]; 37 | self.refreshableScrollView.refreshableSides = BSRefreshableScrollViewSideTop | BSRefreshableScrollViewSideBottom; 38 | } 39 | 40 | 41 | #pragma mark BSRefreshableScrollViewDelegate 42 | 43 | -(BOOL) scrollView:(BSRefreshableScrollView*) aScrollView startRefreshSide:(BSRefreshableScrollViewSide) refreshableSide 44 | { 45 | return YES; 46 | } 47 | 48 | #pragma mark Action Handlers 49 | 50 | - (IBAction)stopRefreshTop:(id)sender 51 | { 52 | [self.refreshableScrollView stopRefreshingSide:BSRefreshableScrollViewSideTop]; 53 | } 54 | 55 | 56 | - (IBAction)stopRefreshBottom:(id)sender 57 | { 58 | [self.refreshableScrollView stopRefreshingSide:BSRefreshableScrollViewSideBottom]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/RefreshableScrollView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.basilsalad.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2012 Basil Salad Software. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/RefreshableScrollView-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'RefreshableScrollView' target in the 'RefreshableScrollView' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/en.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | Default 523 | 524 | 525 | 526 | 527 | 528 | 529 | Left to Right 530 | 531 | 532 | 533 | 534 | 535 | 536 | Right to Left 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | Default 548 | 549 | 550 | 551 | 552 | 553 | 554 | Left to Right 555 | 556 | 557 | 558 | 559 | 560 | 561 | Right to Left 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 688 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | -------------------------------------------------------------------------------- /Example/RefreshableScrollView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // RefreshableScrollView 4 | // 5 | // Created by Sasmito Adibowo on 19-11-12. 6 | // Copyright (c) 2012 Basil Salad Software. All rights reserved. 7 | // http://basilsalad.com 8 | // 9 | // Licensed under the BSD License 10 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 11 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 12 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 13 | // SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 15 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 16 | // BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 18 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | // 20 | 21 | 22 | 23 | #import 24 | 25 | int main(int argc, char *argv[]) 26 | { 27 | return NSApplicationMain(argc, (const char **)argv); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, Sasmito Adibowo 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RefreshableScrollView 2 | 3 | An `NSScrollView` subclass that supports pull-to-refresh both in the top and bottom edges. The intended usage is for hosting a timeline-like view that shows a list of messages in reverse-chronological order. 4 | 5 | 6 | 7 | ## Getting Started 8 | 9 | Attach `RefreshableScrollView.xcodeproj` into your OS X project and add `libRefreshableScrollView.a` as a dependency in your project. Also include the public header file `BSRefreshableScrollView.h` into your project's user header search paths. 10 | 11 | Play around with the included example application to see the class in action and how it is being used. The video demo above shows a recording of this sample application. 12 | 13 | ## How to use 14 | 15 | Use `BSRefreshableScrollView` as a replacement of `NSScrollView`. If you use Xcode's interface builder to add instances of `NSTableView` or `NSOutlineView` (among others), by default it will enclose those objects inside an `NSScrollView`. Highlight this `NSScrollView` container, go to the object' identity inspector and type in `BSRefreshableScrollView` as the scroll view's class name. 16 | 17 | Then you'll need to write a delegate that conforms to `BSRefreshableScrollViewDelegate` protocol. Most likely this will be the `xib`'s file owner – a view controller or a window controller. Attach this delegate class to the `refreshableDelegate` outlet of the scroll view. 18 | 19 | In `awakeFromNib` method of the delegate, setup which sides that supports pull-to-refresh. This is important – without enabling these flags, `BSRefreshableScrollView` will behave just like a plain old `NSScrollView`. If you don't use `xib` files then you'll need to find an appropriate place to set these flags. Note that you can clear any of these flags to disable refresh for their respective sides even when the view is already visible. 20 | 21 | 22 | -(void)awakeFromNib 23 | { 24 | [super awakeFromNib]; 25 | self.refreshableScrollView.refreshableSides = BSRefreshableScrollViewSideTop | BSRefreshableScrollViewSideBottom; 26 | } 27 | 28 | Start the refresh process in your delegate by implementing method `scrollView: startRefreshSide:` and return `YES` if the process was successfully started. The scroll view will then display an indeterminate progress indicator at the appropriate edge. If for some reason you couldn't initiate refresh for that side, simply return `NO` and the scroll view will behave as if nothing happened. 29 | 30 | #pragma mark BSRefreshableScrollViewDelegate 31 | 32 | -(BOOL) scrollView:(BSRefreshableScrollView*) aScrollView startRefreshSide:(BSRefreshableScrollViewSide) refreshableSide 33 | { 34 | if (refreshableSide == BSRefreshableScrollViewSideTop) { 35 | // initiate refresh process for the top edge of the scroll view 36 | // ... 37 | return YES; // tell the scroll view to display the progress indicator 38 | } else if (refreshableSide == BSRefreshableScrollViewSideBottom) { 39 | // initiate refresh process for the bottom edge of the scroll view 40 | // ... 41 | return YES; 42 | } 43 | return NO; 44 | } 45 | 46 | When the refresh process have completed (i.e. the network service have returned a result or timed out), call `stopRefreshingSide:` and give it the appropriate edge to stop. If there was no refresh currently in progress at that side this calling this method will have no effect. For example: 47 | 48 | - (IBAction)stopRefreshTop:(id)sender 49 | { 50 | [self.refreshableScrollView stopRefreshingSide:BSRefreshableScrollViewSideTop]; 51 | } 52 | 53 | ## License 54 | 55 | This project is licensed under the BSD license. Please let me know () if you use it for something interesting. 56 | 57 | 58 | 59 | Sasmito Adibowo 60 | http://cutecoder.org 61 | 62 | 63 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Runs the specs [EMPTY]" 2 | task :spec do 3 | # Provide your own implementation 4 | end 5 | 6 | task :version do 7 | git_remotes = `git remote`.strip.split("\n") 8 | 9 | if git_remotes.count > 0 10 | puts "-- fetching version number from github" 11 | sh 'git fetch' 12 | 13 | remote_version = remote_spec_version 14 | end 15 | 16 | if remote_version.nil? 17 | puts "There is no current released version. You're about to release a new Pod." 18 | version = "0.0.1" 19 | else 20 | puts "The current released version of your pod is " + 21 | remote_spec_version.to_s() 22 | version = suggested_version_number 23 | end 24 | 25 | puts "Enter the version you want to release (" + version + ") " 26 | new_version_number = $stdin.gets.strip 27 | if new_version_number == "" 28 | new_version_number = version 29 | end 30 | 31 | replace_version_number(new_version_number) 32 | end 33 | 34 | desc "Release a new version of the Pod (append repo=name to push to a private spec repo)" 35 | task :release do 36 | # Allow override of spec repo name using `repo=private` after task name 37 | repo = ENV["repo"] || "master" 38 | 39 | puts "* Running version" 40 | sh "rake version" 41 | 42 | unless ENV['SKIP_CHECKS'] 43 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' 44 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." 45 | exit 1 46 | end 47 | 48 | if `git tag`.strip.split("\n").include?(spec_version) 49 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" 50 | exit 1 51 | end 52 | 53 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]" 54 | exit if $stdin.gets.strip.downcase != 'y' 55 | end 56 | 57 | puts "* Running specs" 58 | sh "rake spec" 59 | 60 | puts "* Linting the podspec" 61 | sh "pod lib lint" 62 | 63 | # Then release 64 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}' --allow-empty" 65 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" 66 | sh "git push origin master" 67 | sh "git push origin --tags" 68 | sh "pod push #{repo} #{podspec_path}" 69 | end 70 | 71 | # @return [Pod::Version] The version as reported by the Podspec. 72 | # 73 | def spec_version 74 | require 'cocoapods' 75 | spec = Pod::Specification.from_file(podspec_path) 76 | spec.version 77 | end 78 | 79 | # @return [Pod::Version] The version as reported by the Podspec from remote. 80 | # 81 | def remote_spec_version 82 | require 'cocoapods-core' 83 | 84 | if spec_file_exist_on_remote? 85 | remote_spec = eval(`git show origin/master:#{podspec_path}`) 86 | remote_spec.version 87 | else 88 | nil 89 | end 90 | end 91 | 92 | # @return [Bool] If the remote repository has a copy of the podpesc file or not. 93 | # 94 | def spec_file_exist_on_remote? 95 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; 96 | then 97 | echo 'true' 98 | else 99 | echo 'false' 100 | fi` 101 | 102 | 'true' == test_condition.strip 103 | end 104 | 105 | # @return [String] The relative path of the Podspec. 106 | # 107 | def podspec_path 108 | podspecs = Dir.glob('*.podspec') 109 | if podspecs.count == 1 110 | podspecs.first 111 | else 112 | raise "Could not select a podspec" 113 | end 114 | end 115 | 116 | # @return [String] The suggested version number based on the local and remote 117 | # version numbers. 118 | # 119 | def suggested_version_number 120 | if spec_version != remote_spec_version 121 | spec_version.to_s() 122 | else 123 | next_version(spec_version).to_s() 124 | end 125 | end 126 | 127 | # @param [Pod::Version] version 128 | # the version for which you need the next version 129 | # 130 | # @note It is computed by bumping the last component of 131 | # the version string by 1. 132 | # 133 | # @return [Pod::Version] The version that comes next after 134 | # the version supplied. 135 | # 136 | def next_version(version) 137 | version_components = version.to_s().split("."); 138 | last = (version_components.last.to_i() + 1).to_s 139 | version_components[-1] = last 140 | Pod::Version.new(version_components.join(".")) 141 | end 142 | 143 | # @param [String] new_version_number 144 | # the new version number 145 | # 146 | # @note This methods replaces the version number in the podspec file 147 | # with a new version number. 148 | # 149 | # @return void 150 | # 151 | def replace_version_number(new_version_number) 152 | text = File.read(podspec_path) 153 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/, 154 | "\\1#{new_version_number}\\3") 155 | File.open(podspec_path, "w") { |file| file.puts text } 156 | end 157 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | test 2 | --------------------------------------------------------------------------------