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:
- 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 (
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.