Browse Source

Feature Missing: iOS app is missing “Scroll Bar” to scroll through older ‘Photos’ #519

Marino Faggiana 7 years ago
parent
commit
45e1756a32

+ 123 - 0
Libraries external/TOScrollBar/TOScrollBar.h

@@ -0,0 +1,123 @@
+//
+//  TOScrollBar.h
+//
+//  Copyright 2016-2017 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import <UIKit/UIKit.h>
+#import "UIScrollView+TOScrollBar.h"
+
+typedef NS_ENUM(NSInteger, TOScrollBarStyle) {
+    TOScrollBarStyleDefault,
+    TOScrollBarStyleDark
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TOScrollBar : UIView
+
+/* The visual style of the scroll bar, either light or dark */
+@property (nonatomic, assign) TOScrollBarStyle style;
+
+/** Aligns the scroll bar to the top of the scroll view content offset.
+     Set this to `YES` when using this in a view controller with iOS 11 large titles. */
+@property (nonatomic, assign) BOOL insetForLargeTitles;
+
+/** The amount of padding above and below the scroll bar (Only top and bottom values are counted. Default is {20,20} ) */
+@property (nonatomic, assign) UIEdgeInsets verticalInset;
+
+/** The inset, in points of the middle of track from the edge of the scroll view */
+@property (nonatomic, assign) CGFloat edgeInset;
+
+/** The tint color of the track */
+@property (nonatomic, strong) UIColor *trackTintColor;
+
+/** The width in points, of the track (Default value is 2.0) */
+@property (nonatomic, assign) CGFloat trackWidth;
+
+/** The tint color of the handle (Defaults to the system tint color) */
+@property (nonatomic, strong, nullable) UIColor *handleTintColor;
+
+/** The width in points, of the handle. (Default value is 4.0) */
+@property (nonatomic, assign) CGFloat handleWidth;
+
+/** The minimum height in points the handle may be in relation to the content height. (Default value is 64.0) */
+@property (nonatomic, assign) CGFloat handleMinimiumHeight;
+
+/** The user is currently dragging the handle */
+@property (nonatomic, assign, readonly) BOOL dragging;
+
+/** The minimum required scale of the scroll view's content height before the scroll bar is shown (Default is 5.0) */
+@property (nonatomic, assign) CGFloat minimumContentHeightScale;
+
+/** The scroll view in which this scroll bar has been added. */
+@property (nonatomic, weak, readonly) UIScrollView *scrollView;
+
+/** When enabled, the scroll bar will only respond to direct touches to the handle control.
+ Touches to the track will be passed to the UI controls beneath it.
+ Default is NO. */
+@property (nonatomic, assign) BOOL handleExclusiveInteractionEnabled;
+
+/** 
+ Creates a new instance of the scroll bar view 
+ 
+ @param style The initial style of the scroll bar upon creation
+ */
+- (instancetype)initWithStyle:(TOScrollBarStyle)style;
+
+/**
+ Adds the scroll bar to a scroll view
+ 
+ @param scrollView The scroll view that will receive this scroll bar
+ */
+- (void)addToScrollView:(UIScrollView *)scrollView;
+
+/**
+ Removes the scroll bar from the scroll view and resets the scroll view's state
+ */
+- (void)removeFromScrollView;
+
+/**
+ If added to a table view, this convienience method will compute the appropriate
+ inset values for the table separator so they don't underlap the scroll bar
+ 
+ @param inset The original separator inset value of the table view
+ */
+- (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset;
+
+/**
+ If added to a table view, this convienience method will compute the appropriate
+ insets values for each cell's layout margins in order to appropriately push the cell's
+ content inwards
+ 
+ @param layoutMargins The current `layoutMargins` value of the `UITableViewCell` instance.
+ @param offset If desired, any additional horizontal offset for this specific use case
+ 
+ */
+- (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset;
+
+/**
+ Shows or hides the scroll bar from the scroll view with an optional animation
+ */
+- (void)setHidden:(BOOL)hidden animated:(BOOL)animated;
+
+@end
+
+NS_ASSUME_NONNULL_END
+

+ 691 - 0
Libraries external/TOScrollBar/TOScrollBar.m

@@ -0,0 +1,691 @@
+//
+//  TOScrollBar.m
+//
+//  Copyright 2016-2017 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import "TOScrollBar.h"
+#import "UIScrollView+TOScrollBar.h"
+#import "TOScrollBarGestureRecognizer.h"
+
+/** Default values for the scroll bar */
+static const CGFloat kTOScrollBarTrackWidth      = 2.0f;     // The default width of the scrollable space indicator
+static const CGFloat kTOScrollBarHandleWidth     = 4.0f;     // The default width of the handle control
+static const CGFloat kTOScrollBarEdgeInset       = 7.5f;     // The distance from the edge of the view to the center of the track
+static const CGFloat kTOScrollBarHandleMinHeight = 64.0f;    // The minimum usable size to which the handle can shrink
+static const CGFloat kTOScrollBarWidth           = 30.0f;    // The width of this control (44 is minimum recommended tapping space)
+static const CGFloat kTOScrollBarVerticalPadding = 10.0f;    // The default padding at the top and bottom of the view
+static const CGFloat kTOScrollBarMinimumContentScale = 5.0f; // The minimum scale of the content view before showing the scroll view is necessary
+
+/************************************************************************/
+
+// A struct to hold the scroll view's previous state before this bar was applied
+struct TOScrollBarScrollViewState {
+    BOOL showsVerticalScrollIndicator;
+};
+typedef struct TOScrollBarScrollViewState TOScrollBarScrollViewState;
+
+/************************************************************************/
+// Private interface exposure for scroll view category
+
+@interface UIScrollView () //TOScrollBar
+- (void)setTo_scrollBar:(TOScrollBar *)scrollBar;
+@end
+
+/************************************************************************/
+
+@interface TOScrollBar () <UIGestureRecognizerDelegate> {
+    TOScrollBarScrollViewState _scrollViewState;
+}
+
+@property (nonatomic, weak, readwrite) UIScrollView *scrollView;   // The parent scroll view in which we belong
+
+@property (nonatomic, assign) BOOL userHidden;          // View was explicitly hidden by the user as opposed to us
+
+@property (nonatomic, strong) UIImageView *trackView;   // The track indicating the scrollable distance
+@property (nonatomic, strong) UIImageView *handleView;  // The handle that may be dragged in the scroll bar
+
+@property (nonatomic, assign, readwrite) BOOL dragging; // The user is presently dragging the handle
+@property (nonatomic, assign) CGFloat yOffset;          // The offset from the center of the thumb
+
+@property (nonatomic, assign) CGFloat originalYOffset;  // The original placement of the scroll bar when the user started dragging
+@property (nonatomic, assign) CGFloat originalHeight;   // The original height of the scroll bar when the user started dragging
+@property (nonatomic, assign) CGFloat originalTopInset; // The original safe area inset of the scroll bar when the user started dragging
+
+@property (nonatomic, assign) CGFloat horizontalOffset; // The horizontal offset when the edge inset is too small for the touch region
+
+@property (nonatomic, assign) BOOL disabled;            // Disabled when there's not enough scroll content to merit showing this
+
+@property (nonatomic, strong) UIImpactFeedbackGenerator *feedbackGenerator; // Taptic feedback for iPhone 7 and above
+
+@property (nonatomic, strong) TOScrollBarGestureRecognizer *gestureRecognizer; // Our custom recognizer for handling user interactions with the scroll bar
+
+@end
+
+/************************************************************************/
+
+@implementation TOScrollBar
+
+#pragma mark - Class Creation -
+
+- (instancetype)initWithStyle:(TOScrollBarStyle)style
+{
+    if (self = [super initWithFrame:CGRectZero]) {
+        _style = style;
+        [self setUpInitialProperties];
+    }
+
+    return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame]) {
+        [self setUpInitialProperties];
+    }
+
+    return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder
+{
+    if (self = [super initWithCoder:aDecoder]) {
+        [self setUpInitialProperties];
+    }
+
+    return self;
+}
+
+#pragma mark - Set-up -
+
+- (void)setUpInitialProperties
+{
+    _trackWidth  = kTOScrollBarTrackWidth;
+    _handleWidth = kTOScrollBarHandleWidth;
+    _edgeInset   = kTOScrollBarEdgeInset;
+    _handleMinimiumHeight = kTOScrollBarHandleMinHeight;
+    _minimumContentHeightScale = kTOScrollBarMinimumContentScale;
+    _verticalInset = UIEdgeInsetsMake(kTOScrollBarVerticalPadding, 0.0f, kTOScrollBarVerticalPadding, 0.0f);
+    _feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+    _gestureRecognizer = [[TOScrollBarGestureRecognizer alloc] initWithTarget:self action:@selector(scrollBarGestureRecognized:)];
+}
+
+- (void)setUpViews
+{
+    if (self.trackView || self.handleView) {
+        return;
+    }
+
+    self.backgroundColor = [UIColor clearColor];
+
+    // Create and add the track view
+    self.trackView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.trackWidth]];
+    [self addSubview:self.trackView];
+
+    // Add the handle view
+    self.handleView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.handleWidth]];
+    [self addSubview:self.handleView];
+
+    // Add the initial styling
+    [self configureViewsForStyle:self.style];
+    
+    // Add gesture recognizer
+    [self addGestureRecognizer:self.gestureRecognizer];
+}
+
+- (void)configureViewsForStyle:(TOScrollBarStyle)style
+{
+    BOOL dark = (style == TOScrollBarStyleDark);
+
+    CGFloat whiteColor = 0.0f;
+    if (dark) {
+        whiteColor = 1.0f;
+    }
+    self.trackView.tintColor = [UIColor colorWithWhite:whiteColor alpha:0.1f];
+}
+
+- (void)dealloc
+{
+    [self restoreScrollView:self.scrollView];
+}
+
+- (void)configureScrollView:(UIScrollView *)scrollView
+{
+    if (scrollView == nil) {
+        return;
+    }
+
+    // Make a copy of the scroll view's state and then configure
+    _scrollViewState.showsVerticalScrollIndicator = self.scrollView.showsVerticalScrollIndicator;
+    scrollView.showsVerticalScrollIndicator = NO;
+
+    //Key-value Observers
+    [scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
+    [scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
+}
+
+- (void)restoreScrollView:(UIScrollView *)scrollView
+{
+    if (scrollView == nil) {
+        return;
+    }
+
+    // Restore the scroll view's state
+    scrollView.showsVerticalScrollIndicator = _scrollView.showsVerticalScrollIndicator;
+
+    // Remove the observers
+    [scrollView removeObserver:self forKeyPath:@"contentOffset"];
+    [scrollView removeObserver:self forKeyPath:@"contentSize"];
+}
+
+- (void)willMoveToSuperview:(UIView *)newSuperview
+{
+    [super willMoveToSuperview:newSuperview];
+    [self setUpViews];
+}
+
+#pragma mark - Content Layout -
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
+                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
+{
+    [self updateStateForScrollView];
+    if (self.hidden) { return; }
+    [self layoutInScrollView];
+    [self setNeedsLayout];
+}
+
+- (CGFloat)heightOfHandleForContentSize
+{
+    if (_scrollView == nil) {
+        return _handleMinimiumHeight;
+    }
+
+    CGFloat heightRatio = self.scrollView.frame.size.height / self.scrollView.contentSize.height;
+    CGFloat height = self.frame.size.height * heightRatio;
+
+    return MAX(floorf(height), _handleMinimiumHeight);
+}
+
+- (void)updateStateForScrollView
+{
+    CGRect frame = _scrollView.frame;
+    CGSize contentSize = _scrollView.contentSize;
+    self.disabled = (contentSize.height / frame.size.height) < _minimumContentHeightScale;
+    [self setHidden:(self.disabled || self.userHidden) animated:NO];
+}
+
+- (void)layoutInScrollView
+{
+    CGRect scrollViewFrame = _scrollView.frame;
+    UIEdgeInsets insets    = _scrollView.contentInset;
+    CGPoint contentOffset  = _scrollView.contentOffset;
+    CGFloat halfWidth      = (kTOScrollBarWidth * 0.5f);
+
+    if (@available(iOS 11.0, *)) {
+        insets = _scrollView.adjustedContentInset;
+    }
+
+    // Contract the usable space by the scroll view's content inset (eg navigation/tool bars)
+    scrollViewFrame.size.height -= (insets.top + insets.bottom);
+
+    CGFloat largeTitleDelta = 0.0f;
+    if (_insetForLargeTitles) {
+        largeTitleDelta = fabs(MIN(insets.top + contentOffset.y, 0.0f));
+    }
+
+    // Work out the final height be further contracting by the padding
+    CGFloat height = (scrollViewFrame.size.height - (_verticalInset.top + _verticalInset.bottom)) - largeTitleDelta;
+
+    // Work out how much we have to offset the track by to make sure all of the parent view
+    // is visible at the edge of the screen (Or else we'll be unable to tap properly)
+    CGFloat horizontalOffset = halfWidth - _edgeInset;
+    self.horizontalOffset = (horizontalOffset > 0.0f) ? horizontalOffset : 0.0f;
+
+    // Work out the frame for the scroll view
+    CGRect frame = CGRectZero;
+    
+    // Size
+    frame.size.width = kTOScrollBarWidth;
+    frame.size.height = (_dragging ? _originalHeight : height);
+    
+    // Horizontal placement
+    frame.origin.x = scrollViewFrame.size.width - (_edgeInset + halfWidth);
+    if (@available(iOS 11.0, *)) { frame.origin.x -= _scrollView.safeAreaInsets.right; }
+    frame.origin.x = MIN(frame.origin.x, scrollViewFrame.size.width - kTOScrollBarWidth);
+
+    // Vertical placement in scroll view
+    if (_dragging) {
+        frame.origin.y = _originalYOffset;
+    }
+    else {
+        frame.origin.y = _verticalInset.top;
+        frame.origin.y += insets.top;
+        frame.origin.y += largeTitleDelta;
+    }
+    frame.origin.y += contentOffset.y;
+
+    // Set the frame
+    self.frame = frame;
+    
+    // Bring the scroll bar to the front in case other subviews were subsequently added over it
+    [self.superview bringSubviewToFront:self];
+}
+
+- (void)layoutSubviews
+{
+    CGRect frame = self.frame;
+
+    // The frame of the track
+    CGRect trackFrame = CGRectZero;
+    trackFrame.size.width = _trackWidth;
+    trackFrame.size.height = frame.size.height;
+    trackFrame.origin.x = ceilf(((frame.size.width - _trackWidth) * 0.5f) + _horizontalOffset);
+    self.trackView.frame = CGRectIntegral(trackFrame);
+
+    // Don't handle automatic layout when dragging; we'll do that manually elsewhere
+    if (self.dragging || self.disabled) {
+        return;
+    }
+
+    // The frame of the handle
+    CGRect handleFrame = CGRectZero;
+    handleFrame.size.width = _handleWidth;
+    handleFrame.size.height = [self heightOfHandleForContentSize];
+    handleFrame.origin.x = ceilf(((frame.size.width - _handleWidth) * 0.5f) + _horizontalOffset);
+
+    // Work out the y offset of the handle
+    UIEdgeInsets contentInset = _scrollView.contentInset;
+    if (@available(iOS 11.0, *)) {
+        contentInset = _scrollView.safeAreaInsets;
+    }
+
+    CGPoint contentOffset     = _scrollView.contentOffset;
+    CGSize contentSize        = _scrollView.contentSize;
+    CGRect scrollViewFrame    = _scrollView.frame;
+
+    CGFloat scrollableHeight = (contentSize.height + contentInset.top + contentInset.bottom) - scrollViewFrame.size.height;
+    CGFloat scrollProgress = (contentOffset.y + contentInset.top) / scrollableHeight;
+    handleFrame.origin.y = (frame.size.height - handleFrame.size.height) * scrollProgress;
+
+    // If the scroll view expanded beyond its scrollable range, shrink the handle to match the rubber band effect
+    if (contentOffset.y < -contentInset.top) { // The top
+        handleFrame.size.height -= (-contentOffset.y - contentInset.top);
+        handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
+    }
+    else if (contentOffset.y + scrollViewFrame.size.height > contentSize.height + contentInset.bottom) { // The bottom
+        CGFloat adjustedContentOffset = contentOffset.y + scrollViewFrame.size.height;
+        CGFloat delta = adjustedContentOffset - (contentSize.height + contentInset.bottom);
+        handleFrame.size.height -= delta;
+        handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
+        handleFrame.origin.y = frame.size.height - handleFrame.size.height;
+    }
+
+    // Clamp to the bounds of the frame
+    handleFrame.origin.y = MAX(handleFrame.origin.y, 0.0f);
+    handleFrame.origin.y = MIN(handleFrame.origin.y, (frame.size.height - handleFrame.size.height));
+
+    self.handleView.frame = handleFrame;
+}
+
+- (void)setScrollYOffsetForHandleYOffset:(CGFloat)yOffset animated:(BOOL)animated
+{
+    CGFloat heightRange = _trackView.frame.size.height - _handleView.frame.size.height;
+    yOffset = MAX(0.0f, yOffset);
+    yOffset = MIN(heightRange, yOffset);
+
+    CGFloat positionRatio = yOffset / heightRange;
+
+    CGRect frame       = _scrollView.frame;
+    UIEdgeInsets inset = _scrollView.contentInset;
+    CGSize contentSize = _scrollView.contentSize;
+
+    if (@available(iOS 11.0, *)) {
+        inset = _scrollView.adjustedContentInset;
+    }
+    inset.top = _originalTopInset;
+
+    CGFloat totalScrollSize = (contentSize.height + inset.top + inset.bottom) - frame.size.height;
+    CGFloat scrollOffset = totalScrollSize * positionRatio;
+    scrollOffset -= inset.top;
+
+    CGPoint contentOffset = _scrollView.contentOffset;
+    contentOffset.y = scrollOffset;
+
+    // Animate to help coax the large title navigation bar to behave
+    if (@available(iOS 11.0, *)) {
+        [UIView animateWithDuration:animated ? 0.1f : 0.00001f animations:^{
+            [self.scrollView setContentOffset:contentOffset animated:NO];
+        }];
+    }
+    else {
+        [self.scrollView setContentOffset:contentOffset animated:NO];
+    }
+}
+
+#pragma mark - Scroll View Integration -
+
+- (void)addToScrollView:(UIScrollView *)scrollView
+{
+    if (scrollView == self.scrollView) {
+        return;
+    }
+
+    // Restore the previous scroll view
+    [self restoreScrollView:self.scrollView];
+
+    // Assign the new scroll view
+    self.scrollView = scrollView;
+
+    // Apply the observers/settings to the new scroll view
+    [self configureScrollView:scrollView];
+
+    // Add the scroll bar to the scroll view's content view
+    [self.scrollView addSubview:self];
+
+    // Add ourselves as a property of the scroll view
+    [self.scrollView setTo_scrollBar:self];
+
+    // Begin layout
+    [self layoutInScrollView];
+}
+
+- (void)removeFromScrollView
+{
+    [self restoreScrollView:self.scrollView];
+    [self removeFromSuperview];
+    [self.scrollView setTo_scrollBar:nil];
+    self.scrollView = nil;
+}
+
+- (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset
+{
+    inset.right = _edgeInset * 2.0f;
+    return inset;
+}
+
+- (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset
+{
+    layoutMargins.right = (_edgeInset * 2.0f) + 15.0f; // Magic system number is 20, but we can't infer that from here on time
+    layoutMargins.right += offset;
+    return layoutMargins;
+}
+
+#pragma mark - User Interaction -
+- (void)scrollBarGestureRecognized:(TOScrollBarGestureRecognizer *)recognizer
+{
+    CGPoint touchPoint = [recognizer locationInView:self];
+    
+    switch (recognizer.state) {
+        case UIGestureRecognizerStateBegan:
+            [self gestureBeganAtPoint:touchPoint];
+            break;
+        case UIGestureRecognizerStateChanged:
+            [self gestureMovedToPoint:touchPoint];
+            break;
+        case UIGestureRecognizerStateEnded:
+        case UIGestureRecognizerStateCancelled:
+            [self gestureEnded];
+            break;
+        default:
+            break;
+    }
+}
+
+- (void)gestureBeganAtPoint:(CGPoint)touchPoint
+{
+    if (self.disabled) {
+        return;
+    }
+
+    // Warm-up the feedback generator
+    [_feedbackGenerator prepare];
+
+    self.scrollView.scrollEnabled = NO;
+    self.dragging = YES;
+
+    // Capture the original position
+    self.originalHeight = self.frame.size.height;
+    self.originalYOffset = self.frame.origin.y - self.scrollView.contentOffset.y;
+
+    if (@available(iOS 11.0, *)) {
+        self.originalTopInset = _scrollView.adjustedContentInset.top;
+    } else {
+        self.originalTopInset = _scrollView.contentInset.top;
+    }
+
+    // Check if the user tapped inside the handle
+    CGRect handleFrame = self.handleView.frame;
+    if (touchPoint.y > (handleFrame.origin.y - 20) &&
+        touchPoint.y < handleFrame.origin.y + (handleFrame.size.height + 20))
+    {
+        self.yOffset = (touchPoint.y - handleFrame.origin.y);
+        return;
+    }
+
+	if (!self.handleExclusiveInteractionEnabled) {
+		// User tapped somewhere else, animate the handle to that point
+		CGFloat halfHeight = (handleFrame.size.height * 0.5f);
+
+		CGFloat destinationYOffset = touchPoint.y - halfHeight;
+		destinationYOffset = MAX(0.0f, destinationYOffset);
+		destinationYOffset = MIN(self.frame.size.height - halfHeight, destinationYOffset);
+
+		self.yOffset = (touchPoint.y - destinationYOffset);
+		handleFrame.origin.y = destinationYOffset;
+
+		[UIView animateWithDuration:0.2f
+							  delay:0.0f
+			 usingSpringWithDamping:1.0f
+			  initialSpringVelocity:0.1f options:UIViewAnimationOptionBeginFromCurrentState
+						 animations:^{
+							 self.handleView.frame = handleFrame;
+						 } completion:nil];
+
+		[self setScrollYOffsetForHandleYOffset:floorf(destinationYOffset) animated:NO];
+	}
+}
+
+- (void)gestureMovedToPoint:(CGPoint)touchPoint
+{
+    if (self.disabled) {
+        return;
+    }
+
+    CGFloat delta = 0.0f;
+    CGRect handleFrame = _handleView.frame;
+    CGRect trackFrame = _trackView.frame;
+    CGFloat minimumY = 0.0f;
+    CGFloat maximumY = trackFrame.size.height - handleFrame.size.height;
+
+	if (self.handleExclusiveInteractionEnabled) {
+		if (touchPoint.y < (handleFrame.origin.y - 20) ||
+			touchPoint.y > handleFrame.origin.y + (handleFrame.size.height + 20))
+		{
+			// This touch is not on the handle; eject.
+			return;
+		}
+	}
+	
+    // Apply the updated Y value plus the previous offset
+    delta = handleFrame.origin.y;
+    handleFrame.origin.y = touchPoint.y - _yOffset;
+
+    //Clamp the handle, and adjust the y offset to counter going outside the bounds
+    if (handleFrame.origin.y < minimumY) {
+        _yOffset += handleFrame.origin.y;
+        _yOffset = MAX(minimumY, _yOffset);
+        handleFrame.origin.y = minimumY;
+    }
+    else if (handleFrame.origin.y > maximumY) {
+        CGFloat handleOverflow = CGRectGetMaxY(handleFrame) - trackFrame.size.height;
+        _yOffset += handleOverflow;
+        _yOffset = MIN(self.yOffset, handleFrame.size.height);
+        handleFrame.origin.y = MIN(handleFrame.origin.y, maximumY);
+    }
+
+    _handleView.frame = handleFrame;
+
+    delta -= handleFrame.origin.y;
+    delta = fabs(delta);
+
+    // If the delta is not 0.0, but we're at either extreme,
+    // this is first frame we've since reaching that point.
+    // Play a taptic feedback impact
+    if (delta > FLT_EPSILON && (CGRectGetMinY(handleFrame) < FLT_EPSILON || CGRectGetMinY(handleFrame) >= maximumY - FLT_EPSILON)) {
+        [_feedbackGenerator impactOccurred];
+    }
+
+    // If the user is doing really granualar swipes, add a subtle amount
+    // of vertical animation so the scroll view isn't jumping on each frame
+    [self setScrollYOffsetForHandleYOffset:floorf(handleFrame.origin.y) animated:NO]; //(delta < 0.51f)
+}
+
+- (void)gestureEnded
+{
+    self.scrollView.scrollEnabled = YES;
+    self.dragging = NO;
+
+    [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.5f options:0 animations:^{
+        [self layoutInScrollView];
+        [self layoutIfNeeded];
+    } completion:nil];
+}
+
+- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
+{
+    if (!self.handleExclusiveInteractionEnabled) {
+		return [super pointInside:point withEvent:event];
+	}
+    else {
+		CGFloat handleMinY = CGRectGetMinY(self.handleView.frame);
+		CGFloat handleMaxY = CGRectGetMaxY(self.handleView.frame);
+		return (0 <= point.x) && (handleMinY <= point.y) && (point.y <= handleMaxY);
+	}
+}
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
+{
+    UIView *result = [super hitTest:point withEvent:event];
+
+    if (self.disabled || self.dragging) {
+        return result;
+    }
+
+    // If the user contacts the screen in a swiping motion,
+    // the scroll view will automatically highjack the touch
+    // event unless we explicitly override it here.
+
+    self.scrollView.scrollEnabled = (result != self);
+    return result;
+}
+
+#pragma mark - Accessors -
+- (void)setStyle:(TOScrollBarStyle)style
+{
+    _style = style;
+    [self configureViewsForStyle:style];
+}
+
+- (UIColor *)trackTintColor { return self.trackView.tintColor; }
+
+- (void)setTrackTintColor:(UIColor *)trackTintColor
+{
+    self.trackView.tintColor = trackTintColor;
+}
+
+- (UIColor *)handleTintColor { return self.handleView.tintColor; }
+
+- (void)setHandleTintColor:(UIColor *)handleTintColor
+{
+    self.handleView.tintColor = handleTintColor;
+}
+
+- (void)setHidden:(BOOL)hidden
+{
+    self.userHidden = hidden;
+    [self setHidden:hidden animated:NO];
+}
+
+- (void)setHidden:(BOOL)hidden animated:(BOOL)animated
+{
+    // Override. It cannot be shown if it's disabled
+    if (_disabled) {
+        super.hidden = YES;
+        return;
+    }
+
+    // Simply show or hide it if we're not animating
+    if (animated == NO) {
+        super.hidden = hidden;
+        return;
+    }
+
+    // Show it if we're going to animate it
+    if (self.hidden && hidden == NO) {
+        super.hidden = NO;
+        [self layoutInScrollView];
+        [self setNeedsLayout];
+    }
+
+    CGRect fromFrame = self.frame;
+    CGRect toFrame = self.frame;
+
+    CGFloat widestElement = MAX(_trackWidth, _handleWidth);
+    CGFloat hiddenOffset = fromFrame.origin.x + _edgeInset + (widestElement * 2.0f);
+    if (hidden == NO) {
+        fromFrame.origin.x = hiddenOffset;
+    }
+    else {
+        toFrame.origin.x = hiddenOffset;
+    }
+
+    self.frame = fromFrame;
+    [UIView animateWithDuration:0.3f
+                          delay:0.0f
+         usingSpringWithDamping:1.0f
+          initialSpringVelocity:0.1f
+                        options:UIViewAnimationOptionBeginFromCurrentState
+                     animations:^{
+                         self.frame = toFrame;
+                     } completion:^(BOOL finished) {
+                         super.hidden = hidden;
+                     }];
+
+}
+
+#pragma mark - Image Generation -
++ (UIImage *)verticalCapsuleImageWithWidth:(CGFloat)width
+{
+    UIImage *image = nil;
+    CGFloat radius = width * 0.5f;
+    CGRect frame = (CGRect){0, 0, width+1, width+1};
+
+    UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f);
+    [[UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius] fill];
+    image = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+
+    image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius) resizingMode:UIImageResizingModeStretch];
+    image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+
+    return image;
+}
+
+@end

+ 27 - 0
Libraries external/TOScrollBar/TOScrollBarGestureRecognizer.h

@@ -0,0 +1,27 @@
+//
+//  TOScrollBarGestureRecognizer.h
+//
+//  Copyright 2017 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import <UIKit/UIKit.h>
+
+@interface TOScrollBarGestureRecognizer : UIGestureRecognizer
+
+@end

+ 75 - 0
Libraries external/TOScrollBar/TOScrollBarGestureRecognizer.m

@@ -0,0 +1,75 @@
+//
+//  TOScrollBarGestureRecognizer.h
+//
+//  Copyright 2017 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import "TOScrollBarGestureRecognizer.h"
+#import <UIKit/UIGestureRecognizerSubclass.h>
+#import "TOScrollBar.h"
+
+@interface TOScrollBarGestureRecognizer ()
+
+@property (nonatomic, readonly) TOScrollBar *scrollBar; // The scroll bar this recognizer is attached to
+
+@end
+
+@implementation TOScrollBarGestureRecognizer
+
+#pragma mark - Gesture Recognizer Filtering -
+- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
+{
+    // Ensure that the pan gesture recognizer from the scroll view doesn't override the scroll bar
+    UIView *view = preventedGestureRecognizer.view;
+    if ([view isEqual:self.scrollBar.scrollView]) {
+        return YES;
+    }
+    
+    return NO;
+}
+
+#pragma mark - Touch Interaction -
+- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
+{
+    self.state = UIGestureRecognizerStateBegan;
+}
+
+- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
+{
+    self.state = UIGestureRecognizerStateChanged;
+}
+
+- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
+{
+    self.state = UIGestureRecognizerStateEnded;
+}
+
+- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
+{
+    self.state = UIGestureRecognizerStateCancelled;
+}
+
+#pragma mark - Accessors -
+- (TOScrollBar *)scrollBar
+{
+    if ([self.view isKindOfClass:[TOScrollBar class]] == NO) { return nil; }
+    return (TOScrollBar *)self.view;
+}
+
+@end

+ 43 - 0
Libraries external/TOScrollBar/UIScrollView+TOScrollBar.h

@@ -0,0 +1,43 @@
+//
+//  UIScrollView+TOScrollBar.h
+//
+//  Copyright 2016 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import <UIKit/UIKit.h>
+
+@class TOScrollBar;
+
+@interface UIScrollView (TOScrollBar)
+
+/** The scroll bar view currently added to this scroll view */
+@property (nullable, nonatomic, readonly) TOScrollBar *to_scrollBar;
+
+/**
+ Adds a new scroll bar instance to this scroll bar
+ @param scrollBar The scroll bar in which to add
+ */
+- (void)to_addScrollBar:(nullable TOScrollBar *)scrollBar;
+
+/**
+ Removes the current scroll bar (if any) from the scroll bar
+ */
+- (void)to_removeScrollbar;
+
+@end

+ 51 - 0
Libraries external/TOScrollBar/UIScrollView+TOScrollBar.m

@@ -0,0 +1,51 @@
+//
+//  UIScrollView+TOScrollBar.m
+//
+//  Copyright 2016 Timothy Oliver. All rights reserved.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to
+//  deal in the Software without restriction, including without limitation the
+//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+//  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#import <objc/runtime.h>
+#import "UIScrollView+TOScrollBar.h"
+#import "TOScrollBar.h"
+
+static void * TOScrollBarPropertyKey = &TOScrollBarPropertyKey;
+
+@implementation UIScrollView (TOScrollBar)
+
+- (TOScrollBar *)to_scrollBar
+{
+    return objc_getAssociatedObject(self, TOScrollBarPropertyKey);
+}
+
+- (void)setTo_scrollBar:(TOScrollBar *)scrollBar
+{
+    objc_setAssociatedObject(self, TOScrollBarPropertyKey, scrollBar, OBJC_ASSOCIATION_RETAIN);
+}
+
+- (void)to_addScrollBar:(TOScrollBar *)scrollBar
+{
+    [scrollBar addToScrollView:self];
+}
+
+- (void)to_removeScrollbar
+{
+    [self.to_scrollBar removeFromScrollView];
+}
+
+@end

+ 26 - 0
Nextcloud.xcodeproj/project.pbxproj

@@ -375,6 +375,9 @@
 		F7A321AD1E9E6AD50069AD1B /* CCAdvanced.m in Sources */ = {isa = PBXBuildFile; fileRef = F7A321AC1E9E6AD50069AD1B /* CCAdvanced.m */; };
 		F7A377161EB2364A002856D3 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7A377141EB2364A002856D3 /* Crashlytics.framework */; };
 		F7A3771A1EB2364A002856D3 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7A377151EB2364A002856D3 /* Fabric.framework */; };
