Showing posts with label uicollectionview. Show all posts
Showing posts with label uicollectionview. Show all posts

Friday, August 31, 2018

How to correctly invalidate layout for supplementary views in UICollectionView

Leave a Comment

I am having a dataset displayed in a UICollectionView. The dataset is split into sections and each section has a header. Further, each cell has a detail view underneath it that is expanded when the cell is clicked.

For reference:

enter image description here

For simplicity, I have implemented the details cells as standard cells that are hidden (height: 0) by default and when the non-detail cell is clicked, the height is set to non-zero value. The cells are updates using invalidateItems(at indexPaths: [IndexPath]) instead of reloading cells in performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) as the animations seems glitchy otherwise.

Now to the problem, the invalidateItems function obviously updates only cells, not supplementary views like the section header and therefore calling only this function will result in overflowing the section header:

enter image description here

After some time Googling, I found out that in order to update also the supplementary views, one has to call invalidateSupplementaryElements(ofKind elementKind: String, at indexPaths: [IndexPath]). This might recalculate the section header's bounds correctly, however results in the content not appearing:

enter image description here

This is most likely caused due to the fact that the func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView does not seem to be called.

I would be extremely grateful if somebody could tell me how to correctly invalidate supplementary views to the issues above do not happen.

Code:

   override func numberOfSections(in collectionView: UICollectionView) -> Int {         return dataManager.getSectionCount()     }      override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {         let count = dataManager.getSectionItemCount(section: section)         reminder = count % itemsPerWidth         return count * 2     }      override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {          if isDetailCell(indexPath: indexPath) {             let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Reusable.CELL_SERVICE, for: indexPath) as! ServiceCollectionViewCell             cell.lblName.text = "Americano detail"              cell.layer.borderWidth = 0.5             cell.layer.borderColor = UIColor(hexString: "#999999").cgColor             return cell          } else {             let item = indexPath.item > itemsPerWidth ? indexPath.item - (((indexPath.item / itemsPerWidth) / 2) * itemsPerWidth) : indexPath.item             let product = dataManager.getItem(index: item, section: indexPath.section)              let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Reusable.CELL_SERVICE, for: indexPath) as! ServiceCollectionViewCell             cell.lblName.text = product.name              cell.layer.borderWidth = 0.5             cell.layer.borderColor = UIColor(hexString: "#999999").cgColor              return cell         }     }      override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {         switch kind {         case UICollectionElementKindSectionHeader:             if indexPath.section == 0 {                 let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: Reusable.CELL_SERVICE_HEADER_ROOT, for: indexPath) as! ServiceCollectionViewHeaderRoot                 header.lblCategoryName.text = "Section Header"                 header.imgCategoryBackground.af_imageDownloader = imageDownloader                 header.imgCategoryBackground.af_setImage(withURLRequest: ImageHelper.getURL(file: category.backgroundFile!))                 return header             } else {                 let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: Reusable.CELL_SERVICE_HEADER, for: indexPath) as! ServiceCollectionViewHeader                 header.lblCategoryName.text = "Section Header"                 return header             }         default:             assert(false, "Unexpected element kind")         }     }      // MARK: UICollectionViewDelegate      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {         let width = collectionView.frame.size.width / CGFloat(itemsPerWidth)          if isDetailCell(indexPath: indexPath) {             if expandedCell == indexPath {                 return CGSize(width: collectionView.frame.size.width, height: width)             } else {                 return CGSize(width: collectionView.frame.size.width, height: 0)             }         } else {             return CGSize(width: width, height: width)         }     }      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {         if section == 0 {             return CGSize(width: collectionView.frame.width, height: collectionView.frame.height / 3)         } else {             return CGSize(width: collectionView.frame.width, height: heightHeader)         }     }      override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {         if isDetailCell(indexPath: indexPath) {             return         }          var offset = itemsPerWidth         if isLastRow(indexPath: indexPath) {             offset = reminder         }          let detailPath = IndexPath(item: indexPath.item + offset, section: indexPath.section)         let context = UICollectionViewFlowLayoutInvalidationContext()          let maxItem = collectionView.numberOfItems(inSection: 0) - 1         var minItem = detailPath.item         if let expandedCell = expandedCell {             minItem = min(minItem, expandedCell.item)         }          // TODO: optimize this         var cellIndexPaths = (0 ... maxItem).map { IndexPath(item: $0, section: 0) }          var supplementaryIndexPaths = (0..<collectionView.numberOfSections).map { IndexPath(item: 0, section: $0)}          for i in indexPath.section..<collectionView.numberOfSections {             cellIndexPaths.append(contentsOf: (0 ... collectionView.numberOfItems(inSection: i) - 1).map { IndexPath(item: $0, section: i) })             //supplementaryIndexPaths.append(IndexPath(item: 0, section: i))         }          context.invalidateSupplementaryElements(ofKind: UICollectionElementKindSectionHeader, at: supplementaryIndexPaths)         context.invalidateItems(at: cellIndexPaths)          if detailPath == expandedCell {             expandedCell = nil         } else {             expandedCell = detailPath         }          UIView.animate(withDuration: 0.25) {             collectionView.collectionViewLayout.invalidateLayout(with: context)             collectionView.layoutIfNeeded()         }     } 

1 Answers

Answers 1

If you call your collectionView.layoutIfNeeded() within your animate function the animation doesn't work. I suggest you reload the collectionView as you should have invalidated the supplement views it should give you the required results. If however that will not work then you can try adding a couple of print statements to see if the invalidatedSupplementViews are registered correctly.

// TODO: optimize this     var cellIndexPaths = (0 ... maxItem).map { IndexPath(item: $0, section: 0) }      var supplementaryIndexPaths = (0..<collectionView.numberOfSections).map { IndexPath(item: 0, section: $0)}      for i in indexPath.section..<collectionView.numberOfSections {         cellIndexPaths.append(contentsOf: (0 ... collectionView.numberOfItems(inSection: i) - 1).map { IndexPath(item: $0, section: i) })         //supplementaryIndexPaths.append(IndexPath(item: 0, section: i))     }      context.invalidateSupplementaryElements(ofKind: UICollectionElementKindSectionHeader, at: supplementaryIndexPaths)     context.invalidateItems(at: cellIndexPaths)     print("____________INFO________________")     print("THE SUPPLEMENTARY INDEX PATHS ARE: \(supplementaryIndexPaths))"     print("------CONTEXT-----")     print("\(context)")     print("__________END OF DEBUG INFO_________")      if detailPath == expandedCell {         expandedCell = nil     } else {         expandedCell = detailPath     }      UIView.animate(withDuration: 0.25) {         collectionView.collectionViewLayout.invalidateLayout(with: context)     }     collectionView.layoutIfNeeded() //if this doesn't work then try uncommenting the next line     //collectionView.reloadData()   } 

If all this will not help then paste what you get in those debug print statements. Or you could share the code on github I could try to contribute.

EDIT 1:

// MARK: UICollectionViewDelegate //add @objc to the func below @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {     let width = collectionView.frame.size.width / CGFloat(itemsPerWidth)      if isDetailCell(indexPath: indexPath) {         if expandedCell == indexPath {             return CGSize(width: collectionView.frame.size.width, height: width)         } else {             return CGSize(width: collectionView.frame.size.width, height: 0)         }     } else {         return CGSize(width: width, height: width)     } } 
Read More

Tuesday, July 24, 2018

How to add pagination in UICollectionView in swift?

Leave a Comment

I have a UICollection view which shows items. Now I want to add pagination on UICollectionView. I do not want to use any third party for this functionality. Please let me know how I can achieve this!

I have four one example http://slicode.com/bottom-refresh-control-uicollectionview/

But in this I have to drag scrollview upwards for few second to make it work.

7 Answers

Answers 1

