skip to Main Content

Aligning CollectionView Cells to Left with equal spacing in between (Cells will be of dynamic width depending on the data received i.e the length of the string that will go in the cell).The Blue 1 is my first query and the yellow 2 is my other query.

My Custom View is TrendingView.swift

//
//  TrendingView.swift

//
//  Created by Abhishek Chandrakant Gidde on 24/05/23.

//

import UIKit

protocol TrendingViewDelegate: AnyObject {
    func didSelectScrip(scrip:ScripModel)
    func seeAllScrips()
    func seeAllBreakouts()
}

enum DataType {
    case EQUITY
    case DERIVATIVES
    
    func isEquityData() -> Bool {
        switch self {
        case .EQUITY:
            return true
        case .DERIVATIVES:
            return false
        }
    }
}

class TrendingView: UIView {
    
    @IBOutlet var contentViewRoot: UIView!
    @IBOutlet weak var lblTrendingTitle: UILabel!
    @IBOutlet weak var clvTrendingData: UICollectionView!
    
    var isTrending = true
    
    var dataType: DataType?
    
    var arrayTrendingData = [MarketScripModel]() {
        didSet{
            self.updateCollectionViewUI()
        }
    }
    
    var arrayBreakoutData = [SignalListModel]() {
        didSet{
            self.updateCollectionViewUI()
        }
    }
    
    // MARK: Properties
    
    weak var delegate:TrendingViewDelegate?
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        customInit()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        customInit()
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        customInit()
    }
    
    func customInit(){
        Bundle.main.loadNibNamed("TrendingView",owner: self, options: nil)
        addSubview(self.contentViewRoot)
        

        self.contentViewRoot.frame = self.bounds
        self.contentViewRoot.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        clvTrendingData.dataSource = self
        clvTrendingData.delegate = self
        clvTrendingData.register(UINib(nibName: "TrendingViewCell", bundle: .main),forCellWithReuseIdentifier: "trendingViewCell")
        clvTrendingData.collectionViewLayout = CustomCollectionViewFlowLayout()
    
        
        
    }
    
    func updateCollectionViewUI() {
        DispatchQueue.main.async {
            if self.isTrending {
                if self.dataType == .EQUITY {
                    self.lblTrendingTitle.text = "Trending Stocks"
                } else if self.dataType == .DERIVATIVES {
                    self.lblTrendingTitle.text = "Trending Strikes"
                }
            } else {
                if self.dataType == .EQUITY {
                    self.lblTrendingTitle.text = "Breakout Stocks"
                } else if self.dataType == .DERIVATIVES {
                    self.lblTrendingTitle.text = "Breakout Futures"
                }
            }
            //
            self.clvTrendingData.reloadData()
        }
    }
    
    // MARK: Actions
    
    @IBAction func onSellAllTap(_ sender: Any) {
        if(isTrending){
            self.delegate?.seeAllScrips()
            
        }
        else{
            self.delegate?.seeAllBreakouts()
        }
    }
}

// CollectionView FlowLayout
extension TrendingView:UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    }

}

extension TrendingView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if self.isTrending {
            return self.arrayTrendingData.count
        } else {
            return self.arrayBreakoutData.count
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let trendingViewCell: TrendingViewCell = clvTrendingData.dequeueReusableCell(withReuseIdentifier: "trendingViewCell", for: indexPath) as! TrendingViewCell
        
        if(isTrending) {
            print("Returning Trending Data Cell")
            trendingViewCell.configureCell(scripModel: arrayTrendingData[indexPath.row],isEquityData: dataType!.isEquityData() )
        } else {
            print("Returning Derivative Data Cell")
            trendingViewCell.configureCell(scripModel: arrayBreakoutData[indexPath.row],isEquityData: dataType!.isEquityData())
        }
        
        trendingViewCell.layoutIfNeeded()
        return trendingViewCell
    }
}

