Custom UICollectionViewFlowLayout animation - ios

I've got a custom UICollectionViewFlowLayout animation that staggers views in from the right with insertions and out to the left with deletions. It does it by setting a CABasicAnimation on the UICollectionViewLayoutAttributes and applying this to the cell layer.
CollectionViewAnimations Project on GitHub
The default alpha is 0 and its fading out my cells and ending my custom animation early. If I change the alpha to 1 then i don't see my animation at all. I set it at 0.5 and I get a bit of both.... it's weird. You'd have to run my project to see what I mean.
AnimatingFlowLayout.swift
For some reason, I can't seem to completely remove the default alpha on the attributes in finalLayoutAttributesForDisappearingItemAtIndexPath.
Anyone got any ideas?

You're using performBatchUpdates(_:completion:) which already animates the changes you set in finalLayoutAttributesForDisappearingItemAtIndexPath(_:), so if you add the CABasicAnimation you're adding animation to animation that's already gonna happen. If you drop the animation from your CellLayoutAttributes and just set the transform3D of UICollectionViewLayoutAttributes it's gonna do what you want (except the animation beginTime and fillMode). This piece of code works well for me:
override func finalLayoutAttributesForDisappearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attributes: CellLayoutAttributes = super.finalLayoutAttributesForDisappearingItemAtIndexPath(itemIndexPath) as! CellLayoutAttributes
// Default is 0, if I set it to 1.0 you don't see anything happen..'
attributes.alpha = 1
let endX = -CGRectGetWidth(self.collectionView!.frame)
var endTransform: CATransform3D = CATransform3DMakeTranslation(endX, 0, 0)
attributes.transform3D = endTransform
return attributes
}

This worked for me, for a similar problem:
import UIKit
class NoFadeFlowLayout: UICollectionViewFlowLayout {
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
}
You said you couldn't get the default alpha to go away in this method, but it worked when I tried it on tvOS 11.

Related

Wrong frame from layoutAttributesForItem when using UICollectionViewFlowLayout with an UIDynamicAnimator