I have added the functionality of load more in one of my application so let me give you the code

    func collectionView(_ collectionView: UICollectionView, layout         collectionViewLayout: UICollectionViewLayout,                         referenceSizeForFooterInSection section: Int) -> CGSize {          if arrData.count > 0 && isLastPageReached == false         {             return CGSize(width:(collectionView.frame.size.width), height: 100.0)         }         return CGSize.zero      }     func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {          if kind == UICollectionElementKindSectionFooter {              let vew = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "footer", for: indexPath)               let loading = UIActivityIndicatorView()             loading.activityIndicatorViewStyle = .gray             loading.translatesAutoresizingMaskIntoConstraints = false             loading.tintColor = UIColor.gray             loading.tag = -123456             vew.addSubview(loading)             vew.addConstraint(NSLayoutConstraint(item: loading, attribute: .centerX, relatedBy: .equal, toItem: vew, attribute: .centerX, multiplier: 1, constant: 0))             vew.addConstraint(NSLayoutConstraint(item: loading, attribute: .centerY, relatedBy: .equal, toItem: vew, attribute: .centerY, multiplier: 1, constant: 0))               return vew         }         return UICollectionReusableView()     }      func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {         if elementKind == UICollectionElementKindSectionFooter {              if let loadingView = view.viewWithTag(-123456) as? UIActivityIndicatorView{                 if arrData.count > 0 && isLastPageReached == false                  {                     loadingView.startAnimating()                     self.loadMore()                 }                 else                 {                     self.isLoadMore = false                 }             }         }     }     func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {         if elementKind == UICollectionElementKindSectionFooter{              if let loadingView = view.viewWithTag(-123456) as? UIActivityIndicatorView{                 loadingView.stopAnimating()                 loadingView.removeFromSuperview()                  self.isLoadMore = false             }          }     } 

Answers 2

If you want to add bottom refresh control you have to use below helper class.

https://github.com/vlasov/CCBottomRefreshControl/tree/master/Classes

Download this 2 files which is in Objective-C. You have to import this file in Bridging header

#import "UIScrollView+BottomRefreshControl.h" 

Add refresh control like this.

