Creating a Custom Segmented

Jerry PM
4 min readSep 16, 2024

--

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.

The article is divided into two parts:

  1. Creating a Custom Chip View.
  2. 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 (isSelected property toggles between true and false).
  • Gesture Handling: The chip can be selected via a tap gesture, with an optional callback (didSelect closure).
  • 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 setupChipStackView method 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 UIStackView to 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.

--

--