So I implemented an UICollectionView with a custom UICollectionViewFlowLayout containing a UIDynamicAnimator for animating my cells upon scrolling. I used the 2013 WWDC reference to replicate the Message bounce.
Everything is working fine, except that I noticed a weird cut in one side of my rounded views added in my cell. See screenshots below :
Is a screenshot of a cell when my FlowLayout is initialized with an UIDynamicAnimator. (Nothing fancy, I'm just showing one item which I
added to a UICollisionBehavior linked to my animator, see code below)
Is the same cell when using a simple FlowLayout without animator
If we pay close attention, we can notice that n°1 is missing a one-pixel vertical line on the red side, and that green side has one more.
This result in a cut effect on every subviews contained in my cell (no matter if its a view, an image etc.)
So I investigated to understand what was causing this, and I found that from the second pass into the method layoutAttributesForElements(in rect: CGRect) the returned x position was wrong.
My method sizeForItemAt() is returning a classic CGSize(width: collectionView.bounds.width, height: 100), but dynamicAnimator.items(in: rect) is returning a frame equal to CGRect(0.1666666666666572, 0.0, 375.0, 100.0) for my cell.
This x position is supposed to be 0 as I'm not applying any transform myself.
0.1666666666666572 being equal to 1/6, this looks like a float-precision issue.
Does anyone has an idea of what is causing this, and how to solve it ?
import Foundation
import UIKit
// Minimal implementation of https://developer.apple.com/videos/play/wwdc2013/217/ to reproduce 0.16667 error
class MinimalWWDCFlowLayout: UICollectionViewFlowLayout {
lazy var behavior: UICollisionBehavior = .init()
lazy var dynamicAnimator: UIDynamicAnimator = {
let res = UIDynamicAnimator(collectionViewLayout: self)
res.addBehavior(behavior)
return res
}()
override func prepare() {
super.prepare()
guard let items = super.layoutAttributesForElements(in: .init(origin: .zero, size: collectionViewContentSize)),
let firstItem = items.first else {
return
}
guard behavior.items.isEmpty else {
return
}
behavior.addItem(firstItem) // This create a debug log when called twice (no error, just a log)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]
guard let firstItem = items?.first else { return nil }
print("🕵️ item frame: \(firstItem.frame))") // This is returning frame.x = 0.1666..67. Calling super.layoutAttributesForElements(in:) instead is returning 0 as expected.
return items
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return dynamicAnimator.layoutAttributesForCell(at: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
}
Note: this seems to happen only on devices with 3x res. Running this code on an iPhone 11 works fine, but result in the described bug on an iPhone 11 Pro.

UICollectionView cell reuse causing problems even after taking steps

I know this question has been asked many times.
I'm using a UICollectionView with custom cells which have a few properties, most important being an array of UISwitch's and and array of UILabel's. As you can guess, when I scroll, the labels overlap and the switches change state. I have implemented the method of UICollectionViewCell prepareForReuse in which I empty these arrays and reset the main label text.
I have tried to combine solutions from different answers and I have reached a point where my labels are preserved, but the state of my switches in the cells isn't. My next step was to create an array to preserve the state before removing the switches and then set the on property of a newly created switch to a value of this array at an index. This works, until after I scroll very fast and switches in cells which were not selected previously become selected(or unselected). This is creating a huge problem for me.
This is my collectionView cellForItemAtIndexPath method:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("pitanjeCell", forIndexPath: indexPath) as! PitanjeCell
// remove views previously created and create array to preserve the state of switches
var selectedSwitches: [Bool] = []
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
// get relevant data needed to place cells programmatically
cell.tekstPitanjaLabel.text = _pitanja[indexPath.row].getText()
let numberOfLines: CGFloat = CGFloat(cell.tekstPitanjaLabel.numberOfLines)
cell.setOdgovori(_pitanja[indexPath.row].getOdgovori() as! [Odgovor])
var currIndex: CGFloat = 1
let floatCount: CGFloat = CGFloat(_pitanja.count)
let switchConstant: CGFloat = 0.8
let switchWidth: CGFloat = cell.frame.size.width * 0.18
let heightConstant: CGFloat = (cell.frame.size.height / (floatCount + 2) + (numberOfLines * 4))
let labelWidth: CGFloat = cell.frame.size.width * 0.9
for item in _pitanja[indexPath.row].getOdgovori() {
// create a switch
let odgovorSwitch: UISwitch = UISwitch(frame: CGRectMake((self.view.frame.size.width - (switchWidth * 2)), currIndex * heightConstant , switchWidth, 10))
odgovorSwitch.transform = CGAffineTransformMakeScale(switchConstant, switchConstant)
let switchValue: Bool = selectedSwitches.count > 0 ? selectedSwitches[Int(currIndex) - 1] : false
odgovorSwitch.setOn(switchValue, animated: false)
// cast current item to relevant class
let obj: Odgovor = item as! Odgovor
// create a label
let odgovorLabel: UILabel = UILabel(frame: CGRectMake((self.view.frame.size.width / 12), currIndex * heightConstant , labelWidth, 20))
odgovorLabel.text = obj.getText();
odgovorLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
odgovorLabel.font = UIFont(name: (odgovorLabel.font?.fontName)!, size: 15)
// add to cell
cell.addSwitch(odgovorSwitch)
cell.addLabel(odgovorLabel)
currIndex++
}
return cell
}
My custom cell also implements methods addSwitch and addLabel which add the element to the contentView as a subview.
Is there any way I can consistently preserve the state of switches when scrolling?
EDIT: As per #Victor Sigler suggestion, I created a bydimensional array like this:
var _switchStates: [[Bool]]!
I initialized it like this:
let odgovori: Int = _pitanja[0].getOdgovori().count
_switchStates = [[Bool]](count: _pitanja.count, repeatedValue: [Bool](count: odgovori, repeatedValue: false))
And I changed my method like this:
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
_switchStates[indexPath.row][current] = (item as! UISwitch).on
current++
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
And in the end:
let switchValue: Bool = _switchStates.count > 0 ? _switchStates[indexPath.row][Int(currIndex) - 1] : false
First of all as you said in your question regarding the default behavior of the cell in the UICollectionView you need to save the state of each cell, in your case with the UISwitch, but you need to preserve the state in a local property, not in the cellForRowAtIndexPath method because this method is calles every time a cell is going to be reused.
So first declare the array where you going to save the state of the UISwitch's outside this function, something like this:
var selectedSwitches: [Bool] = []
Or you can declare it and then in your viewDidLoad instantiate it, it's up to you, I recommend you instantiate it in your viewDidLoad and only declare it as a property like this:
var selectedSwitches: [Bool]!
Then you can do whatever you want with the state of your UISwitch's always of course preserving when change to on or off.
I hope this help you.
I have done it!
In the end I implemented this:
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
var current: Int = 0
var cell = cell as! PitanjeCell
for item: UISwitch in cell.getOdgovoriButtons() {
if(current == _switchStates[0].count) { break;}
_switchStates[indexPath.row][current] = (item).on
current++
}
}
In this function I saved the state.
This in combo with prepareForReuse:
public override func prepareForReuse() {
self.tekstPitanjaLabel.text = nil
self._odgovoriButtons.removeAll()
self._odgovoriLabels.removeAll()
self._odgovori.removeAll()
super.prepareForReuse()
}
Finally did it!