+		F7A5541E204EF8AF008468EC /* TOScrollBarGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = F7A55418204EF8AF008468EC /* TOScrollBarGestureRecognizer.m */; };
+		F7A5541F204EF8AF008468EC /* TOScrollBar.m in Sources */ = {isa = PBXBuildFile; fileRef = F7A5541B204EF8AF008468EC /* TOScrollBar.m */; };
+		F7A55420204EF8AF008468EC /* UIScrollView+TOScrollBar.m in Sources */ = {isa = PBXBuildFile; fileRef = F7A5541D204EF8AF008468EC /* UIScrollView+TOScrollBar.m */; };
 		F7B0C0CD1EE7E7750033AC24 /* CCSynchronize.m in Sources */ = {isa = PBXBuildFile; fileRef = F7B0C0CC1EE7E7750033AC24 /* CCSynchronize.m */; };
 		F7B0C1751EE839A30033AC24 /* NCAutoUpload.m in Sources */ = {isa = PBXBuildFile; fileRef = F7B0C1741EE839A30033AC24 /* NCAutoUpload.m */; };
 		F7B1FBC41E72E3D1001781FE /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7B1FBB11E72E3D1001781FE /* Media.xcassets */; };
@@ -1329,6 +1332,12 @@
 		F7A377151EB2364A002856D3 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = "<group>"; };
 		F7A54C341C6267B500E2C8BF /* CCExifGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCExifGeo.h; sourceTree = "<group>"; };
 		F7A54C351C6267B500E2C8BF /* CCExifGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CCExifGeo.m; sourceTree = "<group>"; };
+		F7A55418204EF8AF008468EC /* TOScrollBarGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOScrollBarGestureRecognizer.m; sourceTree = "<group>"; };
+		F7A55419204EF8AF008468EC /* TOScrollBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TOScrollBar.h; sourceTree = "<group>"; };
+		F7A5541A204EF8AF008468EC /* UIScrollView+TOScrollBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+TOScrollBar.h"; sourceTree = "<group>"; };
+		F7A5541B204EF8AF008468EC /* TOScrollBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOScrollBar.m; sourceTree = "<group>"; };
+		F7A5541C204EF8AF008468EC /* TOScrollBarGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TOScrollBarGestureRecognizer.h; sourceTree = "<group>"; };
+		F7A5541D204EF8AF008468EC /* UIScrollView+TOScrollBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+TOScrollBar.m"; sourceTree = "<group>"; };
 		F7A582D61A24DAB500E903D7 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AppDelegate.m; sourceTree = "<group>"; };
 		F7A582D71A24DAB500E903D7 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AppDelegate.h; sourceTree = "<group>"; };
 		F7ACE4291BAC0268006C0017 /* Acknowledgements.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Acknowledgements.h; sourceTree = "<group>"; };
@@ -1874,6 +1883,7 @@
 				F75037421DBFA91A008FB480 /* PureLayout */,
 				F70F05241C889184008DAB36 /* Reachability */,
 				F7DFE24E1EBDC3A400CF5202 /* Realm */,
