Creating a Custom Segmented
Using Snapkit UI with Auto Scroll
In this article, we’ll explore how to create a Custom Chip View and a Custom Segmented Control in iOS using UIKit. A Chip View is a commonly used UI component that displays selectable tags or filter options, while a Segmented Control allows users to switch between multiple options presented horizontally.
Get Jerry PM’s stories in your inbox
Join Medium for free to get updates from this writer.
The article is divided into two parts:
- Creating a Custom Chip View.
- Implementing a Custom Segmented Control that uses the Chip View.
Part 1: Building a Custom Chip View
Overview
The Custom Chip View is a UIView subclass that represents a single selectable chip or filter. It consists of a label inside a rounded container that updates its appearance when selected or deselected.
Code Explanation
Here is the implementation of the CustomChipView:
import UIKit
import SnapKit
final class CustomChipView: UIView {
var didSelect: ((String) -> Void)?
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.darkGray
label.font = UIFont.systemFont(ofSize: 12)
return label
}()
private lazy var backgroundContainerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.lightGray
view.layer.cornerRadius = 4
view.clipsToBounds = true
return view
}()
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: .zero)
setupView()
}
var isSelected: Bool = false {
didSet {
updateSelection()
}
}
func setupView() {
addSubview(backgroundContainerView)
backgroundContainerView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
backgroundContainerView.addSubview(titleLabel)
titleLabel.snp.makeConstraints {
$0.leading.equalToSuperview().offset(8)
$0.trailing.equalToSuperview().inset(8)
$0.top.equalToSuperview().offset(4)
$0.bottom.equalToSuperview().inset(4)
}
let tap = UITapGestureRecognizer(target: self, action: #selector(handleGesture))
titleLabel.addGestureRecognizer(tap)
titleLabel.isUserInteractionEnabled = true
}
func setup(title: String) {
titleLabel.text = title
}
private func updateSelection() {
if isSelected {
titleLabel.textColor = UIColor.black
titleLabel.font = UIFont.systemFont(ofSize: 14)
backgroundContainerView.backgroundColor = UIColor.lightGray
} else {
titleLabel.textColor = UIColor.darkGray
titleLabel.font = UIFont.systemFont(ofSize: 12)
backgroundContainerView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
}
}
@objc func handleGesture() {
didSelect?(titleLabel.text ?? "")
}
}Key Features:
- Dynamic Selection: The chip’s appearance changes when selected (
isSelectedproperty toggles between true and false). - Gesture Handling: The chip can be selected via a tap gesture, with an optional callback (
didSelectclosure). - Auto Layout: We use SnapKit for layout management to ensure the chip’s label and background are properly constrained within the view.
Part 2: Building a Custom Segmented Control
Overview
Next, we’ll use our CustomChipView to create a horizontal Custom Segmented Control. This control will allow users to scroll through and select one of the available chip options.
Code Explanation
Here is the implementation of the CustomSegmentedControl:
import UIKit
import SnapKit
protocol CustomSegmentedControlDelegate: AnyObject {
func didTapIndex(index: Int, str: String)
}
final class CustomSegmentedControl: UIView {
private var chipsTitle: [String] = []
private var selectedItemIndex: Int?
private var chipViews: [CustomChipView] = []
weak var delegate: CustomSegmentedControlDelegate?
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.showsHorizontalScrollIndicator = false
view.showsHorizontalScrollIndicator = false
return view
}()
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
private lazy var filterStackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .fill
view.distribution = .fill
view.spacing = 12
return view
}()
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: .zero)
configUI()
}
func configUI() {
addSubview(scrollView)
scrollView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.leading.equalToSuperview()
$0.trailing.equalToSuperview()
$0.height.equalTo(40)
}
scrollView.addSubview(containerView)
containerView.snp.makeConstraints {
$0.edges.equalToSuperview()
$0.height.equalToSuperview()
}
containerView.addSubview(filterStackView)
filterStackView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.bottom.equalToSuperview()
$0.leading.equalToSuperview().offset(20)
$0.trailing.equalToSuperview().inset(20)
}
}
public func setup(chipsTitle: [String]) {
self.chipsTitle = chipsTitle
setupChipStackView()
}
private func setupChipStackView() {
chipViews.removeAll()
filterStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for (index, title) in chipsTitle.enumerated() {
let chip = CustomChipView()
chip.setup(title: title)
chip.didSelect = { [weak self] title in
self?.selectChip(at: index)
self?.delegate?.didTapIndex(index: index, str: title)
}
filterStackView.addArrangedSubview(chip)
chipViews.append(chip)
chip.snp.makeConstraints { make in
make.width.equalTo(titleToWidth(text: title))
}
layoutIfNeeded()
}
if !chipViews.isEmpty {
selectChip(at: 0)
}
}
private func selectChip(at index: Int) {
selectedItemIndex = index
for (currentIndex, chip) in chipViews.enumerated() {
chip.isSelected = (currentIndex == index)
}
scrollToSelectedChip()
}
private func scrollToSelectedChip() {
guard let selectedItemIndex = selectedItemIndex else { return }
let selectedChip = chipViews[selectedItemIndex]
let chipFrame = selectedChip.frame
let scrollViewFrame = scrollView.frame
let targetContentOffsetX = chipFrame.midX - scrollViewFrame.width / 2
let contentOffsetX = max(0, min(scrollView.contentSize.width - scrollViewFrame.width, targetContentOffsetX))
scrollView.setContentOffset(CGPoint(x: contentOffsetX, y: 0), animated: true)
}
func titleToWidth(text: String) -> CGFloat {
let size = (text as NSString).boundingRect(
with: CGSize(width: CGFloat.infinity, height: 22),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)],
context: nil
)
return size.width + 24
}
}Key Features:
- Dynamic Chip Creation: The
setupChipStackViewmethod dynamically generates chip views based on the provided titles. - Scroll Handling: When a chip is selected, the view scrolls to ensure the selected chip is visible.
- Stack View Layout: We use
UIStackViewto arrange chips horizontally, ensuring a flexible and organized layout.
Now call in your ViewController, example use like this
import SnapKit
import UIKit
class MainSegmentedController: UIViewController {
private var stringArray = ["All", "Financials", "News", "Maps", "Shopping", "Images", "Web", "Other"]
private lazy var segmentedControl: CustomSegmentedControl = {
let view = CustomSegmentedControl()
view.delegate = self
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(segmentedControl)
segmentedControl.snp.makeConstraints { make in
make.top.equalToSuperview().offset(120)
make.left.right.equalToSuperview().inset(0)
make.height.equalTo(44)
}
// Dummy data here
title = stringArray.first
segmentedControl.setup(chipsTitle: stringArray)
}
}
extension MainSegmentedController: CustomSegmentedControlDelegate {
func didTapIndex(index: Int, str: String) {
title = str
}
}Conclusion
This tutorial demonstrates how to create a Custom Chip View and integrate it into a Custom Segmented Control using UIKit. These components are highly reusable and customizable, providing an elegant way to display and manage selections in iOS apps.








