One of my magic hours app feature will be the ability to search for a location and save it for the future use. I need a control that will display my stored locations and I decided that I’ll place it at the bottom of the app’s main screen with the ability to scroll them horizontally. This can be achieved with UICollectionView control. There’s a great introduction on the topic by Matteo Manferdini: The correct way to display lists in iOS and what many developers do wrong. I tried to follow his advice and implement my solution the right way :)
So I started by drag and dropping CollectionView control to my view and placing it at the bottom of my layout.
There’s the UICollectionView control itself and a cell placed directly under it. A cell is part of user interface that displays information about a single item from a collection. My cell contains only label for a location name.
The next step is providing a custom class, I called it LocationCell, deriving from UICollectionViewCell that will be dealing with my cell representing a location. This is my final implementation:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class LocationCell : UICollectionViewCell | |
| { | |
| @IBOutlet weak var locationNameLabel: UILabel!; | |
| static let locationNameFont:UIFont = UIFont.systemFont(ofSize: 17, weight: UIFontWeightThin); | |
| var locationName: String | |
| { | |
| didSet | |
| { | |
| locationNameLabel.text = locationName; | |
| } | |
| } | |
| override var isSelected: Bool | |
| { | |
| didSet | |
| { | |
| locationNameLabel.textColor = isSelected ? UIColor.yellow : UIColor.white; | |
| } | |
| } | |
| required init?(coder aDecoder: NSCoder) | |
| { | |
| self.locationName = ""; | |
| super.init(coder: aDecoder); | |
| } | |
| } |
Custom class has to be linked to cell in identity tab:
I had to also provide a string identifier for the cell, this can be any string, I chose my class name.
The next step was ctrl dragging the label to my new custom class to create an outlet locationNameLabel. I use this outlet for setting label’s contents to a location name and to change text color to yellow after the cell becomes selected. Collection and table views in iOS are designed to work with data source and delegate objects. Data source is an object that provides collection for the view and delegate is for dealing with cell sizes, selection changed actions and so on. I made one class conform to both protocols: UICollectionViewDataSource and UICollectionViewDelegateFlowLayout. In more complex scenarios this should be separated into two classes.
Object dealing as data source has to implement these two methods:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //Asks your data source object for the number of items in the specified section. | |
| //func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int | |
| func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int | |
| { | |
| return locations.count; | |
| } | |
| //Asks your data source object for the cell that corresponds to the specified item in the collection view. | |
| //func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell | |
| func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell | |
| { | |
| let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LocationCell", for: indexPath) as! LocationCell; | |
| let location = locations[indexPath.row]; | |
| cell.locationName = location.name; | |
| return cell; | |
| } |
I initailize my data source object with list of my locations. The first method returns the length of array holding locations. In the second I’m dealing with my custom cell class. Setting locationName on cell object sets proper text in cell’s label through the outlet I’ve created.
Linking collection view control with data source object is done in main view controller in viewDidLoad method:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| locationsList.dataSource = mainModel.dataSource!; | |
| locationsList.reloadData(); |
where locationsList is outlet to UICollectionView control placed in my view.
After launching the app I saw this:
Labels in my collection are of fixed size and don’t expand to its contents. This doesn’t look good. I started googling for a solution, I thought this would be like one switch somewhere. Nope. This is where the delegate object enters the scene.
With this method I can specify how wide my cell should be. This also means that I have to somehow measure the length of the string with a given font..
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //Asks the delegate for the size of the specified item’s cell. | |
| //If you do not implement this method, the flow layout uses the values in its itemSize property to set the size of items instead. Your implementation of this method can return a fixed set of sizes or dynamically adjust the sizes based on the cell’s content. | |
| func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize | |
| { | |
| let width = locations[indexPath.row].name.width(withConstrainedHeight: 50, font: LocationCell.locationNameFont); | |
| return CGSize(width: width + 10, height: 50); | |
| } |
Measuring is done in extension method:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extension String | |
| { | |
| func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat | |
| { | |
| let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height); | |
| let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil) | |
| return boundingBox.width; | |
| } | |
| } |



