Reputation: 169
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).
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")
}
}
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.
Upvotes: 1
Views: 338
Reputation: 975
You may use Composure - an open source library that I wrote 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 and 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)
}
}
// choose a height that is appropriate for your header view
func headerInfo(using layoutEnvironment: NSCollectionLayoutEnvironment) -> CompositionalLayoutOption? {
switch self {
case .trendingStrikesSection:
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
}
}
// 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 me know in the comments if you have additional questions.
Upvotes: 0
Reputation: 1193
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
}
}
Upvotes: 1