Home

Awesome

<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="./Resources/scrollstack-dark.png" width="350"> <img alt="logo-library" src="./Resources/scrollstack-light.png" width="350"> </picture> </p>

Swift Platform Swift Package Manager CocoaPods Compatible

Create complex scrollable layout using UIViewControllers or plain UIViews and simplify your code!

ScrollStackController is a class you can use to create complex layouts using scrollable UIStackView but where each row is handled by a separate UIViewController; this allows you to keep a great separation of concerns.

You can think of it as UITableView but with several differences:

Features Highlights
๐Ÿ•บCreate complex layout without the boilerplate required by view recyling of UICollectionView or UITableView.
๐ŸงฉSimplify your architecture by thinking each screen as a separate-indipendent UIVIewController.
๐ŸงฉSupport for lightweight mode to layout UIView without UIViewController.
๐ŸŒˆAnimate show/hide and resize of rows easily even with custom animations!
โฑ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

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 ScrollStackController or any other of my creations please consider the following options:

<a name="index"/>

Table of Contents

<a name="whentousescrollstackcontrollerandwhennot"/>

When to use ScrollStackController and when not

ScrollStackController is best used for shorter screens with an heterogeneous set of rows: in these cases you don't need to have view recycling.

Thanks to autolayout you will get updates and animations for free.

You can also manage each screen independently with a great separation of concerns; morehover unlike UITableView and UICollectionView, you can keep strong references to UIViewController (and its views) in an ScrollStack view and make changes to them at any point.

ScrollStackController is not suitable in all situations. ScrollStackController lays out the entire UI at first time when your screen loads. If you have a long list of rows you may experience delays.

So, ScrollStackController is generally not appropriate for screens that contain many views of the same type, all showing similar data (in these cases you should use UITableView or UICollectionView).

Demo Project

โ†‘ Back To Top

<a name="howtouseit"/>

How to use it

The main class of the package is ScrollStack, a subclass of UIScrollView. It manages the layout of each row, animations and keep a strong reference to your rows.

This is an overview of the architecture:

As we said, usually you don't want to intantiate a ScrollStack control directly but by using the ScrollStackController class. It's a view controller which allows you to get the child view controller's managment for free, so when you add/remove a row to the stack you will get the standard UIViewController events for free!

This is an example of initialization in a view controller:

class MyViewController: UIViewController {

    private var stackController = ScrollStackViewController()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        stackController.view.frame = contentView.bounds
        contentView.addSubview(stackController.view)
    }
    
}

Now you are ready to use the ScrollStack control inside the stackController class. ScrollStack have an extensible rich set of APIs to manage your layout: add, remove, move, hide or show your rows, including insets and separator management.

Each row managed by ScrollStack is a subclass of ScrollStackRow: it strongly reference a parent UIViewController class where you content is placed. UIViewController's view will be the contentView of the row itself.

You don't need to handle lifecycle of your rows/view controller until they are part of the rows inside the stack.

To get the list of rows of the stack you can use the rows property.

// 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

Let's take a look below.

โ†‘ Back To Top

<a name="addingrows"/>

Adding Rows

ScrollStack provides a comprehensive set of methods for managing rows, including inserting rows at the beginning and end, inserting rows above or below other rows.

To add row you can use one the following methods:

Both of these methods takes as arguments:

The following code add a rows with the view of each view controller passed:

   let welcomeVC = WelcomeVC.create()
   let tagsVC = TagsVC.create(delegate: self)
   let galleryVC = GalleryVC.create()
        
   stackView.addRows(controllers: [welcomeVC, notesVC, tagsVC, galleryVC], animated: false)

As you noticed there is not need to keep a strong reference to any view controller; they are automatically strong referenced by each row created to add them into the stack.

โ†‘ Back To Top

<a name="removingreplacingrows"/>

Removing / Replacing Rows

A similar set of APIs are used to remove existing rows from the stack:

An example:

let newVC: UIViewController = ...
stackView.replaceRow(index: 1, withRow: newVC, animated: true) {
	print("Gallery controller is now in place!!")
}

โ†‘ Back To Top

<a name="moverows"/>

Move Rows

If you need to adjust the hierarchy of the stack by moving a row from a position to another you can use:

The following method move the first row at a random position, by animating the transition:

