Xcode — 客製化 UISegmentedControl
目錄
⦿ 前情提要
⦿ ValueChanged
⦿ items: [Any]?
⦿ 客製化 Segmented Control
⦿ Delegation
前情提要
在之前的文章裡,透過 SegmentedControl 去切換圖片的設置,如下:
@IBAction func changeTag(_ sender: Any) {
switch imageTag.selectedSegmentIndex {
case 0:
myImageView.image = UIImage(named: "014")
classicalSlogan.text = "教練我想打球⋯⋯"
case 1:
myImageView.image = UIImage(named: "008")
classicalSlogan.text = "真相只有一個!"
case 2:
myImageView.image = UIImage(named: "011")
classicalSlogan.text = "來呀~我幫你找啊~"
default:
myImageView.image = UIImage(named: "014")
classicalSlogan.text = "教練我想打球⋯⋯"
}
}
實際上這種方式並不是那麼常用,selectedSegmentIndex 常被用來做兩個 View 的切換,比方說使用兩個 ContainerView 分裝不同的 VC,切換時分別顯現不同的 VC 如下:
設置方法初始化 selectedSegmentIndex 為其中一個 ContainerView 顯現(例如 0),那麼,Index 為 1 的 ContainerView 初始化隱藏。
@IBOutlet weak var mySegmentedControl: UISegmentedControl!
@IBOutlet weak var firstContainer: UIView!
@IBOutlet weak var secondContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
secondContainer.isHidden = true
}
@IBAction func segmentedControlTapped(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
firstContainer.isHidden = false
secondContainer.isHidden = true
case 1:
firstContainer.isHidden = true
secondContainer.isHidden = false
case 2: return
default: return
}
}
我們可透過設置 selectedSegmentIndex 來初始化 SegmentedControl 的預設位置,否則預設為 0。
繼續閱讀|回目錄
ValueChanged
UISegmentedControl 這類動作元件會繼承 UIControl,而 UIControl 的 Event
裡的 ValueChanged
,即是值改變的事件,當在對 UISegmentedControl 拉 IBAction 時會直接採用這個事件,這個改變的值就是 selectedSegmentIndex
。
items: [Any]?
接著,如果要對 UISegmentedControl 的每一個 Index 設置 Text / Image
時,只會存在一個,即是說,我設置文字,就不會有圖像;反之亦然。
而在 UISegmentedControl 的 init 方法中,public init(items: [Any]?)
,雖然 items 是 [Any]?
,設置時並不能放入 UILabel、UIButton 等,其實只能放入 string 或 image,還有 iOS14 後的 UIAction。
放入 string、image
:
let image = UIImage(systemName: "square.and.arrow.up")!
let seg = UISegmentedControl(items: ["窩是標籤", image])
seg.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(seg)
NSLayoutConstraint.activate([
seg.centerXAnchor.constraint(equalTo: view.centerXAnchor),
seg.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
放入 UIAction
:
// First Segment UIAction
let action1 = UIAction(title: "First",
image: UIImage(systemName: "1.circle")) { action in
print("First segment selected")
}
// Second Segment UIAction
let action2 = UIAction(title: "Second",
image: UIImage(systemName: "2.circle")) { action in
print("Second segment selected")
}
let seg = UISegmentedControl.init(items: [action1, action2])
seg.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(seg)
雖然 Text/Image 只能擇一不那麼靈活彈性,不過 UISegmentedControl 還有個 setBackgroundImage
,這個看似簡單的背景設置,實則也帶來許多麻煩,如果沒有美工概念,顯圖最後也是會被做得醜醜的。
繼續閱讀|回目錄
客製化 Segmented Control
不過,Segmented Control 不是那麼容易調整,因其已經有個固定形式,所以我們試著建構一個比較彈性化的 Segmented Control,比方如下:
如果我們希望 Segmented Control 下方有一個跟著跑帶著特定顏色的下底線,並且比較容易調整 View 的顏色、選取 / 未選的字型及大小,那麼可以簡單設置一個客製化的 View。
不過,我們先看看如何使用。
private lazy var customSegmentedView: CustomSegmentedView = {
let segmentedView = CustomSegmentedView()
segmentedView.items = ["即時", "延遲"]
segmentedView.translatesAutoresizingMaskIntoConstraints = false
return segmentedView
}()
override func viewDidLoad() {
super.viewDidLoad()
customSegmentedView.selectedIndex = 0
NSLayoutConstraint.activate([
customSegmentedView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
customSegmentedView.topAnchor.constraint(equalTo: firstContainer.topAnchor, constant: -44),
customSegmentedView.widthAnchor.constraint(equalToConstant: 120),
customSegmentedView.heightAnchor.constraint(equalToConstant: 44)
])
customSegmentedView.selectionChanged = { index in
switch index {
case 0:
self.firstContainer.isHidden = false
self.secondContainer.isHidden = true
case 1:
self.firstContainer.isHidden = true
self.secondContainer.isHidden = false
default:
return
}
}
}
宣告一個 lazy var 的 CustomSegmentedView,這是一個客製化的 View,接著如同 Segmented Control 一樣,設置他的 items 名稱,然後是 selectedIndex(預設為 0),當約束條件定下來時,尤其是長寬,內部元件也定下來了。
最後是設置 selectionChanged,切換 Segment 後要顯示哪個 Container,隱藏哪個 Container,使用起來也是相當簡單,是吧?
最後是程式碼。
class CustomSegmentedView: UIView {
// MARK: - Properties
private let selfColor: UIColor = .systemGray5
private let buttonTintColor: UIColor = .clear
private let underlineColor: UIColor = .systemPink
private let titleNormalColor: UIColor = .systemGray
private let titleSelectedColor: UIColor = .black
private let titleFontSize: CGFloat = 16
private let titleNormalFontWeight: UIFont.Weight = .regular
private let titleSelectedFontWeight: UIFont.Weight = .bold
/// underline 在 view 裡面, 高度為正
/// 在 view 外面, 高度為負
private let underlineHeight: CGFloat = 4
/// underLine 的大小, 參照 Button Width, 最大為相等, 即是 1
/// Button Width 的大小, 參照 View 的 Width,
/// Butoon 塞入越多, Width 被切割越多塊
private let underlineRatio: CGFloat = 3
// UI Element
private var buttons: [UIButton] = []
private var underlineView: UIView!
/// 建構時, item 的數量同 SegmentedControl 的 Segment 數量
var items: [String] = [] {
didSet {
setupButtons()
}
}
/// 選擇 item 時, 改變 title 的顏色, 並更新 underline 的位置
var selectedIndex: Int = 0 {
didSet {
updateBtnSelectedUI()
}
}
var selectionChanged: ((Int) -> Void)?
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupBasicViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
// setupView()
}
override func layoutSubviews() {
super.layoutSubviews()
layoutButtons()
updateUnderlinePosition()
}
// MARK: - Setup
private func setupBasicViews() {
setupSelf()
setupUnderLine()
}
private func setupSelf() {
backgroundColor = selfColor
}
private func setupUnderLine() {
underlineView = UIView()
underlineView.backgroundColor = underlineColor
underlineView.translatesAutoresizingMaskIntoConstraints = false
addSubview(underlineView)
}
private func setupButtons() {
// buttons.forEach { $0.removeFromSuperview() }
// buttons.removeAll()
for (index, title) in items.enumerated() {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(titleNormalColor, for: .normal)
button.setTitleColor(titleSelectedColor, for: .selected)
button.tintColor = buttonTintColor
button.titleLabel?.font = .systemFont(
ofSize: titleFontSize, weight: titleNormalFontWeight)
button.tag = index
button.addTarget(self, action: #selector(buttonTapped),
for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
buttons.append(button)
}
updateBtnSelectedUI()
}
private func layoutButtons() {
guard !buttons.isEmpty else { return }
let buttonWidth = frame.width / CGFloat(buttons.count)
for (index, button) in buttons.enumerated() {
button.frame = CGRect(x: CGFloat(index) * buttonWidth,
y: 0,
width: buttonWidth,
height: frame.height - underlineHeight)
}
}
private func updateBtnSelectedUI() {
for (index, button) in buttons.enumerated() {
button.isSelected = (index == selectedIndex)
button.titleLabel?.font = button.isSelected
? .systemFont(ofSize: titleFontSize,
weight: titleSelectedFontWeight)
: .systemFont(ofSize: titleFontSize,
weight: titleNormalFontWeight)
}
updateUnderlinePosition()
}
private func updateUnderlinePosition() {
guard selectedIndex < buttons.count else { return }
let selectedButton = buttons[selectedIndex]
let underlineWidth = selectedButton.frame.width / self.underlineRatio
UIView.animate(withDuration: 0.3) {
self.underlineView.frame =
CGRect(
x: selectedButton.frame.origin.x + (selectedButton.frame.width - underlineWidth) / 2,
y: self.frame.height - self.underlineHeight,
width: underlineWidth,
height: self.underlineHeight
)
}
}
// MARK: - Actions
@objc private func buttonTapped(_ sender: UIButton) {
selectedIndex = sender.tag
selectionChanged?(selectedIndex)
}
}
除了外部可存取的 items、selectedIndex 之外,透過 var selectionChanged: ((Int) -> Void)?
這個閉包屬性,來設置 button index changed 後會發生什麼事,第一個是改變 selectedIndex,與此同時 updateBtnSelectedUI(),接著將閉包傳遞出去,在外調用時可以知道 selectedIndex 變成多少。
Delegation
當然,我們也可以使用 Delegation 的方式去設計,在 CustomSegmentedView
上方加入一個 protocol。
protocol CustomSegmentedViewDelegate: AnyObject {
func segmentedView(_ segmentedView: CustomSegmentedView,
didSelectIndex index: Int)
}
接著,在 CustomSegmentedView
中宣告一個 weak var delegate: CustomSegmentedViewDelegate? ,然後在 buttonTapped 中這樣改動。
@objc private func buttonTapped(_ sender: UIButton) {
selectedIndex = sender.tag
// selectionChanged?(selectedIndex)
delegate?.segmentedView(self, didSelectIndex: selectedIndex)
}
原先的 selectionChanged 帶入 selectedIndex,改成 delegate?.segmentedView 帶入 selectedIndex。
回到 ViewController,在 viewDidLoad() 中放進 CustomSegmentedView.delegate = self,這時候在 conform CustomSegmentedViewDelegate,就可以在 func 中安排頁面隱藏與呈現,如下:
extension ViewController: CustomSegmentedViewDelegate {
func segmentedView(_ segmentedView: CustomSegmentedView,
didSelectIndex index: Int) {
switch index {
case 0:
self.firstContainer.isHidden = false
self.secondContainer.isHidden = true
case 1:
self.firstContainer.isHidden = true
self.secondContainer.isHidden = false
default:
return
}
}
}
這樣寫也是相當漂亮,不是嗎?
這次就分享到這,感謝您的閱讀。
繼續閱讀|回目錄