Segmented Control With Color Images
The default UISegmentedControl
does not support color images. As I wanted to have some in Eventail, I have developed a quick replacement for the default view. It tries to copy the API as closely as possible.
I use some helper function from my “utility” library:
extension Collection {
/// Return an element of a collection or nil
subscript(optional i: Index) -> Iterator.Element? {
return self.indices.contains(i) ? self[i] : nil
}
/// Iterate over collection elements, progressively yielding the previous, current and next element
func rollOver(closure: (Self.Element?, Self.Element, Self.Element?) -> Void) {
var prev: Self.Element? = nil
for i in self.indices {
let next = self[optional: self.index(after: i)]
closure(prev, self[i], next)
prev = self[i]
}
}
}
The class itself is simple and is @IBDesignable
. Unfortunately, Xcode does not (yet) support @IBInspectable
arrays so I expose three UIImage variables. If you need to add segments, copy and paste a few of those.
// ImageSegmentedControl.swift
import Foundation
import UIKit
@IBDesignable
class ImageSegmentedControl: UIControl {
// @IBInspectable does not support arrays yet, but a segmented control does
// not really need to have too many elements
@IBInspectable var image1: UIImage? = nil {
didSet {
images[0] = image1
}
}
@IBInspectable var image2: UIImage? = nil {
didSet {
images[1] = image2
}
}
@IBInspectable var image3: UIImage? = nil {
didSet {
images[2] = image3
}
}
/// Currently selegted segment
public var selectedSegmentIndex: Int = 0
// A nil image will represent a non-existing segment
private var images = [UIImage?](repeating: nil, count: 3)
private var segments = [UIView]()
private var isSegmentEnabled = [Bool](repeating: true, count: 3)
private var borders = [CALayer]()
private var initialized: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
if !initialized {
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = tintColor.cgColor
clipsToBounds = true
images.forEach {
guard let image = $0 else {
return
}
let imageView = UIImageView(image: image)
imageView.contentMode = .center
imageView.translatesAutoresizingMaskIntoConstraints = false
segments.append(imageView)
}
segments.rollOver {
prev, segment, _ in
addSubview(segment)
let previousAnchor = prev?.trailingAnchor ?? leadingAnchor
NSLayoutConstraint.activate([
segment.leadingAnchor.constraint(equalTo: previousAnchor),
segment.centerYAnchor.constraint(equalTo: centerYAnchor),
segment.heightAnchor.constraint(equalTo: heightAnchor),
segment.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1/CGFloat(segments.count)),
])
if prev != nil {
borders.append(segment.addLeftBorderWithColor(color: tintColor, width: 1))
}
segment.isUserInteractionEnabled = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(ImageSegmentedControl.handleTap(_:)))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.isEnabled = true
segment.addGestureRecognizer(tapRecognizer)
}
initialized = true
}
borders.forEach { $0.frame = CGRect(origin: $0.frame.origin, size: CGSize(width: $0.frame.width, height: frame.height)) }
for i in 0..<segments.count {
segments[i].backgroundColor = i == selectedSegmentIndex ? tintColor : nil
segments[i].isUserInteractionEnabled = isSegmentEnabled[i]
segments[i].alpha = isSegmentEnabled[i] ? 1 : 0.25
}
super.layoutSubviews()
}
@objc func handleTap(_ recognizer: UITapGestureRecognizer) {
if let i = segments.firstIndex(where: { $0 == recognizer.view }) {
selectedSegmentIndex = i
setNeedsLayout()
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
sendActions(for: .valueChanged)
}
}
func setEnabled(_ enabled: Bool, forSegmentAt segmentIndex: Int) {
// We need to store the enabled/disabled state because the segments
// can be actually initialized after this method is called
isSegmentEnabled[segmentIndex] = enabled
if let segment = segments[optional: segmentIndex] {
segment.isUserInteractionEnabled = enabled
segment.alpha = enabled ? 1 : 0.25
}
}
}
Some features of this view:
- Exposes the currently selected segment via
selectedSegmentIndex
akin toUISegmentedControl
- Inherits from UIControl so it has all of the outlets for common events. Only the
valueChanged
event fires though. - Is
@IBDesignable
so you can see how will it look
And this is how it looks in Xcode