Problems with subclassing UICollectionViewLayoutAttributes

I am trying to use a custom layout attributes in in a custom layout with my collectionview.

I have subclassed UICollectionViewLayoutAttributes and UICollectionViewLayout as shown below

But, layoutAttributesClass() is never called so the layout attributes returned from the call to super.layoutAttributesForItemAtIndexPath [Ln 29] are not of type CustomLayoutAttributes. However, this does work if the layout is a UICollectionViewFlowLayout.


Does anyone have any ideas? I can provide a sample project demonstrating this if it would help.

Thanks.


class CustomLayoutAttributes: UICollectionViewLayoutAttributes {
  var displayString: String = "EMPTY"

  override func copyWithZone(zone: NSZone) -> AnyObject {
      let copy = super.copyWithZone(zone) as! CustomLayoutAttributes
      copy.displayString = displayString
      return copy
  }

  override func isEqual(object: AnyObject?) -> Bool {
      if let rhs = object as? CustomLayoutAttributes {
        if displayString != rhs {
            return false
        }
        return super.isEqual(object)
      } else {
        return false
      }
  }
}


class CustomLayout: UICollectionViewLayout {
  override class func layoutAttributesClass() -> AnyClass {
      print("layoutAttributesClass - called")
      return CustomLayoutAttributes.self
  }

  override func prepareLayout() {
      super.prepareLayout()

      if let collectionView = collectionView {
        dataCount = collectionView.numberOfItemsInSection(0)
      }
  }
  override func collectionViewContentSize() -> CGSize {
      return collectionView?.frame.size ?? CGSizeZero
  }

  override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
      attributesList = [UICollectionViewLayoutAttributes]()
      for i in 0..<dataCount {
        if let attributes = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: i, inSection: 0)) {
            attributesList.append(attributes)
        }
      }
      return attributesList
  }

  override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
      if let attributes = super.layoutAttributesForItemAtIndexPath(indexPath) as? CustomLayoutAttributes {
        return attributes
      }
      return nil
  }

  private var attributesList = [UICollectionViewLayoutAttributes]()
  private var dataCount = Int(0)
}

How have you connected your custom layout?

Connecting Your Custom Layout for Use

Yes. It's set as the layout in interface builder and i have used breakpoints to confirm that it is being called

Sorry, I was missing what you are mistaking.


You expect UICollectionViewLayout's instance method layoutAttributesForItemAtIndexPath(_:) to return an instance of the class specified by the layoutAttributesClass() method.


But I couldn't find any descriptions that layoutAttributesForItemAtIndexPath(_:) would behaves like that.

This description The methods for creating layout attributes use this class when creating new layout attributes objects. is so misleading.

But as far as I tested layoutAttributesForItemAtIndexPath(_:) of UICollectionViewLayout always returns nil.


You need to allocate (or retrieve from the cached instances) CustomLayoutAttributes instance and return it yourself.

It wouldn't be useful to call layoutAttributesClass() for UICollectionViewLayout instances because you would still have to allocate layout attributes by yourself. With UICollectionViewFlowLayout you can inject your custom layout attributes by using layoutAttributesClass() and let the flow layout do all the rest.

In the Opening Post, it is clearly stated:

However, this does work if the layout is a UICollectionViewFlowLayout.


If the implemented layout is very similar to UICollettionViewFlowLayout, subclassing it would be a good idea. But if you want to implement a layout which is completely different from UICollettionViewFlowLayout, subclassing it and replacing most of the result with your own layout attributes is not recommended.

I know. I just wanted to say why it works with UICollectionViewFlowLayout.

Oh, sorry for missing your intension.


So, back to the original issue.

UICollectionViewLayout is an abstract class, so its Methods-to-Override (described in the Subclassing Notes in the UICollectionViewLayout Class Reference) are just placeholders, have no useful implementation and return meaningless results.

UICollectionViewFlowLayout is a concrete subclass of UICollectionViewLayout, so all Methods-to-Override work and return results for implementing flow layout, those might be useful when implementing similar layout.


So, calling superclass methods like super.prepareLayout() or super.layoutAttributesForItemAtIndexPath(indexPath) are useless when inheriting from UICollectionViewLayout.


These may be some help.

CircleLayout class in the sample code CircleLayout.

APLStackLayout class in the sample code Collection View Transition.

And the Creating Custom Layouts section and the Custom Layouts: A Worked Example section in the Collection View Programming Guide for iOS.

When creating an instance of UICollectionViewLayoutAttributes you need to specify the type of view it will be applied to: cell, supplementary or decoration.

The property that tells you what kind of view it is representedElementCategory which is read only.


The only way to set this property is to create the Layout attributes with one of the convenience methods:

    public convenience init(forCellWithIndexPath:)
    public convenience init(forSupplementaryViewOfKind:withIndexPath:)
    public convenience init(forDecorationViewOfKind:withIndexPath)


These are class factory functions in Objective-C which (AFAIK) means you can't use them to create instances of subclasses.

So I can't just create and use instance of my CustomLayoutAttributes because I have no way to set representedElementCategory

You don't call layoutAttributesClass() yourself. The system does when you call super.layoutAttributesForItemAtIndexPath(indexPath) when using a UICollectionViewFlowLayout


From the UICollectionViewLayout Class Reference:

"If you subclass

UICollectionViewLayoutAttributes
in order to manage additional layout attributes, you should override this method and return your custom subclass. The methods for creating layout attributes use this class when creating new layout attributes objects.

This method is intended for subclassers only and does not need to be called by your code."

I appreciate the links OOPer but:

CircleLayout and Collection View Transition - don't use custom attributes

Custom Layouts: A Worked Example - doesn't have full source code and doesn't show the custom layout attributes being created.

Accepted Answer

These are class factory functions in Objective-C which (AFAIK) means you can't use them to create instances of subclasses.

I may be missing something, but all three convenience initalizers are available for subclasses such as your CustomLayoutAttributes which inherits all designated initializers.

In fact, even if I replace the layoutAttributesForItemAtIndexPath(_:) method in the sample code CircleLayout as follows, it works:

    override func layoutAttributesForItemAtIndexPath(path: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = CustomLayoutAttributes(forCellWithIndexPath: path)
        attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE)
        attributes.center = CGPointMake(center.x + radius * cos(2 * CGFloat(path.item) * CGFloat(M_PI) / CGFloat(cellCount)),
            center.y + radius * sin(2 * CGFloat(path.item) * CGFloat(M_PI) / CGFloat(cellCount)))
        return attributes
    }


Or, you can do something like this, if you want to make your code compliant with subclassing notes.

        let attributesClass = self.dynamicType.layoutAttributesClass() as! UICollectionViewLayoutAttributes.Type
        let attributes = attributesClass.init(forCellWithIndexPath: path)

Thanks OOPer 🙂 that was it!


So a convenience initialiser defined on the superclass can call the designated initialiser of a subclass because it is inherited? I didn't know that but it works 😊


I looked at Apple's docs on initialisation https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

particularly "Initializer Delegation for Class Types" and this is not obvious to me.


Anyway, thanks again. Much appreciated!

Initialization is one of the toughest parts in Swift. Frankly, many aspects of initialization are not crystal clear for me, even now.


But, in this case, avaiability of convenience initializer is OK. It's a guranteed feature, not a works-as-for-now thing.

Automatic Initializer Inheritance part of the Class Inheritance and Initialization section can be some help.

You are right. It's clear as day in Swift book. I need to re-read it because so much has changed since I last did.

Problems with subclassing UICollectionViewLayoutAttributes
 
 
Q