├── .gitignore ├── .travis.yml ├── DMScrollBar.podspec ├── DMScrollBar ├── DMScrollBar.swift ├── DMScrollBarConfiguration.swift ├── DMScrollBarDelegate.swift ├── Deceleration+Spring │ ├── DecelerationTimingParameters.swift │ ├── GeometryUtils.swift │ ├── RubberBand.swift │ ├── SpringTimingParameters.swift │ ├── TimerAnimation.swift │ └── TimingParameters.swift ├── ScrollBarIndicator.swift ├── ScrollBarInfoView.swift └── Utils │ ├── CASpringAnimation+Utils.swift │ ├── CGPoint+Utils.swift │ ├── Comparable+Utils.swift │ ├── Configuration+Utils.swift │ ├── ConvenienceFunctions.swift │ ├── Publisher+Utils.swift │ ├── Sequence+Utils.swift │ ├── UIColor+Utils.swift │ ├── UIGestureRecognizer+Utils.swift │ ├── UILabel+Utils.swift │ ├── UIScrollView+Utils.swift │ ├── UISpringTimingParameters+Utils.swift │ └── UIView+Utils.swift ├── Example ├── DMScrollBar.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── DMScrollBar-Example.xcscheme ├── DMScrollBar.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── DMScrollBar │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── ViewController.swift ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── DMScrollBar.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Target Support Files │ │ ├── DMScrollBar │ │ ├── DMScrollBar-Info.plist │ │ ├── DMScrollBar-dummy.m │ │ ├── DMScrollBar-prefix.pch │ │ ├── DMScrollBar-umbrella.h │ │ ├── DMScrollBar.debug.xcconfig │ │ ├── DMScrollBar.modulemap │ │ └── DMScrollBar.release.xcconfig │ │ ├── Pods-DMScrollBar_Example │ │ ├── Pods-DMScrollBar_Example-Info.plist │ │ ├── Pods-DMScrollBar_Example-acknowledgements.markdown │ │ ├── Pods-DMScrollBar_Example-acknowledgements.plist │ │ ├── Pods-DMScrollBar_Example-dummy.m │ │ ├── Pods-DMScrollBar_Example-frameworks.sh │ │ ├── Pods-DMScrollBar_Example-umbrella.h │ │ ├── Pods-DMScrollBar_Example.debug.xcconfig │ │ ├── Pods-DMScrollBar_Example.modulemap │ │ └── Pods-DMScrollBar_Example.release.xcconfig │ │ └── Pods-DMScrollBar_Tests │ │ ├── Pods-DMScrollBar_Tests-Info.plist │ │ ├── Pods-DMScrollBar_Tests-acknowledgements.markdown │ │ ├── Pods-DMScrollBar_Tests-acknowledgements.plist │ │ ├── Pods-DMScrollBar_Tests-dummy.m │ │ ├── Pods-DMScrollBar_Tests-umbrella.h │ │ ├── Pods-DMScrollBar_Tests.debug.xcconfig │ │ ├── Pods-DMScrollBar_Tests.modulemap │ │ └── Pods-DMScrollBar_Tests.release.xcconfig └── Tests │ ├── Info.plist │ └── Tests.swift ├── LICENSE ├── Package.swift ├── README.md └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | 21 | # Swift Package Manager 22 | .swiftpm 23 | 24 | # Bundler 25 | .bundle 26 | 27 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 28 | # Carthage/Checkouts 29 | 30 | Carthage/Build 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-ignore-the-pods-directory-in-source-control 35 | # 36 | # Note: if you ignore the Pods directory, make sure to uncomment 37 | # `pod install` in .travis.yml 38 | # 39 | # Pods/ 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/DMScrollBar.xcworkspace -scheme DMScrollBar-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /DMScrollBar.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DMScrollBar' 3 | s.version = '2.1.4' 4 | s.summary = 'Customizable Scroll Bar for Scroll view.' 5 | s.description = "Customizable Scroll Bar for Scroll View with additional info label appearing during the scroll." 6 | s.homepage = 'https://github.com/batanus/DMScrollBar' 7 | s.screenshots = 'https://user-images.githubusercontent.com/25244017/209937470-d76a558c-6350-4d96-a142-13a6ef32e0f8.gif', 'https://user-images.githubusercontent.com/25244017/209937479-e7acbbd1-fba1-4fa8-a34f-9bb4b3ee790e.gif', 'https://user-images.githubusercontent.com/25244017/209937517-be2e6f54-53f9-447d-ad38-4fab39624551.gif' 8 | s.license = { :type => 'MIT', :file => 'LICENSE' } 9 | s.author = { 'Dmitrii Medvedev' => 'dima7711@gmail.com' } 10 | s.source = { :git => 'https://github.com/batanus/DMScrollBar.git', :tag => s.version.to_s } 11 | s.swift_versions = ['5.7'] 12 | s.source_files = 'DMScrollBar/**/*' 13 | s.ios.deployment_target = '14.0' 14 | end -------------------------------------------------------------------------------- /DMScrollBar/DMScrollBar.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | public class DMScrollBar: UIView { 5 | 6 | // MARK: - Public 7 | 8 | public let configuration: Configuration 9 | 10 | // MARK: - Properties 11 | 12 | private weak var scrollView: UIScrollView? 13 | private weak var delegate: DMScrollBarDelegate? 14 | private let scrollIndicator = ScrollBarIndicator() 15 | private let infoView = ScrollBarInfoView() 16 | 17 | private var scrollIndicatorTopConstraint: NSLayoutConstraint? 18 | private var scrollIndicatorTrailingConstraint: NSLayoutConstraint? 19 | private var scrollIndicatorWidthConstraint: NSLayoutConstraint? 20 | private var scrollIndicatorHeightConstraint: NSLayoutConstraint? 21 | private var infoViewToScrollIndicatorConstraint: NSLayoutConstraint? 22 | 23 | private var cancellables = Set() 24 | private var hideTimer: Timer? 25 | private var panGestureRecognizer: UIPanGestureRecognizer? 26 | private var longPressGestureRecognizer: UILongPressGestureRecognizer? 27 | private var decelerateAnimation: TimerAnimation? 28 | 29 | private var scrollIndicatorOffsetOnGestureStart: CGFloat? 30 | private var wasInteractionStartedWithLongPress = false 31 | 32 | private var scrollViewLayoutGuide: UILayoutGuide? { 33 | configuration.indicator.insetsFollowsSafeArea ? 34 | scrollView?.safeAreaLayoutGuide : 35 | scrollView?.frameLayoutGuide 36 | } 37 | 38 | // MARK: - Initial setup 39 | 40 | public init( 41 | scrollView: UIScrollView, 42 | delegate: DMScrollBarDelegate? = nil, 43 | configuration: Configuration = .default 44 | ) { 45 | self.scrollView = scrollView 46 | self.configuration = configuration 47 | self.delegate = delegate 48 | super.init(frame: .zero) 49 | translatesAutoresizingMaskIntoConstraints = false 50 | setupScrollView() 51 | setupConstraints() 52 | setupScrollIndicator() 53 | setupAdditionalInfoView() 54 | setupInitialAlpha() 55 | observeScrollViewProperties() 56 | addGestureRecognizers() 57 | } 58 | 59 | override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 60 | let result = super.hitTest(point, with: event) 61 | guard result == self else { return result } 62 | return scrollIndicator.frame.minY...scrollIndicator.frame.maxY ~= point.y ? self : nil 63 | } 64 | 65 | required init?(coder: NSCoder) { 66 | fatalError() 67 | } 68 | 69 | private func setupConstraints() { 70 | guard let scrollView, let scrollViewLayoutGuide else { return } 71 | scrollView.addSubview(self) 72 | let minimumWidth: CGFloat = 20 73 | trailingAnchor.constraint(equalTo: scrollViewLayoutGuide.trailingAnchor).isActive = true 74 | topAnchor.constraint(equalTo: scrollViewLayoutGuide.topAnchor).isActive = true 75 | bottomAnchor.constraint(equalTo: scrollViewLayoutGuide.bottomAnchor).isActive = true 76 | widthAnchor.constraint(equalToConstant: max(minimumWidth, configuration.indicator.normalState.size.width)).isActive = true 77 | } 78 | 79 | private func setupInitialAlpha() { 80 | alpha = configuration.isAlwaysVisible ? 1 : 0 81 | } 82 | 83 | private func setupScrollView() { 84 | scrollView?.showsVerticalScrollIndicator = false 85 | scrollView?.layoutIfNeeded() 86 | } 87 | 88 | private func setupScrollIndicator() { 89 | addSubview(scrollIndicator) 90 | setup(stateConfig: configuration.indicator.normalState, indicatorTextConfig: nil) 91 | } 92 | 93 | private func setup( 94 | stateConfig: DMScrollBar.Configuration.Indicator.StateConfig, 95 | indicatorTextConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig? 96 | ) { 97 | let scrollIndicatorInitialDistance = configuration.indicator.animation.animationType == .fadeAndSide && !configuration.isAlwaysVisible && alpha == 0 ? 98 | stateConfig.size.width : 99 | -stateConfig.insets.right 100 | setupConstraint( 101 | constraint: &scrollIndicatorTrailingConstraint, 102 | build: { scrollIndicator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: $0) }, 103 | value: scrollIndicatorInitialDistance 104 | ) 105 | setupConstraint( 106 | constraint: &scrollIndicatorWidthConstraint, 107 | build: { scrollIndicator.widthAnchor.constraint(greaterThanOrEqualToConstant: $0) }, 108 | value: stateConfig.size.width 109 | ) 110 | setupConstraint( 111 | constraint: &scrollIndicatorHeightConstraint, 112 | build: scrollIndicator.heightAnchor.constraint(equalToConstant:), 113 | value: stateConfig.size.height 114 | ) 115 | if scrollIndicatorTopConstraint == nil { 116 | let topOffset = scrollIndicatorOffsetFromScrollOffset( 117 | scrollView?.contentOffset.y ?? 0, 118 | shouldAdjustOverscrollOffset: false 119 | ) 120 | scrollIndicatorTopConstraint = scrollIndicator.topAnchor.constraint(equalTo: topAnchor, constant: topOffset) 121 | scrollIndicatorTopConstraint?.isActive = true 122 | } 123 | scrollIndicator.setup( 124 | stateConfig: stateConfig, 125 | textConfig: indicatorTextConfig, 126 | accessibilityIdentifier: configuration.indicator.accessibilityIdentifier 127 | ) 128 | } 129 | 130 | private func setupAdditionalInfoView() { 131 | guard let infoLabelConfig = configuration.infoLabel else { return } 132 | addSubview(infoView) 133 | infoView.setup(config: infoLabelConfig) 134 | 135 | let offsetLabelInitialDistance = infoLabelConfig.animation.animationType == .fadeAndSide ? 0 : infoLabelConfig.distanceToScrollIndicator 136 | infoViewToScrollIndicatorConstraint = scrollIndicator.leadingAnchor.constraint(equalTo: infoView.trailingAnchor, constant: offsetLabelInitialDistance) 137 | infoViewToScrollIndicatorConstraint?.isActive = true 138 | if let maximumWidth = infoLabelConfig.maximumWidth { 139 | infoView.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true 140 | } else if let scrollViewLayoutGuide { 141 | infoView.leadingAnchor.constraint(greaterThanOrEqualTo: scrollViewLayoutGuide.leadingAnchor, constant: 8).isActive = true 142 | } 143 | infoView.centerYAnchor.constraint(equalTo: scrollIndicator.centerYAnchor).isActive = true 144 | } 145 | 146 | // MARK: - Scroll view observation 147 | 148 | private func observeScrollViewProperties() { 149 | scrollView? 150 | .publisher(for: \.contentOffset) 151 | .removeDuplicates() 152 | .withPrevious() 153 | .dropFirst(2) 154 | .sink { [weak self] in self?.handleScrollViewOffsetChange(previousOffset: $0, newOffset: $1) } 155 | .store(in: &cancellables) 156 | scrollView? 157 | .panGestureRecognizer 158 | .publisher(for: \.state) 159 | .dropFirst() 160 | .removeDuplicates() 161 | .sink { [weak self] in self?.handleScrollViewGestureState($0) } 162 | .store(in: &cancellables) 163 | /** 164 | Next observation is needed to keep scrollBar always on top, when new subviews are added to the scrollView. 165 | For example, when adding scrollBar to the tableView, the tableView section headers overlaps scrollBar, and therefore scrollBar gestures are not recognized. 166 | layer.sublayers property is used for observation because subviews property is not KVO compliant. 167 | */ 168 | scrollView? 169 | .publisher(for: \.layer.sublayers) 170 | .sink { [weak self] _ in self?.bringScrollBarToFront() } 171 | .store(in: &cancellables) 172 | } 173 | 174 | private func bringScrollBarToFront() { 175 | scrollView?.bringSubviewToFront(self) 176 | } 177 | 178 | private func handleScrollViewOffsetChange(previousOffset: CGPoint?, newOffset: CGPoint) { 179 | guard maxScrollViewOffset > 30 else { return } // Content size should be 30px larger than scrollView.height 180 | animateScrollBarShow() 181 | scrollIndicatorTopConstraint?.constant = scrollIndicatorOffsetFromScrollOffset( 182 | newOffset.y, 183 | shouldAdjustOverscrollOffset: panGestureRecognizer?.state == .possible && decelerateAnimation == nil 184 | ) 185 | startHideTimerIfNeeded() 186 | /// Next code is needed to keep additional info label and scroll bar titles up-to-date during scroll view decelerate 187 | guard isPanGestureInactive else { return } 188 | if infoView.alpha == 1 { 189 | updateAdditionalInfoViewState(forScrollOffset: newOffset.y, previousOffset: previousOffset?.y) 190 | } 191 | if scrollIndicator.isIndicatorLabelVisible { 192 | updateScrollIndicatorText( 193 | forScrollOffset: newOffset.y, 194 | previousOffset: previousOffset?.y, 195 | textConfig: configuration.indicator.activeState.textConfig 196 | ) 197 | } 198 | } 199 | 200 | private func handleScrollViewGestureState(_ state: UIGestureRecognizer.State) { 201 | invalidateDecelerateAnimation() 202 | animateAdditionalInfoViewHide() 203 | } 204 | 205 | // MARK: - Gesture Recognizers 206 | 207 | private func addGestureRecognizers() { 208 | let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)) 209 | panGestureRecognizer.delegate = self 210 | addGestureRecognizer(panGestureRecognizer) 211 | self.panGestureRecognizer = panGestureRecognizer 212 | 213 | let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture)) 214 | longPressGestureRecognizer.minimumPressDuration = 0.2 215 | longPressGestureRecognizer.delegate = self 216 | addGestureRecognizer(longPressGestureRecognizer) 217 | self.longPressGestureRecognizer = longPressGestureRecognizer 218 | 219 | scrollView?.gestureRecognizers?.forEach { 220 | $0.require(toFail: panGestureRecognizer) 221 | $0.require(toFail: longPressGestureRecognizer) 222 | } 223 | } 224 | 225 | @objc private func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { 226 | switch recognizer.state { 227 | case .began: handlePanGestureBegan(recognizer) 228 | case .changed: handlePanGestureChanged(recognizer) 229 | case .ended, .cancelled, .failed: handlePanGestureEnded(recognizer) 230 | default: break 231 | } 232 | } 233 | 234 | private func handlePanGestureBegan(_ recognizer: UIPanGestureRecognizer) { 235 | invalidateDecelerateAnimation() 236 | scrollIndicatorOffsetOnGestureStart = scrollIndicatorTopConstraint?.constant 237 | if wasInteractionStartedWithLongPress { 238 | wasInteractionStartedWithLongPress = false 239 | longPressGestureRecognizer?.cancel() 240 | } else { 241 | gestureInteractionStarted() 242 | } 243 | } 244 | 245 | private func handlePanGestureChanged(_ recognizer: UIPanGestureRecognizer) { 246 | guard let scrollView else { return } 247 | let offset = recognizer.translation(in: scrollView) 248 | let scrollIndicatorOffsetOnGestureStart = scrollIndicatorOffsetOnGestureStart ?? 0 249 | let scrollIndicatorOffset = scrollIndicatorOffsetOnGestureStart + offset.y 250 | let newScrollOffset = scrollOffsetFromScrollIndicatorOffset(scrollIndicatorOffset) 251 | let previousOffset = scrollView.contentOffset 252 | scrollView.setContentOffset(CGPoint(x: 0, y: newScrollOffset), animated: false) 253 | updateAdditionalInfoViewState(forScrollOffset: newScrollOffset, previousOffset: previousOffset.y) 254 | updateScrollIndicatorText( 255 | forScrollOffset: newScrollOffset, 256 | previousOffset: previousOffset.y, 257 | textConfig: configuration.indicator.activeState.textConfig 258 | ) 259 | } 260 | 261 | private func handlePanGestureEnded(_ recognizer: UIPanGestureRecognizer) { 262 | guard let scrollView else { return } 263 | scrollIndicatorOffsetOnGestureStart = nil 264 | let velocity = recognizer.velocity(in: scrollView).withZeroX 265 | let isSignificantVelocity = abs(velocity.y) > 100 266 | let isOffsetInScrollBounds = maxScrollViewOffset > minScrollViewOffset ? 267 | minScrollViewOffset...maxScrollViewOffset ~= scrollView.contentOffset.y : 268 | false 269 | gestureInteractionEnded(willDecelerate: isSignificantVelocity || !isOffsetInScrollBounds) 270 | switch (isSignificantVelocity, isOffsetInScrollBounds) { 271 | case (true, true): startDeceleration(withVelocity: velocity) 272 | case (true, false): bounceScrollViewToBoundsIfNeeded(velocity: velocity) 273 | case (false, true): 274 | #if !os(visionOS) 275 | generateHapticFeedback() 276 | #endif 277 | case (false, false): bounceScrollViewToBoundsIfNeeded(velocity: .zero) 278 | } 279 | } 280 | 281 | @objc private func handleLongPressGesture(_ recognizer: UILongPressGestureRecognizer) { 282 | switch recognizer.state { 283 | case .began: 284 | wasInteractionStartedWithLongPress = true 285 | gestureInteractionStarted() 286 | case .cancelled where panGestureRecognizer?.state.isInactive == true: 287 | gestureInteractionEnded(willDecelerate: false) 288 | #if !os(visionOS) 289 | generateHapticFeedback() 290 | #endif 291 | case .ended, .failed: 292 | gestureInteractionEnded(willDecelerate: false) 293 | #if !os(visionOS) 294 | generateHapticFeedback() 295 | #endif 296 | default: break 297 | } 298 | } 299 | 300 | private func gestureInteractionStarted() { 301 | let scrollOffset = scrollOffsetFromScrollIndicatorOffset(scrollIndicatorTopConstraint?.constant ?? 0) 302 | updateAdditionalInfoViewState(forScrollOffset: scrollOffset, previousOffset: nil) 303 | invalidateHideTimer() 304 | #if !os(visionOS) 305 | generateHapticFeedback() 306 | #endif 307 | updateScrollIndicatorText( 308 | forScrollOffset: scrollOffset, 309 | previousOffset: nil, 310 | textConfig: configuration.indicator.activeState.textConfig 311 | ) 312 | switch configuration.indicator.activeState { 313 | case .unchanged: break 314 | case .scaled(let factor): animateIndicatorStateChange(to: configuration.indicator.normalState.applying(scaleFactor: factor), textConfig: nil) 315 | case .custom(let config, let textConfig): animateIndicatorStateChange(to: config, textConfig: textConfig) 316 | } 317 | } 318 | 319 | private func gestureInteractionEnded(willDecelerate: Bool) { 320 | startHideTimerIfNeeded() 321 | switch configuration.indicator.activeState { 322 | case .unchanged: return 323 | case .custom(_, let textConfig) where textConfig != nil && willDecelerate: return 324 | case .custom, .scaled: animateIndicatorStateChange(to: configuration.indicator.normalState, textConfig: nil) 325 | } 326 | } 327 | 328 | private func animateIndicatorStateChange( 329 | to stateConfig: DMScrollBar.Configuration.Indicator.StateConfig, 330 | textConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig? 331 | ) { 332 | animate(duration: configuration.indicator.stateChangeAnimationDuration) { [weak self] in 333 | self?.setup(stateConfig: stateConfig, indicatorTextConfig: textConfig) 334 | self?.layoutIfNeeded() 335 | } 336 | } 337 | 338 | // MARK: - Deceleration & Bounce animations 339 | 340 | private var scale: CGFloat { 341 | #if os(visionOS) 342 | 1 343 | #else 344 | UIScreen.main.scale 345 | #endif 346 | } 347 | 348 | private func startDeceleration(withVelocity velocity: CGPoint) { 349 | guard let scrollView else { return } 350 | let parameters = DecelerationTimingParameters( 351 | initialValue: scrollIndicatorTopOffset, 352 | initialVelocity: velocity, 353 | decelerationRate: UIScrollView.DecelerationRate.normal.rawValue, 354 | threshold: 0.5 / scale 355 | ) 356 | 357 | let destination = parameters.destination 358 | let intersection = getIntersection( 359 | rect: scrollIndicatorOffsetBounds, 360 | segment: (scrollIndicatorTopOffset, destination) 361 | ) 362 | 363 | let duration: TimeInterval = { 364 | if let intersection, let intersectionDuration = parameters.duration(to: intersection) { 365 | return intersectionDuration 366 | } else { 367 | return parameters.duration 368 | } 369 | }() 370 | 371 | guard configuration.shouldDecelerate else { return } 372 | 373 | decelerateAnimation = TimerAnimation( 374 | duration: duration, 375 | animations: { [weak self] _, time in 376 | guard let self else { return } 377 | let newY = self.scrollOffsetFromScrollIndicatorOffset(parameters.value(at: time).y) 378 | if abs(scrollView.contentOffset.y - newY) < parameters.threshold { return } 379 | scrollView.setContentOffset(CGPoint(x: 0, y: newY), animated: false) 380 | }, completion: { [weak self] finished in 381 | guard let self else { return } 382 | guard finished && intersection != nil else { 383 | self.invalidateDecelerateAnimation() 384 | if self.configuration.indicator.activeState.textConfig != nil { 385 | self.animateIndicatorStateChange(to: self.configuration.indicator.normalState, textConfig: nil) 386 | } 387 | return 388 | } 389 | let velocity = parameters.velocity(at: duration) 390 | self.bounce(withVelocity: velocity) 391 | }) 392 | } 393 | 394 | private func bounce(withVelocity velocity: CGPoint, spring: Spring = .default) { 395 | guard let scrollView else { return } 396 | let velocityMultiplier = interval(1, maxScrollViewOffset / maxScrollIndicatorOffset, 30) 397 | let velocity = interval(-7000, velocity.y * velocityMultiplier, 7000) 398 | var previousScrollViewOffsetBounds = self.scrollViewOffsetBounds 399 | var restOffset = scrollView.contentOffset.clamped(to: self.scrollViewOffsetBounds) 400 | let displacement = scrollView.contentOffset - restOffset 401 | let threshold = 0.5 / scale 402 | var previousSafeInset = scrollView.safeAreaInsets 403 | 404 | let parameters = SpringTimingParameters( 405 | spring: spring, 406 | displacement: displacement, 407 | initialVelocity: CGPoint(x: 0, y: velocity), 408 | threshold: threshold 409 | ) 410 | 411 | decelerateAnimation = TimerAnimation( 412 | duration: parameters.duration, 413 | animations: { _, time in 414 | let topSafeInsetDif = previousSafeInset.top - scrollView.safeAreaInsets.top 415 | let bottomSafeInsetDif = previousSafeInset.bottom - scrollView.safeAreaInsets.bottom 416 | previousScrollViewOffsetBounds = previousScrollViewOffsetBounds.inset(by: UIEdgeInsets(top: topSafeInsetDif, left: 0, bottom: bottomSafeInsetDif, right: 0)) 417 | restOffset.y += self.scrollViewOffsetBounds.height - previousScrollViewOffsetBounds.height + topSafeInsetDif + bottomSafeInsetDif 418 | previousScrollViewOffsetBounds = self.scrollViewOffsetBounds 419 | previousSafeInset = scrollView.safeAreaInsets 420 | let offset = restOffset + parameters.value(at: time) 421 | scrollView.setContentOffset(offset, animated: false) 422 | }, 423 | completion: { [weak self] finished in 424 | guard let self else { return } 425 | self.invalidateDecelerateAnimation() 426 | if self.configuration.indicator.activeState.textConfig == nil { return } 427 | self.animateIndicatorStateChange(to: self.configuration.indicator.normalState, textConfig: nil) 428 | } 429 | ) 430 | } 431 | 432 | private func bounceScrollViewToBoundsIfNeeded(velocity: CGPoint) { 433 | guard let scrollView else { return } 434 | let overscroll: CGFloat = { 435 | if scrollView.contentOffset.y < minScrollViewOffset { 436 | return minScrollViewOffset - scrollView.contentOffset.y 437 | } else if scrollView.contentOffset.y > maxScrollViewOffset { 438 | return scrollView.contentOffset.y - maxScrollViewOffset 439 | } 440 | return 0 441 | }() 442 | if overscroll == 0 { return } 443 | let additionalStiffness = (overscroll / scrollView.frame.height) * 400 444 | bounce(withVelocity: velocity, spring: Spring(mass: 1, stiffness: 100 + additionalStiffness, dampingRatio: 1)) 445 | } 446 | 447 | private func invalidateDecelerateAnimation() { 448 | decelerateAnimation?.invalidate() 449 | decelerateAnimation = nil 450 | } 451 | 452 | // MARK: - Calculations 453 | 454 | private var minScrollIndicatorOffset: CGFloat { 455 | return configuration.indicator.normalState.insets.top 456 | } 457 | 458 | private var maxScrollIndicatorOffset: CGFloat { 459 | return frame.height - configuration.indicator.normalState.size.height - configuration.indicator.normalState.insets.bottom 460 | } 461 | 462 | private var minScrollViewOffset: CGFloat { 463 | guard let scrollView else { return 0 } 464 | return -scrollView.contentInset.top - scrollView.safeAreaInsets.top 465 | } 466 | 467 | private var maxScrollViewOffset: CGFloat { 468 | guard let scrollView else { return 0 } 469 | return scrollView.contentSize.height - scrollView.frame.height + scrollView.safeAreaInsets.bottom + scrollView.contentInset.bottom 470 | } 471 | 472 | private var scrollIndicatorOffsetBounds: CGRect { 473 | CGRect( 474 | x: 0, 475 | y: minScrollIndicatorOffset, 476 | width: CGFloat.leastNonzeroMagnitude, 477 | height: maxScrollIndicatorOffset - minScrollIndicatorOffset 478 | ) 479 | } 480 | 481 | private var scrollViewOffsetBounds: CGRect { 482 | CGRect( 483 | x: 0, 484 | y: minScrollViewOffset, 485 | width: CGFloat.leastNonzeroMagnitude, 486 | height: maxScrollViewOffset - minScrollViewOffset 487 | ) 488 | } 489 | 490 | private var scrollIndicatorTopOffset: CGPoint { 491 | CGPoint(x: 0, y: scrollIndicatorTopConstraint?.constant ?? 0) 492 | } 493 | 494 | private func scrollOffsetFromScrollIndicatorOffset(_ scrollIndicatorOffset: CGFloat) -> CGFloat { 495 | let adjustedScrollIndicatorOffset = adjustedScrollIndicatorOffsetForOverscroll(scrollIndicatorOffset, isPanGestureSource: true) 496 | let scrollIndicatorOffsetPercent = (adjustedScrollIndicatorOffset - minScrollIndicatorOffset) / (maxScrollIndicatorOffset - minScrollIndicatorOffset) 497 | let scrollOffset = scrollIndicatorOffsetPercent * (maxScrollViewOffset - minScrollViewOffset) + minScrollViewOffset 498 | 499 | return scrollOffset 500 | } 501 | 502 | private func scrollIndicatorOffsetFromScrollOffset(_ scrollOffset: CGFloat, shouldAdjustOverscrollOffset: Bool) -> CGFloat { 503 | let scrollOffsetPercent = (scrollOffset - minScrollViewOffset) / (maxScrollViewOffset - minScrollViewOffset) 504 | let scrollIndicatorOffset = scrollOffsetPercent * (maxScrollIndicatorOffset - minScrollIndicatorOffset) + minScrollIndicatorOffset 505 | 506 | return shouldAdjustOverscrollOffset ? 507 | adjustedScrollIndicatorOffsetForOverscroll(scrollIndicatorOffset, isPanGestureSource: false) : 508 | scrollIndicatorOffset 509 | } 510 | 511 | private func adjustedScrollIndicatorOffsetForOverscroll(_ offset: CGFloat, isPanGestureSource: Bool) -> CGFloat { 512 | let indicatorToScrollRatio = scrollIndicatorOffsetBounds.height / scrollViewOffsetBounds.height 513 | let coefficient = isPanGestureSource ? 514 | RubberBand.defaultCoefficient * indicatorToScrollRatio : 515 | RubberBand.defaultCoefficient / indicatorToScrollRatio 516 | let adjustedCoefficient = interval(0.1, coefficient, RubberBand.defaultCoefficient) 517 | 518 | return RubberBand( 519 | coeff: adjustedCoefficient, 520 | dims: frame.size, 521 | bounds: scrollIndicatorOffsetBounds 522 | ).clamp(CGPoint(x: 0, y: offset)).y 523 | } 524 | 525 | // MARK: - Private methods 526 | 527 | private var isPanGestureInactive: Bool { 528 | return panGestureRecognizer?.state.isInactive == true 529 | } 530 | 531 | private func scrollIndicatorOffset(forContentOffset contentOffset: CGFloat) -> CGFloat { 532 | return contentOffset + scrollIndicatorTopOffset.y + infoView.frame.height / 2 533 | } 534 | 535 | private func startHideTimerIfNeeded() { 536 | guard isPanGestureInactive else { return } 537 | invalidateHideTimer() 538 | hideTimer = Timer.scheduledTimer( 539 | withTimeInterval: configuration.hideTimeInterval, 540 | repeats: false 541 | ) { [weak self] _ in 542 | self?.animateScrollBarHide() 543 | self?.invalidateHideTimer() 544 | } 545 | } 546 | 547 | private func invalidateHideTimer() { 548 | hideTimer?.invalidate() 549 | hideTimer = nil 550 | } 551 | 552 | private func updateAdditionalInfoViewState(forScrollOffset scrollViewOffset: CGFloat, previousOffset: CGFloat?) { 553 | if configuration.infoLabel == nil { return } 554 | guard let offsetLabelText = delegate?.infoLabelText( 555 | forContentOffset: scrollViewOffset, 556 | scrollIndicatorOffset: scrollIndicatorOffset(forContentOffset: scrollViewOffset) 557 | ) else { return animateAdditionalInfoViewHide() } 558 | animateAdditionalInfoViewShow() 559 | let direction: CATransitionSubtype? = { 560 | guard let previousOffset else { return nil } 561 | return scrollViewOffset > previousOffset ? .fromTop : .fromBottom 562 | }() 563 | infoView.updateText(text: offsetLabelText, direction: direction) 564 | } 565 | 566 | private func updateScrollIndicatorText( 567 | forScrollOffset scrollViewOffset: CGFloat, 568 | previousOffset: CGFloat?, 569 | textConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig? 570 | ) { 571 | let direction: CATransitionSubtype? = { 572 | guard let previousOffset else { return nil } 573 | return scrollViewOffset > previousOffset ? .fromTop : .fromBottom 574 | }() 575 | scrollIndicator.updateScrollIndicatorText( 576 | direction: direction, 577 | scrollBarLabelText: delegate?.scrollBarText( 578 | forContentOffset: scrollViewOffset, 579 | scrollIndicatorOffset: scrollIndicatorOffset(forContentOffset: scrollViewOffset) 580 | ), 581 | textConfig: textConfig 582 | ) 583 | } 584 | 585 | private func animateScrollBarShow() { 586 | guard alpha == 0 else { return } 587 | setup(stateConfig: configuration.indicator.normalState, indicatorTextConfig: nil) 588 | layoutIfNeeded() 589 | animate(duration: configuration.indicator.animation.showDuration) { [weak self] in 590 | guard let self else { return } 591 | self.alpha = 1 592 | guard self.configuration.indicator.animation.animationType == .fadeAndSide else { return } 593 | self.scrollIndicatorTrailingConstraint?.constant = -self.configuration.indicator.normalState.insets.right 594 | self.layoutIfNeeded() 595 | } 596 | } 597 | 598 | private func animateScrollBarHide() { 599 | if alpha == 0 { return } 600 | defer { animateAdditionalInfoViewHide() } 601 | if configuration.isAlwaysVisible { return } 602 | animate(duration: configuration.indicator.animation.hideDuration) { [weak self] in 603 | guard let self else { return } 604 | self.alpha = 0 605 | guard self.configuration.indicator.animation.animationType == .fadeAndSide else { return } 606 | self.scrollIndicatorTrailingConstraint?.constant = self.configuration.indicator.normalState.size.width 607 | self.layoutIfNeeded() 608 | } 609 | } 610 | 611 | private func animateAdditionalInfoViewShow() { 612 | guard let infoLabelConfig = configuration.infoLabel, infoView.alpha == 0 else { return } 613 | animate(duration: infoLabelConfig.animation.showDuration) { [weak self] in 614 | self?.infoView.alpha = 1 615 | guard infoLabelConfig.animation.animationType == .fadeAndSide else { return } 616 | self?.infoViewToScrollIndicatorConstraint?.constant = infoLabelConfig.distanceToScrollIndicator 617 | self?.layoutIfNeeded() 618 | } 619 | } 620 | 621 | private func animateAdditionalInfoViewHide() { 622 | guard let infoLabelConfig = configuration.infoLabel, infoView.alpha != 0 else { return } 623 | animate(duration: infoLabelConfig.animation.hideDuration) { [weak self] in 624 | self?.infoView.alpha = 0 625 | guard infoLabelConfig.animation.animationType == .fadeAndSide else { return } 626 | self?.infoViewToScrollIndicatorConstraint?.constant = 0 627 | self?.layoutIfNeeded() 628 | } 629 | } 630 | 631 | private func animate(duration: CGFloat, animation: @escaping () -> Void) { 632 | UIView.animate( 633 | withDuration: duration, 634 | delay: 0, 635 | usingSpringWithDamping: 1, 636 | initialSpringVelocity: 0, 637 | options: [.allowUserInteraction, .beginFromCurrentState, .curveEaseInOut], 638 | animations: animation 639 | ) 640 | } 641 | } 642 | 643 | // MARK: - UIGestureRecognizerDelegate 644 | 645 | extension DMScrollBar: UIGestureRecognizerDelegate { 646 | public func gestureRecognizer( 647 | _ gestureRecognizer: UIGestureRecognizer, 648 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer 649 | ) -> Bool { 650 | return gestureRecognizer == panGestureRecognizer && otherGestureRecognizer == longPressGestureRecognizer || 651 | gestureRecognizer == longPressGestureRecognizer && otherGestureRecognizer == panGestureRecognizer 652 | } 653 | 654 | public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 655 | return true 656 | } 657 | 658 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 659 | return scrollIndicator.frame.minY...scrollIndicator.frame.maxY ~= touch.location(in: self).y 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /DMScrollBar/DMScrollBarConfiguration.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension DMScrollBar { 4 | public struct Configuration: Equatable { 5 | /// Indicates if the scrollbar should always be visible 6 | public let isAlwaysVisible: Bool 7 | 8 | /// Number of seconds after which the scrollbar should be hidden after being inactive 9 | public let hideTimeInterval: TimeInterval 10 | 11 | /// Indicates if scroll view should decelerate when ending scroll bar interaction with velocity 12 | public let shouldDecelerate: Bool 13 | 14 | /// Indicator configuration, which is placed on the right side 15 | public let indicator: Indicator 16 | 17 | /// Info label configuration, which appears during indicator scrolling. If nil - the info label will be hidden 18 | public let infoLabel: InfoLabel? 19 | 20 | /// - Parameters: 21 | /// - isAlwaysVisible: Indicates if the scrollbar should always be visible 22 | /// - hideTimeInterval: Number of seconds after which the scrollbar should be hidden after being inactive 23 | /// - shouldDecelerate: Indicates if scroll view should decelerate when ending scroll bar interaction with velocity 24 | /// - indicator: Scroll bar indicator configuration, which is placed on the right side 25 | /// - infoLabel: Info label configuration, which appears during indicator scrolling. If passing nil - the info label will be hidden 26 | public init( 27 | isAlwaysVisible: Bool = false, 28 | hideTimeInterval: TimeInterval = 2, 29 | shouldDecelerate: Bool = true, 30 | indicator: Indicator = .default, 31 | infoLabel: InfoLabel? = .default 32 | ) { 33 | self.isAlwaysVisible = isAlwaysVisible 34 | self.hideTimeInterval = hideTimeInterval 35 | self.shouldDecelerate = shouldDecelerate 36 | self.indicator = indicator 37 | self.infoLabel = infoLabel 38 | } 39 | 40 | /// Default scroll bar configuration 41 | public static let `default` = Configuration() 42 | 43 | /// iOS native scroll bar style configuration 44 | public static let iosStyle = Configuration( 45 | indicator: .init( 46 | normalState: .iosStyle(width: 3), 47 | activeState: .custom(config: .iosStyle(width: 8)), 48 | animation: .defaultTiming(with: .fade) 49 | ) 50 | ) 51 | } 52 | } 53 | 54 | extension DMScrollBar.Configuration { 55 | public struct RoundedCorners: Equatable { 56 | /// Corner radius, which will be applied to all corners 57 | public let radius: Radius 58 | 59 | /// Set of corners that will be rounded 60 | public let corners: Set 61 | 62 | /// - Parameters: 63 | /// - radius: Corner radius, which will be applied to all corners 64 | /// - corners: Set of corners that will be rounded 65 | public init(radius: Radius, corners: Set) { 66 | self.radius = radius 67 | self.corners = corners 68 | } 69 | 70 | /// All corners will not be rounded 71 | public static let notRounded = RoundedCorners(radius: .notRounded, corners: []) 72 | 73 | /// Top left and bottom left corners will be rounded by a radius equal to half the view's height 74 | public static let roundedLeftCorners = RoundedCorners(radius: .rounded, corners: [.topLeft, .bottomLeft]) 75 | 76 | /// Top right and bottom right corners will be rounded by a radius equal to half the view's height 77 | public static let roundedRightCorners = RoundedCorners(radius: .rounded, corners: [.topRight, .bottomRight]) 78 | 79 | /// All corners will be rounded by a radius equal to half the view's height 80 | public static let allRounded = RoundedCorners(radius: .rounded, corners: Set(Corner.allCases)) 81 | 82 | public enum Radius: Equatable { 83 | /// Not rounded corners 84 | case notRounded 85 | /// Half of the view's height 86 | case rounded 87 | /// User defined corner radius 88 | case custom(CGFloat) 89 | } 90 | 91 | public enum Corner: CaseIterable, Equatable { 92 | /// Represents the top left corner. 93 | case topLeft 94 | /// Represents the top right corner. 95 | case topRight 96 | /// Represents the bottom left corner. 97 | case bottomLeft 98 | /// Represents the bottom right corner. 99 | case bottomRight 100 | } 101 | } 102 | 103 | public enum AnimationType: Equatable { 104 | /// Alpha appearance / disappearance animation 105 | case fade 106 | /// Alpha & side appearance / disappearance animation 107 | case fadeAndSide 108 | } 109 | 110 | public struct Animation: Equatable { 111 | /// Time in seconds for the appearance animation to take place 112 | public let showDuration: TimeInterval 113 | 114 | /// Time in seconds for the disappearance animation to take place 115 | public let hideDuration: TimeInterval 116 | 117 | /// Animation type for appearance / disappearance 118 | public let animationType: AnimationType 119 | 120 | /// - Parameters: 121 | /// - showDuration: Time in seconds for the appearance animation to take place 122 | /// - hideDuration: Time in seconds for the disappearance animation to take place 123 | /// - animationType: Animation type for appearance / disappearance 124 | public init(showDuration: TimeInterval, hideDuration: TimeInterval, animationType: AnimationType) { 125 | self.showDuration = showDuration 126 | self.hideDuration = hideDuration 127 | self.animationType = animationType 128 | } 129 | 130 | /// Default animation configuration 131 | public static var `default` = defaultTiming(with: .fadeAndSide) 132 | 133 | /// Animation with default timings and passed animation type 134 | public static func defaultTiming(with animationType: AnimationType) -> Animation { 135 | Animation(showDuration: 0.2, hideDuration: 0.4, animationType: animationType) 136 | } 137 | } 138 | 139 | public struct Indicator: Equatable { 140 | /// Configuration for indicator state while the user is not interacting with it 141 | public let normalState: StateConfig 142 | 143 | /// Configuration for indicator state while the user interacting with it 144 | public let activeState: ActiveStateConfig 145 | 146 | /// Time in seconds for the state change animation to take place 147 | public let stateChangeAnimationDuration: TimeInterval 148 | 149 | /// Indicates if safe area insets should be taken into account 150 | public let insetsFollowsSafeArea: Bool 151 | 152 | /// Scroll bar indicator show / hide animation settings 153 | public let animation: Animation 154 | 155 | /// Accessibility identifier of the indicator 156 | public let accessibilityIdentifier: String? 157 | 158 | /// - Parameters: 159 | /// - normalState: Configuration for indicator state while the user is not interacting with it 160 | /// - activeState: Configuration for indicator state while the user interacting with it 161 | /// - stateChangeAnimationDuration: Time in seconds for the state change animation to take place 162 | /// - insetsFollowsSafeArea: Indicates if safe area insets should be taken into account 163 | /// - animation: Scroll bar indicator show / hide animation settings 164 | /// - accessibilityIdentifier: Accessibility identifier of the indicator 165 | public init( 166 | normalState: StateConfig = .default, 167 | activeState: ActiveStateConfig = .unchanged, 168 | stateChangeAnimationDuration: TimeInterval = 0.3, 169 | insetsFollowsSafeArea: Bool = true, 170 | animation: Animation = .default, 171 | accessibilityIdentifier: String? = nil 172 | ) { 173 | self.normalState = normalState 174 | self.activeState = activeState 175 | self.stateChangeAnimationDuration = stateChangeAnimationDuration 176 | self.insetsFollowsSafeArea = insetsFollowsSafeArea 177 | self.animation = animation 178 | self.accessibilityIdentifier = accessibilityIdentifier 179 | } 180 | 181 | /// Default indicator configuration 182 | public static let `default` = Indicator() 183 | 184 | public struct StateConfig: Equatable { 185 | /// Size of the scroll bar indicator, which is placed on the right side 186 | public let size: CGSize 187 | 188 | /// Background color of the scroll bar indicator 189 | public let backgroundColor: UIColor 190 | 191 | /// Scroll bar indicator insets 192 | public let insets: UIEdgeInsets 193 | 194 | /// Scroll bar indicator content insets 195 | public let contentInsets: UIEdgeInsets 196 | 197 | /// Scroll bar image 198 | public let image: UIImage? 199 | 200 | /// Scroll bar image size 201 | public let imageSize: CGSize 202 | 203 | /// Accessibility identifier of the image 204 | public let imageAccessibilityIdentifier: String? 205 | 206 | /// Scroll bar indicator corners which should be rounded 207 | public let roundedCorners: RoundedCorners 208 | 209 | /// - Parameters: 210 | /// - size: Size of the scroll bar indicator, which is placed on the right side 211 | /// - backgroundColor: Background color of the scroll bar indicator 212 | /// - insets: Scroll bar indicator insets 213 | /// - contentInsets: Scroll bar indicator content insets 214 | /// - image: Scroll bar image 215 | /// - imageSize: Scroll bar image size. If a nil image is passed - this parameter is ignored 216 | /// - imageAccessibilityIdentifier: Accessibility identifier of the image 217 | /// - roundedCorners: Scroll bar indicator corners which should be rounded 218 | public init( 219 | size: CGSize = CGSize(width: 34, height: 34), 220 | backgroundColor: UIColor = UIColor.defaultScrollBarBackground, 221 | insets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0), 222 | contentInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), 223 | image: UIImage? = UIImage(systemName: "arrow.up.and.down.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.systemBackground), 224 | imageSize: CGSize = CGSize(width: 20, height: 20), 225 | imageAccessibilityIdentifier: String? = nil, 226 | roundedCorners: RoundedCorners = .roundedLeftCorners 227 | ) { 228 | self.size = size 229 | self.backgroundColor = backgroundColor 230 | self.insets = insets 231 | self.contentInsets = contentInsets 232 | self.image = image 233 | self.imageSize = imageSize 234 | self.imageAccessibilityIdentifier = imageAccessibilityIdentifier 235 | self.roundedCorners = roundedCorners 236 | } 237 | 238 | /// Default state configuration 239 | public static let `default` = StateConfig() 240 | 241 | /// iOS native style configuration for scroll bar indicator 242 | public static func iosStyle(width: CGFloat) -> StateConfig { 243 | StateConfig( 244 | size: .init(width: width, height: 100), 245 | backgroundColor: UIColor.label.withAlphaComponent(0.35), 246 | insets: .init(top: 4, left: 0, bottom: 4, right: 2), 247 | contentInsets: .zero, 248 | image: nil, 249 | roundedCorners: .allRounded 250 | ) 251 | } 252 | } 253 | 254 | public enum ActiveStateConfig: Equatable { 255 | /// Use the same configuration as for normal state 256 | case unchanged 257 | /// Use the same configuration as for normal state but scaled with specified factor. F. e. factor = 1 will not scale indicator size, factor = 2 will scale indicator size by 2 times. If in normal state indicator size is 30x30, active state with scale factor = 2 will have size 60x60 258 | case scaled(factor: CGFloat) 259 | /// Use custom configuration for active state 260 | /// - Parameters: 261 | /// - config: State configuration for indicator state while the user is interacting with it 262 | /// - text: Scroll bar text config that appears to the right of the image 263 | case custom(config: StateConfig, textConfig: TextConfig? = nil) 264 | 265 | public struct TextConfig: Equatable { 266 | /// Text label insets from 267 | public let insets: UIEdgeInsets 268 | /// Font that should be used for text 269 | public let font: UIFont 270 | /// Text color of the label 271 | public let color: UIColor 272 | /// Accessibility identifier of the label 273 | public let accessibilityIdentifier: String? 274 | 275 | /// - Parameters: 276 | /// - insets: Text label insets from 277 | /// - font: Font that should be used for text 278 | /// - color: Text color of the label 279 | /// - accessibilityIdentifier: Accessibility identifier of the label 280 | public init( 281 | insets: UIEdgeInsets, 282 | font: UIFont, 283 | color: UIColor, 284 | accessibilityIdentifier: String? = nil 285 | ) { 286 | self.insets = insets 287 | self.font = font 288 | self.color = color 289 | self.accessibilityIdentifier = accessibilityIdentifier 290 | } 291 | } 292 | } 293 | } 294 | 295 | public struct InfoLabel: Equatable { 296 | /// Indicates the font that should be used for info label, which appears during indicator scrolling 297 | public let font: UIFont 298 | 299 | /// Text color of the info label 300 | public let textColor: UIColor 301 | 302 | /// Horizontal distance from the info label to the scroll indicator 303 | public let distanceToScrollIndicator: CGFloat 304 | 305 | /// Background color of the info label 306 | public let backgroundColor: UIColor 307 | 308 | /// Indicates text insets from the info label to its background 309 | public let textInsets: UIEdgeInsets 310 | 311 | /// Indicates maximum width of info label. If nil is passed - the info label will grow maximum to the leading side of the screen 312 | public let maximumWidth: CGFloat? 313 | 314 | /// Info label corners which should be rounded 315 | public let roundedCorners: RoundedCorners 316 | 317 | /// Info label show/hide animation settings 318 | public let animation: Animation 319 | 320 | /// Accessibility identifier of the info label 321 | public let accessibilityIdentifier: String? 322 | 323 | /// - Parameters: 324 | /// - font: Indicates the font that should be used for info label, which appears during indicator scrolling 325 | /// - textColor: Text color of the info label 326 | /// - distanceToScrollIndicator: Horizontal distance from the info label to the scroll indicator 327 | /// - backgroundColor: Background color of the info label 328 | /// - textInsets: Indicates text insets from the info label to its background 329 | /// - maximumWidth: Indicates maximum width of info label. If nil is passed - the info label will grow maximum to the leading side of the screen 330 | /// - roundedCorners: Info label corners which should be rounded 331 | /// - animation: Info label show/hide animation settings 332 | /// - accessibilityIdentifier: Accessibility identifier of the info label 333 | public init( 334 | font: UIFont = UIFont.systemFont(ofSize: 13), 335 | textColor: UIColor = UIColor.systemBackground, 336 | distanceToScrollIndicator: CGFloat = 40, 337 | backgroundColor: UIColor = UIColor.defaultScrollBarBackground, 338 | textInsets: UIEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10), 339 | maximumWidth: CGFloat? = nil, 340 | roundedCorners: RoundedCorners = .allRounded, 341 | animation: Animation = .default, 342 | accessibilityIdentifier: String? = nil 343 | ) { 344 | self.font = font 345 | self.textColor = textColor 346 | self.distanceToScrollIndicator = distanceToScrollIndicator 347 | self.backgroundColor = backgroundColor 348 | self.textInsets = textInsets 349 | self.maximumWidth = maximumWidth 350 | self.roundedCorners = roundedCorners 351 | self.animation = animation 352 | self.accessibilityIdentifier = accessibilityIdentifier 353 | } 354 | 355 | /// Default info label configuration 356 | public static let `default` = InfoLabel() 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /DMScrollBar/DMScrollBarDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol DMScrollBarDelegate: AnyObject { 4 | /// This method is triggered every time when scroll bar offset changes while the user is dragging it 5 | /// - Parameter contentOffset: Scroll view content offset 6 | /// - Parameter scrollIndicatorOffset: Scroll indicator offset 7 | /// - Returns: Text to present in info label (which appears during indicator scrolling to the left of the Scroll Bar) . If returning nil - the info label will not show 8 | func infoLabelText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? 9 | 10 | /// This method is triggered every time when scroll bar offset changes while the user is dragging it 11 | /// - Parameter contentOffset: Scroll view content offset 12 | /// - Parameter scrollIndicatorOffset: Scroll indicator offset 13 | /// - Returns: Text to present in scroll bar label. This method is not triggered when Configuration.Indicator.StateConfig.TextConfig is nil. 14 | func scrollBarText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? 15 | } 16 | 17 | // MARK: - DMScrollBarDelegate extension for Table View 18 | 19 | public extension DMScrollBarDelegate { 20 | func infoLabelText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? { nil } 21 | 22 | func scrollBarText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? { nil } 23 | 24 | /// This is a convenience method to get the header title for the section at the specified content offset. This method will not work for table views with custom headers 25 | /// - Parameters: 26 | /// - tableView: Table view in which the scroll bar is located 27 | /// - offset: Table View content offset 28 | /// - Returns: Indicator title to present in info label. If returning nil - the info label will not show 29 | func headerTitle(in tableView: UITableView, forOffset offset: CGFloat) -> String? { 30 | guard let section = sectionIndex(in: tableView, forOffset: offset) else { return nil } 31 | 32 | return tableView.dataSource?.tableView?(tableView, titleForHeaderInSection: section) ?? 33 | tableView.headerView(forSection: section)?.textLabel?.text ?? 34 | (tableView.headerView(forSection: section)?.contentConfiguration as? UIListContentConfiguration)?.text 35 | } 36 | 37 | /// This is a convenience method to get section index for specified table view content offset 38 | /// - Parameters: 39 | /// - tableView: Table view in which the scroll bar is located 40 | /// - offset: Table View content offset 41 | /// - Returns: Section index in table view for specified table view content offset 42 | func sectionIndex(in tableView: UITableView, forOffset offset: CGFloat) -> Int? { 43 | (0.. Int? { 57 | (0.. CGPoint? { 75 | let indexPath = IndexPath(item: 0, section: section) 76 | let kind = UICollectionView.elementKindSectionHeader 77 | guard isValid(indexPath: indexPath, in: collectionView) else { return nil } 78 | guard let attributes = collectionView.layoutAttributesForSupplementaryElement( 79 | ofKind: kind, 80 | at: indexPath 81 | ) else { return nil } 82 | 83 | return attributes.frame.origin 84 | } 85 | 86 | private func isValid(indexPath: IndexPath, in collectionView: UICollectionView) -> Bool { 87 | guard indexPath.section < collectionView.numberOfSections else { return false } 88 | let numberOfItems = collectionView.numberOfItems(inSection: indexPath.section) 89 | 90 | return indexPath.row < numberOfItems || numberOfItems == 0 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/DecelerationTimingParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | struct DecelerationTimingParameters { 5 | var initialValue: CGPoint 6 | var initialVelocity: CGPoint 7 | var decelerationRate: CGFloat 8 | var threshold: CGFloat 9 | } 10 | 11 | extension DecelerationTimingParameters { 12 | var destination: CGPoint { 13 | let dCoeff = 1000 * log(decelerationRate) 14 | return initialValue - initialVelocity / dCoeff 15 | } 16 | 17 | var duration: TimeInterval { 18 | guard initialVelocity.length > 0 else { return 0 } 19 | let dCoeff = 1000 * log(decelerationRate) 20 | return TimeInterval(log(-dCoeff * threshold / initialVelocity.length) / dCoeff) 21 | } 22 | 23 | func value(at time: TimeInterval) -> CGPoint { 24 | let dCoeff = 1000 * log(decelerationRate) 25 | return initialValue + (pow(decelerationRate, CGFloat(1000 * time)) - 1) / dCoeff * initialVelocity 26 | } 27 | 28 | func duration(to value: CGPoint) -> TimeInterval? { 29 | guard value.distance(toSegment: (initialValue, destination)) < threshold else { return nil } 30 | let dCoeff = 1000 * log(decelerationRate) 31 | return TimeInterval(log(1.0 + dCoeff * (value - initialValue).length / initialVelocity.length) / dCoeff) 32 | } 33 | 34 | func velocity(at time: TimeInterval) -> CGPoint { 35 | return initialVelocity * pow(decelerationRate, CGFloat(1000 * time)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/GeometryUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | func getIntersection(segment1: (CGPoint, CGPoint), segment2: (CGPoint, CGPoint)) -> CGPoint? { 5 | let p1 = segment1.0 6 | let p2 = segment1.1 7 | let p3 = segment2.0 8 | let p4 = segment2.1 9 | let d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x) 10 | 11 | if d == 0 { return nil } // parallel lines 12 | 13 | let u = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d 14 | let v = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / d 15 | 16 | if u < 0.0 || u > 1.0 { return nil } // intersection point is not between p1 and p2 17 | if v < 0.0 || v > 1.0 { return nil } // intersection point is not between p3 and p4 18 | 19 | return CGPoint(x: p1.x + u * (p2.x - p1.x), y: p1.y + u * (p2.y - p1.y)) 20 | } 21 | 22 | 23 | func getIntersection(rect: CGRect, segment: (CGPoint, CGPoint)) -> CGPoint? { 24 | let rMinMin = CGPoint(x: rect.minX, y: rect.minY) 25 | let rMinMax = CGPoint(x: rect.minX, y: rect.maxY) 26 | let rMaxMin = CGPoint(x: rect.maxX, y: rect.minY) 27 | let rMaxMax = CGPoint(x: rect.maxX, y: rect.maxY) 28 | 29 | if let point = getIntersection(segment1: (rMinMin, rMinMax), segment2: segment) { return point } 30 | if let point = getIntersection(segment1: (rMinMin, rMaxMin), segment2: segment) { return point } 31 | if let point = getIntersection(segment1: (rMinMax, rMaxMax), segment2: segment) { return point } 32 | if let point = getIntersection(segment1: (rMaxMin, rMaxMax), segment2: segment) { return point } 33 | 34 | return nil 35 | } 36 | 37 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/RubberBand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | func rubberBandClamp(_ x: CGFloat, coeff: CGFloat, dim: CGFloat) -> CGFloat { 5 | return (1.0 - (1.0 / (x * coeff / dim + 1.0))) * dim 6 | } 7 | 8 | func rubberBandClamp(_ x: CGFloat, coeff: CGFloat, dim: CGFloat, limits: ClosedRange) -> CGFloat { 9 | let clampedX = x.clamped(to: limits) 10 | let diff = abs(x - clampedX) 11 | let sign: CGFloat = clampedX > x ? -1 : 1 12 | return clampedX + sign * rubberBandClamp(diff, coeff: coeff, dim: dim) 13 | } 14 | 15 | struct RubberBand { 16 | static let defaultCoefficient = 0.55 17 | var coeff: CGFloat 18 | var dims: CGSize 19 | var bounds: CGRect 20 | 21 | init(coeff: CGFloat = defaultCoefficient, dims: CGSize, bounds: CGRect) { 22 | self.coeff = coeff 23 | self.dims = dims 24 | self.bounds = bounds 25 | } 26 | 27 | func clamp(_ point: CGPoint) -> CGPoint { 28 | let x = rubberBandClamp(point.x, coeff: coeff, dim: dims.width, limits: bounds.minX...bounds.maxX) 29 | let y = rubberBandClamp(point.y, coeff: coeff, dim: dims.height, limits: bounds.minY...bounds.maxY) 30 | return CGPoint(x: x, y: y) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/SpringTimingParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | /// https://en.wikipedia.org/wiki/Harmonic_oscillator 5 | /// 6 | /// System's equation of motion: 7 | /// 8 | /// 0 < dampingRatio < 1: 9 | /// x(t) = exp(-beta * t) * (c1 * sin(w' * t) + c2 * cos(w' * t)) 10 | /// c1 = x0 11 | /// c2 = (v0 + beta * x0) / w' 12 | /// 13 | /// dampingRatio == 1: 14 | /// x(t) = exp(-beta * t) * (c1 + c2 * t) 15 | /// c1 = x0 16 | /// c2 = (v0 + beta * x0) 17 | /// 18 | /// x0 - initial displacement 19 | /// v0 - initial velocity 20 | /// beta = damping / (2 * mass) 21 | /// w0 = sqrt(stiffness / mass) - natural frequency 22 | /// w' = sqrt(w0 * w0 - beta * beta) - damped natural frequency 23 | 24 | struct Spring { 25 | var mass: CGFloat 26 | var stiffness: CGFloat 27 | var dampingRatio: CGFloat 28 | } 29 | 30 | extension Spring { 31 | static var `default`: Spring { 32 | return Spring(mass: 1, stiffness: 100, dampingRatio: 1) 33 | } 34 | } 35 | 36 | extension Spring { 37 | var damping: CGFloat { 38 | return 2 * dampingRatio * sqrt(mass * stiffness) 39 | } 40 | 41 | var beta: CGFloat { 42 | return damping / (2 * mass) 43 | } 44 | 45 | var dampedNaturalFrequency: CGFloat { 46 | return sqrt(stiffness / mass) * sqrt(1 - dampingRatio * dampingRatio) 47 | } 48 | } 49 | 50 | struct SpringTimingParameters { 51 | let spring: Spring 52 | let displacement: CGPoint 53 | let initialVelocity: CGPoint 54 | let threshold: CGFloat 55 | private let impl: TimingParameters 56 | 57 | init(spring: Spring, displacement: CGPoint, initialVelocity: CGPoint, threshold: CGFloat) { 58 | self.spring = spring 59 | self.displacement = displacement 60 | self.initialVelocity = initialVelocity 61 | self.threshold = threshold 62 | 63 | if spring.dampingRatio == 1 { 64 | impl = CriticallyDampedSpringTimingParameters( 65 | spring: spring, 66 | displacement: displacement, 67 | initialVelocity: initialVelocity, 68 | threshold: threshold 69 | ) 70 | } else if spring.dampingRatio > 0 && spring.dampingRatio < 1 { 71 | impl = UnderdampedSpringTimingParameters( 72 | spring: spring, 73 | displacement: displacement, 74 | initialVelocity: initialVelocity, 75 | threshold: threshold 76 | ) 77 | } else { 78 | fatalError("dampingRatio should be greater than 0 and less than or equal to 1") 79 | } 80 | } 81 | } 82 | 83 | extension SpringTimingParameters: TimingParameters { 84 | var duration: TimeInterval { 85 | return impl.duration 86 | } 87 | 88 | func value(at time: TimeInterval) -> CGPoint { 89 | return impl.value(at: time) 90 | } 91 | } 92 | 93 | // MARK: - Private Impl 94 | 95 | 96 | private struct UnderdampedSpringTimingParameters { 97 | let spring: Spring 98 | let displacement: CGPoint 99 | let initialVelocity: CGPoint 100 | let threshold: CGFloat 101 | } 102 | 103 | extension UnderdampedSpringTimingParameters: TimingParameters { 104 | var duration: TimeInterval { 105 | if displacement.length == 0 && initialVelocity.length == 0 { return 0 } 106 | return TimeInterval(log((c1.length + c2.length) / threshold) / spring.beta) 107 | } 108 | 109 | func value(at time: TimeInterval) -> CGPoint { 110 | let t = CGFloat(time) 111 | let wd = spring.dampedNaturalFrequency 112 | return exp(-spring.beta * t) * (c1 * cos(wd * t) + c2 * sin(wd * t)) 113 | } 114 | 115 | // MARK: - Private 116 | 117 | private var c1: CGPoint { 118 | return displacement 119 | } 120 | 121 | private var c2: CGPoint { 122 | return (initialVelocity + spring.beta * displacement) / spring.dampedNaturalFrequency 123 | } 124 | } 125 | 126 | private struct CriticallyDampedSpringTimingParameters { 127 | let spring: Spring 128 | let displacement: CGPoint 129 | let initialVelocity: CGPoint 130 | let threshold: CGFloat 131 | } 132 | 133 | extension CriticallyDampedSpringTimingParameters: TimingParameters { 134 | var duration: TimeInterval { 135 | if displacement.length == 0 && initialVelocity.length == 0 { return 0 } 136 | 137 | let b = spring.beta 138 | let e = CGFloat(M_E) 139 | 140 | let t1 = 1 / b * log(2 * c1.length / threshold) 141 | let t2 = 2 / b * log(4 * c2.length / (e * b * threshold)) 142 | 143 | return TimeInterval(max(t1, t2)) 144 | } 145 | 146 | func value(at time: TimeInterval) -> CGPoint { 147 | let t = CGFloat(time) 148 | return exp(-spring.beta * t) * (c1 + c2 * t) 149 | } 150 | 151 | // MARK: - Private 152 | 153 | private var c1: CGPoint { 154 | return displacement 155 | } 156 | 157 | private var c2: CGPoint { 158 | return initialVelocity + spring.beta * displacement 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/TimerAnimation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import QuartzCore 3 | 4 | final class TimerAnimation { 5 | typealias Animations = (_ progress: Double, _ time: TimeInterval) -> Void 6 | typealias Completion = (_ finished: Bool) -> Void 7 | 8 | private weak var displayLink: CADisplayLink? 9 | private let duration: TimeInterval 10 | private let animations: Animations 11 | private let completion: Completion? 12 | private let firstFrameTimestamp: CFTimeInterval 13 | private var running = true 14 | 15 | deinit { 16 | invalidate() 17 | } 18 | 19 | init(duration: TimeInterval, animations: @escaping Animations, completion: Completion? = nil) { 20 | self.duration = duration 21 | self.animations = animations 22 | self.completion = completion 23 | self.firstFrameTimestamp = CACurrentMediaTime() 24 | let displayLink = CADisplayLink(target: self, selector: #selector(handleFrame(_:))) 25 | displayLink.add(to: .main, forMode: RunLoop.Mode.common) 26 | self.displayLink = displayLink 27 | } 28 | 29 | func invalidate() { 30 | guard running else { return } 31 | running = false 32 | completion?(false) 33 | displayLink?.invalidate() 34 | } 35 | 36 | @objc private func handleFrame(_ displayLink: CADisplayLink) { 37 | guard running else { return } 38 | let elapsed = CACurrentMediaTime() - firstFrameTimestamp 39 | if elapsed >= duration { 40 | animations(1, duration) 41 | running = false 42 | completion?(true) 43 | displayLink.invalidate() 44 | } else { 45 | animations(elapsed / duration, elapsed) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DMScrollBar/Deceleration+Spring/TimingParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | protocol TimingParameters { 5 | var duration: TimeInterval { get } 6 | func value(at time: TimeInterval) -> CGPoint 7 | } 8 | -------------------------------------------------------------------------------- /DMScrollBar/ScrollBarIndicator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ScrollBarIndicator: UIView { 4 | private var indicatorImageWidthConstraint: NSLayoutConstraint? 5 | private var indicatorImageHeightConstraint: NSLayoutConstraint? 6 | private var indicatorImageLabelStackViewLeadingConstraint: NSLayoutConstraint? 7 | private var indicatorImageLabelStackViewTrailingConstraint: NSLayoutConstraint? 8 | private var indicatorImage: UIImageView? 9 | private var indicatorLabel: UILabel? 10 | private var indicatorImageLabelStackView: UIStackView = { 11 | let stackView = UIStackView() 12 | stackView.axis = .horizontal 13 | stackView.translatesAutoresizingMaskIntoConstraints = false 14 | return stackView 15 | }() 16 | 17 | var isIndicatorLabelVisible: Bool { 18 | indicatorLabel?.alpha == 1 && indicatorLabel?.isHidden == false 19 | } 20 | 21 | init() { 22 | super.init(frame: .zero) 23 | 24 | translatesAutoresizingMaskIntoConstraints = false 25 | clipsToBounds = true 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | 31 | translatesAutoresizingMaskIntoConstraints = false 32 | clipsToBounds = true 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | 38 | translatesAutoresizingMaskIntoConstraints = false 39 | clipsToBounds = true 40 | } 41 | 42 | func setup( 43 | stateConfig: DMScrollBar.Configuration.Indicator.StateConfig, 44 | textConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig?, 45 | accessibilityIdentifier: String? = nil 46 | ) { 47 | self.accessibilityIdentifier = accessibilityIdentifier 48 | self.isAccessibilityElement = false 49 | backgroundColor = stateConfig.backgroundColor 50 | layer.maskedCorners = stateConfig.roundedCorners.corners.cornerMask 51 | layer.cornerRadius = cornerRadius( 52 | from: stateConfig.roundedCorners.radius, 53 | viewSize: stateConfig.size 54 | ) 55 | if indicatorImageLabelStackView.superview == nil { 56 | addSubview(indicatorImageLabelStackView) 57 | let centerX = indicatorImageLabelStackView.centerXAnchor.constraint(equalTo: centerXAnchor) 58 | centerX.priority = .init(999) 59 | centerX.isActive = true 60 | indicatorImageLabelStackView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 61 | } 62 | let leadingInset: CGFloat = { 63 | guard let textConfig else { return stateConfig.contentInsets.left } 64 | return stateConfig.image == nil ? textConfig.insets.left : stateConfig.contentInsets.left 65 | }() 66 | setupConstraint( 67 | constraint: &indicatorImageLabelStackViewLeadingConstraint, 68 | build: { indicatorImageLabelStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: $0) }, 69 | value: leadingInset 70 | ) 71 | setupConstraint( 72 | constraint: &indicatorImageLabelStackViewTrailingConstraint, 73 | build: { trailingAnchor.constraint(equalTo: indicatorImageLabelStackView.trailingAnchor, constant: $0) }, 74 | value: textConfig?.insets.right ?? stateConfig.contentInsets.right 75 | ) 76 | setupIndicatorImageViewState(config: stateConfig) 77 | setupIndicatorLabelState(config: textConfig) 78 | } 79 | 80 | func updateScrollIndicatorText( 81 | direction: CATransitionSubtype?, 82 | scrollBarLabelText: String?, 83 | textConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig? 84 | ) { 85 | guard let scrollBarLabelText = scrollBarLabelText, textConfig != nil else { return hideIndicatorLabel() } 86 | if scrollBarLabelText == indicatorLabel?.text { return } 87 | indicatorLabel?.setup(text: scrollBarLabelText, direction: direction) 88 | indicatorImageLabelStackView.layoutIfNeeded() 89 | #if !os(visionOS) 90 | generateHapticFeedback(style: .light) 91 | #endif 92 | } 93 | 94 | // MARK: - Private 95 | 96 | private func setupIndicatorImageViewState(config: DMScrollBar.Configuration.Indicator.StateConfig) { 97 | buildIndicatorImageViewIfNeeded() 98 | if let image = config.image { 99 | indicatorImage?.isHidden = false 100 | indicatorImage?.alpha = 1 101 | indicatorImage?.image = image 102 | indicatorImage?.accessibilityIdentifier = config.imageAccessibilityIdentifier 103 | setupConstraint( 104 | constraint: &indicatorImageWidthConstraint, 105 | build: indicatorImage?.widthAnchor.constraint(equalToConstant:), 106 | value: config.imageSize.width, 107 | priority: .init(999) 108 | ) 109 | setupConstraint( 110 | constraint: &indicatorImageHeightConstraint, 111 | build: indicatorImage?.heightAnchor.constraint(equalToConstant:), 112 | value: config.imageSize.height 113 | ) 114 | } else { 115 | indicatorImage?.isHidden = true 116 | indicatorImage?.alpha = 0 117 | } 118 | } 119 | 120 | private func setupIndicatorLabelState(config: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig?) { 121 | buildIndicatorLabelIfNeeded() 122 | if let config { 123 | showIndicatorLabel() 124 | indicatorLabel?.font = config.font 125 | indicatorLabel?.textColor = config.color 126 | indicatorLabel?.accessibilityIdentifier = config.accessibilityIdentifier 127 | indicatorImageLabelStackView.spacing = config.insets.left 128 | } else { 129 | hideIndicatorLabel() 130 | } 131 | } 132 | 133 | private func buildIndicatorImageViewIfNeeded() { 134 | guard indicatorImage == nil else { return } 135 | let imageView = UIImageView() 136 | indicatorImageLabelStackView.addArrangedSubview(imageView) 137 | imageView.translatesAutoresizingMaskIntoConstraints = false 138 | imageView.contentMode = .scaleAspectFit 139 | self.indicatorImage = imageView 140 | } 141 | 142 | private func buildIndicatorLabelIfNeeded() { 143 | guard indicatorLabel == nil else { return } 144 | let label = UILabel() 145 | indicatorImageLabelStackView.addArrangedSubview(label) 146 | label.setContentHuggingPriority(.required, for: .horizontal) 147 | label.setContentCompressionResistancePriority(.required, for: .horizontal) 148 | self.indicatorLabel = label 149 | } 150 | 151 | private func showIndicatorLabel() { 152 | indicatorLabel?.alpha = 1 153 | indicatorLabel?.isHidden = false 154 | } 155 | 156 | private func hideIndicatorLabel() { 157 | indicatorLabel?.alpha = 0 158 | indicatorLabel?.isHidden = true 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /DMScrollBar/ScrollBarInfoView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ScrollBarInfoView: UIView { 4 | private let offsetLabel = UILabel() 5 | 6 | init() { 7 | super.init(frame: .zero) 8 | 9 | translatesAutoresizingMaskIntoConstraints = false 10 | clipsToBounds = true 11 | } 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | translatesAutoresizingMaskIntoConstraints = false 17 | clipsToBounds = true 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | 23 | translatesAutoresizingMaskIntoConstraints = false 24 | clipsToBounds = true 25 | } 26 | 27 | func setup(config: DMScrollBar.Configuration.InfoLabel) { 28 | addSubview(offsetLabel) 29 | 30 | let textInsets = config.textInsets 31 | offsetLabel.translatesAutoresizingMaskIntoConstraints = false 32 | offsetLabel.font = config.font 33 | offsetLabel.textColor = config.textColor 34 | offsetLabel.topAnchor.constraint(equalTo: topAnchor, constant: textInsets.top).isActive = true 35 | offsetLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -textInsets.bottom).isActive = true 36 | offsetLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: textInsets.left).isActive = true 37 | offsetLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -textInsets.right).isActive = true 38 | offsetLabel.accessibilityIdentifier = config.accessibilityIdentifier 39 | 40 | backgroundColor = config.backgroundColor 41 | layer.maskedCorners = config.roundedCorners.corners.cornerMask 42 | layer.cornerRadius = cornerRadius( 43 | from: config.roundedCorners.radius, 44 | viewSize: CGSize( 45 | width: config.maximumWidth ?? CGFloat.greatestFiniteMagnitude, 46 | height: config.font.lineHeight + textInsets.top + textInsets.bottom 47 | ) 48 | ) 49 | 50 | alpha = 0 51 | clipsToBounds = true 52 | } 53 | 54 | func updateText(text: String, direction: CATransitionSubtype?) { 55 | if text == offsetLabel.text { return } 56 | offsetLabel.setup(text: text, direction: direction) 57 | layoutIfNeeded() 58 | #if !os(visionOS) 59 | generateHapticFeedback(style: .light) 60 | #endif 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/CASpringAnimation+Utils.swift: -------------------------------------------------------------------------------- 1 | import QuartzCore 2 | 3 | extension CASpringAnimation { 4 | convenience init(mass: CGFloat = 1, stiffness: CGFloat = 100, dampingRatio: CGFloat) { 5 | self.init() 6 | 7 | self.mass = mass 8 | self.stiffness = stiffness 9 | self.damping = 2 * dampingRatio * sqrt(mass * stiffness) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/CGPoint+Utils.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGPoint { 4 | var length: CGFloat { 5 | return sqrt(x * x + y * y) 6 | } 7 | 8 | var withZeroX: CGPoint { 9 | return CGPoint(x: 0, y: y) 10 | } 11 | 12 | func clamped(to rect: CGRect) -> CGPoint { 13 | return CGPoint(x: x.clamped(to: rect.minX...rect.maxX), y: y.clamped(to: rect.minY...rect.maxY)) 14 | } 15 | 16 | func distance(to other: CGPoint) -> CGFloat { 17 | return (self - other).length 18 | } 19 | 20 | func distance(toSegment segment: (CGPoint, CGPoint)) -> CGFloat { 21 | let v = segment.0 22 | let w = segment.1 23 | 24 | let pv_dx = x - v.x 25 | let pv_dy = y - v.y 26 | let wv_dx = w.x - v.x 27 | let wv_dy = w.y - v.y 28 | 29 | let dot = pv_dx * wv_dx + pv_dy * wv_dy 30 | let len_sq = wv_dx * wv_dx + wv_dy * wv_dy 31 | let param = dot / len_sq 32 | 33 | var int_x, int_y: CGFloat /* intersection of normal to vw that goes through p */ 34 | 35 | if param < 0 || (v.x == w.x && v.y == w.y) { 36 | int_x = v.x 37 | int_y = v.y 38 | } else if param > 1 { 39 | int_x = w.x 40 | int_y = w.y 41 | } else { 42 | int_x = v.x + param * wv_dx 43 | int_y = v.y + param * wv_dy 44 | } 45 | 46 | /* Components of normal */ 47 | let dx = x - int_x 48 | let dy = y - int_y 49 | 50 | return sqrt(dx * dx + dy * dy) 51 | } 52 | } 53 | 54 | extension CGPoint { 55 | static prefix func -(lhs: CGPoint) -> CGPoint { 56 | return CGPoint(x: -lhs.x, y: -lhs.y) 57 | } 58 | 59 | static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 60 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 61 | } 62 | 63 | static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 64 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 65 | } 66 | 67 | static func *(lhs: CGFloat, rhs: CGPoint) -> CGPoint { 68 | return CGPoint(x: lhs * rhs.x, y: lhs * rhs.y) 69 | } 70 | 71 | static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint { 72 | return rhs * lhs 73 | } 74 | 75 | static func /(lhs: CGPoint, rhs: CGFloat) -> CGPoint { 76 | return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/Comparable+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Comparable { 4 | func clamped(to limits: ClosedRange) -> Self { 5 | return min(max(self, limits.lowerBound), limits.upperBound) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/Configuration+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension DMScrollBar.Configuration.RoundedCorners.Corner { 4 | var cornerMask: CACornerMask { 5 | switch self { 6 | case .topLeft: return .layerMinXMinYCorner 7 | case .bottomLeft: return .layerMinXMaxYCorner 8 | case .topRight: return .layerMaxXMinYCorner 9 | case .bottomRight: return .layerMaxXMaxYCorner 10 | } 11 | } 12 | } 13 | 14 | extension DMScrollBar.Configuration.Indicator.StateConfig { 15 | func applying(scaleFactor: CGFloat) -> DMScrollBar.Configuration.Indicator.StateConfig { 16 | .init( 17 | size: CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor), 18 | backgroundColor: backgroundColor, 19 | insets: insets, 20 | image: image, 21 | imageSize: CGSize(width: imageSize.width * scaleFactor, height: imageSize.height * scaleFactor), 22 | roundedCorners: roundedCorners 23 | ) 24 | } 25 | } 26 | 27 | extension DMScrollBar.Configuration.Indicator.ActiveStateConfig { 28 | var textConfig: DMScrollBar.Configuration.Indicator.ActiveStateConfig.TextConfig? { 29 | switch self { 30 | case .custom(_, let textConfig): return textConfig 31 | default: return nil 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/ConvenienceFunctions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | func interval(_ minimum: T, _ num: T, _ maximum: T) -> T { 4 | return min(maximum, max(minimum, num)) 5 | } 6 | 7 | func cornerRadius(from radius: DMScrollBar.Configuration.RoundedCorners.Radius, viewSize: CGSize) -> CGFloat { 8 | switch radius { 9 | case .notRounded: return 0 10 | case .rounded: return min(viewSize.height, viewSize.width) / 2 11 | case .custom(let radius): return radius 12 | } 13 | } 14 | 15 | func setupConstraint(constraint: inout NSLayoutConstraint?, build: (CGFloat) -> NSLayoutConstraint, value: CGFloat, priority: UILayoutPriority = .required) { 16 | if let constraint { 17 | constraint.constant = value 18 | constraint.priority = priority 19 | } else { 20 | constraint = build(value) 21 | constraint?.priority = priority 22 | constraint?.isActive = true 23 | } 24 | } 25 | 26 | func setupConstraint(constraint: inout NSLayoutConstraint?, build: ((CGFloat) -> NSLayoutConstraint)?, value: CGFloat, priority: UILayoutPriority = .required) { 27 | guard let build else { return } 28 | setupConstraint(constraint: &constraint, build: build, value: value, priority: priority) 29 | } 30 | 31 | @available(visionOS, unavailable) 32 | func generateHapticFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle = .heavy) { 33 | UIImpactFeedbackGenerator(style: style).impactOccurred() 34 | } 35 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/Publisher+Utils.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> { 5 | scan(nil) { ($0?.1, $1) } 6 | .compactMap { $0 } 7 | .eraseToAnyPublisher() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/Sequence+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension Sequence where Element == DMScrollBar.Configuration.RoundedCorners.Corner { 4 | var cornerMask: CACornerMask { 5 | CACornerMask(map(\.cornerMask)) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UIColor+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | private static var backgroundLightGray = UIColor(red: 160/255, green: 160/255, blue: 160/255, alpha: 1) 5 | private static var backgroundDarkGray = UIColor(red: 128/255, green: 128/255, blue: 128/255, alpha: 1) 6 | 7 | public static var defaultScrollBarBackground: UIColor { 8 | UIColor(dynamicProvider: { $0.userInterfaceStyle == .light ? .backgroundLightGray : .backgroundDarkGray }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UIGestureRecognizer+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIGestureRecognizer { 4 | func cancel() { 5 | isEnabled = false 6 | isEnabled = true 7 | } 8 | } 9 | 10 | extension UIGestureRecognizer.State { 11 | var isInactive: Bool { 12 | switch self { 13 | case .possible, .ended, .cancelled, .failed: return true 14 | case .began, .changed: return false 15 | @unknown default: return true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UILabel+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import QuartzCore 3 | 4 | extension UILabel { 5 | func setup(text: String, direction: CATransitionSubtype?) { 6 | if let direction { 7 | let animation = CATransition() 8 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 9 | animation.type = CATransitionType.push 10 | animation.subtype = direction 11 | animation.duration = 0.15 12 | layer.add(animation, forKey: "pushTextChange") 13 | } 14 | self.text = text 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UIScrollView+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private enum AssociatedKeys { 4 | static var scrollIndicatorStyle = "scrollIndicatorStyle" 5 | static var scrollBar = "scrollBar" 6 | } 7 | 8 | public extension UIScrollView { 9 | var scrollBar: DMScrollBar? { 10 | get { 11 | withUnsafePointer(to: &AssociatedKeys.scrollBar) { 12 | objc_getAssociatedObject(self, $0) as? DMScrollBar 13 | } 14 | } set { 15 | withUnsafePointer(to: &AssociatedKeys.scrollBar) { 16 | objc_setAssociatedObject(self, $0, newValue, .OBJC_ASSOCIATION_ASSIGN) 17 | } 18 | } 19 | } 20 | 21 | func configureScrollBar(with configuration: DMScrollBar.Configuration = .default, delegate: DMScrollBarDelegate? = nil) { 22 | scrollBar?.removeFromSuperview() 23 | let scrollBar = DMScrollBar(scrollView: self, delegate: delegate, configuration: configuration) 24 | self.scrollBar = scrollBar 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UISpringTimingParameters+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UISpringTimingParameters { 4 | convenience init(mass: CGFloat, stiffness: CGFloat, dampingRatio: CGFloat, initialVelocity velocity: CGVector) { 5 | let damping = 2 * dampingRatio * sqrt(mass * stiffness) 6 | self.init(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: velocity) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DMScrollBar/Utils/UIView+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | var frameInWindow: CGRect { 5 | convert(bounds, to: nil) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Example/DMScrollBar.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 11 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 12 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 15 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; 16 | C1142247318C94B5E3342F09 /* Pods_DMScrollBar_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ADA68095A803AE9BEB0E7CFC /* Pods_DMScrollBar_Tests.framework */; }; 17 | D0AA687D8B7A648035431919 /* Pods_DMScrollBar_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86727F10A67E6FD5C73A6A2 /* Pods_DMScrollBar_Example.framework */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 26 | remoteInfo = DMScrollBar; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 4460AF0D48F868567DF08916 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 32 | 4F2A9EFED781DB0DBB8C538A /* Pods-DMScrollBar_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DMScrollBar_Example.release.xcconfig"; path = "Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example.release.xcconfig"; sourceTree = ""; }; 33 | 607FACD01AFB9204008FA782 /* DMScrollBar_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DMScrollBar_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 37 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 39 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 40 | 607FACE51AFB9204008FA782 /* DMScrollBar_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DMScrollBar_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 43 | 9A610708244535B7934933DB /* Pods-DMScrollBar_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DMScrollBar_Example.debug.xcconfig"; path = "Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example.debug.xcconfig"; sourceTree = ""; }; 44 | 9FE7ABF0981597D53A4C2228 /* DMScrollBar.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = DMScrollBar.podspec; path = ../DMScrollBar.podspec; sourceTree = ""; }; 45 | ADA68095A803AE9BEB0E7CFC /* Pods_DMScrollBar_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DMScrollBar_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | B3D4E6F5A3ED8A87D85F9660 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 47 | E0C97CB26DB1322F37C1536C /* Pods-DMScrollBar_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DMScrollBar_Tests.debug.xcconfig"; path = "Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests.debug.xcconfig"; sourceTree = ""; }; 48 | E4C527C3BCA7CEAF93DD4F8C /* Pods-DMScrollBar_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DMScrollBar_Tests.release.xcconfig"; path = "Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests.release.xcconfig"; sourceTree = ""; }; 49 | F86727F10A67E6FD5C73A6A2 /* Pods_DMScrollBar_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DMScrollBar_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | D0AA687D8B7A648035431919 /* Pods_DMScrollBar_Example.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | C1142247318C94B5E3342F09 /* Pods_DMScrollBar_Tests.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | 24A06BE7AFEF4A8815FE0A0D /* Pods */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 9A610708244535B7934933DB /* Pods-DMScrollBar_Example.debug.xcconfig */, 76 | 4F2A9EFED781DB0DBB8C538A /* Pods-DMScrollBar_Example.release.xcconfig */, 77 | E0C97CB26DB1322F37C1536C /* Pods-DMScrollBar_Tests.debug.xcconfig */, 78 | E4C527C3BCA7CEAF93DD4F8C /* Pods-DMScrollBar_Tests.release.xcconfig */, 79 | ); 80 | path = Pods; 81 | sourceTree = ""; 82 | }; 83 | 607FACC71AFB9204008FA782 = { 84 | isa = PBXGroup; 85 | children = ( 86 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 87 | 607FACD21AFB9204008FA782 /* Example for DMScrollBar */, 88 | 607FACE81AFB9204008FA782 /* Tests */, 89 | 607FACD11AFB9204008FA782 /* Products */, 90 | 24A06BE7AFEF4A8815FE0A0D /* Pods */, 91 | FB8CC770F19A2C8826ECE541 /* Frameworks */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | 607FACD11AFB9204008FA782 /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 607FACD01AFB9204008FA782 /* DMScrollBar_Example.app */, 99 | 607FACE51AFB9204008FA782 /* DMScrollBar_Tests.xctest */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 607FACD21AFB9204008FA782 /* Example for DMScrollBar */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 108 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 109 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 110 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 111 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 112 | 607FACD31AFB9204008FA782 /* Supporting Files */, 113 | ); 114 | name = "Example for DMScrollBar"; 115 | path = DMScrollBar; 116 | sourceTree = ""; 117 | }; 118 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 607FACD41AFB9204008FA782 /* Info.plist */, 122 | ); 123 | name = "Supporting Files"; 124 | sourceTree = ""; 125 | }; 126 | 607FACE81AFB9204008FA782 /* Tests */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 607FACEB1AFB9204008FA782 /* Tests.swift */, 130 | 607FACE91AFB9204008FA782 /* Supporting Files */, 131 | ); 132 | path = Tests; 133 | sourceTree = ""; 134 | }; 135 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 607FACEA1AFB9204008FA782 /* Info.plist */, 139 | ); 140 | name = "Supporting Files"; 141 | sourceTree = ""; 142 | }; 143 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 9FE7ABF0981597D53A4C2228 /* DMScrollBar.podspec */, 147 | B3D4E6F5A3ED8A87D85F9660 /* README.md */, 148 | 4460AF0D48F868567DF08916 /* LICENSE */, 149 | ); 150 | name = "Podspec Metadata"; 151 | sourceTree = ""; 152 | }; 153 | FB8CC770F19A2C8826ECE541 /* Frameworks */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | F86727F10A67E6FD5C73A6A2 /* Pods_DMScrollBar_Example.framework */, 157 | ADA68095A803AE9BEB0E7CFC /* Pods_DMScrollBar_Tests.framework */, 158 | ); 159 | name = Frameworks; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | 607FACCF1AFB9204008FA782 /* DMScrollBar_Example */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "DMScrollBar_Example" */; 168 | buildPhases = ( 169 | A8868F181E604014A1D07A62 /* [CP] Check Pods Manifest.lock */, 170 | 607FACCC1AFB9204008FA782 /* Sources */, 171 | 607FACCD1AFB9204008FA782 /* Frameworks */, 172 | 607FACCE1AFB9204008FA782 /* Resources */, 173 | AEDB66AF3A47DB9A69D6D879 /* [CP] Embed Pods Frameworks */, 174 | ); 175 | buildRules = ( 176 | ); 177 | dependencies = ( 178 | ); 179 | name = DMScrollBar_Example; 180 | productName = DMScrollBar; 181 | productReference = 607FACD01AFB9204008FA782 /* DMScrollBar_Example.app */; 182 | productType = "com.apple.product-type.application"; 183 | }; 184 | 607FACE41AFB9204008FA782 /* DMScrollBar_Tests */ = { 185 | isa = PBXNativeTarget; 186 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "DMScrollBar_Tests" */; 187 | buildPhases = ( 188 | 11D127395F9DE3B39222A9CD /* [CP] Check Pods Manifest.lock */, 189 | 607FACE11AFB9204008FA782 /* Sources */, 190 | 607FACE21AFB9204008FA782 /* Frameworks */, 191 | 607FACE31AFB9204008FA782 /* Resources */, 192 | ); 193 | buildRules = ( 194 | ); 195 | dependencies = ( 196 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 197 | ); 198 | name = DMScrollBar_Tests; 199 | productName = Tests; 200 | productReference = 607FACE51AFB9204008FA782 /* DMScrollBar_Tests.xctest */; 201 | productType = "com.apple.product-type.bundle.unit-test"; 202 | }; 203 | /* End PBXNativeTarget section */ 204 | 205 | /* Begin PBXProject section */ 206 | 607FACC81AFB9204008FA782 /* Project object */ = { 207 | isa = PBXProject; 208 | attributes = { 209 | LastSwiftUpdateCheck = 0830; 210 | LastUpgradeCheck = 0830; 211 | ORGANIZATIONNAME = CocoaPods; 212 | TargetAttributes = { 213 | 607FACCF1AFB9204008FA782 = { 214 | CreatedOnToolsVersion = 6.3.1; 215 | DevelopmentTeam = 2S883HFCN8; 216 | LastSwiftMigration = 0900; 217 | }; 218 | 607FACE41AFB9204008FA782 = { 219 | CreatedOnToolsVersion = 6.3.1; 220 | DevelopmentTeam = 2S883HFCN8; 221 | LastSwiftMigration = 0900; 222 | TestTargetID = 607FACCF1AFB9204008FA782; 223 | }; 224 | }; 225 | }; 226 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "DMScrollBar" */; 227 | compatibilityVersion = "Xcode 3.2"; 228 | developmentRegion = English; 229 | hasScannedForEncodings = 0; 230 | knownRegions = ( 231 | English, 232 | en, 233 | Base, 234 | ); 235 | mainGroup = 607FACC71AFB9204008FA782; 236 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 237 | projectDirPath = ""; 238 | projectRoot = ""; 239 | targets = ( 240 | 607FACCF1AFB9204008FA782 /* DMScrollBar_Example */, 241 | 607FACE41AFB9204008FA782 /* DMScrollBar_Tests */, 242 | ); 243 | }; 244 | /* End PBXProject section */ 245 | 246 | /* Begin PBXResourcesBuildPhase section */ 247 | 607FACCE1AFB9204008FA782 /* Resources */ = { 248 | isa = PBXResourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 252 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 253 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | 607FACE31AFB9204008FA782 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXResourcesBuildPhase section */ 265 | 266 | /* Begin PBXShellScriptBuildPhase section */ 267 | 11D127395F9DE3B39222A9CD /* [CP] Check Pods Manifest.lock */ = { 268 | isa = PBXShellScriptBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | ); 272 | inputFileListPaths = ( 273 | ); 274 | inputPaths = ( 275 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 276 | "${PODS_ROOT}/Manifest.lock", 277 | ); 278 | name = "[CP] Check Pods Manifest.lock"; 279 | outputFileListPaths = ( 280 | ); 281 | outputPaths = ( 282 | "$(DERIVED_FILE_DIR)/Pods-DMScrollBar_Tests-checkManifestLockResult.txt", 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | shellPath = /bin/sh; 286 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 287 | showEnvVarsInLog = 0; 288 | }; 289 | A8868F181E604014A1D07A62 /* [CP] Check Pods Manifest.lock */ = { 290 | isa = PBXShellScriptBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | inputFileListPaths = ( 295 | ); 296 | inputPaths = ( 297 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 298 | "${PODS_ROOT}/Manifest.lock", 299 | ); 300 | name = "[CP] Check Pods Manifest.lock"; 301 | outputFileListPaths = ( 302 | ); 303 | outputPaths = ( 304 | "$(DERIVED_FILE_DIR)/Pods-DMScrollBar_Example-checkManifestLockResult.txt", 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | shellPath = /bin/sh; 308 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 309 | showEnvVarsInLog = 0; 310 | }; 311 | AEDB66AF3A47DB9A69D6D879 /* [CP] Embed Pods Frameworks */ = { 312 | isa = PBXShellScriptBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | ); 316 | inputPaths = ( 317 | "${PODS_ROOT}/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-frameworks.sh", 318 | "${BUILT_PRODUCTS_DIR}/DMScrollBar/DMScrollBar.framework", 319 | ); 320 | name = "[CP] Embed Pods Frameworks"; 321 | outputPaths = ( 322 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DMScrollBar.framework", 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | shellPath = /bin/sh; 326 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-frameworks.sh\"\n"; 327 | showEnvVarsInLog = 0; 328 | }; 329 | /* End PBXShellScriptBuildPhase section */ 330 | 331 | /* Begin PBXSourcesBuildPhase section */ 332 | 607FACCC1AFB9204008FA782 /* Sources */ = { 333 | isa = PBXSourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 337 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 607FACE11AFB9204008FA782 /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, 346 | ); 347 | runOnlyForDeploymentPostprocessing = 0; 348 | }; 349 | /* End PBXSourcesBuildPhase section */ 350 | 351 | /* Begin PBXTargetDependency section */ 352 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 353 | isa = PBXTargetDependency; 354 | target = 607FACCF1AFB9204008FA782 /* DMScrollBar_Example */; 355 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 356 | }; 357 | /* End PBXTargetDependency section */ 358 | 359 | /* Begin PBXVariantGroup section */ 360 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 361 | isa = PBXVariantGroup; 362 | children = ( 363 | 607FACDA1AFB9204008FA782 /* Base */, 364 | ); 365 | name = Main.storyboard; 366 | sourceTree = ""; 367 | }; 368 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 369 | isa = PBXVariantGroup; 370 | children = ( 371 | 607FACDF1AFB9204008FA782 /* Base */, 372 | ); 373 | name = LaunchScreen.xib; 374 | sourceTree = ""; 375 | }; 376 | /* End PBXVariantGroup section */ 377 | 378 | /* Begin XCBuildConfiguration section */ 379 | 607FACED1AFB9204008FA782 /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ALWAYS_SEARCH_USER_PATHS = NO; 383 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 384 | CLANG_CXX_LIBRARY = "libc++"; 385 | CLANG_ENABLE_MODULES = YES; 386 | CLANG_ENABLE_OBJC_ARC = YES; 387 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_COMMA = YES; 390 | CLANG_WARN_CONSTANT_CONVERSION = YES; 391 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 392 | CLANG_WARN_EMPTY_BODY = YES; 393 | CLANG_WARN_ENUM_CONVERSION = YES; 394 | CLANG_WARN_INFINITE_RECURSION = YES; 395 | CLANG_WARN_INT_CONVERSION = YES; 396 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 407 | ENABLE_STRICT_OBJC_MSGSEND = YES; 408 | ENABLE_TESTABILITY = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_DYNAMIC_NO_PIC = NO; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_OPTIMIZATION_LEVEL = 0; 413 | GCC_PREPROCESSOR_DEFINITIONS = ( 414 | "DEBUG=1", 415 | "$(inherited)", 416 | ); 417 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 418 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 419 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 420 | GCC_WARN_UNDECLARED_SELECTOR = YES; 421 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 422 | GCC_WARN_UNUSED_FUNCTION = YES; 423 | GCC_WARN_UNUSED_VARIABLE = YES; 424 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 425 | MTL_ENABLE_DEBUG_INFO = YES; 426 | ONLY_ACTIVE_ARCH = YES; 427 | SDKROOT = iphoneos; 428 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 429 | }; 430 | name = Debug; 431 | }; 432 | 607FACEE1AFB9204008FA782 /* Release */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | ALWAYS_SEARCH_USER_PATHS = NO; 436 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 437 | CLANG_CXX_LIBRARY = "libc++"; 438 | CLANG_ENABLE_MODULES = YES; 439 | CLANG_ENABLE_OBJC_ARC = YES; 440 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 441 | CLANG_WARN_BOOL_CONVERSION = YES; 442 | CLANG_WARN_COMMA = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INFINITE_RECURSION = YES; 448 | CLANG_WARN_INT_CONVERSION = YES; 449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 452 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 453 | CLANG_WARN_STRICT_PROTOTYPES = YES; 454 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 455 | CLANG_WARN_UNREACHABLE_CODE = YES; 456 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 457 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 458 | COPY_PHASE_STRIP = NO; 459 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 460 | ENABLE_NS_ASSERTIONS = NO; 461 | ENABLE_STRICT_OBJC_MSGSEND = YES; 462 | GCC_C_LANGUAGE_STANDARD = gnu99; 463 | GCC_NO_COMMON_BLOCKS = YES; 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 471 | MTL_ENABLE_DEBUG_INFO = NO; 472 | SDKROOT = iphoneos; 473 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 474 | VALIDATE_PRODUCT = YES; 475 | }; 476 | name = Release; 477 | }; 478 | 607FACF01AFB9204008FA782 /* Debug */ = { 479 | isa = XCBuildConfiguration; 480 | baseConfigurationReference = 9A610708244535B7934933DB /* Pods-DMScrollBar_Example.debug.xcconfig */; 481 | buildSettings = { 482 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 483 | DEVELOPMENT_TEAM = 2S883HFCN8; 484 | INFOPLIST_FILE = DMScrollBar/Info.plist; 485 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 486 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 487 | MODULE_NAME = ExampleApp; 488 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 489 | PRODUCT_NAME = "$(TARGET_NAME)"; 490 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 491 | SWIFT_VERSION = 4.0; 492 | TARGETED_DEVICE_FAMILY = "1,2"; 493 | }; 494 | name = Debug; 495 | }; 496 | 607FACF11AFB9204008FA782 /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | baseConfigurationReference = 4F2A9EFED781DB0DBB8C538A /* Pods-DMScrollBar_Example.release.xcconfig */; 499 | buildSettings = { 500 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 501 | DEVELOPMENT_TEAM = 2S883HFCN8; 502 | INFOPLIST_FILE = DMScrollBar/Info.plist; 503 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 505 | MODULE_NAME = ExampleApp; 506 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 507 | PRODUCT_NAME = "$(TARGET_NAME)"; 508 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 509 | SWIFT_VERSION = 4.0; 510 | TARGETED_DEVICE_FAMILY = "1,2"; 511 | }; 512 | name = Release; 513 | }; 514 | 607FACF31AFB9204008FA782 /* Debug */ = { 515 | isa = XCBuildConfiguration; 516 | baseConfigurationReference = E0C97CB26DB1322F37C1536C /* Pods-DMScrollBar_Tests.debug.xcconfig */; 517 | buildSettings = { 518 | DEVELOPMENT_TEAM = 2S883HFCN8; 519 | FRAMEWORK_SEARCH_PATHS = ( 520 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 521 | "$(inherited)", 522 | ); 523 | GCC_PREPROCESSOR_DEFINITIONS = ( 524 | "DEBUG=1", 525 | "$(inherited)", 526 | ); 527 | INFOPLIST_FILE = Tests/Info.plist; 528 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 530 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 533 | SWIFT_VERSION = 4.0; 534 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DMScrollBar_Example.app/DMScrollBar_Example"; 535 | }; 536 | name = Debug; 537 | }; 538 | 607FACF41AFB9204008FA782 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | baseConfigurationReference = E4C527C3BCA7CEAF93DD4F8C /* Pods-DMScrollBar_Tests.release.xcconfig */; 541 | buildSettings = { 542 | DEVELOPMENT_TEAM = 2S883HFCN8; 543 | FRAMEWORK_SEARCH_PATHS = ( 544 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 545 | "$(inherited)", 546 | ); 547 | INFOPLIST_FILE = Tests/Info.plist; 548 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 549 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 550 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 553 | SWIFT_VERSION = 4.0; 554 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DMScrollBar_Example.app/DMScrollBar_Example"; 555 | }; 556 | name = Release; 557 | }; 558 | /* End XCBuildConfiguration section */ 559 | 560 | /* Begin XCConfigurationList section */ 561 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "DMScrollBar" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 607FACED1AFB9204008FA782 /* Debug */, 565 | 607FACEE1AFB9204008FA782 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "DMScrollBar_Example" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | 607FACF01AFB9204008FA782 /* Debug */, 574 | 607FACF11AFB9204008FA782 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "DMScrollBar_Tests" */ = { 580 | isa = XCConfigurationList; 581 | buildConfigurations = ( 582 | 607FACF31AFB9204008FA782 /* Debug */, 583 | 607FACF41AFB9204008FA782 /* Release */, 584 | ); 585 | defaultConfigurationIsVisible = 0; 586 | defaultConfigurationName = Release; 587 | }; 588 | /* End XCConfigurationList section */ 589 | }; 590 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 591 | } 592 | -------------------------------------------------------------------------------- /Example/DMScrollBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/DMScrollBar.xcodeproj/xcshareddata/xcschemes/DMScrollBar-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/DMScrollBar.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/DMScrollBar.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/DMScrollBar/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DMScrollBar 4 | // 5 | // Created by Dmitrii Medvedev on 12/26/2022. 6 | // Copyright (c) 2022 Dmitrii Medvedev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/DMScrollBar/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/DMScrollBar/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 | 60 | 61 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Example/DMScrollBar/Images.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" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/DMScrollBar/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/DMScrollBar/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DMScrollBar 3 | 4 | struct Section { 5 | let date: Date 6 | let items: [String] 7 | } 8 | 9 | final class ViewController: UIViewController { 10 | @IBOutlet private var tableView: UITableView! 11 | @IBOutlet private var configsButton: UIButton! 12 | @IBOutlet private var statesStackView: UIStackView! 13 | @IBOutlet private var stackViewLeading: NSLayoutConstraint! 14 | @IBOutlet private var configsButtonLeading: NSLayoutConstraint! 15 | 16 | private var exampleStates: [(name: String, config: DMScrollBar.Configuration)] = [ 17 | ("Default", DMScrollBar.Configuration.default), 18 | ("iOS", DMScrollBar.Configuration.iosStyle), 19 | ("Combined", DMScrollBar.Configuration( 20 | indicator: .init( 21 | normalState: .iosStyle(width: 3), 22 | activeState: .custom(config: .default) 23 | ) 24 | )), 25 | ("Right text", DMScrollBar.Configuration( 26 | indicator: .init( 27 | activeState: .custom( 28 | config: .init(), 29 | textConfig: .init( 30 | insets: .init(top: 0, left: 8, bottom: 0, right: 96), 31 | font: .systemFont(ofSize: 15), 32 | color: .systemBackground 33 | ) 34 | ) 35 | ), 36 | infoLabel: nil 37 | )), 38 | ("Custom", DMScrollBar.Configuration( 39 | isAlwaysVisible: false, 40 | hideTimeInterval: 1.5, 41 | shouldDecelerate: false, 42 | indicator: DMScrollBar.Configuration.Indicator( 43 | normalState: .init( 44 | size: CGSize(width: 35, height: 35), 45 | backgroundColor: UIColor(red: 200 / 255, green: 150 / 255, blue: 80 / 255, alpha: 1), 46 | insets: UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0), 47 | image: UIImage(systemName: "arrow.up.and.down.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.white), 48 | imageSize: CGSize(width: 20, height: 20), 49 | roundedCorners: .roundedLeftCorners 50 | ), 51 | activeState: .custom( 52 | config: .init( 53 | size: CGSize(width: 50, height: 50), 54 | backgroundColor: UIColor.brown, 55 | insets: UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 6), 56 | image: UIImage(systemName: "calendar.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.cyan), 57 | imageSize: CGSize(width: 28, height: 28), 58 | roundedCorners: .allRounded 59 | ), 60 | textConfig: nil 61 | ), 62 | stateChangeAnimationDuration: 0.5, 63 | insetsFollowsSafeArea: true, 64 | animation: .init(showDuration: 0.75, hideDuration: 0.75, animationType: .fadeAndSide) 65 | ), 66 | infoLabel: DMScrollBar.Configuration.InfoLabel( 67 | font: .systemFont(ofSize: 15), 68 | textColor: .white, 69 | distanceToScrollIndicator: 40, 70 | backgroundColor: .brown, 71 | textInsets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), 72 | maximumWidth: 300, 73 | roundedCorners: .init(radius: .rounded, corners: [.topLeft, .bottomRight]), 74 | animation: .init(showDuration: 0.75, hideDuration: 0.75, animationType: .fadeAndSide) 75 | ) 76 | )) 77 | ] 78 | private var sections = [Section]() 79 | private let headerDateFormatter: DateFormatter = { 80 | let dateFormatter = DateFormatter() 81 | dateFormatter.dateStyle = .medium 82 | return dateFormatter 83 | }() 84 | private let shortHeaderDateFormatter: DateFormatter = { 85 | let dateFormatter = DateFormatter() 86 | dateFormatter.dateFormat = "MMM d" 87 | return dateFormatter 88 | }() 89 | 90 | override func viewDidLoad() { 91 | super.viewDidLoad() 92 | 93 | setupTableView() 94 | setupSections() 95 | setupStateButtons() 96 | setupConfigsButton() 97 | setupScrollBarConfig(exampleStates[0].config) 98 | title = "DMScrollBar" 99 | } 100 | 101 | private func setupTableView() { 102 | tableView.dataSource = self 103 | tableView.contentInset.top = 16 104 | } 105 | 106 | private func setupStateButtons() { 107 | exampleStates.enumerated().forEach { offset, item in 108 | let button = UIButton() 109 | button.translatesAutoresizingMaskIntoConstraints = false 110 | button.setTitle(item.name, for: .normal) 111 | button.setTitleColor(.systemBackground, for: .normal) 112 | button.backgroundColor = UIColor(white: 0.7, alpha: 1) 113 | button.layer.cornerRadius = 20 114 | button.contentEdgeInsets = .init(top: 0, left: 8, bottom: 0, right: 8) 115 | button.addAction(UIAction { _ in self.handleStateButtonTap(item.config) }, for: .touchUpInside) 116 | button.heightAnchor.constraint(equalToConstant: 40).isActive = true 117 | statesStackView.addArrangedSubview(button) 118 | } 119 | } 120 | 121 | private func handleStateButtonTap(_ config: DMScrollBar.Configuration) { 122 | hideStateButtons() 123 | setupScrollBarConfig(config) 124 | } 125 | 126 | private func showStateButtons() { 127 | animate { 128 | self.stackViewLeading.constant = 16 129 | self.configsButtonLeading.constant = -50 130 | self.view.layoutIfNeeded() 131 | } 132 | } 133 | 134 | private func hideStateButtons() { 135 | animate { 136 | self.stackViewLeading.constant = -100 137 | self.configsButtonLeading.constant = 0 138 | self.view.layoutIfNeeded() 139 | } 140 | } 141 | 142 | private func animate(duration: CGFloat = 0.3, animation: @escaping () -> Void) { 143 | UIView.animate( 144 | withDuration: duration, 145 | delay: 0, 146 | usingSpringWithDamping: 1, 147 | initialSpringVelocity: 0, 148 | options: [.allowUserInteraction, .beginFromCurrentState, .curveEaseInOut], 149 | animations: animation 150 | ) 151 | } 152 | 153 | private func setupConfigsButton() { 154 | configsButton.layer.cornerRadius = 25 155 | configsButton.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] 156 | configsButton.setTitle("", for: .normal) 157 | configsButton.backgroundColor = UIColor(white: 0.7, alpha: 1) 158 | configsButton.addAction(UIAction { _ in self.showStateButtons() }, for: .touchUpInside) 159 | configsButton.setImage( 160 | UIImage(systemName: "arrow.forward.circle")? 161 | .withRenderingMode(.alwaysOriginal) 162 | .withTintColor(.systemBackground), 163 | for: .normal 164 | ) 165 | } 166 | 167 | private func setupScrollBarConfig(_ config: DMScrollBar.Configuration) { 168 | tableView.configureScrollBar(with: config, delegate: self) 169 | } 170 | 171 | private func setupSections() { 172 | sections = (0..<20).map { sectionNumber in 173 | Section( 174 | date: Date(timeIntervalSinceNow: TimeInterval(86400 * sectionNumber)), 175 | items: (0..<10).map { "Item #\($0)" } 176 | ) 177 | } 178 | } 179 | } 180 | 181 | extension ViewController: UITableViewDataSource { 182 | func numberOfSections(in tableView: UITableView) -> Int { 183 | sections.count 184 | } 185 | 186 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 187 | sections[section].items.count 188 | } 189 | 190 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 191 | headerDateFormatter.string(from: sections[section].date) 192 | } 193 | 194 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 195 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 196 | var configuration = cell.defaultContentConfiguration() 197 | configuration.text = sections[indexPath.section].items[indexPath.item] 198 | cell.contentConfiguration = configuration 199 | 200 | return cell 201 | } 202 | } 203 | 204 | extension ViewController: DMScrollBarDelegate { 205 | /// In this example, this method returns the section header title for the top visible section 206 | func infoLabelText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? { 207 | headerTitle(in: tableView, forOffset: contentOffset) 208 | } 209 | 210 | /// In this example, this method returns the section header title for the top visible section 211 | func scrollBarText(forContentOffset contentOffset: CGFloat, scrollIndicatorOffset: CGFloat) -> String? { 212 | guard let section = sectionIndex(in: tableView, forOffset: contentOffset) else { return nil } 213 | return shortHeaderDateFormatter.string(from: sections[section].date).capitalized 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '13.0' 4 | 5 | target 'DMScrollBar_Example' do 6 | pod 'DMScrollBar', :path => '../' 7 | 8 | target 'DMScrollBar_Tests' do 9 | inherit! :search_paths 10 | 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DMScrollBar (2.0.0) 3 | 4 | DEPENDENCIES: 5 | - DMScrollBar (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | DMScrollBar: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | DMScrollBar: ce3c80b71f76f3adc133e7c71c58ec5e53a30ad9 13 | 14 | PODFILE CHECKSUM: 507fb18072a74f73e82cc05d6cc741591f4798ee 15 | 16 | COCOAPODS: 1.11.3 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/DMScrollBar.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DMScrollBar", 3 | "version": "2.0.0", 4 | "summary": "Customizable Scroll Bar for Scroll view.", 5 | "description": "Customizable Scroll Bar for Scroll View with additional info label appearing during the scroll.", 6 | "homepage": "https://github.com/batanus/DMScrollBar", 7 | "screenshots": [ 8 | "https://user-images.githubusercontent.com/25244017/209937470-d76a558c-6350-4d96-a142-13a6ef32e0f8.gif", 9 | "https://user-images.githubusercontent.com/25244017/209937479-e7acbbd1-fba1-4fa8-a34f-9bb4b3ee790e.gif", 10 | "https://user-images.githubusercontent.com/25244017/209937517-be2e6f54-53f9-447d-ad38-4fab39624551.gif" 11 | ], 12 | "license": { 13 | "type": "MIT", 14 | "file": "LICENSE" 15 | }, 16 | "authors": { 17 | "Dmitrii Medvedev": "dima7711@gmail.com" 18 | }, 19 | "source": { 20 | "git": "https://github.com/batanus/DMScrollBar.git", 21 | "tag": "2.0.0" 22 | }, 23 | "swift_versions": [ 24 | "5.7" 25 | ], 26 | "source_files": "DMScrollBar/**/*", 27 | "platforms": { 28 | "ios": "14.0" 29 | }, 30 | "swift_version": "5.7" 31 | } 32 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DMScrollBar (2.0.0) 3 | 4 | DEPENDENCIES: 5 | - DMScrollBar (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | DMScrollBar: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | DMScrollBar: ce3c80b71f76f3adc133e7c71c58ec5e53a30ad9 13 | 14 | PODFILE CHECKSUM: 507fb18072a74f73e82cc05d6cc741591f4798ee 15 | 16 | COCOAPODS: 1.11.3 17 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar-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 | FMWK 17 | CFBundleShortVersionString 18 | 2.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_DMScrollBar : NSObject 3 | @end 4 | @implementation PodsDummy_DMScrollBar 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double DMScrollBarVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char DMScrollBarVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar.modulemap: -------------------------------------------------------------------------------- 1 | framework module DMScrollBar { 2 | umbrella header "DMScrollBar-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/DMScrollBar/DMScrollBar.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## DMScrollBar 5 | 6 | Copyright (c) 2022 Dmitrii Medvedev 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2022 Dmitrii Medvedev <dima7711@gmail.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | DMScrollBar 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_DMScrollBar_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_DMScrollBar_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | BCSYMBOLMAP_DIR="BCSymbolMaps" 23 | 24 | 25 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 26 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 27 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 28 | 29 | # Copies and strips a vendored framework 30 | install_framework() 31 | { 32 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 33 | local source="${BUILT_PRODUCTS_DIR}/$1" 34 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 35 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 36 | elif [ -r "$1" ]; then 37 | local source="$1" 38 | fi 39 | 40 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 41 | 42 | if [ -L "${source}" ]; then 43 | echo "Symlinked..." 44 | source="$(readlink "${source}")" 45 | fi 46 | 47 | if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then 48 | # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied 49 | find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do 50 | echo "Installing $f" 51 | install_bcsymbolmap "$f" "$destination" 52 | rm "$f" 53 | done 54 | rmdir "${source}/${BCSYMBOLMAP_DIR}" 55 | fi 56 | 57 | # Use filter instead of exclude so missing patterns don't throw errors. 58 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 59 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 60 | 61 | local basename 62 | basename="$(basename -s .framework "$1")" 63 | binary="${destination}/${basename}.framework/${basename}" 64 | 65 | if ! [ -r "$binary" ]; then 66 | binary="${destination}/${basename}" 67 | elif [ -L "${binary}" ]; then 68 | echo "Destination binary is symlinked..." 69 | dirname="$(dirname "${binary}")" 70 | binary="${dirname}/$(readlink "${binary}")" 71 | fi 72 | 73 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 74 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 75 | strip_invalid_archs "$binary" 76 | fi 77 | 78 | # Resign the code if required by the build settings to avoid unstable apps 79 | code_sign_if_enabled "${destination}/$(basename "$1")" 80 | 81 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 82 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 83 | local swift_runtime_libs 84 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 85 | for lib in $swift_runtime_libs; do 86 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 87 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 88 | code_sign_if_enabled "${destination}/${lib}" 89 | done 90 | fi 91 | } 92 | # Copies and strips a vendored dSYM 93 | install_dsym() { 94 | local source="$1" 95 | warn_missing_arch=${2:-true} 96 | if [ -r "$source" ]; then 97 | # Copy the dSYM into the targets temp dir. 98 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 99 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 100 | 101 | local basename 102 | basename="$(basename -s .dSYM "$source")" 103 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 104 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 105 | 106 | # Strip invalid architectures from the dSYM. 107 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 108 | strip_invalid_archs "$binary" "$warn_missing_arch" 109 | fi 110 | if [[ $STRIP_BINARY_RETVAL == 0 ]]; then 111 | # Move the stripped file into its final destination. 112 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 113 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 114 | else 115 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 116 | mkdir -p "${DWARF_DSYM_FOLDER_PATH}" 117 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 118 | fi 119 | fi 120 | } 121 | 122 | # Used as a return value for each invocation of `strip_invalid_archs` function. 123 | STRIP_BINARY_RETVAL=0 124 | 125 | # Strip invalid architectures 126 | strip_invalid_archs() { 127 | binary="$1" 128 | warn_missing_arch=${2:-true} 129 | # Get architectures for current target binary 130 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 131 | # Intersect them with the architectures we are building for 132 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 133 | # If there are no archs supported by this binary then warn the user 134 | if [[ -z "$intersected_archs" ]]; then 135 | if [[ "$warn_missing_arch" == "true" ]]; then 136 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 137 | fi 138 | STRIP_BINARY_RETVAL=1 139 | return 140 | fi 141 | stripped="" 142 | for arch in $binary_archs; do 143 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 144 | # Strip non-valid architectures in-place 145 | lipo -remove "$arch" -output "$binary" "$binary" 146 | stripped="$stripped $arch" 147 | fi 148 | done 149 | if [[ "$stripped" ]]; then 150 | echo "Stripped $binary of architectures:$stripped" 151 | fi 152 | STRIP_BINARY_RETVAL=0 153 | } 154 | 155 | # Copies the bcsymbolmap files of a vendored framework 156 | install_bcsymbolmap() { 157 | local bcsymbolmap_path="$1" 158 | local destination="${BUILT_PRODUCTS_DIR}" 159 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 160 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 161 | } 162 | 163 | # Signs a framework with the provided identity 164 | code_sign_if_enabled() { 165 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 166 | # Use the current code_sign_identity 167 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 168 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 169 | 170 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 171 | code_sign_cmd="$code_sign_cmd &" 172 | fi 173 | echo "$code_sign_cmd" 174 | eval "$code_sign_cmd" 175 | fi 176 | } 177 | 178 | if [[ "$CONFIGURATION" == "Debug" ]]; then 179 | install_framework "${BUILT_PRODUCTS_DIR}/DMScrollBar/DMScrollBar.framework" 180 | fi 181 | if [[ "$CONFIGURATION" == "Release" ]]; then 182 | install_framework "${BUILT_PRODUCTS_DIR}/DMScrollBar/DMScrollBar.framework" 183 | fi 184 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 185 | wait 186 | fi 187 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_DMScrollBar_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_DMScrollBar_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar/DMScrollBar.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "DMScrollBar" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_DMScrollBar_Example { 2 | umbrella header "Pods-DMScrollBar_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Example/Pods-DMScrollBar_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar/DMScrollBar.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "DMScrollBar" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests-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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_DMScrollBar_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_DMScrollBar_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_DMScrollBar_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_DMScrollBar_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar/DMScrollBar.framework/Headers" 5 | OTHER_LDFLAGS = $(inherited) -framework "DMScrollBar" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_DMScrollBar_Tests { 2 | umbrella header "Pods-DMScrollBar_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-DMScrollBar_Tests/Pods-DMScrollBar_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DMScrollBar/DMScrollBar.framework/Headers" 5 | OTHER_LDFLAGS = $(inherited) -framework "DMScrollBar" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Example/Tests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class Tests: XCTestCase { 4 | 5 | override func setUp() { 6 | super.setUp() 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDown() { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | super.tearDown() 13 | } 14 | 15 | func testExample() { 16 | // This is an example of a functional test case. 17 | XCTAssert(true, "Pass") 18 | } 19 | 20 | func testPerformanceExample() { 21 | // This is an example of a performance test case. 22 | self.measure() { 23 | // Put the code you want to measure the time of here. 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Dmitrii Medvedev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "DMScrollBar", 6 | platforms: [.iOS(.v14)], 7 | products: [ 8 | .library( 9 | name: "DMScrollBar", 10 | targets: ["DMScrollBar"] 11 | ) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "DMScrollBar", 16 | path: "DMScrollBar" 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DMScrollBar 2 | 3 | [![CI Status](https://img.shields.io/travis/batanus/DMScrollBar.svg?style=flat)](https://travis-ci.org/batanus/DMScrollBar) 4 | [![Version](https://img.shields.io/cocoapods/v/DMScrollBar.svg?style=flat)](https://cocoapods.org/pods/DMScrollBar) 5 | [![License](https://img.shields.io/cocoapods/l/DMScrollBar.svg?style=flat)](https://cocoapods.org/pods/DMScrollBar) 6 | [![Platform](https://img.shields.io/cocoapods/p/DMScrollBar.svg?style=flat)](https://cocoapods.org/pods/DMScrollBar) 7 | 8 | ## Example 9 | iOS style | Default style | iOS & Default combined style | Absolutely custom style | Right label style |Easy to change 10 | :-: | :-: | :-: | :-: | :-: | :-: 11 | | | | | | | 12 | 13 | 14 | 15 | ## Description 16 | 17 | DMScrollBar is best in class customizable ScrollBar for ScrollView. It has: 18 | - Showing info label when user interaction with ScrollBar is started 19 | - Decelerating, Bounce & Rubber band mechanisms 20 | - Super customizable configuration 21 | - Different states for active / inactive states 22 | - Haptic feedback on interaction start / end and when info label changes on specified offset 23 | - Super Fancy animations 24 | 25 | 26 | ## Installation 27 | 28 | DMScrollBar is available through [CocoaPods](https://cocoapods.org). To install 29 | it, simply add the following line to your Podfile: 30 | 31 | ```ruby 32 | pod 'DMScrollBar', '~> 1.0.0' 33 | ``` 34 | and run 35 | 36 | ```ruby 37 | pod install 38 | ``` 39 | 40 | ## Usage 41 | 42 | On any ScrollView you want to add the ScrollBar with default configuration, just call (see result on Gid #2): 43 | ```swift 44 | scrollView.configureScrollBar() 45 | ``` 46 | 47 | 48 | If you want to provide title for info label, implement `DMScrollBarDelegate` protocol on your ViewController: 49 | ```swift 50 | extension ViewController: DMScrollBarDelegate { 51 | /// In this example, this method returns the section header title for the top visible section 52 | func indicatorTitle(forOffset offset: CGFloat) -> String? { 53 | return "Your title for info label" 54 | } 55 | } 56 | ``` 57 | 58 | 59 | If you want to have iOS style scroll bar, configure ScrollBar with `.iosStyle` config (_Next code will create config for the Scroll Bar for Gif #1_): 60 | ```swift 61 | scrollView.configureScrollBar(with: .iosStyle, delegate: self) 62 | ``` 63 | 64 | 65 | Any ScrollBar configuration can be easily combined with another one (_Next code will create config for the Scroll Bar for Gif #3_): 66 | ```swift 67 | let iosCombinedDefaultConfig = DMScrollBar.Configuration( 68 | indicator: .init( 69 | normalState: .iosStyle(width: 3), 70 | activeState: .default 71 | ) 72 | ) 73 | scrollView.configureScrollBar(with: iosCombinedDefaultConfig, delegate: self) 74 | ``` 75 | 76 | 77 | If you want to configure scroll bar, with custom config, create configuration and call `configureScrollBar` (_Next code will create config for the Scroll Bar for Gif #4_): 78 | ```swift 79 | let customConfig = DMScrollBar.Configuration( 80 | isAlwaysVisible: false, 81 | hideTimeInterval: 1.5, 82 | shouldDecelerate: false, 83 | indicator: DMScrollBar.Configuration.Indicator( 84 | normalState: .init( 85 | size: CGSize(width: 35, height: 35), 86 | backgroundColor: UIColor(red: 200 / 255, green: 150 / 255, blue: 80 / 255, alpha: 1), 87 | insets: UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0), 88 | image: UIImage(systemName: "arrow.up.and.down.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.white), 89 | imageSize: CGSize(width: 20, height: 20), 90 | roundedCorners: .roundedLeftCorners 91 | ), 92 | activeState: .custom( 93 | config: .init( 94 | size: CGSize(width: 50, height: 50), 95 | backgroundColor: UIColor.brown, 96 | insets: UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 6), 97 | image: UIImage(systemName: "calendar.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.cyan), 98 | imageSize: CGSize(width: 28, height: 28), 99 | roundedCorners: .allRounded 100 | ), 101 | textConfig: nil 102 | ), 103 | stateChangeAnimationDuration: 0.5, 104 | insetsFollowsSafeArea: true, 105 | animation: .init(showDuration: 0.75, hideDuration: 0.75, animationType: .fadeAndSide) 106 | ), 107 | infoLabel: DMScrollBar.Configuration.InfoLabel( 108 | font: .systemFont(ofSize: 15), 109 | textColor: .white, 110 | distanceToScrollIndicator: 40, 111 | backgroundColor: .brown, 112 | textInsets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), 113 | maximumWidth: 300, 114 | roundedCorners: .init(radius: .rounded, corners: [.topLeft, .bottomRight]), 115 | animation: .init(showDuration: 0.75, hideDuration: 0.75, animationType: .fadeAndSide) 116 | ) 117 | ) 118 | scrollView.configureScrollBar(with: customConfig, delegate: self) 119 | ``` 120 | 121 | ## Author 122 | 123 | Dmitrii Medvedev, dima7711@gmail.com 124 | 125 | ## License 126 | 127 | DMScrollBar is available under the MIT license. See the LICENSE file for more info. 128 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------