let randomDst = Int.random(in: 1..<stackView.rows.count)
stackView.moveRow(index: 0, to: randomDst, animated: true, completion: nil)

โ†‘ Back To Top

<a name="hideshowrows"/>

Hide / Show Rows

ScrollStack uses the power of UIStackView: you can show and hide rows easily with a gorgeous animation by using one of the following methods:

Example:

stackView.setRowsHidden(indexes: [0,1,2], isHidden: true, animated: true)

Keep in mind: when you hide a rows the row still part of the stack and it's not removed, just hidden! If you get the list of rows by calling rows property of the ScrollStack you still see it.

โ†‘ Back To Top

<a name="customanimations"/>

Hide / Show Rows with custom animations

You can easily show or hide rows with any custom transition; your view controller just need to be conform to the ScrollStackRowAnimatable protocol.
This protocol defines a set of animation infos (duration, delay, spring etc.) and two events you can override to perform actions:

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)
}

So for example you can replicate the following animation:

by using the following code:

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
        }
    }
    
}
<a name="reloadrows"/>

Reload Rows

Reload rows method allows you to refresh the layout of the entire stack (using layoutIfNeeded()) while you have a chance to update a specific row's contentView (aka the view of the managed UIViewController).

There are three methods:

If your UIViewController implements ScrollStackContainableController protocol you will get notified inside the class about this request, so you have the opportunity to refresh your data:

Example:

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
	}
	
}

โ†‘ Back To Top

<a name="sizingrows"/>

Sizing Rows

You can control the size of your UIViewController inside a row of a ScrollStack in two ways:

In both case ScrollStack class will use only one dimension depending by the active scroll axis to layout the view controller content into the stack (if scroll axis is horizontal you can control only the height of the row, if it's vertical only the width. The other dimension will be the same of the scroll stack itself.

Each of the following cases is covered inside the demo application:

โ†‘ Back To Top

<a name="fixedrowsize"/>

Fixed Row Size

If your view controller has a fixed size you can just return it as follows:


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)
       }
    }
    
}

If your stack support single axis you can obivously avoid switch condition. When you will add this view controller in a scroll stack it will be sized as you requested (any height/width constraint already in place will be removed).

โ†‘ Back To Top

<a name="fittinglayoutrowsize"/>

Fitting Layout Row Size

Sometimes you may want to have the content view sized by fitting the contents of the view controller's view. In these cases you can use . fitLayoutForAxis.

Example:

public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? {
	return .fitLayoutForAxis
}

ScrollStack will use the systemLayoutSizeFitting() method on your view controller's view to get the best size to fit the content.

โ†‘ Back To Top

<a name="collapsiblerows"/>

Collapsible Rows

Sometimes you may want to create collapsible rows. These row can have different heights depending of a variable.

In this case you just need to implement a isExpanded: Bool variable in your view controller and return a different height based on it.


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))
    }
}

In your main view controller you may call this:

	 // 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)

And your rows will perform a great animation to resize its content.

โ†‘ Back To Top

<a name="workingwithdynamicuicollectionviewuitableviewuitextview"/>

Working with dynamic UICollectionView/UITableView/UITextView

There are some special cases where you may need to resize the row according to the changing content in your view controller's view.

Consider for example an UIViewController with a UITableView inside; you may want to show the entire table content's as it grown. In this case you need to make some further changes:

Then you must override the updateViewConstraints() to change the value of the table's height constraint to the right value.

This is the code:


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()
    }
}

In this way as you add new value to the table the size of the row in stack view will grown.

โ†‘ Back To Top

<a name="rowsseparator"/>

Rows Separator

Each row managed by ScrollStack is of a subview class of type ScrollStackRow. It has a strong referenced to managed UIViewController but also have a subview on bottom called ScrollStackSeparator.

You can hide/show separators by using the following properties of the row:

Moreover you can set these values directly on ScrollStack controller in order to have a default value for each new row.

ScrollStack also have a property called autoHideLastRowSeparator to hide the last separator of the stack automatically.

โ†‘ Back To Top

<a name="lightweightplainuiview"/>

Using plain UIViews instead of view controllers

Since version 1.3.x ScrollStack can also be used to layout plain UIView instances which not belong to a parent view controllers.
This is especially useful when you don't have a complex logic in your views and you want to use ScrollStack to make custom layout and keep your code lightweight.