+				F7A55417204EF8AF008468EC /* TOScrollBar */,
 				F75AE3C51E9D12900088BB09 /* SwiftyAvatar */,
 				F73CCE271DC13798007E38D8 /* UICKeyChainStore */,
 				F70F05561C889184008DAB36 /* UIImage+animatedGIF */,
@@ -2545,6 +2555,19 @@
 			path = "Libraries external/Fabric";
 			sourceTree = SOURCE_ROOT;
 		};
+		F7A55417204EF8AF008468EC /* TOScrollBar */ = {
+			isa = PBXGroup;
+			children = (
+				F7A55419204EF8AF008468EC /* TOScrollBar.h */,
+				F7A5541B204EF8AF008468EC /* TOScrollBar.m */,
+				F7A5541C204EF8AF008468EC /* TOScrollBarGestureRecognizer.h */,
+				F7A55418204EF8AF008468EC /* TOScrollBarGestureRecognizer.m */,
+				F7A5541A204EF8AF008468EC /* UIScrollView+TOScrollBar.h */,
+				F7A5541D204EF8AF008468EC /* UIScrollView+TOScrollBar.m */,
+			);
+			path = TOScrollBar;
+			sourceTree = "<group>";
+		};
 		F7ACE4281BAC0268006C0017 /* Settings */ = {
 			isa = PBXGroup;
 			children = (
@@ -3799,6 +3822,7 @@
 				F7D4238A1F0596C6009C9782 /* ThumbsMainToolbar.m in Sources */,
 				F70022EC1EC4C9100080073F /* OCXMLSharedParser.m in Sources */,
 				F7F54D061E5B14C800E19C62 /* MWCaptionView.m in Sources */,
+				F7A55420204EF8AF008468EC /* UIScrollView+TOScrollBar.m in Sources */,
 				F762CB001EACB66200B38484 /* XLFormSegmentedCell.m in Sources */,
 				F732BA061D76CE1500E9878B /* CCNetworking.m in Sources */,
 				F70022B01EC4C9100080073F /* AFURLSessionManager.m in Sources */,
@@ -3814,6 +3838,7 @@
 				F7F54D0C1E5B14C800E19C62 /* MWTapDetectingView.m in Sources */,
 				F7D424631F063B82009C9782 /* CTAssetSelectionLabel.m in Sources */,
 				F7B1FBC61E72E3D1001781FE /* SwiftModalWebVC.swift in Sources */,
+				F7A5541F204EF8AF008468EC /* TOScrollBar.m in Sources */,
 				F7A321651E9E37960069AD1B /* CCActivity.m in Sources */,
 				F762CB0C1EACB66200B38484 /* XLFormSectionDescriptor.m in Sources */,
 				F77B0E131D118A16002130FE /* AppDelegate.m in Sources */,
@@ -3885,6 +3910,7 @@
 				F7FCFFE01D707B83000E6E29 /* CCPeekPop.m in Sources */,
 				F7BAADC81ED5A87C00B7EAD4 /* NCDatabase.swift in Sources */,
 				F77B0E541D118A16002130FE /* CCMove.m in Sources */,
+				F7A5541E204EF8AF008468EC /* TOScrollBarGestureRecognizer.m in Sources */,
 				F70022E61EC4C9100080073F /* OCXMLServerErrorsParser.m in Sources */,
 				F762CB171EACB66200B38484 /* XLFormRegexValidator.m in Sources */,
 				F73CC0691E813DFF006E3047 /* BKPasscodeDummyViewController.m in Sources */,

+ 14 - 0
iOSClient/Photos/CCPhotos.m

@@ -24,6 +24,7 @@
 #import "CCPhotos.h"
 #import "AppDelegate.h"
 #import "CCManageAutoUpload.h"
+#import "TOScrollBar.h"
 #import "NCBridgeSwift.h"
 
 @interface CCPhotos () <CCActionsDeleteDelegate, CCActionsDownloadThumbnailDelegate>
@@ -41,6 +42,8 @@
     CCSectionDataSourceMetadata *_sectionDataSource;
     
     CCHud *_hud;
+    
+    TOScrollBar *_scrollBar;
 }
 @end
 
@@ -92,6 +95,16 @@
     // empty Data Source
     self.collectionView.emptyDataSetDelegate = self;
     self.collectionView.emptyDataSetSource = self;
+
+    // scroll bar
+    _scrollBar = [TOScrollBar new];
+    [self.collectionView to_addScrollBar:_scrollBar];
+    
+    _scrollBar.handleTintColor = [NCBrandColor sharedInstance].brand;
+    _scrollBar.handleWidth = 20;
+    _scrollBar.handleMinimiumHeight = 20;
+    _scrollBar.trackWidth = 0;
+    _scrollBar.edgeInset = 12;
 }
 
 // Apparirà
@@ -122,6 +135,7 @@
     if (self.isViewLoaded && self.view.window)
         [appDelegate changeTheming:self];
     
+    _scrollBar.handleTintColor = [NCBrandColor sharedInstance].brand;
     [self.collectionView reloadData];
 }
 

+ 10 - 1
iOSClient/Settings/Acknowledgements.rtf

@@ -1,4 +1,4 @@
-{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830
+{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200
 {\fonttbl\f0\fswiss\fcharset0 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 {\*\expandedcolortbl;;}
@@ -219,4 +219,13 @@ The MIT License (MIT)\
 \
 Copyright (c) Xmartlabs ( http://xmartlabs.com )\
 ____________________________________________\
+\
+
+\b TOScrollBar
+\b0 \
+\
+The MIT License (MIT)\
+\
+Copyright (c) Tim Oliver\
+____________________________________________\
 }