Skip to content

Instantly share code, notes, and snippets.

@shawn-frank
Created March 22, 2022 10:56
Show Gist options
  • Save shawn-frank/03bc06d13f90a54e23e9ea8c6f30a70e to your computer and use it in GitHub Desktop.
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
//
// 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