Wrong frame from layoutAttributesForItem when using UICollectionViewFlowLayout with an UIDynamicAnimator - ios

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.

Related

Align Nodes in tree hierarchy

Note:
I am looking for alternate solution, Since I have tried changing lots of Sprite Kit properties values, and came upto conclusion that it is not possible with those changes. So I ain't posting any code. You can try the mentioned library for more information.
Original Question:
I am using BLBubbleFilters library to create Bubbles.
This library used magnetic property to pull align items to the centre of the screen. And each time the screen opens the positioning of each node is different.
What have I tried so far?
I have tried playing around with the library and its physics attributes(e.g. Magnetic property) to achieve my goal.
The goal is:
Place 'Family' node over and above all nodes, and rest of the nodes below it.
What I thought is to that, each node will have a property for example "priority(Integer)". That is, the Priority 0 nodes will be placed above all nodes, priority 1 nodes below 0 priority nodes and so on.
Since SpriteKit deals with Physics properties, is there is any such property or way to achieve my goal?
See, how the family bubble is at the Top.
I had to do something like that and I did By creating a custom SelectInterestsCollectionViewFlowLayOut and I added the bubbles in the UICollectionViewCells.
The result was this.
Every cell is a white square and inside I have an image view with corner radius 1/2 * height.
The difference is that in my case I had the same bubble size for all my bubbles.
My code is:
enum InterestBubbleType {
case A
case B
case C
case D
case E
func getYPosition(cellSideSize: CGFloat) -> CGFloat {
let spaceFromTopForTwoCellsOnTop: CGFloat = cellSideSize / 2
switch self {
case .A:
return cellSideSize
case .B:
return cellSideSize * 2
case .C:
return spaceFromTopForTwoCellsOnTop
case .D:
return cellSideSize + spaceFromTopForTwoCellsOnTop
case .E:
return 0
}
}
}
class SelectInterestsCollectionViewFlowLayOut: UICollectionViewFlowLayout {
var cache: [UICollectionViewLayoutAttributes] = []
var layoutAttributes: [UICollectionViewLayoutAttributes] = []
var cellSideSize: CGFloat!
var xPosition: CGFloat = 0
var itemPositions: [InterestBubbleType] = [.A,.C,.D,.E,.A,.B,.C,.D,.E,.A,.B,.C,.D,.A,.B,.C,.D,.E,.A,.B]
var positionsToIncreaseXPosition = [0,2,5,7,10,12,14,16]
override func prepare() {
cache = []
super.prepare()
//Prepare constants
self.xPosition = 0
cellSideSize = self.collectionView!.height() / 3
self.configureCasheForTwentyInterests()
}
private func configureCasheForTwentyInterests() {
for i in 0...(self.itemPositions.count - 1) {
let indexPath = IndexPath(item: i, section: 0)
let xPosition = self.xPosition
let frame = CGRect(x: xPosition, y: self.itemPositions[i].getYPosition(cellSideSize: self.cellSideSize), width: self.cellSideSize, height: self.cellSideSize)
let atribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
atribute.frame = frame
cache.append(atribute)
if self.positionsToIncreaseXPosition.contains(i) {
self.xPosition += self.cellSideSize
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
self.layoutAttributes = []
for uiCollectionViewLayoutAttribute in self.cache {
self.layoutAttributes.append(uiCollectionViewLayoutAttribute)
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.row]
}
override var collectionViewContentSize: CGSize {
let totalWidth = cellSideSize * 9
return CGSize(width: totalWidth, height: self.collectionView!.height())
}
}
Now how it works is that in my collection view there are 5 different y positions that a bubble can be inside the collection view and they are A,B,C,D,E and I also keep in mind when you need to move 1 more x position which is 1 * collectionViewWidth size.
You can configure the code to achive your own result.
In my case I had a fixed number of bubbles and a fixed view to show them. The reason I chose to do it with collection view was because if the number change you can just add positions to change x position in positionsToIncreaseXPosition and it will work.

Getting error when using FirebaseUI-IOS with custom UICollectionViewCell

I'm using Firebase-UI-IOS for my iOS app. When i run the app i get the following error:
fatal error: ”unexpectedly found nil while unwrapping an Optional
value”.
Here is my code in ViewDidLoad:
let ref = Firebase(url: "https://XXXXXX.firebaseio.com")
var dataSource: FirebaseCollectionViewDataSource!
let messageRef = ref
.childByAppendingPath("brainstorms")
.childByAppendingPath(invite.brainstormId)
.childByAppendingPath("messages")
self.dataSource = FirebaseCollectionViewDataSource(ref: messageRef, cellClass: MessageCollectionViewCell.self, reuseIdentifier: "messageCell", view: self.collectionView)
self.dataSource.populateCellWithBlock { (cell: UICollectionViewCell, obj: NSObject) -> Void in
// Populate cell as you see fit
if let cell = cell as? MessageCollectionViewCell {
println(cell)
cell.messageText.text = "Hello world" <--- ERROR HERE
}
}
self.collectionView.dataSource = self.dataSource
I've tried to unwrap cell.messageText, but it always returns nil. My cell class MessageCollectionViewCell is registered in my Storyboard.
My println(cell) prints the following line: <xxxxx.MessageCollectionViewCell: 0x7fc26d8d9080; baseClass = UICollectionViewCell; frame = (17.5 155; 340 80); layer = <CALayer: 0x7fc26d8d92e0>>.
This looks to me like it should be working. Reference to populateCellWithBlock can be found here.
Anyone got any suggestions?
Creator of FirebaseUI here.
Looks like the FirebaseUI parts are working fine (our contract will return a populated cell that is a subclass of UICollectionViewCell and an object that is a subclass of NSObject, which appears to be working).
The issue seems to be that in your custom cell hasn't properly initialized the UILabel. Can you please post your MessageCollectionViewCell.swift file?
It should look something like this:
import Foundation
import UIKit
class MessageCollectionViewCell: UICollectionViewCell {
#IBOutlet var mainLabel: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
// Custom initialization code for label
let size = self.contentView.frame.size
let frame = CGRectMake(0.0, 0.0, size.width, size.height)
self.mainLabel = UILabel(frame: frame)
// Make sure you add the label as a subview
self.contentView.addSubview(self.mainLabel!)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Once you've got an init(frame:) call, it should populate appropriately.
I tested this out with the following populateCellWithBlock:
dataSource.populateCellWithBlock { (cell: UICollectionViewCell, snap: NSObject) -> Void in
let cell: MessageCollectionViewCell = cell as! MessageCollectionViewCell
cell.mainLabel?.text = "Hello!"
}
I'm working on adding __kindof support to make the signature look more like:
dataSource.populateCellWithBlock { (cell: MessageCollectionViewCell, snap: Message) -> Void in
cell.mainLabel?.text = "Hello!"
}
But support for __kindof seems to be spotty and not really working (XCode 7 Beta 5 claims to have support, but I can't get Cocoapods + Swift to accept it, even though it builds in XCode and works in Objective-C).
Let me know if you've got any other feedback on FirebaseUI, and if you see any other bugs, feel free to add them to the issue tracker :)

when using subclassed collectionViewFlowLayout I'm getting weird error

I made a subclass of collectionViewFlowLayout. After that, I implemented the following code:
override func finalLayoutAttributesForDisappearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attr = self.layoutAttributesForItemAtIndexPath(itemIndexPath)
attr?.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.8, 0.8), CGFloat(M_PI))
attr?.center = CGPointMake(CGRectGetMidX(self.collectionView!.bounds), CGRectGetMidY(self.collectionView!.bounds))
return attr
}
When I delete items in collection view using performBatchUpdates: method the debugger throws this error message. The deletion actually succeeds and is fully working, but I am a little confused about this debugger output. Can someone please explain should I do to please debugger? I don't really understand what code and where should be added.
//ERROR MESSAGE
2015-08-02 12:39:42.208 nameOfMyProject[1888:51831] Logging only once
for UICollectionViewFlowLayout cache mismatched frame 2015-08-02
12:39:42.209 nameOfMyProject[1888:51831] UICollectionViewFlowLayout
has cached frame mismatch for index path {length = 2, path = 0 - 11} - cached value:
{{106.13333333333333, 131.13333333333333}, {75.733333333333348,
75.733333333333348}}; expected value: {{192.5, 288}, {94.666666666666671, 94.666666666666671}}
2015-08-02 12:39:42.209 nameOfMyProject[1888:51831] This is likely
occurring because the flow layout subclass nameOfMyProject.ShopLayout
is modifying attributes returned by UICollectionViewFlowLayout without
copying them
2015-08-02 12:39:42.209 nameOfMyProject[1888:51831] Snapshotting a
view that has not been rendered results in an empty snapshot. Ensure
your view has been rendered at least once before snapshotting or
snapshot after screen updates.
The error occurs because you are manipulating the attribute without copying it first. So this should fix the error:
override func finalLayoutAttributesForDisappearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attr = self.layoutAttributesForItemAtIndexPath(itemIndexPath)?.copy() as! UICollectionViewLayoutAttributes
// manipulate the attr
return attr
}
When you come across the same error in layoutAttributesForElementsInRect(rect: CGRect) you have to copy each of the items in the array instead of just copying the array:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElementsInRect(rect)
var attributesCopy = [UICollectionViewLayoutAttributes]()
for itemAttributes in attributes! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
// manipulate itemAttributesCopy
attributesCopy.append(itemAttributesCopy)
}
return attributesCopy
}

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