Search code examples
iosswiftbuttonlayout

How can I dynamically set multiple buttons having different length of string according to screen's width?


example
(source: uimovement.com)

I want to implement layout like the above(auto line break when screen's width is not enough to accommodate buttons' widths). But I can't come up with any idea about how to make that image like layout. I just can implement statically, not dynamically.

In Android, there is a layout that can implement the above. But I don't know what can help me implement the above image in swift. Please help me.


Following @Matthew Mitchell 's suggestion. I implemented it like below.

My ViewController.swift

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var collectionView: UICollectionView!
    var hobbyArray = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
//        self.collectionView!.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        
        hobbyArray.append("test1")
        hobbyArray.append("test2")
        hobbyArray.append("test3")
        hobbyArray.append("test4")
        hobbyArray.append("test5")
        hobbyArray.append("test5")
        hobbyArray.append("test5123123")
        
        collectionView.reloadData()
    }
}

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return hobbyArray.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
        
        cell.title.text = self.hobbyArray[indexPath.row]
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let text = self.hobbyArray[indexPath.row]
        let cellWidth = text.size(withAttributes:[.font: UIFont.systemFont(ofSize:17)]).width + 25
        return CGSize(width: cellWidth, height: 35.0)
    }
}

Other codes are implemented exactly equal to @Matthew Mitchell's codes. However, still I can't get what I wanted to implement.

enter image description here

I failed to make what I had wanted.


Solution

  • To do this efficiently you need to have a UICollectionView with a custom FlowLayout. I am going to do a storyboard example. This is quite complicated so I will try my best. All the code will be below the steps.

    Step 1: Create a swift file named CollectionViewFlowLayout and use UICollectionViewLayout code in the newly created class.

    Step 2: Add a UICollectionView to your ViewController

    Step 3: Link new UICollectionView layout with the CollectionViewFlowLayout class

    Picture 1

    Step 4: Create a UICollectionViewCell inside the UICollectionView, add a label to that cell and constrain it to left and right in the cell and center it vertically. In the attributes inspector of the cell give it a reusable identifier ("cell" for this example)

    UICollectionViewCell

    Step 6: Create a swift file named collectionViewCell and use UICollectionViewCell class that links to your collectionViewCell (same way you linked your flowlayout in step 3).

    Step 7: Add ViewController code to your ViewController Class. This code allows you to add cells to your collection view. The sizeForItemAt function will allow you to resize the cells according to the width of the string that you put inside each cell.

    Code:

    ViewController:

        import UIKit
    
    
            class viewController: UIViewController {
    
                //Outlets
                @IBOutlet weak var collectionView: UICollectionView!
    
                override func viewDidLoad() {
                    collectionView.delegate = self
                    collectionView.dataSource = self
                }
            }
    
            extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{
                func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                    return YOUR_ITEM_COUNT
                }
    
                func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
    
                    self.title.text = YOUR_ITEMS_LIST[indexPath.row]
                    return cell
                }
    
                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
                let text = YOUR_ITEMS_LIST[indexPath.row]
                let cellWidth = text!.size(withAttributes:[.font: UIFont.systemFont(ofSize:17)]).width + 25
                return CGSize(width: cellWidth, height: 35.0)
            }
        }
    

    UICollectionViewCell:

    class CollectionViewCell: UICollectionViewCell {
    
       //Outlets
        @IBOutlet weak var title: UILabel!
    }
    

    UICollectionViewFlowLayout:

    import UIKit
    
    class CollectionViewFlowLayout: UICollectionViewFlowLayout {
        var tempCellAttributesArray = [UICollectionViewLayoutAttributes]()
        let leftEdgeInset: CGFloat = 0
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let cellAttributesArray = super.layoutAttributesForElements(in: rect)
            //Oth position cellAttr is InConvience Emoji Cell, from 1st onwards info cells are there, thats why we start count from 2nd position.
            if(cellAttributesArray != nil && cellAttributesArray!.count > 1) {
                for i in 1..<(cellAttributesArray!.count) {
                    let prevLayoutAttributes: UICollectionViewLayoutAttributes = cellAttributesArray![i - 1]
                    let currentLayoutAttributes: UICollectionViewLayoutAttributes = cellAttributesArray![i]
                    let maximumSpacing: CGFloat = 8
                    let prevCellMaxX: CGFloat = prevLayoutAttributes.frame.maxX
                    //UIEdgeInset 30 from left
                    let collectionViewSectionWidth = self.collectionViewContentSize.width - leftEdgeInset
                    let currentCellExpectedMaxX = prevCellMaxX + maximumSpacing + (currentLayoutAttributes.frame.size.width )
                    if currentCellExpectedMaxX < collectionViewSectionWidth {
                        var frame: CGRect? = currentLayoutAttributes.frame
                        frame?.origin.x = prevCellMaxX + maximumSpacing
                        frame?.origin.y = prevLayoutAttributes.frame.origin.y
                        currentLayoutAttributes.frame = frame ?? CGRect.zero
                    } else {
                        // self.shiftCellsToCenter()
                        currentLayoutAttributes.frame.origin.x = leftEdgeInset
                        //To Avoid InConvience Emoji Cell
                        if (prevLayoutAttributes.frame.origin.x != 0) {
                            currentLayoutAttributes.frame.origin.y = prevLayoutAttributes.frame.origin.y + prevLayoutAttributes.frame.size.height + 08
                        }
                    }
                }
            }
    
            return cellAttributesArray
        }
    
        func shiftCellsToCenter() {
            if (tempCellAttributesArray.count == 0) {return}
            let lastCellLayoutAttributes = self.tempCellAttributesArray[self.tempCellAttributesArray.count-1]
            let lastCellMaxX: CGFloat = lastCellLayoutAttributes.frame.maxX
            let collectionViewSectionWidth = self.collectionViewContentSize.width - leftEdgeInset
            let xAxisDifference = collectionViewSectionWidth - lastCellMaxX
            if xAxisDifference > 0 {
                for each in self.tempCellAttributesArray{
                    each.frame.origin.x += xAxisDifference/2
                }
            }
        }
    }