

(1) MainViewController.swift
import UIKit
import SnapKit
import RxSwift
import RxCocoa
class MainViewController: UIViewController {
private lazy var underlineSegmentedControl: UISegmentedControl = {
let segmentedControl = UnderlineSegmentedControl(items: ["First", "Second", "Third"])
segmentedControl.selectedSegmentIndex = 0
segmentedControl.setTitleTextAttributes(
[
NSAttributedString.Key.foregroundColor: UIColor.gray,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .semibold)
],
for: .normal
)
segmentedControl.setTitleTextAttributes(
[
NSAttributedString.Key.foregroundColor: UIColor.systemBlue,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .bold)
],
for: .selected
)
return segmentedControl
}()
private lazy var customCornerRadiusSegmentedControl: UISegmentedControl = {
let segmentedControl = CustomCornerRadiusSegmentedControl(items: ["First", "Second", "Third"])
segmentedControl.customCornerRadius = 24
segmentedControl.selectedBackgroundColor = .white
segmentedControl.normalBackgroundColor = .systemGray5
segmentedControl.borderColor = .systemGray5
segmentedControl.borderWidth = 1.5
segmentedControl.selectedSegmentIndex = 0
segmentedControl.setTitleTextAttributes(
[
NSAttributedString.Key.foregroundColor: UIColor.gray,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .semibold)
],
for: .normal
)
segmentedControl.setTitleTextAttributes(
[
NSAttributedString.Key.foregroundColor: UIColor.systemBlue,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .bold)
],
for: .selected
)
return segmentedControl
}()
private let firstViewController: FirstViewController = FirstViewController()
private let secondViewController: SecondViewController = SecondViewController()
private let thirdViewController: ThirdViewController = ThirdViewController()
private var dataViewControllers: [UIViewController] {
return [firstViewController, secondViewController, thirdViewController]
}
private lazy var pageViewController: UIPageViewController = {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: nil
)
pageViewController.setViewControllers(
[dataViewControllers[0]],
direction: .forward,
animated: false,
completion: nil
)
pageViewController.dataSource = self
pageViewController.delegate = self
return pageViewController
}()
private var currentPage: Int = 0 {
didSet {
// from segmentedControl -> pageViewController update
print("### currentPage - oldValue: \(oldValue), currentPage: \(currentPage)")
let direction: UIPageViewController.NavigationDirection = oldValue <= currentPage ? .forward : .reverse
pageViewController.setViewControllers(
[dataViewControllers[currentPage]],
direction: direction,
animated: true,
completion: nil
)
}
}
private let disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: UnderlineSegmentedControl
setUIWithUnderlineSegmentedControl()
bindWithUnderlineSegmentedControl()
// MARK: CustomCornerRadiusSegmentedControl
// setUIWithCustomCornerRadiusSegmentedControl()
// bindWithCustomCornerRadiusSegmentedControl()
}
private func setUIWithUnderlineSegmentedControl() {
view.addSubview(underlineSegmentedControl)
view.addSubview(pageViewController.view)
underlineSegmentedControl.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.horizontalEdges.equalToSuperview().inset(4)
make.height.equalTo(50)
}
pageViewController.view.snp.makeConstraints { make in
make.top.equalTo(underlineSegmentedControl.snp.bottom).offset(5)
make.horizontalEdges.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
}
}
private func bindWithUnderlineSegmentedControl() {
underlineSegmentedControl.rx.selectedSegmentIndex
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.subscribe(with: self, onNext: { owner, selectedIndex in
print("### subscribe - selectedIndex: \(selectedIndex)")
owner.currentPage = selectedIndex
})
.disposed(by: disposeBag)
}
private func setUIWithCustomCornerRadiusSegmentedControl() {
view.addSubview(customCornerRadiusSegmentedControl)
view.addSubview(pageViewController.view)
customCornerRadiusSegmentedControl.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.horizontalEdges.equalToSuperview().inset(4)
make.height.equalTo(50)
}
pageViewController.view.snp.makeConstraints { make in
make.top.equalTo(customCornerRadiusSegmentedControl.snp.bottom).offset(5)
make.horizontalEdges.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
}
}
private func bindWithCustomCornerRadiusSegmentedControl() {
customCornerRadiusSegmentedControl.rx.selectedSegmentIndex
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.subscribe(with: self, onNext: { owner, selectedIndex in
print("### subscribe - selectedIndex: \(selectedIndex)")
owner.currentPage = selectedIndex
})
.disposed(by: disposeBag)
}
}
extension MainViewController: UIPageViewControllerDataSource {
/*
MARK: 한국어
기능: 현재 보여지고 있는 뷰 컨트롤러 기준으로 이전(왼쪽) 뷰 컨트롤러를 반환
호출 시점: 사용자가 이전(왼쪽)으로 스와이프할 때 호출
반환값: 현재 뷰 컨트롤러가 첫 번째라면(nil 반환), 더 이상 이전 페이지가 없으므로 스와이프가 멈춤
MARK: English
Function: Returns the previous (left) view controller based on the currently displayed view controller
When called: Called when the user swipes to the previous (left) page
Return value: Returns nil if the current view controller is the first one, so swiping stops as there are no more previous pages
*/
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let index = dataViewControllers.firstIndex(of: viewController),
index - 1 >= 0
else { return nil }
return dataViewControllers[index - 1]
}
/*
MARK: 한국어
기능: 현재 보여지고 있는 뷰 컨트롤러 기준으로 다음(오른쪽) 뷰 컨트롤러를 반환
호출 시점: 사용자가 다음(오른쪽)으로 스와이프할 때 호출
반환값: 현재 뷰 컨트롤러가 마지막이라면(nil 반환), 더 이상 다음 페이지가 없으므로 스와이프가 멈춤
MARK: English
Function: Returns the next (right) view controller based on the currently displayed view controller
When called: Called when the user swipes to the next (right) page
Return value: Returns nil if the current view controller is the last one, so swiping stops as there are no more next pages
*/
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let index = dataViewControllers.firstIndex(of: viewController),
index + 1 < dataViewControllers.count
else { return nil }
return dataViewControllers[index + 1]
}
}
extension MainViewController: UIPageViewControllerDelegate {
/*
MARK: 한국어
기능: 사용자가 페이지를 스와이프(드래그)해서 이동하는 애니메이션이 끝났을 때 호출됨
MARK: English
Function: Called when the animation for swiping (dragging) between pages finishes
*/
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
guard let viewController = pageViewController.viewControllers?.first,
let index = dataViewControllers.firstIndex(of: viewController)
else { return }
currentPage = index
underlineSegmentedControl.selectedSegmentIndex = index
customCornerRadiusSegmentedControl.selectedSegmentIndex = index
}
}
(2) UnderlineSegmentedControl.swift
import UIKit
final class UnderlineSegmentedControl: UISegmentedControl {
private lazy var underlineView: UIView = {
let width: CGFloat = self.bounds.width / CGFloat(self.numberOfSegments)
let height: CGFloat = 5
let xPosition: CGFloat = CGFloat(self.selectedSegmentIndex * Int(width))
let yPosition: CGFloat = self.bounds.height - height
let frame: CGRect = CGRect(
x: xPosition,
y: yPosition,
width: width,
height: height
)
let view = UIView(frame: frame)
view.backgroundColor = .systemBlue
self.addSubview(view)
self.clipsToBounds = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
removeBackgroundAndDivider()
}
override init(items: [Any]?) {
super.init(items: items)
removeBackgroundAndDivider()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func removeBackgroundAndDivider() {
let image = UIImage()
self.setBackgroundImage(image, for: .normal, barMetrics: .default)
self.setBackgroundImage(image, for: .selected, barMetrics: .default)
self.setBackgroundImage(image, for: .highlighted, barMetrics: .default)
self.setDividerImage(image, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default)
}
override func layoutSubviews() {
super.layoutSubviews()
let underlineFinalXPosition: CGFloat = (self.bounds.width / CGFloat(self.numberOfSegments)) * CGFloat(self.selectedSegmentIndex)
UIView.animate(
withDuration: 0.1,
animations: {
self.underlineView.frame.origin.x = underlineFinalXPosition
}
)
}
}
(3) CustomCornerRadiusSegmentedControl.swift
import UIKit
// UIColor를 UIImage로 변환하는 확장 (필수)
extension UIImage {
convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
class CustomCornerRadiusSegmentedControl: UISegmentedControl {
// 원하는 inset 및 cornerRadius 값 지정
var segmentInset: CGFloat = 4
var customCornerRadius: CGFloat = 16 // 원하는 값으로 변경
var selectedBackgroundColor: UIColor = .white
var normalBackgroundColor: UIColor = .systemGray5
var borderColor: UIColor = .systemGray3
var borderWidth: CGFloat = 1
override func layoutSubviews() {
super.layoutSubviews()
// 전체 배경 및 border
self.backgroundColor = normalBackgroundColor
self.layer.cornerRadius = customCornerRadius
self.layer.borderWidth = borderWidth
self.layer.borderColor = borderColor.cgColor
self.layer.masksToBounds = true
// 선택된 segment 배경 커스텀
let foregroundIndex = self.numberOfSegments
if self.subviews.indices.contains(foregroundIndex),
let foregroundImageView = self.subviews[foregroundIndex] as? UIImageView {
foregroundImageView.bounds = foregroundImageView.bounds.insetBy(dx: segmentInset, dy: segmentInset)
foregroundImageView.image = UIImage(color: selectedBackgroundColor)
foregroundImageView.layer.removeAnimation(forKey: "SelectionBounds")
foregroundImageView.layer.masksToBounds = true
foregroundImageView.layer.cornerRadius = customCornerRadius - segmentInset
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
}
}
(4) FirstViewController.swift
import UIKit
final class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemRed
print("### FirstViewController - viewDidLoad")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("### FirstViewController - viewDidAppear")
}
}
(5) SecondViewController.swift
import UIKit
final class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
print("### SecondViewController viewDidLoad")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("### SecondViewController - viewDidAppear")
}
}
(6) ThirdViewController.swift
import UIKit
final class ThirdViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
print("### ThirdViewController viewDidLoad")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("### ThirdViewController - viewDidAppear")
}
}
You can download full source code here
: https://github.com/TDCIAN/CustomSegmentedControl
Reference:
- UnderlineSegmentedControl: https://ios-development.tistory.com/963
- CustomCornerRadius SegmentedControl: https://medium.com/poatek/customizing-segmented-control-in-swiftui-and-uikit-bee26e52a561
'iOS > UIKit' 카테고리의 다른 글
[iOS / UIKit] Infinite Paging Carousel + Pinch Zoom (0) | 2025.02.23 |
---|---|
Setting up UIKit project without Storyboard (feat. Xcode 16) (0) | 2024.07.27 |
[AutoLayout] Hugging과 Resistance (feat. intrinsic content size) (0) | 2023.04.24 |
[iOS / UIKit] @IBDesignable과 @IBInspectable (0) | 2023.04.24 |
[iOS / UIKit] Notification과 Delegate으로 Data를 주고받는 것에 관하여 (0) | 2023.04.10 |