extension TrendingView: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if(isTrending){
            self.delegate?.didSelectScrip(scrip: arrayTrendingData[indexPath.row].scrip)}
        else{
            self.delegate?.didSelectScrip(scrip: arrayBreakoutData[indexPath.row].scrip)
        }
    }
}




My Trending View Cell is TrendingViewCell.swift

//
//  TrendingViewCell.swift
//
//  Created by Abhishek Chandrakant Gidde on 24/05/23.
//

import UIKit

enum CallType: String {
    case BUY = "buy"
    case SELL = "sell"
}


class TrendingViewCell: UICollectionViewCell {
    
    @IBOutlet weak var lblScripTitle: UILabel!
    @IBOutlet weak var ivTrendingIcon: UIImageView!
    
    private var isStockPositive: Bool?{
        didSet{
            updateUI()
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
    

    func configureCell(scripModel: MarketScripModel, isEquityData:Bool){
        
        if(!isEquityData){
            
            let nameOfScrip = scripModel.marketScripModel.Name
            
            let components = nameOfScrip?.components(separatedBy: "|")
            
            // Extracting the underlying asset (e.g., "NIFTY")
            let underlyingAsset = components?[0].trimmingCharacters(in: .whitespaces)
            
            // Extracting the expiration date (e.g., "25MAY23")
            let expirationDate = components?[1].trimmingCharacters(in: .whitespaces)
            
            let strikePrice = scripModel.marketScripModel.LTP
            
            let trendingStrikeText = "(underlyingAsset!) (strikePrice!) (expirationDate!)"
            print("debugabhi:")
            print(underlyingAsset!)
            print(expirationDate!)
            print(strikePrice!)
            print(trendingStrikeText)
            
            self.lblScripTitle.text=trendingStrikeText
//            lblScripTitle.text = "NIFTY 18,400 25MAY2023"
            
        }
        else{
            lblScripTitle.text = scripModel.scrip.scripNameWithExpiry()}
           
        
        if let ltpChange = scripModel.marketScripModel.PerChange {
            isStockPositive = ltpChange >= 0
        }
        
    }
    
    func configureCell(scripModel: SignalListModel, isEquityData:Bool){
        
        if(!isEquityData){
            
            let nameOfScrip = scripModel.scrip.scripNameWithExpiry()
            
            self.lblScripTitle.text = nameOfScrip
            if scripModel.callType.removingWhitespaces().lowercased() == CallType.BUY.rawValue  {
                isStockPositive =  true
            }
            else {
                isStockPositive = false
            }

            
        }
        else{
            lblScripTitle.text = scripModel.scrip.scripNameWithExpiry()
            
            
            if scripModel.scrip.changePer() >= 0{
                isStockPositive =  true
            }
            else{
                isStockPositive = false
            }
        }
           
       
        
    }
    
    func updateUI(){
        self.ivTrendingIcon.image = isStockPositive! ? UIImage(named: "icn_stockPositive") : UIImage(named: "icn_stockNegative")

    }
    

}

This is the required design
THIS IS THE UI DESIGN REQUIRED

I have tried my best to do everything I can but it is too frustrating to get it the way it is needed, I have also tried using a custom class as suggested by ChatGPT like class WrapFlowLayout: UICollectionViewFlowLayout {... and setting collectionView.collectionViewLayout = WrapflowLayout() but that leaves me with the cells aligning to the left and wrapping just as i need but then the cell width is not as per my constrains or my data it looks like this. enter image description here

2

Answers


  1. Use this layout class for tags layout

    import UIKit
    
    class TagFlowLayout: UICollectionViewFlowLayout {
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            guard let attributes = super.layoutAttributesForElements(in: rect) else {
                return nil
            }
    
            var rows = [Row]()
            var currentRowY: CGFloat = -1
    
            for attribute in attributes {
                if currentRowY != attribute.frame.origin.y {
                    currentRowY = attribute.frame.origin.y
                    rows.append(Row(spacing: 10))
                }
                rows.last?.add(attribute: attribute)
            }
    
            rows.forEach {
                $0.tagLayout(collectionViewWidth: collectionView?.frame.width ?? 0)
            }
            return rows.flatMap { $0.attributes }
        }
    }
    
    
    class Row {
        var attributes = [UICollectionViewLayoutAttributes]()
        var spacing: CGFloat = 0
    
        init(spacing: CGFloat) {
            self.spacing = spacing
        }
    
        func add(attribute: UICollectionViewLayoutAttributes) {
            attributes.append(attribute)
        }
    
        func tagLayout(collectionViewWidth: CGFloat) {
            let padding = 10
            var offset = padding
            for attribute in attributes {
                attribute.frame.origin.x = CGFloat(offset)
                offset += Int(attribute.frame.width + spacing)
            }
        }
    }
    

    after that give estimated size to cell

    class ViewController {
    
     override func viewDidLoad() {
            super.viewDidLoad()
       
            let layout = TagFlowLayout()
            layout.estimatedItemSize = CGSize(width: 140, height: 40)
            collectionView.collectionViewLayout = layout
        }
    
    
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell",
                                                                for: indexPath) as? TagCollectionViewCell else {
                return MyCell()
            }
            cell.tagLabel.text = titles[indexPath.section][indexPath.row]
            cell.tagLabel.preferredMaxLayoutWidth = collectionView.frame.width // if there is nothing next to label
    
           cell.tagLabel.preferredMaxLayoutWidth = collectionView.frame.width - 16 // if there is button or icon next to label with size of 16 or whatever
            
            return cell
        }
    
    }
    
    Login or Signup to reply.
  2. You may use Composure – a 3rd party open source library to achieve this fairly easily. No need to create your own custom class or flow layout. It looks like you have 2 sections in your view but both of these sections need to be laid out the same way. Your cells appear to have a fixed height but dynamic width. In other words, your cells in both sections need to grow width-wise depending on the contents.

    Step 1: Once you add Composure to your project define an enum like so:

    import UIKit
    import Composure
    
    enum MyCollectionViewLayout: Int, CaseIterable, DefinesCompositionalLayout {
        case trendingStrikesSection
        case breakoutFuturesSection
        
        func layoutInfo(using layoutEnvironment: NSCollectionLayoutEnvironment) -> CompositionalLayoutOption {
            switch self {
            //try to choose a realistic estimate for your width. Don't worry, your cells will grow or shrink even when your estimate is off. 
            case .trendingStrikesSection:
                return .dynamicWidthFixedHeight(estimatedWidth: 120, fixedHeight: 75)
            case .breakoutFuturesSection:
                return .dynamicWidthFixedHeight(estimatedWidth: 150, fixedHeight: 150)
            }
        }
    
        // Optional: Only needed if you have header or footer views for each section
        func headerInfo(using layoutEnvironment: NSCollectionLayoutEnvironment) -> CompositionalLayoutOption? {
            switch self {
            case .trendingStrikesSection:
                //recommended layout for headers and footers is fullWidthFixedHeight
                return .fullWidthFixedHeight(fixedHeight: 45)
            case .breakoutFuturesSection:
                return .fullWidthFixedHeight(fixedHeight: 45)
            }
        }
    
        // Optional: determines the space between two cells
        var interItemSpacing: CGFloat {
            switch self {
            case .trendingStrikesSection:
                return 10
            case .fullWidthFixedHeight:
                return 15
            }
        }
    
        // Optional: determines the space between two rows of cells
        var interGroupSpacing: CGFloat {
            return 20
        }
    }
    

    Step 2: In your View Controller while configuring your collection view, use this line for your collectionViewLayout instead of whatever you currently have.

    ....
    import Composure //don't forget to add Composure to your view controller
    ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        //this replaces your existing layout code
        collectionView.collectionViewLayout = generateComposionalLayout(with: MyCollectionViewLayout.allCases)
        ...
    }
    

    Result

    If your cell is properly constrained, you should be able to achieve the layout you are looking for. Let us know in the comments if you have additional questions.

    an example collection view with self-sizing cells

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search