let refreshControl = UIRefreshControl.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) refreshControl.triggerVerticalOffset = 50.0  refreshControl.addTarget(self, action: #selector(paginateMore), for: .valueChanged)  collectionView.bottomRefreshControl = refreshControl 

This helper class provide you new bottomRefreshControl property, which helps you to add refresh control in bottom.

Answers 3

Please try this it works for me.

Step 1:

Declare a global variable :

var totalCount : NSInteger? var isGetResponse : Bool = true var activityIndiator : UIActivityIndicatorView? 

Set the viewDidLoad:

activityIndiator = UIActivityIndicatorView(frame: CGRect(x: self.view.frame.midX - 15, y: self.view.frame.height - 140, width: 30, height: 30))     activityIndiator?.activityIndicatorViewStyle = .white     activityIndiator?.color = UIColor.black     activityIndiator?.hidesWhenStopped = true     activityIndiator?.backgroundColor = COLOR.COLOR_TABLEVIEW_BACKGROUND     activityIndiator?.layer.cornerRadius = 15     self.view.addSubview(activityIndiator!)     self.view.bringSubview(toFront: activityIndiator!) 

Step 2: Set the value in the api call method:

self.totalCount = array.count self.isGetResponse = true 

Step 3: Write this code :

func scrollViewDidScroll(_ scrollView: UIScrollView) {   if isGetResponse {     if (scrollView.contentOffset.y == scrollView.contentSize.height - scrollView.frame.size.height)     {         if totalArrayCount! > self.arrProductListing.count{                  isGetResponse = false                 activityIndiator?.startAnimating()                 self.view.bringSubview(toFront: activityIndiator!)                 pageIndex = pageIndex + 1                 print(pageIndex)                 self.getProductListing(withPageCount: pageIndex)             }         }          //LOAD MORE         // you can also add a isLoading bool value for better dealing :D     } } 

In service call you can send the page index and from the server end they send back the response.

Thank you.

Answers 4

The code in willDisplay cell method calls load more function while your scroll is 5 cells away from the end. While fetching data show loader in collection view footer. Hide footer when loading is completed.

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {                 let currentPage = collectionView.contentOffset.y / collectionView.frame.size.width;         if (aryDataSource.count >= yourAPIResponsebatchCount && aryDataSource.count - indexPath.row == 5 && currentPage >= currentPage && isDataLoading) {             currentPage++;             self.fetchMoreData()         }         else { } } 

Answers 5

I used that code while doing pagination. The point is; catching the loaded cell index while collection view is loading and control it's mod with a number and fetch datas seperately with page size field in the api (if there is). You can decide which number will be smaller than this calculation and you will control it.

For example you fetched 5 datas and loaded it and, if this state is ok, you will fetch the other 5. Here is the sample code for that.

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {         if let vc = self.parentViewController as? SellController {             if vc.dontFetch == false {                 DispatchQueue.main.async {                     if self.salesData.count > 3 {                         if indexPath.row != 1 {                             if indexPath.row % 3 == 1 {                                 print("indexpath: \(indexPath.row)")                                 vc.pagination += 1                                 vc.reload()                              }                         }                     }                 }             }         }     } 

In this code i am controlling indexpath.row % 3 == 1 or not. It can be little bit complex so i can share a code block if you want. Good luck:)

Answers 6

CollectionView Delegate method: Load More data on Scroll Demand on CollectionView.

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {         if indexPath.row == numberofitem.count - 1 {  //numberofitem count             updateNextSet()           } }  func updateNextSet(){        print("On Completetion")        //requests another set of data (20 more items) from the server. } 

Answers 7

-- itemList - your data array.

-- pageCount - variable used to keep track of the number of pages.

-- totalCount - total number of data

--- append data to itemList every time you call the API.

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {     if indexPath.row == itemList.count - 1 && itemList.count < totalCount{         pageCount += 1         // Call API here     } } 
Read More

Sunday, January 14, 2018

Making UITableView with embedded UICollectionView using UITableViewAutomaticDimension

Leave a Comment

I want to create an UITableView with Title label and embedded UICollectionView (which knows it's size) with some icons using UITableViewAutomaticDimension. The problem is UITableView has a problem with figuring out cell height when I have UICollectionView inside. I have to scroll an UITableView in order for it to recalculate sizes. But even then it has problems with height (it's too big if it was reused from bigger one). On top of that icons inside UICollectionView aren't known from the begginging, but they are intended to be loaded from the server.

I've also tried to create a height constraint for an UICollectionView, but this way I'm getting "Unable to simultaneously satisfy constraints" caused by conflict with UIView-Encapsulated-Layout-Height and my own constraint is being removed anyway.

I've created a GitHub repository with a sample project (I did it as simple as possible):

https://github.com/piotrros/CollectionViewInTableView

5 Answers

Answers 1

As you are doing very much stuff in cellForRow so it need time to ready and so when you scroll it is not showing properly

Check following things.

ViewController.swift

In View Did load

Add tableView.rowHeight = UITableViewAutomaticDimension

and replace this method

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {     let foo = foos[indexPath.row]     let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FooTableViewCell        cell.titleLabel.text = foo.title     cell.descriptionLabel.text = foo.description      self.view.layoutIfNeeded()      return cell } 

FooTableViewCell.swift

class FooTableViewCell: UITableViewCell {      @IBOutlet weak var titleLabel: UILabel!     @IBOutlet weak var stackView: UIStackView!     @IBOutlet weak var descriptionLabel: UILabel!     @IBOutlet weak var iconsCollectionView: IconsCollectionView!     @IBOutlet weak var const_Height_CollectionView: NSLayoutConstraint!       override func awakeFromNib() {         iconsCollectionView.translatesAutoresizingMaskIntoConstraints = false         iconsCollectionView.initFlowLayout(superviewWidth: self.frame.width)         iconsCollectionView.loadIconsSync()         iconsCollectionView.setNeedsLayout()      } } 

And I have removed StackView Form your storyboard and just give leading , trailing ,top and bottom constraints (Nothing complicated )

enter image description here

Here is output

enter image description here

Hope it is helpful to you

EDIT/UPDATE

YOu have many issues in your demo project. I have made many changes in your demo project.

Copy and paste XML in storyboard.

DON'T FORGOT TO CONNECT HEIGHT CONSTRAINT

Here is complete storyboard XML

 <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">     <device id="retina4_7" orientation="portrait">         <adaptation id="fullscreen"/>     </device>     <dependencies>         <deployment identifier="iOS"/>         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>         <capability name="Safe area layout guides" minToolsVersion="9.0"/>         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>     </dependencies>     <scenes>         <!--View Controller-->         <scene sceneID="tne-QT-ifu">             <objects>                 <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="CollectionViewInTableView" customModuleProvider="target" sceneMemberID="viewController">                     <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">                         <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>                         <subviews>                             <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="196" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="ZhZ-if-Yia">                                 <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>                                 <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>                                 <prototypes>                                     <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="cell" rowHeight="196" id="1WO-2S-MI9" customClass="FooTableViewCell" customModule="CollectionViewInTableView" customModuleProvider="target">                                         <rect key="frame" x="0.0" y="28" width="375" height="196"/>                                         <autoresizingMask key="autoresizingMask"/>                                         <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="1WO-2S-MI9" id="2lF-aM-Z2U">                                             <rect key="frame" x="0.0" y="0.0" width="375" height="195.5"/>                                             <autoresizingMask key="autoresizingMask"/>                                             <subviews>                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vs4-91-woo">                                                     <rect key="frame" x="8" y="8" width="359" height="20.5"/>                                                     <fontDescription key="fontDescription" type="system" pointSize="17"/>                                                     <nil key="textColor"/>                                                     <nil key="highlightedColor"/>                                                 </label>                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lD2-bR-lnm">                                                     <rect key="frame" x="8" y="36.5" width="359" height="20.5"/>                                                     <fontDescription key="fontDescription" type="system" pointSize="17"/>                                                     <nil key="textColor"/>                                                     <nil key="highlightedColor"/>                                                 </label>                                                 <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="6sQ-gf-Y6x" customClass="IconsCollectionView" customModule="CollectionViewInTableView" customModuleProvider="target">                                                     <rect key="frame" x="8" y="65" width="359" height="92.5"/>                                                     <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>                                                     <constraints>                                                         <constraint firstAttribute="height" relation="greaterThanOrEqual" priority="750" constant="88" id="f9g-Du-pYg"/>                                                     </constraints>                                                     <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="Nts-Lf-FPD">                                                         <size key="itemSize" width="50" height="50"/>                                                         <size key="headerReferenceSize" width="0.0" height="0.0"/>                                                         <size key="footerReferenceSize" width="0.0" height="0.0"/>                                                         <inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>                                                     </collectionViewFlowLayout>                                                     <cells>                                                         <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="item" id="8gX-Q1-0jG" customClass="BarCollectionViewCell" customModule="CollectionViewInTableView" customModuleProvider="target">                                                             <rect key="frame" x="0.0" y="0.0" width="50" height="50"/>                                                             <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>                                                             <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">                                                                 <rect key="frame" x="0.0" y="0.0" width="50" height="50"/>                                                                 <autoresizingMask key="autoresizingMask"/>                                                                 <subviews>                                                                     <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="6cf-uD-BQl">                                                                         <rect key="frame" x="0.0" y="0.0" width="50" height="50"/>                                                                     </imageView>                                                                 </subviews>                                                             </view>                                                             <constraints>                                                                 <constraint firstAttribute="bottom" secondItem="6cf-uD-BQl" secondAttribute="bottom" id="0c3-ug-8tN"/>                                                                 <constraint firstItem="6cf-uD-BQl" firstAttribute="top" secondItem="8gX-Q1-0jG" secondAttribute="top" id="9xW-dN-c0m"/>                                                                 <constraint firstItem="6cf-uD-BQl" firstAttribute="leading" secondItem="8gX-Q1-0jG" secondAttribute="leading" id="dT6-RU-eE4"/>                                                                 <constraint firstAttribute="trailing" secondItem="6cf-uD-BQl" secondAttribute="trailing" id="nnz-oA-GgP"/>                                                             </constraints>                                                             <connections>                                                                 <outlet property="iconImageView" destination="6cf-uD-BQl" id="lFo-SS-Ego"/>                                                             </connections>                                                         </collectionViewCell>                                                     </cells>                                                 </collectionView>                                                 <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sXa-Mn-xxW">                                                     <rect key="frame" x="8" y="165.5" width="359" height="30"/>                                                     <state key="normal" title="Button"/>                                                 </button>                                             </subviews>                                             <constraints>                                                 <constraint firstItem="sXa-Mn-xxW" firstAttribute="leading" secondItem="2lF-aM-Z2U" secondAttribute="leading" constant="8" id="4ix-8u-0lO"/>                                                 <constraint firstAttribute="bottom" secondItem="sXa-Mn-xxW" secondAttribute="bottom" id="50m-Bv-FF8"/>                                                 <constraint firstAttribute="trailing" secondItem="sXa-Mn-xxW" secondAttribute="trailing" constant="8" id="8UW-vI-hge"/>                                                 <constraint firstItem="6sQ-gf-Y6x" firstAttribute="leading" secondItem="2lF-aM-Z2U" secondAttribute="leading" constant="8" id="9ht-Ez-lJX"/>                                                 <constraint firstAttribute="trailing" secondItem="vs4-91-woo" secondAttribute="trailing" constant="8" id="NOH-if-o7C"/>                                                 <constraint firstItem="lD2-bR-lnm" firstAttribute="leading" secondItem="2lF-aM-Z2U" secondAttribute="leading" constant="8" id="S2D-Kj-5Og"/>                                                 <constraint firstItem="vs4-91-woo" firstAttribute="leading" secondItem="2lF-aM-Z2U" secondAttribute="leading" constant="8" id="atk-7U-Mrw"/>                                                 <constraint firstAttribute="trailing" secondItem="6sQ-gf-Y6x" secondAttribute="trailing" constant="8" id="bfY-uh-Su2"/>                                                 <constraint firstItem="vs4-91-woo" firstAttribute="top" secondItem="2lF-aM-Z2U" secondAttribute="top" constant="8" id="gYO-XW-lmk"/>                                                 <constraint firstAttribute="trailing" secondItem="lD2-bR-lnm" secondAttribute="trailing" constant="8" id="pkH-Pf-xE1"/>                                                 <constraint firstItem="sXa-Mn-xxW" firstAttribute="top" secondItem="6sQ-gf-Y6x" secondAttribute="bottom" constant="8" id="w2O-4g-q6B"/>                                                 <constraint firstItem="6sQ-gf-Y6x" firstAttribute="top" secondItem="lD2-bR-lnm" secondAttribute="bottom" constant="8" id="xky-sw-IcM"/>                                                 <constraint firstItem="lD2-bR-lnm" firstAttribute="top" secondItem="vs4-91-woo" secondAttribute="bottom" constant="8" id="yG3-dE-CjF"/>                                             </constraints>                                         </tableViewCellContentView>                                         <connections>                                             <outlet property="const_Height_CollectionView" destination="f9g-Du-pYg" id="gw7-9T-hiU"/>                                             <outlet property="descriptionLabel" destination="lD2-bR-lnm" id="M4K-k5-6LN"/>                                             <outlet property="iconsCollectionView" destination="6sQ-gf-Y6x" id="FO2-dP-VNH"/>                                             <outlet property="titleLabel" destination="vs4-91-woo" id="HHy-1V-bTW"/>                                         </connections>                                     </tableViewCell>                                 </prototypes>                             </tableView>                         </subviews>                         <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>                         <constraints>                             <constraint firstItem="ZhZ-if-Yia" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" id="J8c-wQ-FxB"/>                             <constraint firstItem="ZhZ-if-Yia" firstAttribute="bottom" secondItem="6Tk-OE-BBY" secondAttribute="bottom" id="KCX-nj-zXy"/>                             <constraint firstItem="ZhZ-if-Yia" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="QMU-w2-uUY"/>                             <constraint firstItem="ZhZ-if-Yia" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="yx7-yg-aqC"/>                         </constraints>                         <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>                     </view>                     <connections>                         <outlet property="tableView" destination="ZhZ-if-Yia" id="WdR-nu-gjc"/>                     </connections>                 </viewController>                 <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>             </objects>             <point key="canvasLocation" x="117.59999999999999" y="118.29085457271366"/>         </scene>     </scenes> </document> 

FOOTableviewCell.swift

protocol TableViewDelegate {     func cellTapped (for:FooTableViewCell) }   class FooTableViewCell: UITableViewCell {       static let singleCellHeight = 88;      @IBOutlet weak var titleLabel: UILabel!     @IBOutlet weak var descriptionLabel: UILabel!     @IBOutlet weak var iconsCollectionView: IconsCollectionView!     @IBOutlet weak var const_Height_CollectionView: NSLayoutConstraint!      var delegateCollection : TableViewDelegate?     var bars:[Bar] = [] {         didSet {             self.iconsCollectionView.reloadData()             iconsCollectionView.setNeedsLayout()             self.layoutIfNeeded()             const_Height_CollectionView.constant =  iconsCollectionView.contentSize.height             self.layoutIfNeeded()         }     }      override func awakeFromNib() {         iconsCollectionView.translatesAutoresizingMaskIntoConstraints = false         iconsCollectionView.initFlowLayout(superviewWidth: self.frame.width)         iconsCollectionView.setNeedsLayout()         iconsCollectionView.dataSource = self         iconsCollectionView.delegate = self         const_Height_CollectionView.constant =  iconsCollectionView.contentSize.height         self.layoutIfNeeded()         self.setNeedsLayout()     }      func cellTapped () {         iconsCollectionView.setNeedsLayout()         self.layoutIfNeeded()         self.setNeedsLayout()          const_Height_CollectionView.constant =  iconsCollectionView.contentSize.height          self.delegateCollection?.cellTapped(for: self)      }  }  extension FooTableViewCell : UICollectionViewDataSource {      func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {         return bars.count + 1     }      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {          let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "item", for: indexPath) as! BarCollectionViewCell          let row = indexPath.row         if(row >= bars.count) {             cell.iconImageView.image = UIImage(named: "add.png")             return cell         } else {             let bar = bars[row]             cell.iconImageView.image = UIImage(named: bar.imageName)             print(bar.imageName)              return cell         }      }  }  extension FooTableViewCell : UICollectionViewDelegate {      func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {         //deselectItem(at: indexPath, animated: true)         if(indexPath.row >= bars.count) { //it's a plus button             self.delegateCollection?.cellTapped(for: self)         }     } } 

IconCollectionView.swift

import UIKit    class IconsCollectionView: DynamicCollectionView {       var columnLayout:ColumnFlowLayout?        override func awakeFromNib() {     }      func initFlowLayout(superviewWidth:CGFloat) {         let layout = ColumnFlowLayout(             cellsPerRow: 4,             superviewWidth: superviewWidth,             minimumInteritemSpacing: 0,             minimumLineSpacing: 0,             sectionInset: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)         )         columnLayout = layout         collectionViewLayout = layout     } } 

ViewController.Swift

import UIKit  class ViewController: UIViewController {      @IBOutlet weak var tableView: UITableView!      var foos:[Foo] = []      var bars:[[Bar]] = [[]]      override func viewDidLoad() {         super.viewDidLoad()          tableView.dataSource = self         tableView.delegate = self         tableView.estimatedRowHeight = 188         tableView.rowHeight = UITableViewAutomaticDimension          for i in stride(from: 1, to: 10, by: 1) {             let foo = Foo()             foo.title = "Item \(i)"             foo.description = "Description \(i)"             foos.append(foo)         }         bars.removeAll()         for _ in 0 ..< foos.count {             bars.append(self.loadIconsSync())         }     }      func loadIconsSync() -> [Bar] {          var barObjects :[Bar] = []          let iconsCount = Utils.rnd(3, 8)         for _ in stride(from: 1, to: iconsCount, by: 1) {             barObjects.append(self.getRandomItem())         }          return barObjects     }      func getRandomItem() -> Bar {         let randomIndex = Utils.rnd(1, 10)         let bar = Bar()         bar.imageName = "icon_\(randomIndex).png"         return bar     }  }  extension ViewController : UITableViewDataSource,UITableViewDelegate {      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {         let foo = foos[indexPath.row]         let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FooTableViewCell          var bar = bars[indexPath.row]         cell.bars = bar          cell.titleLabel.text = foo.title         cell.descriptionLabel.text = foo.description         cell.delegateCollection = self         self.view.layoutIfNeeded()         cell.const_Height_CollectionView.constant =  cell.iconsCollectionView.contentSize.height         self.view.layoutIfNeeded()          return cell     }      func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {         return foos.count     }  }  extension ViewController : TableViewDelegate {     func cellTapped(for obj: FooTableViewCell) {         if let indexPath = tableView.indexPath(for: obj) {             bars[indexPath.row].append(getRandomItem())             self.tableView.beginUpdates()             self.tableView.reloadRows(at: [indexPath], with: .automatic)             self.tableView.endUpdates()         }     }  } 

OUTPUT

enter image description here

EDIT / UPDATE 2

I was not aware about orientation supports and ipad support.

now when the orientation changes we have to re-layout the collection view.

So logic is

total items + 1 (+ 1 because of that plus icon )

Item Size * (total Items / 3).rounded

suppose you have 7 Items

so item size is 93 (Per row) * ( 8 / 3).rounded = 279

So here you need to manage some hardcoded values as per your requirement for iPad and Landscape mode. For now I am considering 3 objects per row same as iPhone design.

Here Hardcoded cell number is 3 You can manage your own.

Step1:

Add Following method in viewContorller.swift

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {          super.viewWillTransition(to: size, with: coordinator)             self.tableView.beginUpdates()             self.tableView.reloadData()             self.tableView.endUpdates()         } 

Replace cellForRowAtIndexPath

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {         let foo = foos[indexPath.row]         let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FooTableViewCell          cell.iconsCollectionView.initFlowLayout(superviewWidth: self.tableView.frame.width)          let bar = bars[indexPath.row]         cell.bars = bar          cell.titleLabel.text = foo.title         cell.descriptionLabel.text = foo.description         cell.delegateCollection = self         self.view.layoutIfNeeded()          let items:CGFloat = CGFloat(bar.count + 1)         let value = (items / 3.0).rounded(.awayFromZero)         cell.const_Height_CollectionView.constant =  CGFloat((cell.iconsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize.height * value)          self.view.layoutIfNeeded()         cell.iconsCollectionView.setNeedsLayout()         return cell     } 

And

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {         if let  footCell =  cell as?  FooTableViewCell {             footCell.const_Height_CollectionView.constant = footCell.iconsCollectionView.contentSize.height             self.view.layoutIfNeeded()          }     } 

In TableviewCell

var bars:[Bar] = [] {         didSet {             self.iconsCollectionView.reloadData()             iconsCollectionView.setNeedsLayout()             self.layoutIfNeeded()              let items:CGFloat = CGFloat(bars.count + 1)             let value = (items / 3.0).rounded(.awayFromZero)              const_Height_CollectionView.constant =  CGFloat((iconsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize.height * value)              self.layoutIfNeeded()         }     } 

and

func cellTapped () {         iconsCollectionView.setNeedsLayout()         self.layoutIfNeeded()         self.setNeedsLayout()         let items:CGFloat = CGFloat(bars.count + 1)         let value = (items / 3.0).rounded(.awayFromZero)         const_Height_CollectionView.constant =  CGFloat((iconsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize.height * value)         self.delegateCollection?.cellTapped(for: self)      } 

Answers 2

I think you need to implement estimatedHeightForRowAt, so instead of below method

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {     return UITableViewAutomaticDimension } 

Use below method:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {     return UITableViewAutomaticDimension } 

This should work, try it!

Answers 3

The problem is :

// In DynamicCollectionView, when the icons fetch is not yet finished override var intrinsicContentSize: CGSize {     return contentSize.height // == 0 } 

So the tableView does not calculate the right cell heights.

The solution is to give at your collectionView a height while you are fetching the icons. Then, when the fetch is done, ask your tableView to reload the corresponding row. Therefore, you should base your height calculation on the layout of your collectionView rather than on contenSize height : the contenSize of the collectionView may be incorrect because reloadData is actually asynchronous.

Answers 4

I downloaded the git hub source code an fixed the issue. Your code is perfect for setting the cell size , the problem lies you column flow layout. You were setting to show 4 cells in a row, but according to your source code only 3 cells were displayed.I digged deeper into it and found out that because of this the number of cells are calculated wrong in your "intrinsicContentSize" in Icons collection view. The width of super view you are passing in the flow layout function is causing problem. i guess there was some spacing issue for the width. So here is how my new code looks :

override var itemSize: CGSize {         get {             let itemWidth = ((superviewWidth - 50) / CGFloat(cellsPerRow)).rounded(.down)             return CGSize(width: itemWidth, height: itemWidth)         }         set {             super.itemSize = newValue         }     }  override var intrinsicContentSize: CGSize {         guard let columnLayout = columnLayout else { return CGSize(width: 0, height: 0) }         let itemSize = columnLayout.itemSize         let rows = ceil(Double(bars.count) / Double(columnLayout.cellsPerRow))         let w = columnLayout.superviewWidth         let h = itemSize.height * CGFloat(rows)           print("itemSize: \(itemSize.width), \(itemSize.height), intrinsicContentSize: \(w), \(h); rows = \(rows)")          return CGSize(width: w, height: h)     } 

I have changed only these two function and your code worked perfectly. enter image description here

If you want source code I can push it on git too. Hope this helps you out!

Answers 5

UICollectionView itself does not have an intrinsic content size, thus you have to create an explicit height constraint for it, as you say you did.

If the layout seems to be working then, but you are getting unsatisfiable constraints with UIView-Encapsulated-Layout-Height, then read the following.

That's a result of how UITableViewAutomaticDimension works - it sets height of the cell based on some default value (I still don't know how does it get that value), then uses autolayout to calculate the height your cell wants, and then updates the UIView-Encapsulated-Layout-Height to fit the new height. But in the process your constraints and the UIView-Encapsulated-Layout-Height constraint gets into conflict. The solution is to set the priority of one of your constraints to collectionViewHeightConstraint.priority = UILayoutPriority(rawValue: 999). This way the constraints won't get broken, and autolayout will work without warnings + note that in the end UIView-Encapsulated-Layout-Height will get updated to the proper height, so lowering priority will not prevent layout to work. See my answer to another similar question for reference.

EDIT

I forked your project, fixed it, and created a pull request (see github).

The main problem I believe was a very trivial bug. You used bars.count to calculate the intrinsic size of the collection, but in the end you had bars.count + 1 items in collection (the + icon). Therefore if + icon had to be put on a new row alone, it seemed that your layout wasn't working.

So just change

let rows = ceil(Double(bars.count) / Double(columnLayout.cellsPerRow)) 

to

let rows = ceil(Double(bars.count + 1) / Double(columnLayout.cellsPerRow)) 

in intrinsicContentSize of the IconsCollectionView.

There were other things I would change, and I changed it in the project - in your cell prototype in storyboards.

First, I removed that explicit constraint on height of the collection set to 88 points. You have intrinsic size, so you don't need that (if storyboards complain, don't mind them, they do not know that you implemented intrinsic size.

Second, the button at the bottom was not constrained to the bottom of the cell. Thus the cell height calculation could not work (you want the cell to size itself to fit its contents, so you need to constrain all the sides of the cell to its contents). But that maybe was a result of someone else's advices, because I think older commits had it working.

Third, a minor note, is to the way you used superviewWidth to calculate the size. I changed it to selfView, and changed the way its value was calculated. Because in the end the width of collection view is not equal to cell's width, but to cell's width -16. That's because collectionView was constrained to start 8 points from the left of the cell's contentView, and 8 points from the right of the cell's contentView. Although that was not the main issue, it might cause some problems later.

Finally, I left you a comment in you async loading of the items. If you load it asynchronously, the cell will most probably get presented before you get your data back, and you will want to refresh the tableView to adapt to newly loaded data. Conceptually, that's the same as dynamically expanding and collapsing cells - for that I will refer you to my answer to that question. Please, if you find it helpful, leave an upvote there too to make it more visible.

Read More

Tuesday, August 15, 2017

UICollectionView Crash on reloadData

Leave a Comment

UICollectionview is loaded with simple cells and there is a simple int variable for calculating the number of items in the section.

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {     return viewCount } 

When the value of viewCount (initially at 45) is changed to a lower number (say 12) the app crashes.

This is error message to update the item count:

 *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x17402fbe0> {length = 2, path = 0 - 12}' 

I tried reloading data and invalidating the cache as well as said here. This didn't help. I also tried reloading the indexSet before invalidating the cache. That didn't help either.

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {         if textField == countTextField {             viewCount = Int(textField.text!)!             textField.resignFirstResponder()             let indexSet = IndexSet(integer: 0)  //            collectionView.reloadSections(indexSet)             collectionView.reloadData()             collectionView.collectionViewLayout.invalidateLayout()         }          return true     } 

I use 2 custom UICollectinViewLayout and I also set shouldInvalidateLayout to be true. Any help?

Update

func numberOfSections(in collectionView: UICollectionView) -> Int {         return 1     }      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {         let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DemoCell", for: indexPath) as! DemoCollectionViewCell         cell.indexLabel.text = "\(indexPath.item + 1)"          return cell     }      @IBAction func showLayout1(_ sender: Any) {         currentLayout = DemoACollectionViewLayout()         animateLayout()     }      @IBAction func showLayout2(_ sender: Any) {         currentLayout = DemoBCollectionViewLayout()         animateLayout()     }      func animateLayout() {         UIView.animate(withDuration: 0.3) { [weak self] value in             guard let `self` = self else { return }             self.collectionView.collectionViewLayout.invalidateLayout()             self.collectionView.collectionViewLayout = self.currentLayout!         }     } 

Here is the sample project.

Update: I have updated to use an dataObject to reload the UICollectionView but the cache in UICollectionViewLayout is not being cleared when I invalidate.

1 Answers

Answers 1

The cache must be cleared when the number of items for the collectionView has changed.

guard let views = collectionView?.numberOfItems(inSection: 0)             else {     cache.removeAll()     return }  if views != cache.count {     cache.removeAll() }  if cache.isEmpty {     //Layout attributes } 
Read More

Sunday, July 30, 2017

Default UICollectionView layout not respecting iPad split screen

Leave a Comment

My layout when on split screen is not respecting the width of the split screen.

My custom view is respecting it(the black bar at the top) but anything using autolayout is not respecting the width.

I am handling rotation using

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 

Is there a helper method for handling split view? Do I handle it in layoutSubview? I would have expected the UICollectionView to handle this for us.

In viewWillTransition I use

  guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {             return         }    flowLayout.invalidateLayout() 

enter image description here

1 Answers

Answers 1

I assume by "default" layout you mean a UICollectionViewFlowLayout. It looks like the itemSize width is bigger then the width of the view. Try adding a check in collectionView:layout:sizeForItemAtIndexPath: to make sure that the width is less than or equal to the collection's width.

Read More

Sunday, June 4, 2017

UICollectionView, full width cells, allow autolayout dynamic height?

Leave a Comment

Here's a fairly basic question about current iOS development, to which I've never found a good answer.


In a (say) vertical UICollectionView ,

Is it possible to have full-width cells, but, allow the height to be controlled by autolayout?

If so how?

This strikes me as perhaps the "most important question in iOS with no really good answer."

6 Answers

Answers 1

Problem

You are looking for automatic height and also want to have full in width , it is not possible to get both in using UICollectionViewFlowLayoutAutomaticSize. why you don't take UITableVIew instead of UICollectionView in which you can just return in the heightForRow as UITableViewAutomaticDimension it will be managed automatically . anyways you want to do using UICollectionView so below is the solution for you.

Solution

Step- I: : Calculate the expected height of Cell

1. : If you have only UILable in ColllectionViewCell than set the numberOfLines=0 and that calculated the expected height of UIlable , pass the all three paramters

 func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat{   // pass string ,font , LableWidth           let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))         label.numberOfLines = 0         label.lineBreakMode = NSLineBreakMode.byWordWrapping         label.font = font         label.text = text         label.sizeToFit()          return label.frame.height     } 

2. : if your ColllectionViewCell contains only UIImageView and if it's is supposed to be dynamic in Height than you need to get the height of UIImage (your UIImageView must have AspectRatio constraints )

// this will give you the height of your Image let heightInPoints = image.size.height let heightInPixels = heightInPoints * image.scale 

3. if it contains both than calculated their height and add them together.

STEP-II : Return the Size of CollectionViewCell

1. Add UICollectionViewDelegateFlowLayout delegate in your viewController

2. Implement the delegate method

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {  // This is Just for example , for the scenario Step-I -> 1  let yourWidthOfLable=self.view.size.width let font = UIFont(name: "Helvetica", size: 20.0)  var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable )    return CGSize(width: view.frame.width, height: expectedHeight) } 

i hope this will help you out .

Answers 2

Not sure if this qualifies as a "really good answer", but it's what I'm using to accomplish this. My flow layout is horizontal, and I'm trying to make the width adjust with autolayout, so it's similar to your situation.

extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {   func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {     // My height is static, but it could use the screen size if you wanted     return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60)    } } 

Then in the view controller where the autolayout constraint gets modified, I fire off an NSNotification.

NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil) 

In my UICollectionView subclass, I listen for that notification:

// viewDidLoad NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil) 

and invalidate the layout:

func handleConstraintNotification(notification: Notification) {     self.collectionView?.collectionViewLayout.invalidateLayout() } 

This causes sizeForItemAt to be called again using the collection view's new size. In your case, it should be able to update given the new constraints available in the layout.

Answers 3

There are couple of ways you could tackle this problem.

One way is you can give the collection view flow layout an estimated size.

(assuming you are using storyboards and the collection view is connected via IB)

override func viewDidLoad() {     super.viewDidLoad()     let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout     layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size } 

For simple cells that will usually be enough. If size is still incorrect, in the collection view cell you can override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, which will give you more fine grain control over the cell size. Note: You will still need to give the flow layout an estimated size.

Alternatively, instead you can use a sizing cell to calculate the size of the cell in the UICollectionViewDelegateFlowLayout.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {     let width = collectionView.frame.width     let size = CGSize(width: width, height: 0)     // assuming your collection view cell is a nib     let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell     sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]     sizingCell.frame.size = size     sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell     return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow) } 

While these are not perfect production ready examples, they should get you started in the right direction.

Answers 4

You have to inherit the class UICollectionViewDelegateFlowLayout on your collectionViewController. Then add the function:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {     return CGSize(width: view.frame.width, height: 100) } 

Using that, you have the width size of the screen width.

And you now have a collectionViewController with rows as a tableViewController

If you want the size of the height of each cell to be dynamically, perhaps you should create custom cells for each cell you need.

Answers 5

Personally I found the best ways to have a UICollectionView where AutoLayout determines the size while each Cell can have a different size is to implement the UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath function while using an actual Cell to measure the size.

I talked about this in one of my blog posts: https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-swift-3/

Hopefully this one will help you to achieve what you want. I'm not 100% sure but I believe unlike UITableView where you can actually have a fully automatic height of cells by using AutoLayout inconjunction with

tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 44 

UICollectionView does not have such a way of letting AutoLayout determine the size because UICollectionViewCell does not necessarily fills the whole width of the screen.

But here is a question for you: If you need full screen width cells, why do you even bother using the UICollectionView over a good old UITableView which comes with the auto sizing cells?

Answers 6

From iOS 10, we've got new API on flow layout to do that.

All you have to do is set your flowLayout.estimatedItemSize to a new constant, UICollectionViewFlowLayoutAutomaticSize.

Source: http://asciiwwdc.com/2016/sessions/219

Read More

Monday, April 17, 2017

Weird bug returning a CGSize in UICollectionView sizeForItemAtIndexPath

Leave a Comment

I have variable widths in my UICollectionView and in my sizeForItemAtIndexPath function, I ultimately return a CGSize

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {      var width: CGFloat = indexPath.row == 0 ? 37 : 20 // default paddings based on spacing in cell     let font = UIFont.systemFont(ofSize: 14, weight: UIFontWeightRegular)      if indexPath.row == 0 {         width += listingsFilter.stringValue.width(withConstrainedHeight: 28, font: font)     } else {         let string = viewModel.valueRangeForButton[indexPath.row - 1]         width += string.width(withConstrainedHeight: 28, font: font)     }      print(width) // some decimal e.g. 138.1239581     let w: CGFloat = width.rounded() // rounded e.g. 13      return CGSize(width: w, height: 28) } 

If I stub the return value with a number for the width, say 128, rather than have a variable, the collection view respects UIEdgeInsets. If I return the CGSize with a variable for the width, UIEdgeInsets gets ignored for every element but the first and the last.

I feel like I found some deep CoreGraphics bug.

For the last two lines, if I change them to

let x: CGFloat = 128 // or 128.0, doesn't matter return CGSize(width: x, height: 28) 

It still doesn't work. I've tried returning a CGSize with the Int initializer. I've tried casting to Ints and back to CGFloats. Nothing seems to work.

I've narrowed down the problem to this point. It doesn't have anything to do with the width code above it (which is a string extension) or anything else.

Super weird. Anyone have experience with this?

Edit: Some images

Top one is with CGSize(width: w, height: 28) where w is a CGFloat that could equal 128 or whatever other value. Bottom one is CGSize(width: 128, height: 28)

enter image description here

enter image description here

5 Answers

Answers 1

I had similar issue few months ago. As far as I remember it was really annoying trying to fix this (as in your case) but ridiculously simple.
You need to make sure that your class inherits from UICollectionViewDelegateFlowLayout. It inherits from UICollectionViewDelegate. That should make the trick.
Let me know if that works for you.

Answers 2

As i can understand from op's question

to move content within cell you need

func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout:     UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {      return UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0) } 

to move spaces between respective cells you need

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {     return 15 } 

be sure that you confront the delegate UICollectionViewDelegateFlowLayout

Answers 3

Having never even tried to return a hardcoded value instead of a variable from sizeForItemAtIndexPath(), I've always assumed that UIEdgeInsets weren't even supposed to affect the spacing between the cells of a UICollectionView, but only the spaces surrounding an entire section of cells. Looks like UIEdgeInsets is not intended to affect the spacing between the cells, in which case Apple's bug is the reverse of what it seems to be -- that UIEdgeInsets is oddly affecting the spacing between cells in one outlier case, when in fact it was never intended to do so in any scenario. This red herring had you reasonably assume that there must be a reliable and programmatic way to get UIEdgeInsets to set the spacing between UICollectionView cells. I have yet to find a programmatic way to do so. When I found the following storyboard solution I stopped looking for one.

After having spent a great deal of time tweaking UICollectionViews to get them to look the way I want, I have found that the only reliable way to set the spacing between their cells is to use storyboard settings as follows:

In the storyboard, select your UICollectionView, go to the attributes inspector, find the "Min Spacing" section, and type in the "For Cells" box the value you'd like (in your case, 10). The "For Cells" value is the horizontal spacing between cells. Do the same for the "For Lines" value, which is the vertical spacing between cells.

Answers 4

Please check the code below,

class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {  override func viewDidLoad() {     super.viewDidLoad()     // Do any additional setup after loading the view, typically from a nib.     collectionView?.delegate = self     collectionView?.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) }  override func didReceiveMemoryWarning() {     super.didReceiveMemoryWarning()     // Dispose of any resources that can be recreated. }    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {      let x: CGFloat = 50 + CGFloat(arc4random_uniform(50)) // or 128.0, doesn't matter     return CGSize(width: x, height: 28) }  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {     return UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0) }  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {     return 15 }   override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {     return 50 }   override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {     return collectionView.dequeueReusableCell(withReuseIdentifier: "aaa", for: indexPath) }   override func numberOfSections(in collectionView: UICollectionView) -> Int {     return 1 } 

}

Result is enter image description here

Answers 5

It's not related to UIEdgeInsets, it relates to width that you're calculating in code. When you set width: 128, the width provided for your cells are bigger than what they actually need (let's say 90), hence depending on your UICollectionViewCell's design, it might feel there is an UIEdgeInsets for each cell.

To solve your problem:

If you're using IB, you can set cellSpacing and lineSpacing from IB, other-wise you have to implement UICollectionViewDelegateFlowLayout

Read More

Tuesday, July 19, 2016

Collection View Tree Representation

Leave a Comment

I'm looking at creating some sort of visual representation of a tree data structure in iOS. The data held by a node in the tree is an image and a label, and a node can have up to 6 children.

Currently, I have a collection view with a custom layout where I programmatically calculate the x and y of each node as I traverse my homemade tree.

This solution works, but just barely. As I build more functionality, I expect it to fall apart.

I've considered making an image once the tree is constructed and just using an imageview, but I plan on implementing some sort of expand/collapse on branches. I also need a way to zoom in and out on the whole tree, which doesn't seem very easy with collection views.

Is there a better solution out there?

3 Answers

Answers 1

What about using just simple views inside a UIScrollView?

This way you could:

  • Control expanding and collapsing each node.
  • Zoom in and out for an overall or detail view.
  • Scrolling in case of huge tree structures.

Here I created an example project using UIViews: https://github.com/crisisGriega/swift-simple-tree-drawer

It was a quick development so there are a lot of things that can be improved, like the way the lines (connectors) between the nodes are drawn. Also in this example the nodes are added to a UIView instead of a UIScrollView. But you can tap on the nodes to show/hide its children.

Answers 2

Consider using SpriteKit rather than using UIKit components. It's not easy to create dynamic tree-like layout using UICollectionView as you can look at its data source definition, it's not meant to model tree data, but flat list data. If the data model is fundamentally different, everything becomes difficult to procceed.

With SpriteKit, your node objects can each be renderred by a SKSpriteNode object. Layouting child nodes is managed by their parent node. You can use the physics engine to automatically position nodes as well, and it comes with the additional benefit that overlapping can be avoided with minimum efferot. Last but not least, scaling and scrolling are supported out of the box by SpriteKit.

Answers 3

I think, it is good that you design your own view programmatically. It seems difficult but it is the easiest method. Designing your own view is really simple and just use your imagination you can create what you want.

Read More

Thursday, March 31, 2016

Align cell in center(horizontally) in UICollectionView

Leave a Comment

I am first time working with UICollectionView not sure how to do this. I am trying to create an app for tvOS and want to display the menu like airbnb tvos app. I have somehow tried to achieve that particular format didUpdateFocusInContext but the problem is about the first appearance because the first appearance takes place on default points i.e 0,0 of collection view which results in a mess. Heres my code what I have done so far.

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {          if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as? ShowCell {               menuData = dataArray[indexPath.row] as! NSDictionary               cell.configureCell(menuData.objectForKey("name") as! String)              return cell         }         else {             return ShowCell()         }     }  override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {          if let previousItem = context.previouslyFocusedView as? ShowCell {             UIView.animateWithDuration(0.2, animations: { () -> Void in                 previousItem.showImg.frame.size = self.originalCellSize             })         }         if let nextItem = context.nextFocusedView as? ShowCell {             UIView.animateWithDuration(0.2, animations: { () -> Void in                 nextItem.showImg.frame = CGRectMake(self.view.frame.width/2 - self.focusCellSize.width/2, 169.0 , self.focusCellSize.width, self.focusCellSize.height)              })         } 

This is the view which I want to achieve and I have achieved it but for later indexes means index after 1,2enter image description here

This is the starting behaviour when it appears for the first time and when I move the control to it it happens like this enter image description here

enter image description here

This is what i actually want, but i am struggling in getting my focused cell to the middle of the screen and similarly my previous and next on the either sides . I know i am giving frames coordinates explicitly which isn't correct this was just a test case i was running and nothing else but i couldn't find the way to do this

1 Answers

Answers 1

I'd control the focus and the collection content offset (scroll position) separately.

For the content offset you should set the section margins and the inter-item spacing so that you have one cell centred and the adjacent cells visible at the edges. You can get this setup and tested without showing any focus.

Now, the focus should be managed by the cell itself, not the collection view. Below is a (slightly large) example of a cell focus change animation. It uses cartography to change image view constraints and applies a transform to a label. You can do something similar depending on how you want the image and label to interact with each other.

Note that the below code also applies a motion effect transform similar to the stock supplied by apple when a UIImageView has focus and has adjustsImageWhenAncestorFocused set. If you don't want that you can simplify and shorten the code quite a bit.

override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {     super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)      if (context.nextFocusedView == self) {         UIView.animateWithDuration(0.1,             animations: { () -> Void in                 self.imageConstraints = constrain(self.itemImageView, replace: self.imageConstraints!) {                     $0.top == $0.superview!.top                     $0.bottom == $0.superview!.bottom                     $0.leading == $0.superview!.leading                     $0.trailing == $0.superview!.trailing                 }                 self.itemLabel.transform = CGAffineTransformMakeTranslation(0, 60)                 self.itemLabel.layer.backgroundColor = UIColor.darkGrayColor().colorWithAlphaComponent(0).CGColor                  self.layer.shadowOpacity = 1                  self.layoutIfNeeded()             }, completion: nil)          let minMaxAngle = 10.0         let m34 = CGFloat(1.0 / -1250)         let angle = CGFloat(minMaxAngle * M_PI / 180.0)          var baseTransform = CATransform3DIdentity         baseTransform.m34 = m34          let rotateXmin = CATransform3DRotate(baseTransform, -1 * angle, 1.0, 0.0, 0.0);         let rotateXmax = CATransform3DRotate(baseTransform, angle, 1.0, 0.0, 0.0);         let rotateYmin = CATransform3DRotate(baseTransform, angle, 0.0, 1.0, 0.0);         let rotateYmax = CATransform3DRotate(baseTransform, -1 * angle, 0.0, 1.0, 0.0);          let verticalMotionEffect = UIInterpolatingMotionEffect(keyPath: "layer.transform",             type: .TiltAlongVerticalAxis)         verticalMotionEffect.minimumRelativeValue = NSValue(CATransform3D: rotateXmin)         verticalMotionEffect.maximumRelativeValue = NSValue(CATransform3D: rotateXmax)          let horizontalMotionEffect = UIInterpolatingMotionEffect(keyPath: "layer.transform",             type: .TiltAlongHorizontalAxis)         horizontalMotionEffect.minimumRelativeValue = NSValue(CATransform3D: rotateYmin)         horizontalMotionEffect.maximumRelativeValue = NSValue(CATransform3D: rotateYmax)          let group = UIMotionEffectGroup()         group.motionEffects = [horizontalMotionEffect, verticalMotionEffect]          self.addMotionEffect(group)      }     else {         UIView.animateWithDuration(0.3,             animations: { () -> Void in                 self.imageConstraints = constrain(self.itemImageView, replace: self.imageConstraints!) {                     $0.top == $0.superview!.top + 20                     $0.bottom == $0.superview!.bottom - 20                     $0.leading == $0.superview!.leading + 20                     $0.trailing == $0.superview!.trailing - 20                 }                 self.itemLabel.transform = CGAffineTransformIdentity                 self.itemLabel.layer.backgroundColor = UIColor.darkGrayColor().colorWithAlphaComponent(0.75).CGColor                  self.layer.shadowOpacity = 0                  self.layoutIfNeeded()             }, completion: nil)          for effect in self.motionEffects {             self.removeMotionEffect(effect)         }     } } 
Read More

Wednesday, March 30, 2016

How to drag a row between other rows in UITableView or UICollectionView

Leave a Comment

I am trying to implement the view shown in the animation below:

enter image description here

I found a great control called TGLStackedViewController which does almost what I need. The way it performs the moving is by taking a snapshot of the card you want to move and adding it as a subview to the collectionView, while hiding the original card. When dragging has stopped the movable view is removed and the original card is made visible again.

This works well, but doesn't produce the result I want since the movable view covers all cards. I wish for the card being moved to always be between the two cells above and below, just like in the animation. But I am not sure how to do this since all items are on the same level. I would really like to avoid making a completely custom view control and if possible do this with either UICollectionView or UITableView.

0 Answers

Read More

Sunday, March 20, 2016

Adding a HeaderView to CollectionView when using custom FlowLayout (Swift)

Leave a Comment

I've implemented a custom FlowLayout subclass, which apparently has precluded me from adding a headerView via the storyboard (the check box is gone). Is there any other way to do so using storyboards?

There are some answers about how to add a headerView programmatically but they're in objective-C, how can I add one using Swift?

The below doesn't produce a header view and I can't figure out why?

 CollectionViewController {       override func viewDidLoad() {         super.viewDidLoad()              // Setup Header         self.collectionView!.registerClass(PinHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: headerIdentifier)  }      override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {                 //1                 switch kind {                     //2                 case UICollectionElementKindSectionHeader:                     //3                     let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind,withReuseIdentifier: "PinHeaderView",                         forIndexPath: indexPath)                         as! PinHeaderView                     headerView.pinHeaderLabel.text = boardName                     return headerView                 default:                     //4                     fatalError("Unexpected element kind")                 }         }     }  class PinHeaderView: UICollectionReusableView {       @IBOutlet weak var pinHeaderLabel: UILabel!    } 

My Layout class:

import UIKit protocol PinterestLayoutDelegate {     // 1. Method to ask the delegate for the height of the image     func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath , withWidth:CGFloat) -> CGFloat     // 2. Method to ask the delegate for the height of the annotation text     func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat      func columnsForDevice() -> Int  }  class PinterestLayoutAttributes:UICollectionViewLayoutAttributes {      // 1. Custom attribute     var photoHeight: CGFloat = 0.0     var headerHeight: CGFloat = 0.0      // 2. Override copyWithZone to conform to NSCopying protocol     override func copyWithZone(zone: NSZone) -> AnyObject {         let copy = super.copyWithZone(zone) as! PinterestLayoutAttributes         copy.photoHeight = photoHeight         return copy     }      // 3. Override isEqual     override func isEqual(object: AnyObject?) -> Bool {         if let attributtes = object as? PinterestLayoutAttributes {             if( attributtes.photoHeight == photoHeight  ) {                 return super.isEqual(object)             }         }         return false     } }   class PinterestLayout: UICollectionViewLayout {     //1. Pinterest Layout Delegate     var delegate:PinterestLayoutDelegate!      //2. Configurable properties     //moved numberOfColumns     var cellPadding: CGFloat = 6.0      //3. Array to keep a cache of attributes.     private var cache = [PinterestLayoutAttributes]()      //4. Content height and size     private var contentHeight:CGFloat  = 0.0     private var contentWidth: CGFloat {         let insets = collectionView!.contentInset         return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)     }      override class func layoutAttributesClass() -> AnyClass {          return PinterestLayoutAttributes.self       }      override func prepareLayout() {         // 1. Only calculate once         //if cache.isEmpty {              // 2. Pre-Calculates the X Offset for every column and adds an array to increment the currently max Y Offset for each column             let numberOfColumns = delegate.columnsForDevice()             let columnWidth = contentWidth / CGFloat(numberOfColumns)             var xOffset = [CGFloat]()             for column in 0 ..< numberOfColumns {                 xOffset.append(CGFloat(column) * columnWidth )             }             var column = 0             var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)              // 3. Iterates through the list of items in the first section             for item in 0 ..< collectionView!.numberOfItemsInSection(0) {                  let indexPath = NSIndexPath(forItem: item, inSection: 0)                  // 4. Asks the delegate for the height of the picture and the annotation and calculates the cell frame.                 let width = columnWidth - cellPadding*2                 let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath , withWidth:width)                 let annotationHeight = delegate.collectionView(collectionView!, heightForAnnotationAtIndexPath: indexPath, withWidth: width)                 let height = cellPadding +  photoHeight + annotationHeight + cellPadding                 let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)                 let insetFrame = CGRectInset(frame, cellPadding, cellPadding)                  // 5. Creates an UICollectionViewLayoutItem with the frame and add it to the cache                 let attributes = PinterestLayoutAttributes(forCellWithIndexPath: indexPath)                 attributes.photoHeight = photoHeight                 attributes.frame = insetFrame                 cache.append(attributes)                  // 6. Updates the collection view content height                 contentHeight = max(contentHeight, CGRectGetMaxY(frame))                 yOffset[column] = yOffset[column] + height                  column = column >= (numberOfColumns - 1) ? 0 : ++column             }         //}     }       override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {         let attributes = PinterestLayoutAttributes(forSupplementaryViewOfKind: elementKind, withIndexPath: indexPath)     attributes.headerHeight = 100.0     attributes.frame = (self.collectionView?.frame)!     return attributes        }       override func collectionViewContentSize() -> CGSize {         return CGSize(width: contentWidth, height: contentHeight)     }      override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {          var layoutAttributes = [UICollectionViewLayoutAttributes]()          // Loop through the cache and look for items in the rect         for attributes  in cache {             if CGRectIntersectsRect(attributes.frame, rect ) {                 layoutAttributes.append(attributes)             }         }         return layoutAttributes     } } 

2 Answers

Answers 1

You can add header view in storyboard, drag a "Collection Reusable View" and drop it inside the collectionView, then set its class and identifier in storyboard. Or you can register your custom header class programmatically as shown in your code.

self.collectionView!.registerClass(PinHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: headerIdentifier)

viewForSupplementaryElementOfKind delegate method is incomplete, case UICollectionElementKindSectionFooter isn't handled, do the same for footer view as what you did for header view. If you don't see it, set borderWidth of header view's layer to a value greater than 0, it might show up.

Answers 2

Why are you using PinterestLayoutAttributes(forCellWithIndexPath: indexPath) in method layoutAttributesForSupplementaryViewOfKind?

You should use PinterestLayoutAttributes(forSupplementaryViewOfKind elementKind: String, withIndexPath indexPath: NSIndexPath).

You are generating layout attributes for cell instead of suplementary view. As a result your layout attributes don't contain right category type and view kind. That's why even if you register class for suplementary view - collection view doesn't layout its instances.

Read More