Topics
#308: Automatic Layout to Layout Transitions with UICollectionViewController π±β‘οΈπ±
Topics
Today we'll continue looking at UICollectionView
's more advanced capabilities by checking out a little-known feature hiding inside UICollectionViewController
.
It allows us to automatically animate the transition between layouts when pushing on to a navigation controller stack. Neat!
Let's give it a try.
We'll start with the same simple baseline collection view controller we used in Bite #306, and Bite #307, SquaresViewController
.
It looks like this:
class SquaresViewController: UICollectionViewController {
var items = [Item]()
override func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
return items.count
}
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView
.dequeueReusableCell(
withReuseIdentifier: "ItemCell",
for: indexPath
)
cell.backgroundColor = items[indexPath.item].color
return cell
}
}
Nothing fancy, just standard UICollectionViewController
stuff.
Next, we'll subclass it twice. Once for our "small" cells:
class SmallViewController : SquaresViewController {
init() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 50, height: 50)
super.init(collectionViewLayout: layout)
items = (0...50).map { _ in Item(color: .random()) }
}
We'll fill it up with some random items, like we did previously, here in our "small" subclassed view controller.
Then, when a user selects one of the cells, we'll push on a "big" view controller, setting its items
property to our own:
override func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
let bigVC = BigViewController()
bigVC.items = items
navigationController?
.pushViewController(bigVC, animated: true)
}
}
Again, nothing really too crazy here. Lastly, before we can try it out, lets make our BigViewController
:
class BigViewController : SquaresViewController {
init() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
super.init(collectionViewLayout: layout)
}
We'll make our layout's itemSize
property a little larger than before, then we'll override collectionView(_:didSelectItemAt:)
one more time.
This time we'll simply call popViewController
:
override func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
navigationController?.popViewController(animated: true)
}
}
If we build and run now, everything works as expected, but there's no fancy transition happening. Just a regular navigation controller push.
Let's fix this.
We'll add one line of code to SmallViewController
's init
function, setting useLayoutToLayoutNavigationTransitions
to false
:
class SmallViewController : SquaresViewController {
init() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 50, height: 50)
super.init(collectionViewLayout: layout)
useLayoutToLayoutNavigationTransitions = false
// ..
}
// ..
Then we'll do set the same property to the true
in BigViewController
:
class BigViewController : SquaresViewController {
init() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
super.init(collectionViewLayout: layout)
useLayoutToLayoutNavigationTransitions = true
}
// ..
That's it. UIKit will notice that we've set our BigViewController
's useLayoutToLayoutNavigationTransitions
to true
, and will automatically animate the transition between the two layouts.
Note that the automatic transition animation that UIKit renders is smart enough to make sure the selected cell is still visible after we push in. A nice touch.
It's always fun to discover the handy little behaviors hiding out in to UIKit.
Finally, a couple of important notes:
First, the same UICollectionView
is actually reused when all of this magic happens. The pushed on view controller will not create its own. This may or may not matter for any given app, but good to know.
Second, the root view controller (SmallViewController
in our case) will still be set as the delegate
and dataSource
when the new view controller is pushed on.
If we needed to change this behavior, we could conform to the UINavigationControllerDelegate
protocol and change these values each time a different view controller is about to be be shown. The code might look something like this:
extension SquaresViewController : UINavigationControllerDelegate {
func navigationController(
_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool
) {
guard let squaresVC = viewController as? SquaresViewController else { return }
squaresVC.collectionView?.delegate = squaresVC
squaresVC.collectionView?.dataSource = squaresVC
}
}
That's all for today, have a specific UICollectionView
question you'd like answered? Send it along!.
Download the Xcode project we built in this Bite right here.