Using plain views is pretty easy; each row method supports both UIView or UIViewController as parameter.

Since you are working with plain UIView instances in order to size it correctly you must set its heightAnchor or widthAncor (depending of your stack orientation) before adding it to the stack. As for controllers, ScrollStack keeps a strong reference to the managed view which is added as contentView of the parent ScrollStackRow instance as it happens for UIViewController's .view property.

This is a small example:

let myCustomView = UIView(frame: .zero)
myCustomView.backgroundColor = .green
myCustomView.heightAnchor.constraint(equalToConstant: 300).isActive = true
stackView.addRow(view: myCustomView)
<a name="taponrows"/>

Tap On Rows

By default rows are not tappable but if you need to implement some sort of tap features like in UITableView you can add it by setting a default callback for onTap property on ScrollStackRow instances.

For example:

scrollStack.firstRow?.onTap = { row in
	// do something on tap
}

Once you can set a tap handler you can also provide highlight color for tap. To do it you must implement ScrollStackRowHighlightable protocol in your row managed view controller.

For example:

class GalleryVC: UIViewController, ScrollStackRowHighlightable {

    public var isHighlightable: Bool {
    	return true
    }
    
    func setIsHighlighted(_ isHighlighted: Bool) {
    	self.view.backgroundColor = (isHighlighted ? .red : .white)
    }

}

Transition between highlights state will be animated automatically.

โ†‘ Back To Top

<a name="utilsmethods"/>

Get the row/controller

Get the (first) row which manage a specific view controller type You can get the first row which manage a specific view controller class using firstRowForControllerOfType<T: UIViewController>(:) -> ScrollStackRow? function.

let tagsVC = scrollStack.firstRowForControllerOfType(TagsVC.self) // TagsVC instance

Get the row which manage a specific controller instance To get the row associated with a specific controller you can use rowForController() function:

let row = scrollStack.rowForController(tagsVC) // ScrollStackRow
<a name="setrowinsets"/>

Set Row Insets

To set an insets for a specific row you can use setRowInsets() function:

let newInsets: UIEdgeInsets = ...
scrollStack.setRowInsets(index: 0, insets: newInsets)

You can also use setRowsInsets() to set multiple rows.

Moreover by setting .rowInsets in your ScrollStack class you can set a default insets value for new row added.

<a name="changescrollaxis"/>

Change ScrollStack scrolling axis

In order to change the axis of scroll for your ScrollStack instances you can set the axis property to horizontal or `vertical.

<a name="rowevents"/>

Subscribe to Row Events

You can listen when a row is removed or added into the stack view by subscribing the onChangeRow property.

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))")
  }
}

You can also subscribe events for events about row visibility state changes by setting the stackDelegate. Your destination object must therefore conforms to the ScrollStackControllerDelegate protocol:

Example:

class ViewController: ScrollStackController, ScrollStackControllerDelegate {

    func viewDidLoad() {
        super.viewDidLoad()
        
        self.scrollStack.stackDelegate = self
    }

    func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) {
        // Stack did scroll
    }
    
      
    func scrollStackDidEndScrollingAnimation(_ stackView: ScrollStack) {
        // Scrolling animation has ended
    }

    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 is an enum with the following cases:

โ†‘ Back To Top

<a name="systemrequirements"/>

System Requirements

โ†‘ Back To Top

<a name="exampleapp"/>

Example App

ScrollStackController comes with a demo application which show how easy you can create complex scrollable layoyut and some of the major features of the library.

You should look at it in order to implement your own layout, create dynamically sized rows and dispatch events.

โ†‘ Back To Top

<a name="installation"/>

Installation

ScrollStackController can be installed with CocoaPods by adding pod 'ScrollStackController' to your Podfile.

pod 'ScrollStackController'

It also supports Swift Package Maneger aka SPM in your Package.swift:

import PackageDescription

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

โ†‘ Back To Top

<a name="authorlicense"/>

Consider โค๏ธ support the development of this library!

Contributing

Copyright & Acknowledgements

ScrollStackController is currently owned and maintained by Daniele Margutti.
You can follow me on Twitter @danielemargutti.
My web site is https://www.danielemargutti.com

This software is licensed under MIT License.

Follow me on: