Skip to content

Instantly share code, notes, and snippets.

@saschagordner
Last active April 18, 2026 18:09
Show Gist options
  • Select an option

  • Save saschagordner/a33b5efc9f1179bc1bb3713b8b95bbf2 to your computer and use it in GitHub Desktop.

Select an option

Save saschagordner/a33b5efc9f1179bc1bb3713b8b95bbf2 to your computer and use it in GitHub Desktop.
Expanding Segmented Control with Liquid Glass
// This gist shows how to use a `UISegmentedControl` to achieve the goal of having expandable segment items,
// where the active segment would show a label, while the others do not.
// Since `UISegmentedControl` only supports images *or* text for their items, we draw a custom image for the items,
// which is basically a HStack with a label and an optional label.
// I've used fixed widths for the items since otherwise the whole segment item would animate, leading to funky image
// resizing effects.
// This only serves as a demo and is not meant to be production ready.
final class SampleVc: UIViewController {
private enum Constants {
static let collapsedWidth: CGFloat = 72
static let expandedWidth: CGFloat = 140
static let animationDuration: TimeInterval = 0.35
}
private let segmentedControl = UISegmentedControl(items: ["", "", ""])
override func viewDidLoad() {
super.viewDidLoad()
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
view.addSubview(segmentedControl)
applySegmentAppearance(selectedIndex: 0, animated: false)
}
@objc
private func segmentChanged(_ sender: UISegmentedControl) {
applySegmentAppearance(selectedIndex: sender.selectedSegmentIndex, animated: true)
}
private func applySegmentAppearance(selectedIndex: Int, animated: Bool) {
// Swap images without animation to avoid glitches during the width animation. Calling `layoutIfNeeded` inside
// the no-animation block ensures this.
UIView.performWithoutAnimation {
for index in 0..<self.segmentedControl.numberOfSegments {
let label = index == selectedIndex ? self.selectedLabel(for: index) : nil
self.segmentedControl.setImage(self.makeSegmentImage(label: label), forSegmentAt: index)
}
self.segmentedControl.layoutIfNeeded()
}
// Animate width changes. Calling `layoutIfNeeded` inside the animation block ensures the width changes are
// animated, since we pass this closure to UIView.animate.
let widthUpdates = {
for index in 0..<self.segmentedControl.numberOfSegments {
let targetWidth = index == selectedIndex ? Constants.expandedWidth : Constants.collapsedWidth
self.segmentedControl.setWidth(targetWidth, forSegmentAt: index)
}
self.segmentedControl.layoutIfNeeded()
}
if animated {
UIView.animate(
withDuration: Constants.animationDuration,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.15,
options: [.curveEaseInOut],
animations: widthUpdates
)
} else {
UIView.performWithoutAnimation(widthUpdates)
}
}
private func selectedLabel(for index: Int) -> String? {
switch index {
case 0: return "Miniatures"
case 1: return "Recipes"
case 2: return "Paints"
default: return nil
}
}
/// Renders a segment image which is basically a HStack with a SFSymbol (star) and an optional label.
private func makeSegmentImage(label: String?) -> UIImage {
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
let icon = UIImage(systemName: "star.fill", withConfiguration: symbolConfig)!
.withTintColor(.label, renderingMode: .alwaysOriginal)
let spacing: CGFloat = 6
let horizontalPadding: CGFloat = 8
let verticalPadding: CGFloat = 4
let textAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 13, weight: .medium),
.foregroundColor: UIColor.label
]
let textSize: CGSize
if let label, !label.isEmpty {
textSize = (label as NSString).size(withAttributes: textAttributes)
} else {
textSize = .zero
}
let width = horizontalPadding * 2 + icon.size.width + (textSize == .zero ? 0 : spacing + textSize.width)
let height = verticalPadding * 2 + max(icon.size.height, textSize.height)
let imageSize = CGSize(width: ceil(width), height: ceil(height))
return UIGraphicsImageRenderer(size: imageSize).image { _ in
let contentHeight = max(icon.size.height, textSize.height)
let originY = (imageSize.height - contentHeight) / 2
var x = horizontalPadding
icon.draw(
in: CGRect(
x: x,
y: originY + (contentHeight - icon.size.height) / 2,
width: icon.size.width,
height: icon.size.height
)
)
if let label, !label.isEmpty {
x += icon.size.width + spacing
let textRect = CGRect(
x: x,
y: originY + (contentHeight - textSize.height) / 2,
width: textSize.width,
height: textSize.height
)
(label as NSString).draw(in: textRect, withAttributes: textAttributes)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment