Created
March 22, 2022 10:56
-
-
Save shawn-frank/03bc06d13f90a54e23e9ea8c6f30a70e to your computer and use it in GitHub Desktop.
This is an example of using a UICollectionView to create a custom segment control. This example shows a strategy to resize a UILabel's font so that it fits within the frame provided. This sample code is for iOS using Swift and was created in response to this stack overflow question: https://stackoverflow.com/q/71566815/1619193
This file contains 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
// | |
// SegmentCollectionViewVC.swift | |
// TestApp | |
// | |
// Created by Shawn Frank on 22/03/2022. | |
// | |
import UIKit | |
fileprivate class SegmentCell: UICollectionViewCell { | |
static let reuseIdentifier = "SegmentCell" | |
let title = UILabel() | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
configureLabel() | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
private func configureLabel() { | |
title.translatesAutoresizingMaskIntoConstraints = false | |
title.textAlignment = .center | |
title.numberOfLines = 1 | |
title.textColor = .black | |
contentView.addSubview(title) | |
// Auto layout config to pin label to the edges of the content view | |
title.leadingAnchor | |
.constraint(equalTo: contentView.leadingAnchor) | |
.isActive = true | |
title.topAnchor | |
.constraint(equalTo: contentView.topAnchor) | |
.isActive = true | |
title.trailingAnchor | |
.constraint(equalTo: contentView.trailingAnchor) | |
.isActive = true | |
title.bottomAnchor | |
.constraint(equalTo: contentView.bottomAnchor) | |
.isActive = true | |
} | |
} | |
class SegmentCollectionViewVC: UIViewController { | |
var collectionView: UICollectionView! | |
// Different segments | |
let segments = ["Active Time", | |
"Reminders", | |
"Vibration Intensify"] | |
// Padding between segments | |
let horizontalPadding: CGFloat = 5 | |
// Height of the collection view and segments | |
let collectionViewHeight: CGFloat = 50 | |
// Font size for each segment | |
var segmentFontSize = CGFloat.zero | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
title = "Segment CV" | |
view.backgroundColor = .white | |
configureCollectionView() | |
} | |
// Just some normal set up, you can skip over it | |
// Added for completeness | |
private func configureCollectionView() { | |
collectionView = UICollectionView(frame: CGRect.zero, | |
collectionViewLayout: createLayout()) | |
collectionView.backgroundColor = .white | |
collectionView.register(SegmentCell.self, | |
forCellWithReuseIdentifier: SegmentCell.reuseIdentifier) | |
collectionView.dataSource = self | |
collectionView.translatesAutoresizingMaskIntoConstraints = false | |
view.addSubview(collectionView) | |
// Auto layout config | |
collectionView.leadingAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) | |
.isActive = true | |
collectionView.topAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) | |
.isActive = true | |
collectionView.trailingAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) | |
.isActive = true | |
collectionView.heightAnchor | |
.constraint(equalToConstant: collectionViewHeight) | |
.isActive = true | |
} | |
// Flow layout configuration, mainly to figure out width of the cells | |
private func createLayout() -> UICollectionViewFlowLayout { | |
let flowLayout = UICollectionViewFlowLayout() | |
flowLayout.minimumLineSpacing = horizontalPadding | |
flowLayout.minimumInteritemSpacing = 0 | |
flowLayout.scrollDirection = .horizontal | |
flowLayout.sectionInset = UIEdgeInsets(top: 0, | |
left: horizontalPadding, | |
bottom: 0, | |
right: horizontalPadding) | |
// Calculate the available width to divide the segments evenly | |
var availableWidth = UIScreen.main.bounds.width | |
// There will always be segments - 1 gaps, for 3 segments, there will be | |
// 2 gaps and for 4 segments there will be 3 gaps etc | |
availableWidth -= horizontalPadding * CGFloat(segments.count - 1) | |
// Remove the insets | |
availableWidth -= flowLayout.sectionInset.left + flowLayout.sectionInset.right | |
let cellWidth = availableWidth / CGFloat(segments.count) | |
// Add this function | |
calculateApproxFontSize(forWidth: cellWidth) | |
flowLayout.itemSize = CGSize(width: cellWidth, | |
height: collectionViewHeight) | |
return flowLayout | |
} | |
private func calculateApproxFontSize(forWidth width: CGFloat) { | |
if let longestSegmentTitle = segments.max(by: { $1.count > $0.count }) { | |
let tempLabel = UILabel() | |
tempLabel.numberOfLines = 1 | |
tempLabel.text = longestSegmentTitle | |
tempLabel.sizeToFit() | |
guard var currentFont = tempLabel.font else { return } | |
var intrinsicSize | |
= (longestSegmentTitle as NSString).size(withAttributes: [.font : currentFont]) | |
// Keep looping and reduce the font size till the text | |
// fits into the label | |
// This could be optimized further using binary search | |
// However this should be ok for small strings | |
while intrinsicSize.width > width | |
{ | |
currentFont = currentFont.withSize(currentFont.pointSize - 1) | |
tempLabel.font = currentFont | |
intrinsicSize | |
= (longestSegmentTitle as NSString).size(withAttributes: [.font : currentFont]) | |
} | |
// Set the font of the current label | |
// segmentFontSize is a global var in the VC | |
segmentFontSize = currentFont.pointSize | |
} | |
} | |
} | |
extension SegmentCollectionViewVC: UICollectionViewDataSource { | |
func collectionView(_ collectionView: UICollectionView, | |
numberOfItemsInSection section: Int) -> Int { | |
return segments.count | |
} | |
func collectionView(_ collectionView: UICollectionView, | |
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |
let cell = collectionView | |
.dequeueReusableCell(withReuseIdentifier: SegmentCell.reuseIdentifier, | |
for: indexPath) as! SegmentCell | |
cell.backgroundColor = .orange | |
cell.title.text = segments[indexPath.item] | |
// Adjust the font | |
cell.title.font = cell.title.font.withSize(segmentFontSize) | |
return cell | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment