├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── TOScrollBar-2016.jpg ├── TOScrollBar.jpg ├── TOScrollBar.podspec ├── TOScrollBar ├── TOScrollBar.h ├── TOScrollBar.m ├── TOScrollBarGestureRecognizer.h ├── TOScrollBarGestureRecognizer.m ├── UIScrollView+TOScrollBar.h └── UIScrollView+TOScrollBar.m ├── TOScrollBarExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── TOScrollBarExample.xcscheme │ └── TOScrollBarExampleTests.xcscheme ├── TOScrollBarExample ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.h ├── ViewController.m └── main.m └── TOScrollBarExampleTests ├── Info.plist └── TOScrollBarExampleTests.m /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: timoliver 2 | custom: https://tim.dev/paypal 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9 3 | xcode_project: TOScrollBarExample.xcodeproj 4 | xcode_scheme: TOScrollBarExample 5 | script: xcodebuild -verbose -scheme TOScrollBarExample -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone SE,OS=latest' test 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## 0.0.5 - 2017-12-11 10 | 11 | ### Added 12 | - An option to let taps pass through the scroll bar track view to the scroll views behind it. 13 | 14 | ### Fixed 15 | - A bug in iOS 11.2 where tapping on the track wouldn't perform the scroll until the user released their finger. 16 | - A bug where content in the scroll view was appearing over the scroll bar. 17 | 18 | ## 0.0.4 - 2017-09-21 19 | 20 | ### Added 21 | - Added a CHANGELOG. 22 | - Added support for iOS 11 large titles. 23 | - Added basic support for iPhone X horizontal insets. 24 | 25 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Thanks for considering filing an issue! Before proceeding, please consider 2 | > the type of issue you're filing and make sure to supply the proper details 3 | > needed for it! :) 4 | > 5 | > --- 6 | > 7 | > **Questions**: Please check the closed issues to see if it's already been asked 8 | > before. 9 | > 10 | > **Feature Request**: Please fill in just the first two sections. Please be as thorough 11 | > as possible and explain how you would expect this feature to work both visually, and from an 12 | > API perspective. 13 | > 14 | > **Bugs**: Please be as thorough as possible when describe the bug you've discovered. If it's 15 | > a visual bug, please add a screenshot. If it's an animation bug, please provide a recording 16 | > of the bug in action. 17 | > 18 | > --- 19 | > 20 | > Please note that since library is done as a side-project outside of work hours, 21 | > a timely response cannot be guaranteed. ;) 22 | > 23 | > Please remove this line and everything above it before submitting. 24 | 25 | ## Goals 26 | 27 | What is the outcome result you want to achieve with this library? 28 | 29 | ## Expected Results 30 | 31 | What did you expect to happen? 32 | 33 | ## Actual Results 34 | 35 | What happened instead? (Please attach a screenshot/screen recording if possible) 36 | 37 | ## Steps to Reproduce 38 | 39 | What are the steps needed to reproduce this issue? 40 | If this bug was caused by a specific image, please post it here. 41 | 42 | ## Hardware / Software 43 | 44 | On which version of iOS, and what sort of device did you experience this bug? 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tim Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOScrollBar 2 | 3 | ![TOScrollBar](TOScrollBar.jpg) 4 | 5 | [![CI Status](http://img.shields.io/travis/TimOliver/TOScrollBar.svg?style=flat)](http://api.travis-ci.org/TimOliver/TOScrollBar.svg) 6 | [![CocoaPods](https://img.shields.io/cocoapods/dt/TOScrollBar.svg?maxAge=3600)](https://cocoapods.org/pods/TOScrollBar) 7 | [![Version](https://img.shields.io/cocoapods/v/TOScrollBar.svg?style=flat)](http://cocoadocs.org/docsets/TOScrollBar) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/TimOliver/TOScrollBar/master/LICENSE) 9 | [![Platform](https://img.shields.io/cocoapods/p/TOScrollBar.svg?style=flat)](http://cocoadocs.org/docsets/TOScrollBar) 10 | [![Beerpay](https://beerpay.io/TimOliver/TOScrollBar/badge.svg?style=flat)](https://beerpay.io/TimOliver/TOScrollBar) 11 | [![PayPal](https://img.shields.io/badge/paypal-donate-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M4RKULAVKV7K8) 12 | 13 | `TOScrollBar` is a UI component that can be inserted into `UIScrollView` instances, allowing the user to traverse the entire scroll view in one swiping gesture. 14 | 15 | It has been designed to appear and behave like a standard system control, and has been optimized to ensure it has minimal impact on scroll performance. 16 | 17 | # Features 18 | 19 | * Allows for fine-grained scrolling of a `UIScrollView`'s entire content height. 20 | * Interoperates directly with `UIScrollView` through the Objective-C runtime and KVO. 21 | * Animates the same way as the standard scroll indicators (including rubber banding). 22 | * Exposes 44 points of horizontal touch space, so it is very easy to activate. 23 | * Tapping at different positions allows for instant traversal along the scroll view. 24 | * Plays a scrolling animation during slow swiping, making it easier to follow along. 25 | * Comes with initial style settings for dark themes. 26 | * Includes Taptic Engine impact effects in a similar style to `UISlider`, available on iPhone 7. 27 | 28 | # Examples 29 | 30 | `TOScrollBar` has been designed to be added directly to a `UIScrollView`, not as a view above. 31 | 32 | ```objc 33 | 34 | // Create a scroll bar object 35 | TOScrollBar *scrollBar = [[TOScrollBar alloc] init]; 36 | 37 | // Add the scroll bar to our table view 38 | [self.tableView to_addScrollBar:scrollBar]; 39 | 40 | //Adjust the table separators so they won't underlap the scroll bar 41 | self.tableView.separatorInset = [self.tableView.to_scrollBar adjustedTableViewSeparatorInsetForInset:self.tableView.separatorInset]; 42 | 43 | ``` 44 | 45 | Once added to a scroll view, a scroll bar can be accessed via the `to_scrollBar` property. Convienience methods are 46 | also applied to make it easier to configure the margins 47 | 48 | 49 | # Installation 50 | 51 | `TOScrollBar` will work with iOS 7 and above. While written in Objective-C, it should easily import into Swift as well. 52 | 53 | ## Manual Installation 54 | 55 | Copy the contents of the `TOScrollBar` folder to your app project. 56 | 57 | ## CocoaPods 58 | 59 | ``` 60 | pod 'TOScrollBar' 61 | ``` 62 | 63 | ## Carthage 64 | 65 | Feel free to file a PR. :) 66 | 67 | # Why build this? 68 | 69 | I'm building a [comic reader app](http://icomics.co) that allows users to group collections of comics into single view controllers. 70 | 71 | Unfortunately, some users have reported that certain comic series have a very large number of issues. It doesn't make sense to break these issues out 72 | of their collections, but at the same time, traversing the comic has become a gruelling process. 73 | 74 | This scroll bar is the first component in a series of upgrades I'm planning in an attempt to make navigation large comic collections more manageable. 75 | 76 | # Credits 77 | 78 | `TOScrollBar` was created by [Tim Oliver](http://twitter.com/TimOliverAU) as a component of [iComics](http://icomics.co). 79 | 80 | # License 81 | 82 | `TOScrollBar` is available under the MIT license. Please see the [LICENSE](LICENSE) file for more information. ![analytics](https://ga-beacon.appspot.com/UA-5643664-16/TOScrollBar/README.md?pixel) 83 | -------------------------------------------------------------------------------- /TOScrollBar-2016.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimOliver/TOScrollBar/216a2d7c67191bae4e6f1282211e656b1af63291/TOScrollBar-2016.jpg -------------------------------------------------------------------------------- /TOScrollBar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimOliver/TOScrollBar/216a2d7c67191bae4e6f1282211e656b1af63291/TOScrollBar.jpg -------------------------------------------------------------------------------- /TOScrollBar.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'TOScrollBar' 3 | s.version = '0.0.5' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'An interactive scroll bar for to easily traverse comically massive scroll views.' 6 | s.homepage = 'https://github.com/TimOliver/TOScrollBar' 7 | s.author = 'Tim Oliver' 8 | s.source = { :git => 'https://github.com/TimOliver/TOScrollBar.git', :tag => s.version } 9 | s.platform = :ios, '7.0' 10 | s.source_files = 'TOScrollBar/**/*.{h,m}' 11 | s.requires_arc = true 12 | end 13 | -------------------------------------------------------------------------------- /TOScrollBar/TOScrollBar.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOScrollBar.h 3 | // 4 | // Copyright 2016-2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "UIScrollView+TOScrollBar.h" 25 | 26 | typedef NS_ENUM(NSInteger, TOScrollBarStyle) { 27 | TOScrollBarStyleDefault, 28 | TOScrollBarStyleDark 29 | }; 30 | 31 | NS_ASSUME_NONNULL_BEGIN 32 | 33 | @interface TOScrollBar : UIView 34 | 35 | /* The visual style of the scroll bar, either light or dark */ 36 | @property (nonatomic, assign) TOScrollBarStyle style; 37 | 38 | /** Aligns the scroll bar to the top of the scroll view content offset. 39 | Set this to `YES` when using this in a view controller with iOS 11 large titles. */ 40 | @property (nonatomic, assign) BOOL insetForLargeTitles; 41 | 42 | /** The amount of padding above and below the scroll bar (Only top and bottom values are counted. Default is {20,20} ) */ 43 | @property (nonatomic, assign) UIEdgeInsets verticalInset; 44 | 45 | /** The inset, in points of the middle of track from the edge of the scroll view */ 46 | @property (nonatomic, assign) CGFloat edgeInset; 47 | 48 | /** The tint color of the track */ 49 | @property (nonatomic, strong) UIColor *trackTintColor; 50 | 51 | /** The width in points, of the track (Default value is 2.0) */ 52 | @property (nonatomic, assign) CGFloat trackWidth; 53 | 54 | /** The tint color of the handle (Defaults to the system tint color) */ 55 | @property (nonatomic, strong, nullable) UIColor *handleTintColor; 56 | 57 | /** The width in points, of the handle. (Default value is 4.0) */ 58 | @property (nonatomic, assign) CGFloat handleWidth; 59 | 60 | /** The minimum height in points the handle may be in relation to the content height. (Default value is 64.0) */ 61 | @property (nonatomic, assign) CGFloat handleMinimiumHeight; 62 | 63 | /** The user is currently dragging the handle */ 64 | @property (nonatomic, assign, readonly) BOOL dragging; 65 | 66 | /** The minimum required scale of the scroll view's content height before the scroll bar is shown (Default is 5.0) */ 67 | @property (nonatomic, assign) CGFloat minimumContentHeightScale; 68 | 69 | /** The scroll view in which this scroll bar has been added. */ 70 | @property (nonatomic, weak, readonly) UIScrollView *scrollView; 71 | 72 | /** When enabled, the scroll bar will only respond to direct touches to the handle control. 73 | Touches to the track will be passed to the UI controls beneath it. 74 | Default is NO. */ 75 | @property (nonatomic, assign) BOOL handleExclusiveInteractionEnabled; 76 | 77 | /** 78 | Creates a new instance of the scroll bar view 79 | 80 | @param style The initial style of the scroll bar upon creation 81 | */ 82 | - (instancetype)initWithStyle:(TOScrollBarStyle)style; 83 | 84 | /** 85 | Adds the scroll bar to a scroll view 86 | 87 | @param scrollView The scroll view that will receive this scroll bar 88 | */ 89 | - (void)addToScrollView:(UIScrollView *)scrollView; 90 | 91 | /** 92 | Removes the scroll bar from the scroll view and resets the scroll view's state 93 | */ 94 | - (void)removeFromScrollView; 95 | 96 | /** 97 | If added to a table view, this convienience method will compute the appropriate 98 | inset values for the table separator so they don't underlap the scroll bar 99 | 100 | @param inset The original separator inset value of the table view 101 | */ 102 | - (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset; 103 | 104 | /** 105 | If added to a table view, this convienience method will compute the appropriate 106 | insets values for each cell's layout margins in order to appropriately push the cell's 107 | content inwards 108 | 109 | @param layoutMargins The current `layoutMargins` value of the `UITableViewCell` instance. 110 | @param offset If desired, any additional horizontal offset for this specific use case 111 | 112 | */ 113 | - (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset; 114 | 115 | /** 116 | Shows or hides the scroll bar from the scroll view with an optional animation 117 | */ 118 | - (void)setHidden:(BOOL)hidden animated:(BOOL)animated; 119 | 120 | @end 121 | 122 | NS_ASSUME_NONNULL_END 123 | 124 | -------------------------------------------------------------------------------- /TOScrollBar/TOScrollBar.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOScrollBar.m 3 | // 4 | // Copyright 2016-2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOScrollBar.h" 24 | #import "UIScrollView+TOScrollBar.h" 25 | #import "TOScrollBarGestureRecognizer.h" 26 | 27 | /** Default values for the scroll bar */ 28 | static const CGFloat kTOScrollBarTrackWidth = 2.0f; // The default width of the scrollable space indicator 29 | static const CGFloat kTOScrollBarHandleWidth = 4.0f; // The default width of the handle control 30 | static const CGFloat kTOScrollBarEdgeInset = 7.5f; // The distance from the edge of the view to the center of the track 31 | static const CGFloat kTOScrollBarHandleMinHeight = 64.0f; // The minimum usable size to which the handle can shrink 32 | static const CGFloat kTOScrollBarWidth = 44.0f; // The width of this control (44 is minimum recommended tapping space) 33 | static const CGFloat kTOScrollBarVerticalPadding = 10.0f; // The default padding at the top and bottom of the view 34 | static const CGFloat kTOScrollBarMinimumContentScale = 5.0f; // The minimum scale of the content view before showing the scroll view is necessary 35 | 36 | /************************************************************************/ 37 | 38 | // A struct to hold the scroll view's previous state before this bar was applied 39 | struct TOScrollBarScrollViewState { 40 | BOOL showsVerticalScrollIndicator; 41 | }; 42 | typedef struct TOScrollBarScrollViewState TOScrollBarScrollViewState; 43 | 44 | /************************************************************************/ 45 | // Private interface exposure for scroll view category 46 | 47 | @interface UIScrollView () //TOScrollBar 48 | - (void)setTo_scrollBar:(TOScrollBar *)scrollBar; 49 | @end 50 | 51 | /************************************************************************/ 52 | 53 | @interface TOScrollBar () { 54 | TOScrollBarScrollViewState _scrollViewState; 55 | } 56 | 57 | @property (nonatomic, weak, readwrite) UIScrollView *scrollView; // The parent scroll view in which we belong 58 | 59 | @property (nonatomic, assign) BOOL userHidden; // View was explicitly hidden by the user as opposed to us 60 | 61 | @property (nonatomic, strong) UIImageView *trackView; // The track indicating the scrollable distance 62 | @property (nonatomic, strong) UIImageView *handleView; // The handle that may be dragged in the scroll bar 63 | 64 | @property (nonatomic, assign, readwrite) BOOL dragging; // The user is presently dragging the handle 65 | @property (nonatomic, assign) CGFloat yOffset; // The offset from the center of the thumb 66 | 67 | @property (nonatomic, assign) CGFloat originalYOffset; // The original placement of the scroll bar when the user started dragging 68 | @property (nonatomic, assign) CGFloat originalHeight; // The original height of the scroll bar when the user started dragging 69 | @property (nonatomic, assign) CGFloat originalTopInset; // The original safe area inset of the scroll bar when the user started dragging 70 | 71 | @property (nonatomic, assign) CGFloat horizontalOffset; // The horizontal offset when the edge inset is too small for the touch region 72 | 73 | @property (nonatomic, assign) BOOL disabled; // Disabled when there's not enough scroll content to merit showing this 74 | 75 | @property (nonatomic, strong) UIImpactFeedbackGenerator *feedbackGenerator; // Taptic feedback for iPhone 7 and above 76 | 77 | @property (nonatomic, strong) TOScrollBarGestureRecognizer *gestureRecognizer; // Our custom recognizer for handling user interactions with the scroll bar 78 | 79 | @end 80 | 81 | /************************************************************************/ 82 | 83 | @implementation TOScrollBar 84 | 85 | #pragma mark - Class Creation - 86 | 87 | - (instancetype)initWithStyle:(TOScrollBarStyle)style 88 | { 89 | if (self = [super initWithFrame:CGRectZero]) { 90 | _style = style; 91 | [self setUpInitialProperties]; 92 | } 93 | 94 | return self; 95 | } 96 | 97 | - (instancetype)initWithFrame:(CGRect)frame 98 | { 99 | if (self = [super initWithFrame:frame]) { 100 | [self setUpInitialProperties]; 101 | } 102 | 103 | return self; 104 | } 105 | 106 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 107 | { 108 | if (self = [super initWithCoder:aDecoder]) { 109 | [self setUpInitialProperties]; 110 | } 111 | 112 | return self; 113 | } 114 | 115 | #pragma mark - Set-up - 116 | 117 | - (void)setUpInitialProperties 118 | { 119 | _trackWidth = kTOScrollBarTrackWidth; 120 | _handleWidth = kTOScrollBarHandleWidth; 121 | _edgeInset = kTOScrollBarEdgeInset; 122 | _handleMinimiumHeight = kTOScrollBarHandleMinHeight; 123 | _minimumContentHeightScale = kTOScrollBarMinimumContentScale; 124 | _verticalInset = UIEdgeInsetsMake(kTOScrollBarVerticalPadding, 0.0f, kTOScrollBarVerticalPadding, 0.0f); 125 | _feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; 126 | _gestureRecognizer = [[TOScrollBarGestureRecognizer alloc] initWithTarget:self action:@selector(scrollBarGestureRecognized:)]; 127 | } 128 | 129 | - (void)setUpViews 130 | { 131 | if (self.trackView || self.handleView) { 132 | return; 133 | } 134 | 135 | self.backgroundColor = [UIColor clearColor]; 136 | 137 | // Create and add the track view 138 | self.trackView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.trackWidth]]; 139 | [self addSubview:self.trackView]; 140 | 141 | // Add the handle view 142 | self.handleView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.handleWidth]]; 143 | [self addSubview:self.handleView]; 144 | 145 | // Add the initial styling 146 | [self configureViewsForStyle:self.style]; 147 | 148 | // Add gesture recognizer 149 | [self addGestureRecognizer:self.gestureRecognizer]; 150 | } 151 | 152 | - (void)configureViewsForStyle:(TOScrollBarStyle)style 153 | { 154 | BOOL dark = (style == TOScrollBarStyleDark); 155 | 156 | CGFloat whiteColor = 0.0f; 157 | if (dark) { 158 | whiteColor = 1.0f; 159 | } 160 | self.trackView.tintColor = [UIColor colorWithWhite:whiteColor alpha:0.1f]; 161 | } 162 | 163 | - (void)dealloc 164 | { 165 | [self restoreScrollView:self.scrollView]; 166 | } 167 | 168 | - (void)configureScrollView:(UIScrollView *)scrollView 169 | { 170 | if (scrollView == nil) { 171 | return; 172 | } 173 | 174 | // Make a copy of the scroll view's state and then configure 175 | _scrollViewState.showsVerticalScrollIndicator = self.scrollView.showsVerticalScrollIndicator; 176 | scrollView.showsVerticalScrollIndicator = NO; 177 | 178 | //Key-value Observers 179 | [scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil]; 180 | [scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; 181 | } 182 | 183 | - (void)restoreScrollView:(UIScrollView *)scrollView 184 | { 185 | if (scrollView == nil) { 186 | return; 187 | } 188 | 189 | // Restore the scroll view's state 190 | scrollView.showsVerticalScrollIndicator = _scrollView.showsVerticalScrollIndicator; 191 | 192 | // Remove the observers 193 | [scrollView removeObserver:self forKeyPath:@"contentOffset"]; 194 | [scrollView removeObserver:self forKeyPath:@"contentSize"]; 195 | } 196 | 197 | - (void)willMoveToSuperview:(UIView *)newSuperview 198 | { 199 | [super willMoveToSuperview:newSuperview]; 200 | [self setUpViews]; 201 | } 202 | 203 | #pragma mark - Content Layout - 204 | 205 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 206 | change:(NSDictionary *)change context:(void *)context 207 | { 208 | [self updateStateForScrollView]; 209 | if (self.hidden) { return; } 210 | [self layoutInScrollView]; 211 | [self setNeedsLayout]; 212 | } 213 | 214 | - (CGFloat)heightOfHandleForContentSize 215 | { 216 | if (_scrollView == nil) { 217 | return _handleMinimiumHeight; 218 | } 219 | 220 | CGFloat heightRatio = self.scrollView.frame.size.height / self.scrollView.contentSize.height; 221 | CGFloat height = self.frame.size.height * heightRatio; 222 | 223 | return MAX(floorf(height), _handleMinimiumHeight); 224 | } 225 | 226 | - (void)updateStateForScrollView 227 | { 228 | CGRect frame = _scrollView.frame; 229 | CGSize contentSize = _scrollView.contentSize; 230 | self.disabled = (contentSize.height / frame.size.height) < _minimumContentHeightScale; 231 | [self setHidden:(self.disabled || self.userHidden) animated:NO]; 232 | } 233 | 234 | - (void)layoutInScrollView 235 | { 236 | CGRect scrollViewFrame = _scrollView.frame; 237 | UIEdgeInsets insets = _scrollView.contentInset; 238 | CGPoint contentOffset = _scrollView.contentOffset; 239 | CGFloat halfWidth = (kTOScrollBarWidth * 0.5f); 240 | 241 | if (@available(iOS 11.0, *)) { 242 | insets = _scrollView.adjustedContentInset; 243 | } 244 | 245 | // Contract the usable space by the scroll view's content inset (eg navigation/tool bars) 246 | scrollViewFrame.size.height -= (insets.top + insets.bottom); 247 | 248 | CGFloat largeTitleDelta = 0.0f; 249 | if (_insetForLargeTitles) { 250 | largeTitleDelta = fabs(MIN(insets.top + contentOffset.y, 0.0f)); 251 | } 252 | 253 | // Work out the final height be further contracting by the padding 254 | CGFloat height = (scrollViewFrame.size.height - (_verticalInset.top + _verticalInset.bottom)) - largeTitleDelta; 255 | 256 | // Work out how much we have to offset the track by to make sure all of the parent view 257 | // is visible at the edge of the screen (Or else we'll be unable to tap properly) 258 | CGFloat horizontalOffset = halfWidth - _edgeInset; 259 | self.horizontalOffset = (horizontalOffset > 0.0f) ? horizontalOffset : 0.0f; 260 | 261 | // Work out the frame for the scroll view 262 | CGRect frame = CGRectZero; 263 | 264 | // Size 265 | frame.size.width = kTOScrollBarWidth; 266 | frame.size.height = (_dragging ? _originalHeight : height); 267 | 268 | // Horizontal placement 269 | frame.origin.x = scrollViewFrame.size.width - (_edgeInset + halfWidth); 270 | if (@available(iOS 11.0, *)) { frame.origin.x -= _scrollView.safeAreaInsets.right; } 271 | frame.origin.x = MIN(frame.origin.x, scrollViewFrame.size.width - kTOScrollBarWidth); 272 | 273 | // Vertical placement in scroll view 274 | if (_dragging) { 275 | frame.origin.y = _originalYOffset; 276 | } 277 | else { 278 | frame.origin.y = _verticalInset.top; 279 | frame.origin.y += insets.top; 280 | frame.origin.y += largeTitleDelta; 281 | } 282 | frame.origin.y += contentOffset.y; 283 | 284 | // Set the frame 285 | self.frame = frame; 286 | 287 | // Bring the scroll bar to the front in case other subviews were subsequently added over it 288 | [self.superview bringSubviewToFront:self]; 289 | } 290 | 291 | - (void)layoutSubviews 292 | { 293 | CGRect frame = self.frame; 294 | 295 | // The frame of the track 296 | CGRect trackFrame = CGRectZero; 297 | trackFrame.size.width = _trackWidth; 298 | trackFrame.size.height = frame.size.height; 299 | trackFrame.origin.x = ceilf(((frame.size.width - _trackWidth) * 0.5f) + _horizontalOffset); 300 | self.trackView.frame = CGRectIntegral(trackFrame); 301 | 302 | // Don't handle automatic layout when dragging; we'll do that manually elsewhere 303 | if (self.dragging || self.disabled) { 304 | return; 305 | } 306 | 307 | // The frame of the handle 308 | CGRect handleFrame = CGRectZero; 309 | handleFrame.size.width = _handleWidth; 310 | handleFrame.size.height = [self heightOfHandleForContentSize]; 311 | handleFrame.origin.x = ceilf(((frame.size.width - _handleWidth) * 0.5f) + _horizontalOffset); 312 | 313 | // Work out the y offset of the handle 314 | UIEdgeInsets contentInset = _scrollView.contentInset; 315 | if (@available(iOS 11.0, *)) { 316 | contentInset = _scrollView.safeAreaInsets; 317 | } 318 | 319 | CGPoint contentOffset = _scrollView.contentOffset; 320 | CGSize contentSize = _scrollView.contentSize; 321 | CGRect scrollViewFrame = _scrollView.frame; 322 | 323 | CGFloat scrollableHeight = (contentSize.height + contentInset.top + contentInset.bottom) - scrollViewFrame.size.height; 324 | CGFloat scrollProgress = (contentOffset.y + contentInset.top) / scrollableHeight; 325 | handleFrame.origin.y = (frame.size.height - handleFrame.size.height) * scrollProgress; 326 | 327 | // If the scroll view expanded beyond its scrollable range, shrink the handle to match the rubber band effect 328 | if (contentOffset.y < -contentInset.top) { // The top 329 | handleFrame.size.height -= (-contentOffset.y - contentInset.top); 330 | handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2)); 331 | } 332 | else if (contentOffset.y + scrollViewFrame.size.height > contentSize.height + contentInset.bottom) { // The bottom 333 | CGFloat adjustedContentOffset = contentOffset.y + scrollViewFrame.size.height; 334 | CGFloat delta = adjustedContentOffset - (contentSize.height + contentInset.bottom); 335 | handleFrame.size.height -= delta; 336 | handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2)); 337 | handleFrame.origin.y = frame.size.height - handleFrame.size.height; 338 | } 339 | 340 | // Clamp to the bounds of the frame 341 | handleFrame.origin.y = MAX(handleFrame.origin.y, 0.0f); 342 | handleFrame.origin.y = MIN(handleFrame.origin.y, (frame.size.height - handleFrame.size.height)); 343 | 344 | self.handleView.frame = handleFrame; 345 | } 346 | 347 | - (void)setScrollYOffsetForHandleYOffset:(CGFloat)yOffset animated:(BOOL)animated 348 | { 349 | CGFloat heightRange = _trackView.frame.size.height - _handleView.frame.size.height; 350 | yOffset = MAX(0.0f, yOffset); 351 | yOffset = MIN(heightRange, yOffset); 352 | 353 | CGFloat positionRatio = yOffset / heightRange; 354 | 355 | CGRect frame = _scrollView.frame; 356 | UIEdgeInsets inset = _scrollView.contentInset; 357 | CGSize contentSize = _scrollView.contentSize; 358 | 359 | if (@available(iOS 11.0, *)) { 360 | inset = _scrollView.adjustedContentInset; 361 | } 362 | inset.top = _originalTopInset; 363 | 364 | CGFloat totalScrollSize = (contentSize.height + inset.top + inset.bottom) - frame.size.height; 365 | CGFloat scrollOffset = totalScrollSize * positionRatio; 366 | scrollOffset -= inset.top; 367 | 368 | CGPoint contentOffset = _scrollView.contentOffset; 369 | contentOffset.y = scrollOffset; 370 | 371 | // Animate to help coax the large title navigation bar to behave 372 | if (@available(iOS 11.0, *)) { 373 | [UIView animateWithDuration:animated ? 0.1f : 0.00001f animations:^{ 374 | [self.scrollView setContentOffset:contentOffset animated:NO]; 375 | }]; 376 | } 377 | else { 378 | [self.scrollView setContentOffset:contentOffset animated:NO]; 379 | } 380 | } 381 | 382 | #pragma mark - Scroll View Integration - 383 | 384 | - (void)addToScrollView:(UIScrollView *)scrollView 385 | { 386 | if (scrollView == self.scrollView) { 387 | return; 388 | } 389 | 390 | // Restore the previous scroll view 391 | [self restoreScrollView:self.scrollView]; 392 | 393 | // Assign the new scroll view 394 | self.scrollView = scrollView; 395 | 396 | // Apply the observers/settings to the new scroll view 397 | [self configureScrollView:scrollView]; 398 | 399 | // Add the scroll bar to the scroll view's content view 400 | [self.scrollView addSubview:self]; 401 | 402 | // Add ourselves as a property of the scroll view 403 | [self.scrollView setTo_scrollBar:self]; 404 | 405 | // Begin layout 406 | [self layoutInScrollView]; 407 | } 408 | 409 | - (void)removeFromScrollView 410 | { 411 | [self restoreScrollView:self.scrollView]; 412 | [self removeFromSuperview]; 413 | [self.scrollView setTo_scrollBar:nil]; 414 | self.scrollView = nil; 415 | } 416 | 417 | - (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset 418 | { 419 | inset.right = _edgeInset * 2.0f; 420 | return inset; 421 | } 422 | 423 | - (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset 424 | { 425 | layoutMargins.right = (_edgeInset * 2.0f) + 15.0f; // Magic system number is 20, but we can't infer that from here on time 426 | layoutMargins.right += offset; 427 | return layoutMargins; 428 | } 429 | 430 | #pragma mark - User Interaction - 431 | - (void)scrollBarGestureRecognized:(TOScrollBarGestureRecognizer *)recognizer 432 | { 433 | CGPoint touchPoint = [recognizer locationInView:self]; 434 | 435 | switch (recognizer.state) { 436 | case UIGestureRecognizerStateBegan: 437 | [self gestureBeganAtPoint:touchPoint]; 438 | break; 439 | case UIGestureRecognizerStateChanged: 440 | [self gestureMovedToPoint:touchPoint]; 441 | break; 442 | case UIGestureRecognizerStateEnded: 443 | case UIGestureRecognizerStateCancelled: 444 | [self gestureEnded]; 445 | break; 446 | default: 447 | break; 448 | } 449 | } 450 | 451 | - (void)gestureBeganAtPoint:(CGPoint)touchPoint 452 | { 453 | if (self.disabled) { 454 | return; 455 | } 456 | 457 | // Warm-up the feedback generator 458 | [_feedbackGenerator prepare]; 459 | 460 | self.scrollView.scrollEnabled = NO; 461 | self.dragging = YES; 462 | 463 | // Capture the original position 464 | self.originalHeight = self.frame.size.height; 465 | self.originalYOffset = self.frame.origin.y - self.scrollView.contentOffset.y; 466 | 467 | if (@available(iOS 11.0, *)) { 468 | self.originalTopInset = _scrollView.adjustedContentInset.top; 469 | } else { 470 | self.originalTopInset = _scrollView.contentInset.top; 471 | } 472 | 473 | // Check if the user tapped inside the handle 474 | CGRect handleFrame = self.handleView.frame; 475 | if (touchPoint.y > (handleFrame.origin.y - 20) && 476 | touchPoint.y < handleFrame.origin.y + (handleFrame.size.height + 20)) 477 | { 478 | self.yOffset = (touchPoint.y - handleFrame.origin.y); 479 | return; 480 | } 481 | 482 | if (!self.handleExclusiveInteractionEnabled) { 483 | // User tapped somewhere else, animate the handle to that point 484 | CGFloat halfHeight = (handleFrame.size.height * 0.5f); 485 | 486 | CGFloat destinationYOffset = touchPoint.y - halfHeight; 487 | destinationYOffset = MAX(0.0f, destinationYOffset); 488 | destinationYOffset = MIN(self.frame.size.height - halfHeight, destinationYOffset); 489 | 490 | self.yOffset = (touchPoint.y - destinationYOffset); 491 | handleFrame.origin.y = destinationYOffset; 492 | 493 | [UIView animateWithDuration:0.2f 494 | delay:0.0f 495 | usingSpringWithDamping:1.0f 496 | initialSpringVelocity:0.1f options:UIViewAnimationOptionBeginFromCurrentState 497 | animations:^{ 498 | self.handleView.frame = handleFrame; 499 | } completion:nil]; 500 | 501 | [self setScrollYOffsetForHandleYOffset:floorf(destinationYOffset) animated:NO]; 502 | } 503 | } 504 | 505 | - (void)gestureMovedToPoint:(CGPoint)touchPoint 506 | { 507 | if (self.disabled) { 508 | return; 509 | } 510 | 511 | CGFloat delta = 0.0f; 512 | CGRect handleFrame = _handleView.frame; 513 | CGRect trackFrame = _trackView.frame; 514 | CGFloat minimumY = 0.0f; 515 | CGFloat maximumY = trackFrame.size.height - handleFrame.size.height; 516 | 517 | if (self.handleExclusiveInteractionEnabled) { 518 | if (touchPoint.y < (handleFrame.origin.y - 20) || 519 | touchPoint.y > handleFrame.origin.y + (handleFrame.size.height + 20)) 520 | { 521 | // This touch is not on the handle; eject. 522 | return; 523 | } 524 | } 525 | 526 | // Apply the updated Y value plus the previous offset 527 | delta = handleFrame.origin.y; 528 | handleFrame.origin.y = touchPoint.y - _yOffset; 529 | 530 | //Clamp the handle, and adjust the y offset to counter going outside the bounds 531 | if (handleFrame.origin.y < minimumY) { 532 | _yOffset += handleFrame.origin.y; 533 | _yOffset = MAX(minimumY, _yOffset); 534 | handleFrame.origin.y = minimumY; 535 | } 536 | else if (handleFrame.origin.y > maximumY) { 537 | CGFloat handleOverflow = CGRectGetMaxY(handleFrame) - trackFrame.size.height; 538 | _yOffset += handleOverflow; 539 | _yOffset = MIN(self.yOffset, handleFrame.size.height); 540 | handleFrame.origin.y = MIN(handleFrame.origin.y, maximumY); 541 | } 542 | 543 | _handleView.frame = handleFrame; 544 | 545 | delta -= handleFrame.origin.y; 546 | delta = fabs(delta); 547 | 548 | // If the delta is not 0.0, but we're at either extreme, 549 | // this is first frame we've since reaching that point. 550 | // Play a taptic feedback impact 551 | if (delta > FLT_EPSILON && (CGRectGetMinY(handleFrame) < FLT_EPSILON || CGRectGetMinY(handleFrame) >= maximumY - FLT_EPSILON)) { 552 | [_feedbackGenerator impactOccurred]; 553 | } 554 | 555 | // If the user is doing really granualar swipes, add a subtle amount 556 | // of vertical animation so the scroll view isn't jumping on each frame 557 | [self setScrollYOffsetForHandleYOffset:floorf(handleFrame.origin.y) animated:NO]; //(delta < 0.51f) 558 | } 559 | 560 | - (void)gestureEnded 561 | { 562 | self.scrollView.scrollEnabled = YES; 563 | self.dragging = NO; 564 | 565 | [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.5f options:0 animations:^{ 566 | [self layoutInScrollView]; 567 | [self layoutIfNeeded]; 568 | } completion:nil]; 569 | } 570 | 571 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 572 | { 573 | if (!self.handleExclusiveInteractionEnabled) { 574 | return [super pointInside:point withEvent:event]; 575 | } 576 | else { 577 | CGFloat handleMinY = CGRectGetMinY(self.handleView.frame); 578 | CGFloat handleMaxY = CGRectGetMaxY(self.handleView.frame); 579 | return (0 <= point.x) && (handleMinY <= point.y) && (point.y <= handleMaxY); 580 | } 581 | } 582 | 583 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 584 | { 585 | UIView *result = [super hitTest:point withEvent:event]; 586 | 587 | if (self.disabled || self.dragging) { 588 | return result; 589 | } 590 | 591 | // If the user contacts the screen in a swiping motion, 592 | // the scroll view will automatically highjack the touch 593 | // event unless we explicitly override it here. 594 | 595 | self.scrollView.scrollEnabled = (result != self); 596 | return result; 597 | } 598 | 599 | #pragma mark - Accessors - 600 | - (void)setStyle:(TOScrollBarStyle)style 601 | { 602 | _style = style; 603 | [self configureViewsForStyle:style]; 604 | } 605 | 606 | - (UIColor *)trackTintColor { return self.trackView.tintColor; } 607 | 608 | - (void)setTrackTintColor:(UIColor *)trackTintColor 609 | { 610 | self.trackView.tintColor = trackTintColor; 611 | } 612 | 613 | - (UIColor *)handleTintColor { return self.handleView.tintColor; } 614 | 615 | - (void)setHandleTintColor:(UIColor *)handleTintColor 616 | { 617 | self.handleView.tintColor = handleTintColor; 618 | } 619 | 620 | - (void)setHidden:(BOOL)hidden 621 | { 622 | self.userHidden = hidden; 623 | [self setHidden:hidden animated:NO]; 624 | } 625 | 626 | - (void)setHidden:(BOOL)hidden animated:(BOOL)animated 627 | { 628 | // Override. It cannot be shown if it's disabled 629 | if (_disabled) { 630 | super.hidden = YES; 631 | return; 632 | } 633 | 634 | // Simply show or hide it if we're not animating 635 | if (animated == NO) { 636 | super.hidden = hidden; 637 | return; 638 | } 639 | 640 | // Show it if we're going to animate it 641 | if (self.hidden && hidden == NO) { 642 | super.hidden = NO; 643 | [self layoutInScrollView]; 644 | [self setNeedsLayout]; 645 | } 646 | 647 | CGRect fromFrame = self.frame; 648 | CGRect toFrame = self.frame; 649 | 650 | CGFloat widestElement = MAX(_trackWidth, _handleWidth); 651 | CGFloat hiddenOffset = fromFrame.origin.x + _edgeInset + (widestElement * 2.0f); 652 | if (hidden == NO) { 653 | fromFrame.origin.x = hiddenOffset; 654 | } 655 | else { 656 | toFrame.origin.x = hiddenOffset; 657 | } 658 | 659 | self.frame = fromFrame; 660 | [UIView animateWithDuration:0.3f 661 | delay:0.0f 662 | usingSpringWithDamping:1.0f 663 | initialSpringVelocity:0.1f 664 | options:UIViewAnimationOptionBeginFromCurrentState 665 | animations:^{ 666 | self.frame = toFrame; 667 | } completion:^(BOOL finished) { 668 | super.hidden = hidden; 669 | }]; 670 | 671 | } 672 | 673 | #pragma mark - Image Generation - 674 | + (UIImage *)verticalCapsuleImageWithWidth:(CGFloat)width 675 | { 676 | UIImage *image = nil; 677 | CGFloat radius = width * 0.5f; 678 | CGRect frame = (CGRect){0, 0, width+1, width+1}; 679 | 680 | UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); 681 | [[UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius] fill]; 682 | image = UIGraphicsGetImageFromCurrentImageContext(); 683 | UIGraphicsEndImageContext(); 684 | 685 | image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius) resizingMode:UIImageResizingModeStretch]; 686 | image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 687 | 688 | return image; 689 | } 690 | 691 | @end 692 | -------------------------------------------------------------------------------- /TOScrollBar/TOScrollBarGestureRecognizer.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOScrollBarGestureRecognizer.h 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @interface TOScrollBarGestureRecognizer : UIGestureRecognizer 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /TOScrollBar/TOScrollBarGestureRecognizer.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOScrollBarGestureRecognizer.h 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOScrollBarGestureRecognizer.h" 24 | #import 25 | #import "TOScrollBar.h" 26 | 27 | @interface TOScrollBarGestureRecognizer () 28 | 29 | @property (nonatomic, readonly) TOScrollBar *scrollBar; // The scroll bar this recognizer is attached to 30 | 31 | @end 32 | 33 | @implementation TOScrollBarGestureRecognizer 34 | 35 | #pragma mark - Gesture Recognizer Filtering - 36 | - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer 37 | { 38 | // Ensure that the pan gesture recognizer from the scroll view doesn't override the scroll bar 39 | UIView *view = preventedGestureRecognizer.view; 40 | if ([view isEqual:self.scrollBar.scrollView]) { 41 | return YES; 42 | } 43 | 44 | return NO; 45 | } 46 | 47 | #pragma mark - Touch Interaction - 48 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 49 | { 50 | self.state = UIGestureRecognizerStateBegan; 51 | } 52 | 53 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 54 | { 55 | self.state = UIGestureRecognizerStateChanged; 56 | } 57 | 58 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 59 | { 60 | self.state = UIGestureRecognizerStateEnded; 61 | } 62 | 63 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 64 | { 65 | self.state = UIGestureRecognizerStateCancelled; 66 | } 67 | 68 | #pragma mark - Accessors - 69 | - (TOScrollBar *)scrollBar 70 | { 71 | if ([self.view isKindOfClass:[TOScrollBar class]] == NO) { return nil; } 72 | return (TOScrollBar *)self.view; 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /TOScrollBar/UIScrollView+TOScrollBar.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+TOScrollBar.h 3 | // 4 | // Copyright 2016 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @class TOScrollBar; 26 | 27 | @interface UIScrollView (TOScrollBar) 28 | 29 | /** The scroll bar view currently added to this scroll view */ 30 | @property (nullable, nonatomic, readonly) TOScrollBar *to_scrollBar; 31 | 32 | /** 33 | Adds a new scroll bar instance to this scroll bar 34 | @param scrollBar The scroll bar in which to add 35 | */ 36 | - (void)to_addScrollBar:(nullable TOScrollBar *)scrollBar; 37 | 38 | /** 39 | Removes the current scroll bar (if any) from the scroll bar 40 | */ 41 | - (void)to_removeScrollbar; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /TOScrollBar/UIScrollView+TOScrollBar.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+TOScrollBar.m 3 | // 4 | // Copyright 2016 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "UIScrollView+TOScrollBar.h" 25 | #import "TOScrollBar.h" 26 | 27 | static void * TOScrollBarPropertyKey = &TOScrollBarPropertyKey; 28 | 29 | @implementation UIScrollView (TOScrollBar) 30 | 31 | - (TOScrollBar *)to_scrollBar 32 | { 33 | return objc_getAssociatedObject(self, TOScrollBarPropertyKey); 34 | } 35 | 36 | - (void)setTo_scrollBar:(TOScrollBar *)scrollBar 37 | { 38 | objc_setAssociatedObject(self, TOScrollBarPropertyKey, scrollBar, OBJC_ASSOCIATION_RETAIN); 39 | } 40 | 41 | - (void)to_addScrollBar:(TOScrollBar *)scrollBar 42 | { 43 | [scrollBar addToScrollView:self]; 44 | } 45 | 46 | - (void)to_removeScrollbar 47 | { 48 | [self.to_scrollBar removeFromScrollView]; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /TOScrollBarExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 222F87BC1DCE453B0068DE2F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 222F87BB1DCE453B0068DE2F /* main.m */; }; 11 | 222F87BF1DCE453B0068DE2F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 222F87BE1DCE453B0068DE2F /* AppDelegate.m */; }; 12 | 222F87C21DCE453B0068DE2F /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 222F87C11DCE453B0068DE2F /* ViewController.m */; }; 13 | 222F87C51DCE453B0068DE2F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 222F87C31DCE453B0068DE2F /* Main.storyboard */; }; 14 | 222F87C71DCE453B0068DE2F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 222F87C61DCE453B0068DE2F /* Assets.xcassets */; }; 15 | 222F87CA1DCE453B0068DE2F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 222F87C81DCE453B0068DE2F /* LaunchScreen.storyboard */; }; 16 | 223B9BF71DCE496F008601D2 /* TOScrollBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 223B9BF61DCE496F008601D2 /* TOScrollBar.m */; }; 17 | 225FD7941DDAF85C008CF7E4 /* TOScrollBarExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 225FD7931DDAF85C008CF7E4 /* TOScrollBarExampleTests.m */; }; 18 | 22B460601FDBE5EF00386CED /* TOScrollBarGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 22B4605F1FDBE5EF00386CED /* TOScrollBarGestureRecognizer.m */; }; 19 | 22CCCCD11DD064B90092A89D /* UIScrollView+TOScrollBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 22CCCCD01DD064B90092A89D /* UIScrollView+TOScrollBar.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 225FD7961DDAF85C008CF7E4 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 222F87AF1DCE453B0068DE2F /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 222F87B61DCE453B0068DE2F; 28 | remoteInfo = TOScrollBarExample; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 222F87B71DCE453B0068DE2F /* TOScrollBarExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TOScrollBarExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 222F87BB1DCE453B0068DE2F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 35 | 222F87BD1DCE453B0068DE2F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 36 | 222F87BE1DCE453B0068DE2F /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 37 | 222F87C01DCE453B0068DE2F /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 38 | 222F87C11DCE453B0068DE2F /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 39 | 222F87C41DCE453B0068DE2F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 40 | 222F87C61DCE453B0068DE2F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 222F87C91DCE453B0068DE2F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 222F87CB1DCE453B0068DE2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 223B9BF51DCE496F008601D2 /* TOScrollBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TOScrollBar.h; sourceTree = ""; }; 44 | 223B9BF61DCE496F008601D2 /* TOScrollBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOScrollBar.m; sourceTree = ""; }; 45 | 225FD7911DDAF85C008CF7E4 /* TOScrollBarExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TOScrollBarExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 225FD7931DDAF85C008CF7E4 /* TOScrollBarExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOScrollBarExampleTests.m; sourceTree = ""; }; 47 | 225FD7951DDAF85C008CF7E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 22B4605E1FDBE5EF00386CED /* TOScrollBarGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOScrollBarGestureRecognizer.h; sourceTree = ""; }; 49 | 22B4605F1FDBE5EF00386CED /* TOScrollBarGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOScrollBarGestureRecognizer.m; sourceTree = ""; }; 50 | 22CCCCCF1DD064B90092A89D /* UIScrollView+TOScrollBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+TOScrollBar.h"; sourceTree = ""; }; 51 | 22CCCCD01DD064B90092A89D /* UIScrollView+TOScrollBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+TOScrollBar.m"; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 222F87B41DCE453B0068DE2F /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | 225FD78E1DDAF85C008CF7E4 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | 222F87AE1DCE453B0068DE2F = { 73 | isa = PBXGroup; 74 | children = ( 75 | 223B9BF41DCE487A008601D2 /* TOScrollBar */, 76 | 222F87B91DCE453B0068DE2F /* TOScrollBarExample */, 77 | 225FD7921DDAF85C008CF7E4 /* TOScrollBarExampleTests */, 78 | 222F87B81DCE453B0068DE2F /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 222F87B81DCE453B0068DE2F /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 222F87B71DCE453B0068DE2F /* TOScrollBarExample.app */, 86 | 225FD7911DDAF85C008CF7E4 /* TOScrollBarExampleTests.xctest */, 87 | ); 88 | name = Products; 89 | sourceTree = ""; 90 | }; 91 | 222F87B91DCE453B0068DE2F /* TOScrollBarExample */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 222F87BD1DCE453B0068DE2F /* AppDelegate.h */, 95 | 222F87BE1DCE453B0068DE2F /* AppDelegate.m */, 96 | 222F87C01DCE453B0068DE2F /* ViewController.h */, 97 | 222F87C11DCE453B0068DE2F /* ViewController.m */, 98 | 222F87C31DCE453B0068DE2F /* Main.storyboard */, 99 | 222F87C61DCE453B0068DE2F /* Assets.xcassets */, 100 | 222F87C81DCE453B0068DE2F /* LaunchScreen.storyboard */, 101 | 222F87CB1DCE453B0068DE2F /* Info.plist */, 102 | 222F87BA1DCE453B0068DE2F /* Supporting Files */, 103 | ); 104 | path = TOScrollBarExample; 105 | sourceTree = ""; 106 | }; 107 | 222F87BA1DCE453B0068DE2F /* Supporting Files */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 222F87BB1DCE453B0068DE2F /* main.m */, 111 | ); 112 | name = "Supporting Files"; 113 | sourceTree = ""; 114 | }; 115 | 223B9BF41DCE487A008601D2 /* TOScrollBar */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 223B9BF51DCE496F008601D2 /* TOScrollBar.h */, 119 | 223B9BF61DCE496F008601D2 /* TOScrollBar.m */, 120 | 22B4605E1FDBE5EF00386CED /* TOScrollBarGestureRecognizer.h */, 121 | 22B4605F1FDBE5EF00386CED /* TOScrollBarGestureRecognizer.m */, 122 | 22CCCCCF1DD064B90092A89D /* UIScrollView+TOScrollBar.h */, 123 | 22CCCCD01DD064B90092A89D /* UIScrollView+TOScrollBar.m */, 124 | ); 125 | path = TOScrollBar; 126 | sourceTree = ""; 127 | }; 128 | 225FD7921DDAF85C008CF7E4 /* TOScrollBarExampleTests */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 225FD7931DDAF85C008CF7E4 /* TOScrollBarExampleTests.m */, 132 | 225FD7951DDAF85C008CF7E4 /* Info.plist */, 133 | ); 134 | path = TOScrollBarExampleTests; 135 | sourceTree = ""; 136 | }; 137 | /* End PBXGroup section */ 138 | 139 | /* Begin PBXNativeTarget section */ 140 | 222F87B61DCE453B0068DE2F /* TOScrollBarExample */ = { 141 | isa = PBXNativeTarget; 142 | buildConfigurationList = 222F87CE1DCE453B0068DE2F /* Build configuration list for PBXNativeTarget "TOScrollBarExample" */; 143 | buildPhases = ( 144 | 222F87B31DCE453B0068DE2F /* Sources */, 145 | 222F87B41DCE453B0068DE2F /* Frameworks */, 146 | 222F87B51DCE453B0068DE2F /* Resources */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = TOScrollBarExample; 153 | productName = TOScrollBarExample; 154 | productReference = 222F87B71DCE453B0068DE2F /* TOScrollBarExample.app */; 155 | productType = "com.apple.product-type.application"; 156 | }; 157 | 225FD7901DDAF85C008CF7E4 /* TOScrollBarExampleTests */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = 225FD79A1DDAF85C008CF7E4 /* Build configuration list for PBXNativeTarget "TOScrollBarExampleTests" */; 160 | buildPhases = ( 161 | 225FD78D1DDAF85C008CF7E4 /* Sources */, 162 | 225FD78E1DDAF85C008CF7E4 /* Frameworks */, 163 | 225FD78F1DDAF85C008CF7E4 /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | 225FD7971DDAF85C008CF7E4 /* PBXTargetDependency */, 169 | ); 170 | name = TOScrollBarExampleTests; 171 | productName = TOScrollBarExampleTests; 172 | productReference = 225FD7911DDAF85C008CF7E4 /* TOScrollBarExampleTests.xctest */; 173 | productType = "com.apple.product-type.bundle.unit-test"; 174 | }; 175 | /* End PBXNativeTarget section */ 176 | 177 | /* Begin PBXProject section */ 178 | 222F87AF1DCE453B0068DE2F /* Project object */ = { 179 | isa = PBXProject; 180 | attributes = { 181 | LastUpgradeCheck = 0920; 182 | ORGANIZATIONNAME = "Tim Oliver"; 183 | TargetAttributes = { 184 | 222F87B61DCE453B0068DE2F = { 185 | CreatedOnToolsVersion = 8.1; 186 | DevelopmentTeam = 6LF3GMKZAB; 187 | ProvisioningStyle = Automatic; 188 | }; 189 | 225FD7901DDAF85C008CF7E4 = { 190 | CreatedOnToolsVersion = 8.2; 191 | ProvisioningStyle = Automatic; 192 | TestTargetID = 222F87B61DCE453B0068DE2F; 193 | }; 194 | }; 195 | }; 196 | buildConfigurationList = 222F87B21DCE453B0068DE2F /* Build configuration list for PBXProject "TOScrollBarExample" */; 197 | compatibilityVersion = "Xcode 3.2"; 198 | developmentRegion = English; 199 | hasScannedForEncodings = 0; 200 | knownRegions = ( 201 | en, 202 | Base, 203 | ); 204 | mainGroup = 222F87AE1DCE453B0068DE2F; 205 | productRefGroup = 222F87B81DCE453B0068DE2F /* Products */; 206 | projectDirPath = ""; 207 | projectRoot = ""; 208 | targets = ( 209 | 222F87B61DCE453B0068DE2F /* TOScrollBarExample */, 210 | 225FD7901DDAF85C008CF7E4 /* TOScrollBarExampleTests */, 211 | ); 212 | }; 213 | /* End PBXProject section */ 214 | 215 | /* Begin PBXResourcesBuildPhase section */ 216 | 222F87B51DCE453B0068DE2F /* Resources */ = { 217 | isa = PBXResourcesBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 222F87CA1DCE453B0068DE2F /* LaunchScreen.storyboard in Resources */, 221 | 222F87C71DCE453B0068DE2F /* Assets.xcassets in Resources */, 222 | 222F87C51DCE453B0068DE2F /* Main.storyboard in Resources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | 225FD78F1DDAF85C008CF7E4 /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXResourcesBuildPhase section */ 234 | 235 | /* Begin PBXSourcesBuildPhase section */ 236 | 222F87B31DCE453B0068DE2F /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 222F87C21DCE453B0068DE2F /* ViewController.m in Sources */, 241 | 22B460601FDBE5EF00386CED /* TOScrollBarGestureRecognizer.m in Sources */, 242 | 222F87BF1DCE453B0068DE2F /* AppDelegate.m in Sources */, 243 | 222F87BC1DCE453B0068DE2F /* main.m in Sources */, 244 | 223B9BF71DCE496F008601D2 /* TOScrollBar.m in Sources */, 245 | 22CCCCD11DD064B90092A89D /* UIScrollView+TOScrollBar.m in Sources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | 225FD78D1DDAF85C008CF7E4 /* Sources */ = { 250 | isa = PBXSourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 225FD7941DDAF85C008CF7E4 /* TOScrollBarExampleTests.m in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXSourcesBuildPhase section */ 258 | 259 | /* Begin PBXTargetDependency section */ 260 | 225FD7971DDAF85C008CF7E4 /* PBXTargetDependency */ = { 261 | isa = PBXTargetDependency; 262 | target = 222F87B61DCE453B0068DE2F /* TOScrollBarExample */; 263 | targetProxy = 225FD7961DDAF85C008CF7E4 /* PBXContainerItemProxy */; 264 | }; 265 | /* End PBXTargetDependency section */ 266 | 267 | /* Begin PBXVariantGroup section */ 268 | 222F87C31DCE453B0068DE2F /* Main.storyboard */ = { 269 | isa = PBXVariantGroup; 270 | children = ( 271 | 222F87C41DCE453B0068DE2F /* Base */, 272 | ); 273 | name = Main.storyboard; 274 | sourceTree = ""; 275 | }; 276 | 222F87C81DCE453B0068DE2F /* LaunchScreen.storyboard */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | 222F87C91DCE453B0068DE2F /* Base */, 280 | ); 281 | name = LaunchScreen.storyboard; 282 | sourceTree = ""; 283 | }; 284 | /* End PBXVariantGroup section */ 285 | 286 | /* Begin XCBuildConfiguration section */ 287 | 222F87CC1DCE453B0068DE2F /* Debug */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 293 | CLANG_CXX_LIBRARY = "libc++"; 294 | CLANG_ENABLE_MODULES = YES; 295 | CLANG_ENABLE_OBJC_ARC = YES; 296 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 297 | CLANG_WARN_BOOL_CONVERSION = YES; 298 | CLANG_WARN_COMMA = YES; 299 | CLANG_WARN_CONSTANT_CONVERSION = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 302 | CLANG_WARN_EMPTY_BODY = YES; 303 | CLANG_WARN_ENUM_CONVERSION = YES; 304 | CLANG_WARN_INFINITE_RECURSION = YES; 305 | CLANG_WARN_INT_CONVERSION = YES; 306 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 309 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 310 | CLANG_WARN_STRICT_PROTOTYPES = YES; 311 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 312 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 313 | CLANG_WARN_UNREACHABLE_CODE = YES; 314 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 315 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 316 | COPY_PHASE_STRIP = NO; 317 | DEBUG_INFORMATION_FORMAT = dwarf; 318 | ENABLE_STRICT_OBJC_MSGSEND = YES; 319 | ENABLE_TESTABILITY = YES; 320 | GCC_C_LANGUAGE_STANDARD = gnu99; 321 | GCC_DYNAMIC_NO_PIC = NO; 322 | GCC_NO_COMMON_BLOCKS = YES; 323 | GCC_OPTIMIZATION_LEVEL = 0; 324 | GCC_PREPROCESSOR_DEFINITIONS = ( 325 | "DEBUG=1", 326 | "$(inherited)", 327 | ); 328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 329 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 330 | GCC_WARN_UNDECLARED_SELECTOR = YES; 331 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 332 | GCC_WARN_UNUSED_FUNCTION = YES; 333 | GCC_WARN_UNUSED_VARIABLE = YES; 334 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 335 | MTL_ENABLE_DEBUG_INFO = YES; 336 | ONLY_ACTIVE_ARCH = YES; 337 | SDKROOT = iphoneos; 338 | TARGETED_DEVICE_FAMILY = "1,2"; 339 | }; 340 | name = Debug; 341 | }; 342 | 222F87CD1DCE453B0068DE2F /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ALWAYS_SEARCH_USER_PATHS = NO; 346 | CLANG_ANALYZER_NONNULL = YES; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 364 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 365 | CLANG_WARN_STRICT_PROTOTYPES = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 367 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 368 | CLANG_WARN_UNREACHABLE_CODE = YES; 369 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 370 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 371 | COPY_PHASE_STRIP = NO; 372 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu99; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 379 | GCC_WARN_UNDECLARED_SELECTOR = YES; 380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 381 | GCC_WARN_UNUSED_FUNCTION = YES; 382 | GCC_WARN_UNUSED_VARIABLE = YES; 383 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 384 | MTL_ENABLE_DEBUG_INFO = NO; 385 | SDKROOT = iphoneos; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | VALIDATE_PRODUCT = YES; 388 | }; 389 | name = Release; 390 | }; 391 | 222F87CF1DCE453B0068DE2F /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 396 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 397 | INFOPLIST_FILE = TOScrollBarExample/Info.plist; 398 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 399 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 400 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOScrollBarExample; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | }; 403 | name = Debug; 404 | }; 405 | 222F87D01DCE453B0068DE2F /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 410 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 411 | INFOPLIST_FILE = TOScrollBarExample/Info.plist; 412 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 413 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 414 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOScrollBarExample; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | }; 417 | name = Release; 418 | }; 419 | 225FD7981DDAF85C008CF7E4 /* Debug */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | BUNDLE_LOADER = "$(TEST_HOST)"; 423 | DEVELOPMENT_TEAM = ""; 424 | INFOPLIST_FILE = TOScrollBarExampleTests/Info.plist; 425 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 426 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 427 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOScrollBarExampleTests; 428 | PRODUCT_NAME = "$(TARGET_NAME)"; 429 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TOScrollBarExample.app/TOScrollBarExample"; 430 | }; 431 | name = Debug; 432 | }; 433 | 225FD7991DDAF85C008CF7E4 /* Release */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | BUNDLE_LOADER = "$(TEST_HOST)"; 437 | DEVELOPMENT_TEAM = ""; 438 | INFOPLIST_FILE = TOScrollBarExampleTests/Info.plist; 439 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 441 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOScrollBarExampleTests; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TOScrollBarExample.app/TOScrollBarExample"; 444 | }; 445 | name = Release; 446 | }; 447 | /* End XCBuildConfiguration section */ 448 | 449 | /* Begin XCConfigurationList section */ 450 | 222F87B21DCE453B0068DE2F /* Build configuration list for PBXProject "TOScrollBarExample" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | 222F87CC1DCE453B0068DE2F /* Debug */, 454 | 222F87CD1DCE453B0068DE2F /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Release; 458 | }; 459 | 222F87CE1DCE453B0068DE2F /* Build configuration list for PBXNativeTarget "TOScrollBarExample" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | 222F87CF1DCE453B0068DE2F /* Debug */, 463 | 222F87D01DCE453B0068DE2F /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 225FD79A1DDAF85C008CF7E4 /* Build configuration list for PBXNativeTarget "TOScrollBarExampleTests" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 225FD7981DDAF85C008CF7E4 /* Debug */, 472 | 225FD7991DDAF85C008CF7E4 /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | /* End XCConfigurationList section */ 478 | }; 479 | rootObject = 222F87AF1DCE453B0068DE2F /* Project object */; 480 | } 481 | -------------------------------------------------------------------------------- /TOScrollBarExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TOScrollBarExample.xcodeproj/xcshareddata/xcschemes/TOScrollBarExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /TOScrollBarExample.xcodeproj/xcshareddata/xcschemes/TOScrollBarExampleTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 45 | 46 | 47 | 48 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /TOScrollBarExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TOScrollBarExample 4 | // 5 | // Created by Tim Oliver on 5/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /TOScrollBarExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TOScrollBarExample 4 | // 5 | // Created by Tim Oliver on 5/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /TOScrollBarExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TOScrollBarExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TOScrollBarExample/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "8.0", 8 | "subtype" : "736h", 9 | "scale" : "3x" 10 | }, 11 | { 12 | "orientation" : "landscape", 13 | "idiom" : "iphone", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "8.0", 16 | "subtype" : "736h", 17 | "scale" : "3x" 18 | }, 19 | { 20 | "orientation" : "portrait", 21 | "idiom" : "iphone", 22 | "extent" : "full-screen", 23 | "minimum-system-version" : "8.0", 24 | "subtype" : "667h", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "orientation" : "portrait", 29 | "idiom" : "iphone", 30 | "extent" : "full-screen", 31 | "minimum-system-version" : "7.0", 32 | "scale" : "2x" 33 | }, 34 | { 35 | "orientation" : "portrait", 36 | "idiom" : "iphone", 37 | "extent" : "full-screen", 38 | "minimum-system-version" : "7.0", 39 | "subtype" : "retina4", 40 | "scale" : "2x" 41 | }, 42 | { 43 | "orientation" : "portrait", 44 | "idiom" : "ipad", 45 | "extent" : "full-screen", 46 | "minimum-system-version" : "7.0", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "orientation" : "landscape", 51 | "idiom" : "ipad", 52 | "extent" : "full-screen", 53 | "minimum-system-version" : "7.0", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "orientation" : "portrait", 58 | "idiom" : "ipad", 59 | "extent" : "full-screen", 60 | "minimum-system-version" : "7.0", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "orientation" : "landscape", 65 | "idiom" : "ipad", 66 | "extent" : "full-screen", 67 | "minimum-system-version" : "7.0", 68 | "scale" : "2x" 69 | }, 70 | { 71 | "orientation" : "portrait", 72 | "idiom" : "iphone", 73 | "extent" : "full-screen", 74 | "scale" : "1x" 75 | }, 76 | { 77 | "orientation" : "portrait", 78 | "idiom" : "iphone", 79 | "extent" : "full-screen", 80 | "scale" : "2x" 81 | }, 82 | { 83 | "orientation" : "portrait", 84 | "idiom" : "iphone", 85 | "extent" : "full-screen", 86 | "subtype" : "retina4", 87 | "scale" : "2x" 88 | }, 89 | { 90 | "orientation" : "portrait", 91 | "idiom" : "ipad", 92 | "extent" : "to-status-bar", 93 | "scale" : "1x" 94 | }, 95 | { 96 | "orientation" : "portrait", 97 | "idiom" : "ipad", 98 | "extent" : "full-screen", 99 | "scale" : "1x" 100 | }, 101 | { 102 | "orientation" : "landscape", 103 | "idiom" : "ipad", 104 | "extent" : "to-status-bar", 105 | "scale" : "1x" 106 | }, 107 | { 108 | "orientation" : "landscape", 109 | "idiom" : "ipad", 110 | "extent" : "full-screen", 111 | "scale" : "1x" 112 | }, 113 | { 114 | "orientation" : "portrait", 115 | "idiom" : "ipad", 116 | "extent" : "to-status-bar", 117 | "scale" : "2x" 118 | }, 119 | { 120 | "orientation" : "portrait", 121 | "idiom" : "ipad", 122 | "extent" : "full-screen", 123 | "scale" : "2x" 124 | }, 125 | { 126 | "orientation" : "landscape", 127 | "idiom" : "ipad", 128 | "extent" : "to-status-bar", 129 | "scale" : "2x" 130 | }, 131 | { 132 | "orientation" : "landscape", 133 | "idiom" : "ipad", 134 | "extent" : "full-screen", 135 | "scale" : "2x" 136 | } 137 | ], 138 | "info" : { 139 | "version" : 1, 140 | "author" : "xcode" 141 | } 142 | } -------------------------------------------------------------------------------- /TOScrollBarExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TOScrollBarExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | -------------------------------------------------------------------------------- /TOScrollBarExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /TOScrollBarExample/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // TOScrollBarExample 4 | // 5 | // Created by Tim Oliver on 5/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UITableViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TOScrollBarExample/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // TOScrollBarExample 4 | // 5 | // Created by Tim Oliver on 5/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "TOScrollBar.h" 11 | 12 | @interface ViewController () 13 | 14 | @property (nonatomic, assign) BOOL darkMode; 15 | @property (nonatomic, assign) BOOL hidden; 16 | 17 | - (void)darkModeButtonTapped:(id)sender; 18 | - (void)hideButtonTapped:(id)sender; 19 | 20 | - (void)configureStyleForDarkMode:(BOOL)darkMode; 21 | 22 | @end 23 | 24 | @implementation ViewController 25 | 26 | - (void)viewDidLoad { 27 | [super viewDidLoad]; 28 | 29 | // Create a scroll bar object 30 | TOScrollBar *scrollBar = [[TOScrollBar alloc] init]; 31 | 32 | // Uncomment this to disable tapping the track view to jump around 33 | // scrollBar.handleExclusiveInteractionEnabled = YES; 34 | 35 | // Add the scroll bar to the table view 36 | [self.tableView to_addScrollBar:scrollBar]; 37 | 38 | //Adjust the table separators so they won't underlap the scroll bar 39 | self.tableView.separatorInset = [scrollBar adjustedTableViewSeparatorInsetForInset:self.tableView.separatorInset]; 40 | 41 | // ======================================================================== 42 | 43 | // Make sure it's not nil before we start styling 44 | self.tableView.backgroundColor = [UIColor whiteColor]; 45 | 46 | // Add a button to toggle dark mode 47 | UIBarButtonItem *darkItem = [[UIBarButtonItem alloc] initWithTitle:@"Dark" style:UIBarButtonItemStylePlain target:self action:@selector(darkModeButtonTapped:)]; 48 | self.navigationItem.rightBarButtonItem = darkItem; 49 | 50 | // Add a button to toggle showing the scroll bar 51 | UIBarButtonItem *hideItem = [[UIBarButtonItem alloc] initWithTitle:@"Hide" style:UIBarButtonItemStylePlain target:self action:@selector(hideButtonTapped:)]; 52 | self.navigationItem.leftBarButtonItem = hideItem; 53 | 54 | if (@available(iOS 11.0, *)) { 55 | self.navigationController.navigationBar.prefersLargeTitles = YES; 56 | scrollBar.insetForLargeTitles = YES; 57 | 58 | UISearchController *searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; 59 | searchController.searchResultsUpdater = self; 60 | self.navigationItem.searchController = searchController; 61 | } 62 | } 63 | 64 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 65 | { 66 | return 1000; 67 | } 68 | 69 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 70 | { 71 | static NSString *identifier = @"MyCell"; 72 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; 73 | if (cell == nil) { 74 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; 75 | //cell.selectionStyle = UITableViewCellSelectionStyleNone; 76 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 77 | } 78 | 79 | 80 | cell.textLabel.textColor = self.darkMode ? [UIColor whiteColor] : [UIColor blackColor]; 81 | cell.textLabel.backgroundColor = self.tableView.backgroundColor; 82 | cell.backgroundColor = self.tableView.backgroundColor; 83 | 84 | cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", indexPath.row+1]; 85 | cell.layoutMargins = [tableView.to_scrollBar adjustedTableViewCellLayoutMarginsForMargins:cell.layoutMargins manualOffset:0.0f]; 86 | 87 | return cell; 88 | } 89 | 90 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 91 | { 92 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 93 | } 94 | 95 | - (void)darkModeButtonTapped:(id)sender 96 | { 97 | UIBarButtonItem *button = (UIBarButtonItem *)sender; 98 | self.darkMode = !self.darkMode; 99 | button.title = self.darkMode ? @"Light" : @"Dark"; 100 | [self configureStyleForDarkMode:self.darkMode]; 101 | } 102 | 103 | - (void)configureStyleForDarkMode:(BOOL)darkMode 104 | { 105 | self.navigationController.navigationBar.barStyle = darkMode ? UIBarStyleBlack : UIBarStyleDefault; 106 | self.tableView.backgroundColor = darkMode ? [UIColor colorWithWhite:0.09f alpha:1.0f] : [UIColor whiteColor]; 107 | self.view.window.tintColor = darkMode ? [UIColor colorWithRed:90.0f/255.0f green:120.0f/255.0f blue:218.0f/255.0f alpha:1.0f] : nil; 108 | self.tableView.separatorColor = darkMode ? [UIColor colorWithWhite:0.3f alpha:1.0f] : nil; 109 | [self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone]; 110 | self.tableView.to_scrollBar.style = darkMode ? TOScrollBarStyleDark : TOScrollBarStyleDefault; 111 | } 112 | 113 | - (void)hideButtonTapped:(id)sender 114 | { 115 | UIBarButtonItem *button = (UIBarButtonItem *)sender; 116 | self.hidden = !self.hidden; 117 | button.title = self.hidden ? @"Show" : @"Hide"; 118 | [self.tableView.to_scrollBar setHidden:self.hidden animated:YES]; 119 | } 120 | 121 | - (void)updateSearchResultsForSearchController:(UISearchController *)searchController { 122 | 123 | } 124 | 125 | @end 126 | -------------------------------------------------------------------------------- /TOScrollBarExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TOScrollBarExample 4 | // 5 | // Created by Tim Oliver on 5/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TOScrollBarExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TOScrollBarExampleTests/TOScrollBarExampleTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOScrollBarExampleTests.m 3 | // TOScrollBarExampleTests 4 | // 5 | // Created by Tim Oliver on 15/11/16. 6 | // Copyright © 2016 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TOScrollBar.h" 11 | 12 | @interface TOScrollBarExampleTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation TOScrollBarExampleTests 17 | 18 | - (void)testViewCreation { 19 | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; 20 | TOScrollBar *scrollBar = [[TOScrollBar alloc] initWithStyle:TOScrollBarStyleDefault]; 21 | [scrollView to_addScrollBar:scrollBar]; 22 | XCTAssert(scrollBar.scrollView != nil); 23 | [scrollView to_removeScrollbar]; 24 | } 25 | 26 | 27 | @end 28 | --------------------------------------------------------------------------------