ScrollStackController 1.5.1

ScrollStackController 1.5.1

Daniele Margutti 维护。



ScrollStackController

在 UIKit 中实现可滚动布局

使用 UIViewControllers 或纯 UIView 创建复杂可滚动布局,简化您的代码!

ScrollStackController 是一个类,您可以使用它通过可滚动的 UIStackView 创建复杂布局,但每个行都由单独的 UIViewController 处理;这允许您保持非常好的一致性。

您可以将其视为 UITableView 但有很多不同之处:

  • 每行可以独立管理不同的 UIViewController:无需更多的大型控制器,更加清洁且易于维护的架构。
  • 如果您需要一个轻量级的解决方案,则可以继续使用纯 UIView 实例:当您使用 ScrollStackController 作为布局助手或您的视图不需要复杂的逻辑时,这特别有用。
  • 从一开始就由 AutoLayout 支持;它通过使用 UIScrollView + UIStackView 的组合来提供一个适用于固定和动态行尺寸的动画友好型控制器。
  • 您无需手动处理视图复用:假设您的布局由多个不同的屏幕构成。不需要视图复用,但这可能会导致更困难的布局管理。使用更简单、更安全的 API 集合 ScrollStackView 是实现此类布局的理想方式。
功能亮点
🕺 创建复杂布局,无需像 UICollectionViewUITableView 视图复用那样需要的样板代码。
🧩 将每个屏幕视为独立存在的 UIVIewController 以简化您的架构。
🧩 支持轻量级模式,即可在无需 UIViewController 的情况下布局 UIView
🌈 轻松实现行的显示/隐藏和尺寸调整,即使带有自定义动画也是如此!
紧凑的代码库,小于 1k LOC,没有外部依赖。
🎯 易于使用且可扩展的 API 集合。
🧬 它在其核心使用标准的 UIKit 组件。没有魔法,只是 UIScrollView+UIStackView 的组合。
🐦 Swift 5 完全制作,由 Swift 爱好者编写。

❤️您的支持

你好,开发者同行!
你知道,维护和开发工具需要资源和时间。虽然我喜欢制作它们,但 您的支持是我继续其开发的基本

如果您正在使用 SwiftLocation 或我的任何其他创作,请考虑以下选项

目录

何时使用 ScrollStackController 以及何时不使用

ScrollStackController 最好用于较短的屏幕,且行集合异质:在这些情况下,你不需要有视图回收。

由于自动布局,你将免费获得更新和动画。

你也可以独立管理每个屏幕,具有良好的事务分离;更棒的是,与 UITableViewUICollectionView 不同,你可以在 ScrollStack 视图中保留对 UIViewController(及其视图)的强引用,并在任何时刻对它们进行更改。

ScrollStackController 并不适用于所有情况。ScrollStackController 在屏幕加载时第一次布局整个 UI。如果你有一长串行,可能会遇到延迟。

因此,ScrollStackController 通常不适用于包含同类视图、显示相似数据的屏幕(在这种情况下,你应该使用 UITableViewUICollectionView)。

Demo Project

↑ 返回顶部

如何使用它

本包的主类是 ScrollStack,它是 UIScrollView 的一个子类。它管理每一行的布局、动画,并保留您行的强引用。

这是架构的概述

  • ScrollStackController :是 UIViewController 的一个子类。您可以在您的视图控制器中将其用作子控制器。这允许您管理为堆栈控制器添加的每一行的相关子控制器事件。
  • ScrollStackScrollStackController 的视图是一个 ScrollStack,它是 UIScrollView 的一个子类,带有一个 UIStackView,这使得您能够管理堆栈的布局。您可以通过控制器中的 scrollStack 属性访问它。
  • 每一行都是 ScrollStackRow,它是 UIView 的一个子类。内部有两个视图,即 contentView(对管理的 UIViewControllerview 的引用)和 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 类有强引用。UIViewControllerview 将成为行的 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(特定索引),afterbelow(在包含特定 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)

请注意:当你隐藏一行时,该行仍然属于堆栈的一部分,并未被删除,只是被隐藏了!如果你通过调用 ScrollStackrows 属性来获取行列表,你仍然可以看到它。

↑ 返回顶部

使用自定义动画隐藏/显示行

您可以通过任何自定义过渡轻松显示或隐藏行;只需确保您的视图控制器符合 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

在某些特殊情况下,您可能需要根据视图控制器中的视图变化来调整行的大小。

例如,考虑一个包含 UITableViewUIViewController;您可能希望显示整个表格的所有内容。在这种情况下,您需要做进一步的修改

  • 您需要返回 .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 视图非常简单;每个行方法都支持 UIViewUIViewController 作为参数。

由于您正在处理 plain UIView 实例,为了正确设置其大小,您必须在将其添加到堆栈之前设置其 heightAnchorwidthAncor(根据堆栈方向)。至于控制器,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附带了一个演示应用程序,演示了如何轻松创建复杂的可滚动布局和一些库的主要功能。

您应该查看它以实现自己的布局,创建动态大小的行和分配事件。

↑ 返回顶部

安装

ScrollStackController可以通过在Podfile中添加

pod 'ScrollStackController'

它也支持在您的

Package.swift中使用Swift Package Maneger(SPM)。

import PackageDescription

  let package = Package(name: "YourPackage",
    dependencies: [
      .Package(url: "https://github.com/malcommac/ScrollStackController.git", majorVersion: 0),
    ]
  )

↑ 返回顶部

考虑

❤️ 支持这个库的开发!

贡献

  • 如果您需要帮助,或者想提出一个普通问题,请创建一个问题。
  • 如果您发现了一个错误,请创建一个问题。
  • 如果您有任何功能请求,请创建一个问题。
  • 如果您想贡献,提交一个拉取请求。

版权 & 致谢

ScrollStackController 目前由 Daniele Margutti 拥有和维护。
您可以在 Twitter 上关注我 @danielemargutti
我的网站是 https://www.danielemargutti.com

本软件符合 MIT 许可证

关注我于