Why does modifying this CALayer within a UITableViewCell cause a nasty graphics glitch?

In my cellForRowAtIndexPath method I set the cornerRadius of the layer on my image:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("feedCell")!
if self.pictunes?.count > 0 {
// There are some pictunes to show; Create and update the posts
if self.pictuneImages.count > indexPath.row {
// We have an image for the user who made the current pictune
if let pictunerImageView = cell.contentView.viewWithTag(2) as? UIImageView {
pictunerImageView.layer.cornerRadius = 17.5 // Cut it to a circle
pictunerImageView.image = self.pictunerImages[0]
}
// We also have an image for the pictune at the current post
if let pictuneImageView = cell.contentView.viewWithTag(1) as? UIImageView {
pictuneImageView.image = self.pictuneImages[indexPath.row]
}
}
return cell
} else {
// No pictunes have loaded yet; We probably need more time
// to load the image and audio assets for some of them
cell.textLabel!.text = "No Pictunes Available Yet"
return cell
}
}
But when I scroll I see this nasty background effect:
How should I get rid of this effect? Clearing the background context didn't help at all. Thanks!
Set:
pictunerImageView.layer.masksToBounds = true
after:
pictunerImageView.layer.cornerRadius = 17.5
You could also change this to:
pictunerImageView.layer.cornerRadius = pictunerImageView.frame.size.height *0.5
In case you ever want to change the size but still want it to be a circle.

UICollectionViewFlowLayout: collectionView!.contentSize vs collectionViewContentSize

Update
The issue seems to be resolved when using the designated collectionViewContentSize() method like so:
let contentSize = collectionViewContentSize()
Nevertheless, I would be very interested in an explanation behind this behaviour, so I've updated my question accordingly.
Original Question
I am trying to recreate the steps found in the first example of this article, but using Swift and Storyboards.
I have a custom UICollectionViewFlowLayout subclass with the following content:
import UIKit
class SpringyFlowLayout: UICollectionViewFlowLayout {
private lazy var animator: UIDynamicAnimator = {
return UIDynamicAnimator(collectionViewLayout: self)
}()
override func prepareLayout() {
super.prepareLayout()
let contentSize = collectionView!.contentSize
let items = super.layoutAttributesForElementsInRect(CGRect(origin: CGPointZero, size: contentSize)) as! [UICollectionViewLayoutAttributes]
if animator.behaviors.isEmpty {
for item in items {
let spring = UIAttachmentBehavior(item: item, attachedToAnchor: item.center)
spring.length = 0
spring.damping = 0.8
spring.frequency = 1.0
animator.addBehavior(spring)
}
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
return animator.itemsInRect(rect)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
return animator.layoutAttributesForCellAtIndexPath(indexPath)
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let delta = newBounds.origin.y - collectionView!.bounds.origin.y
for spring in animator.behaviors {
let items = spring.items as! [UICollectionViewLayoutAttributes]
if let attributes = items.first {
attributes.center.y += delta
animator.updateItemUsingCurrentState(attributes)
}
}
return false
}
I have the correct Layout class set in Storyboards. When I run the app, my Collection View is empty.
I have determined that the issue is in the following snipped:
override func prepareLayout() {
super.prepareLayout()
let contentSize = collectionView!.contentSize
let items = super.layoutAttributesForElementsInRect(CGRect(origin: CGPointZero, size: contentSize)) as! [UICollectionViewLayoutAttributes]
//...
}
Since I subclass UICollectionViewFlowLayout, I thought I can rely on the super implementation to lay out the elements for me, and then modify their attributes. But when I check contentSize, it reports the width correctly, but the height is 0.
This leads to an empty items array -> no behaviors in animator -> animator.itemsInRect(_) returning an empty array -> the empty Collection View.
I just can't seem to find out what I'm missing. There should be no need to override the contentSize() method, since I'm using a flow layout.
The issue it that collectionView doesn't yet have a content size, because it just started to prepare it's layout. I don't believe that calling collectionView!.contentSize will actually compute the size. The reason collectionViewContentSize() works is because it will compute the size using your other layout code.
Here's something you might find interesting. I noticed in Ash Furrow's example in viewDidAppear a call to collectionViewLayout.invalidateLayout. Ash notes this isn't necessary when using storyboards due a difference in timing of the first invocation of prepareLayout versus when storyboards are not used.
I tried invalidating the collection view layout in viewDidAppear along with your code and a storyboard. Here is what I found:
Test Scenarios:
1) collectionView!.contentSize without invalidateLayout = cv is EMPTY
2) collectionViewContentSize() without invalidateLayout = cv WORKS
3) collectionView!.contentSize with invalidateLayout = cv WORKS
4) collectionViewContentSize() with invalidateLayout = cv WORKS
FYI

