diff --git a/README.md b/README.md index f687e0e..7de0307 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ -

- ScrollStackController +

+ + + logo-library +

-

Easy scrollable layouts in UIKit

+[![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-Orange?style=flat-square) +[![Platform](https://img.shields.io/badge/Platforms-iOS-4E4E4E.svg?colorA=28a745)](#installation) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) +[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/ScrollStackController.svg?style=flat-square)](https://img.shields.io/cocoapods/v/ScrollStackController.svg) + Create complex scrollable layout using UIViewControllers or plain UIViews and simplify your code! @@ -25,6 +32,7 @@ You can think of it as `UITableView` but with several differences: | ⏱ | Compact code base, less than 1k LOC with no external dependencies. | | 🎯 | Easy to use and extensible APIs set. | | 🧬 | It uses standard UIKit components at its core. No magic, just a combination of `UIScrollView`+`UIStackView`. | +| 🧨 | Support SwiftUI's View and autosizing based upon View's content | | 🐦 | Fully made in Swift 5 from Swift β₯ lovers | ## ❀️ Your Support @@ -32,41 +40,43 @@ You can think of it as `UITableView` but with several differences: *Hi fellow developer!* You know, maintaing and developing tools consumes resources and time. While I enjoy making them **your support is foundamental to allow me continue its development**. -If you are using SwiftLocation or any other of my creations please consider the following options: +If you are using `ScrollStackController` or any other of my creations please consider the following options: - [**Make a donation with PayPal**](https://www.paypal.com/paypalme/danielemargutti/20) - [**Become a Sponsor**](https://github.com/sponsors/malcommac) - - [Follow Me](https://github.com/malcommac) ## Table of Contents -- [When to use `ScrollStackController` and when not](#whentousescrollstackcontrollerandwhennot) -- [How to use it](#howtouseit) - - [Adding Rows](#addingrows) - - [Removing / Replacing Rows](#removingreplacingrows) - - [Move Rows](#moverows) - - [Hide / Show Rows](#hideshowrows) - - [Hide / Show Rows with custom animations](#customanimations) - - [Reload Rows](#reloadrows) - - [Sizing Rows](#sizingrows) - - [Fixed Row Size](#fixedrowsize) - - [Fitting Layout Row Size](#fittinglayoutrowsize) - - [Collapsible Rows](#collapsiblerows) - - [Working with dynamic UICollectionView/UITableView/UITextView](#workingwithdynamicuicollectionviewuitableviewuitextview) - - [Using plain UIViews instead of view controllers](#lightweightplainuiview) - - [Rows Separator](#rowsseparator) - - [Tap On Rows](#taponrows) - - [Get the row/controller](#utilsmethods) - - [Set Row Insets](#setrowinsets) - - [Change ScrollStack scrolling axis](#changescrollaxis) - - [Subscribe to Events](#rowevents) -- [Example App](#exampleapp) -- [Installation](#installation) -- [System Requirements](#systemrequirements) -- [Author & License](#authorlicense) +- [❀️ Your Support](#️-your-support) +- [Table of Contents](#table-of-contents) + - [When to use `ScrollStackController` and when not](#when-to-use-scrollstackcontroller-and-when-not) + - [How to use it](#how-to-use-it) + - [Adding Rows](#adding-rows) + - [Removing / Replacing Rows](#removing--replacing-rows) + - [Move Rows](#move-rows) + - [Hide / Show Rows](#hide--show-rows) + - [Hide / Show Rows with custom animations](#hide--show-rows-with-custom-animations) + - [Reload Rows](#reload-rows) + - [Sizing Rows](#sizing-rows) + - [Fixed Row Size](#fixed-row-size) + - [Fitting Layout Row Size](#fitting-layout-row-size) + - [Collapsible Rows](#collapsible-rows) + - [Working with dynamic UICollectionView/UITableView/UITextView](#working-with-dynamic-uicollectionviewuitableviewuitextview) + - [Rows Separator](#rows-separator) + - [Using plain UIViews instead of view controllers](#using-plain-uiviews-instead-of-view-controllers) + - [Tap On Rows](#tap-on-rows) + - [Get the row/controller](#get-the-rowcontroller) + - [Set Row Insets](#set-row-insets) + - [Change ScrollStack scrolling axis](#change-scrollstack-scrolling-axis) + - [Subscribe to Row Events](#subscribe-to-row-events) + - [System Requirements](#system-requirements) + - [Example App](#example-app) + - [Installation](#installation) +- [Contributing](#contributing) +- [Copyright \& Acknowledgements](#copyright--acknowledgements) @@ -624,33 +634,37 @@ Example: ```swift class ViewController: ScrollStackController, ScrollStackControllerDelegate { - - func viewDidLoad() { - super.viewDidLoad() - - self.scrollStack.stackDelegate = self - } - - func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) { - // stack did scroll - } - - func scrollStackRowDidBecomeVisible(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { - // Row did become partially or entirely visible. - } + + func viewDidLoad() { + super.viewDidLoad() + + self.scrollStack.stackDelegate = self + } + + func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) { + // Stack did scroll + } - func scrollStackRowDidBecomeHidden(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { - // Row did become partially or entirely invisible. - } + + func scrollStackDidEndScrollingAnimation(_ stackView: ScrollStack) { + // Scrolling animation has ended + } - func scrollStackDidUpdateLayout(_ stackView: ScrollStack) { - // This function is called when layout is updated (added, removed, hide or show one or more rows). - } + func scrollStackRowDidBecomeVisible(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { + // Row did become partially or entirely visible. + } - func scrollStackContentSizeDidChange(_ stackView: ScrollStack, from oldValue: CGSize, to newValue: CGSize) { - // This function is called when content size of the stack did change (remove/add, hide/show rows). - } + func scrollStackRowDidBecomeHidden(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { + // Row did become partially or entirely invisible. + } + func scrollStackDidUpdateLayout(_ stackView: ScrollStack) { + // This function is called when layout is updated (added, removed, hide or show one or more rows). + } + + func scrollStackContentSizeDidChange(_ stackView: ScrollStack, from oldValue: CGSize, to newValue: CGSize) { + // This function is called when content size of the stack did change (remove/add, hide/show rows). + } } ``` diff --git a/Resources/scrollstack-dark.png b/Resources/scrollstack-dark.png new file mode 100644 index 0000000..5c68519 Binary files /dev/null and b/Resources/scrollstack-dark.png differ diff --git a/Resources/scrollstack-light.png b/Resources/scrollstack-light.png new file mode 100644 index 0000000..9d2beb7 Binary files /dev/null and b/Resources/scrollstack-light.png differ diff --git a/ScrollStackController.podspec b/ScrollStackController.podspec index 3c42951..14b0645 100644 --- a/ScrollStackController.podspec +++ b/ScrollStackController.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ScrollStackController" - s.version = "1.5.1" + s.version = "1.7.1" s.summary = "Create complex scrollable layout using UIViewController and simplify your code" s.homepage = "https://github.com/malcommac/ScrollStackController" s.license = { :type => "MIT", :file => "LICENSE" } @@ -10,5 +10,5 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/malcommac/ScrollStackController.git", :tag => s.version.to_s } s.frameworks = "Foundation", "UIKit" s.source_files = 'Sources/**/*.swift' - s.swift_versions = ['5.0', '5.1', '5.3', '5.4', '5.5', '5.7', '5.8'] + s.swift_versions = ['5.0', '5.1', '5.3', '5.4', '5.5', '5.7', '5.8', '5.9', '5.10'] end diff --git a/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate b/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate index 25fc294..07a8032 100644 Binary files a/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate and b/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sources/ScrollStackController/ScrollStack.swift b/Sources/ScrollStackController/ScrollStack.swift index 992f3a3..a8e3210 100644 --- a/Sources/ScrollStackController/ScrollStack.swift +++ b/Sources/ScrollStackController/ScrollStack.swift @@ -794,7 +794,13 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { return .offscreen } - return (bounds.contains(rowFrame) ? .entire : .partial) + if bounds.contains(rowFrame) { + return .entire + } else { + let intersection = bounds.intersection(rowFrame) + let intersectionPercentage = ((intersection.width * intersection.height) / (rowFrame.width * rowFrame.height)) * 100 + return .partial(percentage: intersectionPercentage) + } } /// Remove passed row from stack view. @@ -860,6 +866,8 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { return createRow(newRow, at: index, cellToRemove: cellToRemove, animated: animated, completion: completion) } + private var rowVisibilityChangesDispatchWorkItem: DispatchWorkItem? + /// Private implementation to add new row. private func createRow(_ newRow: ScrollStackRow, at index: Int, cellToRemove: ScrollStackRow?, @@ -878,7 +886,21 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { }, completion: nil) } - scrollViewDidScroll(self) + if rowVisibilityChangesDispatchWorkItem == nil { + + rowVisibilityChangesDispatchWorkItem = DispatchWorkItem(block: { [weak self] in + if let stackDelegate = self?.stackDelegate { + self?.dispatchRowsVisibilityChangesTo(stackDelegate) + } + + self?.rowVisibilityChangesDispatchWorkItem = nil + }) + + /// Schedule a single `dispatchRowsVisibilityChangesTo(_:)` call. + /// + /// In this way, when rows are created inside a for-loop, the delegate is called only once after the `ScrollStack` has been fully laid out. + DispatchQueue.main.async(execute: rowVisibilityChangesDispatchWorkItem!) + } return newRow } @@ -979,25 +1001,27 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { } private func dispatchRowsVisibilityChangesTo(_ delegate: ScrollStackControllerDelegate) { - delegate.scrollStackDidScroll(self, offset: contentOffset) - rows.enumerated().forEach { (idx, row) in let current = isRowVisible(index: idx) - if let previous = prevVisibilityState[row] { - switch (previous, current) { - case (.offscreen, .partial), // row will become invisible - (.hidden, .partial), - (.hidden, .entire): - delegate.scrollStackRowDidBecomeVisible(self, row: row, index: idx, state: current) - - case (.partial, .offscreen), // row will become visible - (.partial, .hidden), - (.entire, .hidden): - delegate.scrollStackRowDidBecomeHidden(self, row: row, index: idx, state: current) - - default: - break - } + let previous = prevVisibilityState[row] + + switch (previous, current) { + case (.offscreen, .partial), // row will become visible + (nil, .entire), + (nil, .partial), + (.partial, .entire), + (.hidden, .partial), + (.hidden, .entire): + delegate.scrollStackRowDidBecomeVisible(self, row: row, index: idx, state: current) + + case (.partial, .offscreen), // row will become invisible + (.entire, .partial), + (.partial, .hidden), + (.entire, .hidden): + delegate.scrollStackRowDidBecomeHidden(self, row: row, index: idx, state: current) + + default: + break } // store previous state @@ -1062,10 +1086,19 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { guard let stackDelegate = stackDelegate else { return } - + stackDelegate.scrollStackDidScroll(self, offset: contentOffset) + dispatchRowsVisibilityChangesTo(stackDelegate) } + public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + guard let stackDelegate = stackDelegate else { + return + } + + stackDelegate.scrollStackDidEndScrollingAnimation(self) + } + open override func layoutSubviews() { super.layoutSubviews() diff --git a/Sources/ScrollStackController/ScrollStackRow.swift b/Sources/ScrollStackController/ScrollStackRow.swift index 35c4a22..b334712 100644 --- a/Sources/ScrollStackController/ScrollStackRow.swift +++ b/Sources/ScrollStackController/ScrollStackRow.swift @@ -130,12 +130,6 @@ open class ScrollStackRow: UIView, UIGestureRecognizerDelegate { didSet { separatorView.isHidden = isSeparatorHidden } -// get { -// return separatorView.isHidden -// } -// set { -// separatorView.isHidden = newValue -// } } // MARK: Private Properties @@ -246,7 +240,9 @@ open class ScrollStackRow: UIView, UIGestureRecognizerDelegate { } open override func updateConstraints() { + // called the event to update the height of the row. askForCutomizedSizeOfContentView(animated: false) + super.updateConstraints() } @@ -366,16 +362,25 @@ open class ScrollStackRow: UIView, UIGestureRecognizerDelegate { var bestSize: CGSize! if stackView.axis == .vertical { - let maxAllowedSize = CGSize(width: stackView.bounds.size.width, height: CGFloat.greatestFiniteMagnitude) - bestSize = contentView.systemLayoutSizeFitting(maxAllowedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) + let maxAllowedSize = CGSize(width: contentView.bounds.width, height: 0) + bestSize = contentView.systemLayoutSizeFitting( + maxAllowedSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ) } else { - let maxAllowedSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: stackView.bounds.size.height) - bestSize = contentView.systemLayoutSizeFitting(maxAllowedSize, withHorizontalFittingPriority: .defaultLow, verticalFittingPriority: .required) + let maxAllowedSize = CGSize(width: 0, height: contentView.bounds.height) + bestSize = contentView.systemLayoutSizeFitting( + maxAllowedSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .required + ) } setupRowToFixedValue(bestSize.height) } + // MARK: - Handle Touch public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { diff --git a/Sources/ScrollStackController/Support/ScrollStack+Protocols.swift b/Sources/ScrollStackController/Support/ScrollStack+Protocols.swift index 204ac98..aa3e11f 100644 --- a/Sources/ScrollStackController/Support/ScrollStack+Protocols.swift +++ b/Sources/ScrollStackController/Support/ScrollStack+Protocols.swift @@ -68,6 +68,11 @@ public protocol ScrollStackControllerDelegate: AnyObject { /// - Parameter offset: current scroll offset. func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) + /// Tells the delegate when a scrolling animation in the scroll view concludes. + /// + /// - Parameter stackView: The ScrollStack object that’s performing the scrolling animation. + func scrollStackDidEndScrollingAnimation(_ stackView: ScrollStack) + /// Row did become partially or entirely visible. /// /// - Parameter row: target row. @@ -177,9 +182,9 @@ public extension ScrollStack { /// - `hidden`: row is invisible and hidden. /// - `offscreen`: row is not hidden but currently offscreen due to scroll position. /// - `removed`: row is removed manually. - enum RowVisibility { + enum RowVisibility: Equatable { case hidden - case partial + case partial(percentage: Double) case entire case offscreen case removed diff --git a/banner.png b/banner.png deleted file mode 100644 index 1f0bdbb..0000000 Binary files a/banner.png and /dev/null differ

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /