在 UIKit 中实现可滚动布局
使用 UIViewControllers 或纯 UIView 创建复杂可滚动布局,简化您的代码!
ScrollStackController 是一个类,您可以使用它通过可滚动的 UIStackView
创建复杂布局,但每个行都由单独的 UIViewController
处理;这允许您保持非常好的一致性。
您可以将其视为 UITableView
但有很多不同之处:
- 每行可以独立管理不同的
UIViewController
:无需更多的大型控制器,更加清洁且易于维护的架构。 - 如果您需要一个轻量级的解决方案,则可以继续使用纯
UIView
实例:当您使用 ScrollStackController 作为布局助手或您的视图不需要复杂的逻辑时,这特别有用。 - 从一开始就由 AutoLayout 支持;它通过使用
UIScrollView + UIStackView
的组合来提供一个适用于固定和动态行尺寸的动画友好型控制器。 - 您无需手动处理视图复用:假设您的布局由多个不同的屏幕构成。不需要视图复用,但这可能会导致更困难的布局管理。使用更简单、更安全的 API 集合
ScrollStackView
是实现此类布局的理想方式。
功能亮点 | |
---|---|
创建复杂布局,无需像 UICollectionView 或 UITableView 视图复用那样需要的样板代码。 |
|
将每个屏幕视为独立存在的 UIVIewController 以简化您的架构。 |
|
支持轻量级模式,即可在无需 UIViewController 的情况下布局 UIView 。 |
|
轻松实现行的显示/隐藏和尺寸调整,即使带有自定义动画也是如此! | |
紧凑的代码库,小于 1k LOC,没有外部依赖。 | |
易于使用且可扩展的 API 集合。 | |
它在其核心使用标准的 UIKit 组件。没有魔法,只是 UIScrollView +UIStackView 的组合。 |
|
Swift 5 完全制作,由 Swift 爱好者编写。 |
❤️ 您的支持
你好,开发者同行!
你知道,维护和开发工具需要资源和时间。虽然我喜欢制作它们,但 您的支持是我继续其开发的基本。
如果您正在使用 SwiftLocation 或我的任何其他创作,请考虑以下选项
目录
何时使用 ScrollStackController
以及何时不使用
ScrollStackController
最好用于较短的屏幕,且行集合异质:在这些情况下,你不需要有视图回收。
由于自动布局,你将免费获得更新和动画。
你也可以独立管理每个屏幕,具有良好的事务分离;更棒的是,与 UITableView
和 UICollectionView
不同,你可以在 ScrollStack
视图中保留对 UIViewController
(及其视图)的强引用,并在任何时刻对它们进行更改。
ScrollStackController
并不适用于所有情况。ScrollStackController
在屏幕加载时第一次布局整个 UI。如果你有一长串行,可能会遇到延迟。
因此,ScrollStackController
通常不适用于包含同类视图、显示相似数据的屏幕(在这种情况下,你应该使用 UITableView
或 UICollectionView
)。
如何使用它
本包的主类是 ScrollStack
,它是 UIScrollView
的一个子类。它管理每一行的布局、动画,并保留您行的强引用。
这是架构的概述
ScrollStackController
:是UIViewController
的一个子类。您可以在您的视图控制器中将其用作子控制器。这允许您管理为堆栈控制器添加的每一行的相关子控制器事件。ScrollStack
:ScrollStackController
的视图是一个ScrollStack
,它是UIScrollView
的一个子类,带有一个UIStackView
,这使得您能够管理堆栈的布局。您可以通过控制器中的scrollStack
属性访问它。- 每一行都是
ScrollStackRow
,它是UIView
的一个子类。内部有两个视图,即contentView
(对管理的UIViewController
的view
的引用)和separatorView
。行对管理的视图控制器有强引用,因此您不需要自行保留强引用。 - 分隔视图是
ScrollStackSeparator
类的子类。
正如我们所说的,通常您不希望直接实例化 ScrollStack
控件,而是使用 ScrollStackController
类。这是一个视图控制器,它允许您免费获取子视图控制器管理,所以当您向堆栈中添加或删除行时,您将免费获得标准的 UIViewController
事件!
这是在视图控制器中初始化的一个例子
class MyViewController: UIViewController {
private var stackController = ScrollStackViewController()
override func viewDidLoad() {
super.viewDidLoad()
stackController.view.frame = contentView.bounds
contentView.addSubview(stackController.view)
}
}
现在您已经准备好在 stackController
类中使用 ScrollStack
控件了。ScrollStack
提供了丰富的 API 来管理布局:添加、删除、移动、隐藏或显示行,包括内边距和分隔视图管理。
由 ScrollStack
管理的每一行都是 ScrollStackRow
的子类:它对放置内容的父 UIViewController
类有强引用。UIViewController
的 view
将成为行的 contentView
。
直到它们成为堆栈内部的行时,您不需要处理行/视图控制器的生命周期。
要获取堆栈行的列表,您可以使用 rows
属性。
// Standard methods
let allRows = scrollStack.rows
let isEmpty = scrollStack.isEmpty // true if it does not contains row
let notHiddenRows = scrollStack.rows.filter { !$0.isHidden }
// By Visibility
let currentlyVisibleRows = scrollStack.visibleRows // only currently visible rows (partially or enterly)
let enterlyVisibleRows = scrollStack.enterlyVisibleRows // only enterly visible rows into the stack
// Shortcuts
let firstRow = scrollStack.firstRow
let lastRow = scrollStack.lastRow
让我们看一下下面的内容。
添加行
ScrollStack
提供了一整套管理行的综合方法,包括在开始和结束处插入行,在其他行之上或之下插入行。
要添加行,您可以使用以下方法之一
addRow(controller:at:animated:) -> ScrollStackRow?
addRows(controllers:at:animated:) -> [ScrollStackRow]?
这两个方法都接受以下参数
controller/s
:一个或多个UIViewController
实例;这些控制器中的每个视图都将成为堆栈内部的ScrollStackRow
(类似单元格)中的一行。at
:指定插入点。它是一个枚举,有以下选项:top
(在第一个索引处),bottom
(追加到列表底部),atIndex
(特定索引),after
或below
(在包含特定UIViewController
的行之后/之下)。animated
:如果为 true,则插入将进行动画处理。completion
:在操作结束时调用的完成回调。
以下代码将添加具有每个视图控制器视图的行。
let welcomeVC = WelcomeVC.create()
let tagsVC = TagsVC.create(delegate: self)
let galleryVC = GalleryVC.create()
stackView.addRows(controllers: [welcomeVC, notesVC, tagsVC, galleryVC], animated: false)
如你所注意到的,不需要保留任何视图控制器的强引用;它们被自动强引用,以便将它们添加到堆栈中。
移除/替换行
一组类似的 API 用于从堆栈中移除现有行。
removeAllRows(animated:)
:移除堆栈中的所有行。removeRow(index:animated:) -> UIViewController?
:移除给定索引处的特定行。它返回移除的视图控制器引用。removeRows(indexes:animated:) -> [UIViewController]?
:从堆栈中移除指定索引处的行。返回移除的UIViewController
实例。replaceRow(index:withRow:animated:completion:)
:用管理新传过的视图控制器的新的行替换现有行。
示例
let newVC: UIViewController = ...
stackView.replaceRow(index: 1, withRow: newVC, animated: true) {
print("Gallery controller is now in place!!")
}
移动行
如果您需要通过将行移动到另一个位置来调整堆栈的层次结构,您可以使用:
moveRow(index:to:animated:completion:)
:将指定索引处行的位置移动到另一个索引(两个索引都必须有效)。
以下方法通过动画过渡将随机位置的第一行移动。
let randomDst = Int.random(in: 1..<stackView.rows.count)
stackView.moveRow(index: 0, to: randomDst, animated: true, completion: nil)
隐藏/显示行
ScrollStack
使用了 UIStackView
的力量:您可以使用以下方法之一轻松显示和隐藏行,并进行华丽的动画:
setRowHidden(index:isHidden:animated:completion:)
:隐藏或显示索引处的行。setRowsHidden(indexes:isHidden:animated:completion:)
:隐藏或显示指定索引处的多行。
示例
stackView.setRowsHidden(indexes: [0,1,2], isHidden: true, animated: true)
请注意:当你隐藏一行时,该行仍然属于堆栈的一部分,并未被删除,只是被隐藏了!如果你通过调用 ScrollStack
的 rows
属性来获取行列表,你仍然可以看到它。
使用自定义动画隐藏/显示行
您可以通过任何自定义过渡轻松显示或隐藏行;只需确保您的视图控制器符合 ScrollStackRowAnimatable
协议。
此协议定义了一组动画信息(持续时间、延迟、弹性等)以及您可以覆盖的两个事件以执行操作。
public protocol ScrollStackRowAnimatable {
/// Animation main info.
var animationInfo: ScrollStackAnimationInfo { get }
/// Animation will start to hide or show the row.
func willBeginAnimationTransition(toHide: Bool)
/// Animation to hide/show the row did end.
func didEndAnimationTransition(toHide: Bool)
/// Animation transition.
func animateTransition(toHide: Bool)
}
因此,例如,您可以使用以下代码来复制以下动画:
extension WelcomeVC: ScrollStackRowAnimatable {
public var animationInfo: ScrollStackAnimationInfo {
return ScrollStackAnimationInfo(duration: 1, delay: 0, springDamping: 0.8)
}
public func animateTransition(toHide: Bool) {
switch toHide {
case true:
self.view.transform = CGAffineTransform(translationX: -100, y: 0)
self.view.alpha = 0
case false:
self.view.transform = .identity
self.view.alpha = 1
}
}
public func willBeginAnimationTransition(toHide: Bool) {
if toHide == false {
self.view.transform = CGAffineTransform(translationX: -100, y: 0)
self.view.alpha = 0
}
}
}
重新加载行
重新加载行方法允许你在有机会更新特定行的 contentView
(即管理的 UIViewController
视图)的同时重新设计整个堆栈的布局(使用 layoutIfNeeded()
)。
有三个方法:
reloadRow(index:animated:completion:)
:重新加载指定索引的特定行。reloadRows(indexes:animated:completion:)
:重新加载指定的一系列行。reloadAllRows(animated:completion:)
:重新加载所有行。
如果您实现的 UIViewController
符合 ScrollStackContainableController
协议,您将在类中收到通知,这样您就有机会刷新您的数据。
示例
class MyViewController: UIViewController {
private let scrollStackController = ScrollStackController()
@IBAction func someAction() {
scrollStackController.scrollStack.reloadRow(0)
}
}
// Your row 0 manages the GalleryVC, so in your GalleryVC implementation:
class GalleryVC: UIViewController, ScrollStackContainableController {
public func func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) {
// update your UI
}
}
行的大小调整
您可以通过以下两种方式来控制 ScrollStack
行中的 UIViewController
的大小:
- 在
UIViewController
的视图中使用 Autolayout 创建约束。 - 在您的
UIViewController
类中实现ScrollStackContainableController
协议,并在scrollStackRowSizeForAxis(:row:in:)
代理方法中返回非nil
值。
在两种情况下,ScrollStack
类都只会使用一个维度,根据活动滚动轴来布局视图控制器内容进入堆栈(如果滚动轴是horizontal
,则只能控制行的height
,如果它是vertical
,则只能控制width
。另一个维度将与滚动堆栈本身相同。
以下每个情况都包含在演示应用程序中
- 在GalleryVC中使用固定行大小
- 在TagsVC中使用可折叠/可展开的行
- 在NotesVC中根据
UITextView
的内容调整行大小 - 在PricingVC中根据
UITableView
的内容调整行大小
固定行大小
如果你的视图控制器有一个固定的大小,你可以简单地按如下方式返回它
class GalleryVC: UIViewController, ScrollStackContainableController {
public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? {
switch axis {
case .horizontal:
return .fixed(300)
case .vertical:
return .fixed(500)
}
}
}
如果你的堆栈支持单个轴,你可以显然避免switch条件。当你在滚动堆栈中添加此视图控制器时,它将按你的要求进行 sizing(已放置的任何高度/宽度约束将被删除)。
适应布局行大小
有时你可能想将内容视图的大小设置为适合视图控制器视图的内容。在这些情况下,你可以使用fitLayoutForAxis
。
示例
public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? {
return .fitLayoutForAxis
}
ScrollStack
将使用视图控制器视图的systemLayoutSizeFitting()
方法来获取适合内容的最佳大小。
可折叠行
有时你可能想创建可折叠的行。这些行可以根据一个变量具有不同的高度。
在这种情况下,你只需在你的视图控制器中实现一个isExpanded: Bool
变量并根据它返回不同的高度。
public class TagsVC: UIViewController, ScrollStackContainableController {
public var isExpanded = false
public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? {
return (isExpanded == false ? .fixed(170) : .fixed(170 + collectionView.contentSize.height + 20))
}
}
在你的主视图控制器中,你可以调用这个
// get the first row which manages this controller
let tagsRow = stackView.firstRowForControllerOfType(TagsVC.self)
// or if you have already the instance you can get the row directly
// let tagsRow = stackView.rowForController(tagsVCInstance)
let tagsVCInstance = (tagsRow.controller as! TagsVC)
tagsVCInstance.isExpanded = !tagsVCInstance.isExpanded
stackView.reloadRow(tagsRow, animated: true)
并且您的行将进行一次精彩的动画来调整内容的大小。
使用动态 UICollectionView/UITableView/UITextView
在某些特殊情况下,您可能需要根据视图控制器中的视图变化来调整行的大小。
例如,考虑一个包含 UITableView
的 UIViewController
;您可能希望显示整个表格的所有内容。在这种情况下,您需要做进一步的修改
- 您需要返回
.fitLayoutForAxis
。 - 在视图控制器视图中,您需要创建对表格高度的引用。
- 您需要创建一个约束,将表格到视图底部安全区域(这将由 AL 用于扩展视图的大小)。
然后您必须重写 updateViewConstraints()
以改变表格高度约束的值到正确的值。
以下是代码
public class PricingVC: UIViewController, ScrollStackContainableController {
public weak var delegate: PricingVCProtocol?
@IBOutlet public var pricingTable: UITableView!
@IBOutlet public var pricingTableHeightConstraint: NSLayoutConstraint!
public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? {
return .fitLayoutForAxis
}
override public func updateViewConstraints() {
pricingTableHeightConstraint.constant = pricingTable.contentSize.height // the size of the table as the size of its content
view.height(constant: nil) // cancel any height constraint already in place in the view
super.updateViewConstraints()
}
}
这样,随着您向表格添加新值,堆叠视图中行的大小将会增长。
行分隔符
每个由 ScrollStack
管理的行都是一个类型为 ScrollStackRow
的子视图类。它对管理的 UIViewController
有一个强引用,同时在底部也有一个 ScrollStackSeparator
子视图。
您可以使用行的以下属性来隐藏/显示分隔符
isSeparatorHidden
:用于隐藏分隔符。separatorInsets
:用于设置分隔符的内边距(默认值与UITableView
实例使用的值相同)separatorView.color
:用于更改颜色separatorView.thickness
:用于设置分隔符的厚度(默认为 1)
此外,您可以直接在 ScrollStack
控制器上设置这些值,以便为每个新行设置默认值。
ScrollStack
还有一个名为 autoHideLastRowSeparator
的属性,用于自动隐藏堆栈的最后分隔符。
使用 plain UIViews 代替 view controllers
从 1.3.x 版本开始,ScrollStack 也支持布局不属于父视图控制器中间件的 plain UIView
实例。
这在您视图中的逻辑不太复杂,并且希望使用 ScrollStack 来创建自定义布局并保持代码轻量级时特别有用。
使用 plain 视图非常简单;每个行方法都支持 UIView
或 UIViewController
作为参数。
由于您正在处理 plain UIView
实例,为了正确设置其大小,您必须在将其添加到堆栈之前设置其 heightAnchor
或 widthAncor
(根据堆栈方向)。至于控制器,ScrollStack
保持对作为父 ScrollStackRow
实例 contentView
添加的管理的视图的强引用,正如 UIViewController
的 .view
属性所发生的那样。
这是一个小示例
let myCustomView = UIView(frame: .zero)
myCustomView.backgroundColor = .green
myCustomView.heightAnchor.constraint(equalToConstant: 300).isActive = true
stackView.addRow(view: myCustomView)
点击行
默认情况下,行不可点击,但如果是必需的话,实现类似于 UITableView
中的某种点击功能,您可以通过在 ScrollStackRow
实例上设置 onTap
属性的默认回调来添加它。
例如
scrollStack.firstRow?.onTap = { row in
// do something on tap
}
一旦可以设置点击处理程序,也可以提供点击时的突出显示颜色。为此,您必须在行管理视图中实现 ScrollStackRowHighlightable
协议。
例如
class GalleryVC: UIViewController, ScrollStackRowHighlightable {
public var isHighlightable: Bool {
return true
}
func setIsHighlighted(_ isHighlighted: Bool) {
self.view.backgroundColor = (isHighlighted ? .red : .white)
}
}
突出显示状态之间的过渡将自动动画化。
获取行/控制器
获取管理特定视图控制器的第一个行 您可以使用 firstRowForControllerOfType<T: UIViewController>(:) -> ScrollStackRow?
函数获取管理特定视图控制器类的第一个行。
let tagsVC = scrollStack.firstRowForControllerOfType(TagsVC.self) // TagsVC instance
获取管理特定控制器实例的行 要获取与特定控制器关联的行,您可以使用 rowForController()
函数
let row = scrollStack.rowForController(tagsVC) // ScrollStackRow
设置行内边距
要为特定行设置内边距,您可以使用 setRowInsets()
函数
let newInsets: UIEdgeInsets = ...
scrollStack.setRowInsets(index: 0, insets: newInsets)
您还可以使用 setRowsInsets()
来设置多行。
此外,通过在您的 ScrollStack
类中设置 .rowInsets
,您可以为新添加的行设置默认的内边距值。
更改 ScrollStack 滚动轴
为了更改 ScrollStack
实例的滚动轴,您可以设置 axis
属性为 horizontal
或 `vertical。
订阅行事件
您可以通过订阅 onChangeRow
属性来监听行何时被移除或添加到堆叠视图中。
scrollStackView.onChangeRow = { (row, isRemoved) in
if isRemoved {
print("Row at index \(row.index) was removed"
} else {
print("A new row is added at index: \(row.index). It manages \(type(of: row.controller))")
}
}
您还可以通过设置 stackDelegate
来订阅关于行可见状态变化的事件。因此,您的目标对象必须符合 ScrollStackControllerDelegate
协议
示例
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 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).
}
}
ScrollStack.RowVisibility
是一个枚举,包含以下情况
partial
: 行部分可见。entire
: 行完全可见。hidden
: 行不可见且隐藏。offscreen
: 行没有隐藏,但当前由于滚动位置而不可见。
系统要求
- iOS 11+
- Xcode 10+
- Swift 5+
示例应用
ScrollStackController
附带了一个演示应用程序,演示了如何轻松创建复杂的可滚动布局和一些库的主要功能。
您应该查看它以实现自己的布局,创建动态大小的行和分配事件。
安装
它也支持在您的 Package.swift中使用Swift Package Maneger(SPM)。 考虑 ❤️ 支持这个库的开发! ScrollStackController 目前由 Daniele Margutti 拥有和维护。 本软件符合 MIT 许可证。 关注我于ScrollStackController
可以通过在Podfile中添加pod 'ScrollStackController'
import PackageDescription
let package = Package(name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/malcommac/ScrollStackController.git", majorVersion: 0),
]
)
贡献
版权 & 致谢
您可以在 Twitter 上关注我 @danielemargutti。
我的网站是 https://www.danielemargutti.com