Fixed spacing between item in UICollectionView with flow layout [duplicate]

This question already has answers here:
How do you determine spacing between cells in UICollectionView flowLayout
(12 answers)
Closed 5 years ago.
So, I'm trying to implement a tag list with UICollectionView. I'm following this tutorial: http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
The issue is flow layout in UICollectionView tries to space items on the same row evenly.
As a developer, I can only specify minimumInteritemSpacingForSectionAtIndex, it's really up to the UICollectionView to determine the actual item spacing.
But what I really want to achieve is like this:
Any ideas?
I've converted Milo's solution to Swift: https://github.com/Coeur/UICollectionViewLeftAlignedLayout/
It simply subclasses UICollectionViewFlowLayout.
import UIKit
/**
* Simple UICollectionViewFlowLayout that aligns the cells to the left rather than justify them
*
* Based on https://stackoverflow.com/questions/13017257/how-do-you-determine-spacing-between-cells-in-uicollectionview-flowlayout
*/
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
}
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
let collectionView = self.collectionView else {
// should never happen
return nil
}
let sectionInset = evaluatedSectionInsetForSection(at: indexPath.section)
guard indexPath.item != 0 else {
currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
return currentItemAttributes
}
guard let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame else {
// should never happen
return nil
}
// if the current frame, once left aligned to the left and stretched to the full collection view
// width intersects the previous frame then they are on the same line
guard previousFrame.intersects(CGRect(x: sectionInset.left, y: currentItemAttributes.frame.origin.y, width: collectionView.frame.width - sectionInset.left - sectionInset.right, height: currentItemAttributes.frame.size.height)) else {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
return currentItemAttributes
}
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
return currentItemAttributes
}
func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
}
func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
}
}
extension UICollectionViewLayoutAttributes {
func leftAlignFrame(withSectionInset sectionInset: UIEdgeInsets) {
frame.origin.x = sectionInset.left
}
}
Apple has provided the UICollectionViewFlowLayout class for us developers, which should be enough to solve the 'typical use case' of collection views. However, I believe you're correct in your assessment that the default layout does not allow you to create this tag cloud effect. If you need something different from the normal flow layout, you'll have to write your own subclass of UICollectionViewLayout.
Apple covers this topic in their 2012 WWDC session titled, "Advanced Collection Views and Building Custom Layouts"
Some additional Apple docs: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
At the risk of seeming biased, I also wrote a quick blog post running through the basic steps: http://bradbambara.wordpress.com/2014/05/24/getting-started-with-custom-uicollectionview-layouts/
Hope that helps.

Resources