Open-source project
tbxark/PinterestSegment avatar
tbxark/PinterestSegment

PinterestSegment: a sliding capsule segment control for UIKit and SwiftUI

A Pinterest-like segment control with masking animation.

693 stars52 forksSwiftMIT

At a glance

What is it?
PinterestSegment is a Swift component that reproduces the Pinterest style segmented control, where a capsule slides between titles and the selected title colour is revealed through a mask rather than drawn on top. It ships as a UIControl subclass and as a separate SwiftUI picker, and the interesting part for an adopter is the API split between them and a style value type that does not reload when mutated in place.
Who is it for?
Adopt PinterestSegment if you are building an iOS app and want the Pinterest style segmented control with a mask-reveal animation rather than the system UISegmentedControl, since the capsule slides and the title colour is revealed through a mask, which is not something the system control offers. Do not adopt it if you need the accessibility and behaviour guarantees of a native control without testing them, or if your project cannot take the iOS 13 and Swift 5.9 floor.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 40 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The mask is what makes it look like Pinterest

The description is short and the mechanism is in it: a Pinterest-style segment control where an indicator capsule slides between titles while the selected title color is revealed through a mask.

That last clause is the interesting one. The obvious implementation draws a filled capsule behind the selected title and switches the title colour from normal to selected. This one uses the capsule as a mask, so the selected title appears to be lit up by a sliding shape rather than being recoloured. The visual difference is subtle in a still image and obvious in motion, which is the whole point of copying a specific interaction design.

The feature list adds the practical constraints. It is horizontally scrollable, so any number of titles works. There are optional leading icons per title. The style is configurable across fonts, colors, spacing, minimum item width and borders. Light and dark mode are supported, and the SwiftUI defaults are adaptive.

So the audience is an iOS developer who wants this specific look. It is a component, not a framework: the repository holds Sources/, Tests/, a Demo/ project, a podspec, a Package.swift, Resources/ with the preview gifs, and a Scripts/ directory.

The project is MIT licensed and written in Swift, with the last push on 2026-09-14.

Two implementations, and they are not the same object

The library ships as two separate implementations, and picking one is the first decision an adopter makes.

The UIKit implementation is built on UIControl, with @IBDesignable and @IBInspectable support, which means it can be dropped into a storyboard or XIB as a plain UIView with the class set to PinterestSegment, and its style properties edited in the attributes inspector. That is a real advantage for teams whose UI is not built in code.

The SwiftUI implementation is native, with no UIKit bridging, and it is built around a Binding so that data changes drive the view. The documentation makes a point of this: the selection is a Binding and data changes drive the view, rather than the control reporting changes out to the owner.

The consequence for an adopter is that the two are not interchangeable. A view model holding a selected index works naturally with the SwiftUI version, since the binding is the source of truth. With the UIKit version the owner holds the value and receives a callback. Mixing them in one app means carrying two patterns, and the style types differ accordingly, PinterestSegmentStyle for UIKit with UIColor and UIFont, and PinterestSegmentPicker.Style for SwiftUI with Color and Font.

The documentation is careful to say the available options match one to one, so nothing is lost by choosing the SwiftUI path, but the types are different and the code will not be shared between them.

Installation through SPM or CocoaPods

The package supports Swift Package Manager and CocoaPods, and the SPM route is the one the documentation leads with.

In Xcode you select File, then Add Package Dependencies, and enter the repository URL:

code
https://github.com/TBXark/PinterestSegment.git

Or you add the dependency to your own Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/TBXark/PinterestSegment.git", from: "2.0.0")
]

CocoaPods is a single line:

ruby
pod 'PinterestSegment', '~> 2.0'

The requirements are iOS 13.0 and up, with Swift 5.9 and up and Xcode 15 and up. The badge in the readme also advertises Swift 6 compatibility and SPM support.

One thing to notice about the version numbers. The release history in the readme lists 2.0 as Swift Package Manager with a native SwiftUI implementation and bug fixes, then 1.2.4 for Swift 4.2, 1.2.0 for Swift 4.0 and 1.0.1 and 1.0.0 in December 2016. The tagged releases in the repository list stop at 1.2.3 from 2019, so the 2.x line named in the installation instructions and in the podspec is newer than anything in that release list.

For a component with a two-year gap in its tagged releases, the practical advice is to pin the exact version you test with rather than floating on a range.

The UIKit usage, and the value type trap

The UIKit example shows the full configuration surface in a few lines. You create a PinterestSegmentStyle, set the indicator color, the title margin, the title font and the normal and selected title colors, then construct the control with a frame, the style and an array of titles, and hook up a valueChange callback.

swift
let segment = PinterestSegment(frame: CGRect(x: 20, y: 100, width: 335, height: 40),
                               segmentStyle: style,
                               titles: ["Everything", "Geek", "Humor", "Art", "Food", "Home", "DIY"])
segment.valueChange = { index in
    // Called when the selection changes
}

The notes under that example are the part to read carefully, because they describe a real footgun. PinterestSegmentStyle is a value type. Assigning a whole style, segment.style = newStyle, reloads the control automatically. Mutating a property in place, segment.style.titleFont = font, does not, and you need to call segment.reloadData() afterwards.

The reason is Swift value semantics rather than a bug. The style you hold is a copy, and mutating a property of a copy does not write back to the control's stored copy, so the control never learns about the change. A whole-style assignment replaces its copy and triggers the reload path. If your code mutates styles at runtime, which a theme switcher would, you need to know which of the two forms you used.

The second note concerns the callback. The valueChange callback and the sendActions(for: .valueChanged) action fire only when the selected index actually changes, including taps and programmatic setSelectIndex calls. Initializing, reloading data, or setting the same index never fires the event. That is the correct behaviour for a segment control, but it means a binding-style expectation of a notification on every assignment will not hold.

SwiftUI usage and icons

The SwiftUI implementation is short enough to read in one screen. The selection is a piece of state, bound to the picker, and the titles are passed in:

swift
struct ContentView: View {
    @State private var selection = 0

    var body: some View {
        PinterestSegmentPicker(selection: $selection,
                               titles: ["Everything", "Geek", "Humor", "Art", "Food", "Home", "DIY"])
            .frame(height: 40)
    }
}

A variant takes items with a leading icon and a custom style, and the initialiser also accepts an onSelectionChange closure:

swift
var body: some View {
    PinterestSegmentPicker(selection: $selection,
                           items: [
                               .init(title: "Face-1", icon: Image(systemName: "face.smiling")),
                               .init(title: "Face-2", icon: Image(systemName: "heart")),
                           ],
                           style: style,
                           onSelectionChange: { index in ... })
}

The UIKit equivalent of that feature is PinterestSegment.TitleElement, which takes a title, a selected image and a normal image, and the setRichTextTitles method applies them. The two image variants matter because the icon is supposed to change with the selection, which is the same mask-reveal idea applied to the artwork rather than the text.

swift
let symbols = ["face.smiling", "heart", "star"]
let titles = symbols.map { symbolName in
    let image = UIImage(systemName: symbolName)
    return PinterestSegment.TitleElement(title: symbolName,
                                         selectedImage: image?.withTintColor(.darkGray, renderingMode: .alwaysOriginal),
                                         normalImage: image?.withTintColor(.lightGray, renderingMode: .alwaysOriginal))
}
segment.setRichTextTitles(titles)

The icons are SF Symbols created with UIImage(systemName:) and tinted with withTintColor using the .alwaysOriginal rendering mode, which is what keeps the two tints distinct from the system image rendering.

Building the demo and running the tests

The demo app shows the UIKit and SwiftUI implementations side by side in two tabs, and it is generated with XcodeGen rather than checked in as an .xcodeproj, which is why the repository has a Demo/ directory with a project.yml and a Sources/ subdirectory instead of a committed Xcode project.

The build script is:

shell
./Scripts/demo.sh

or the Makefile equivalent:

shell
make demo

The Makefile gives the full picture of the development setup. The demo-build target runs xcodegen generate with the spec at Demo/project.yml and then xcodebuild with the PinterestSegmentDemo scheme against a generic iOS Simulator destination. The build target does the same for the PinterestSegment scheme itself, and the test target runs xcodebuild test with the same scheme against a named simulator, with the device defaulting to iPhone 16 and overridable through the SIMULATOR variable.

Formatting and linting are done with the swift format tool rather than a separate linter, with format applying swift format --in-place --recursive over Sources, Tests and Demo/Sources, and lint running the same tool in lint mode with --strict. The ci target chains build, test, lint and demo-build, which is a reasonable definition of what has to pass before a change lands.

The one environment assumption is Xcode 15 or later, which is consistent with the Swift 5.9 floor and with the Swift 6 compatibility claim in the badge.

Editorial conclusion

Adopt PinterestSegment if you are building an iOS app and want the Pinterest style segmented control with a mask-reveal animation rather than the system UISegmentedControl, since the capsule slides and the title colour is revealed through a mask, which is not something the system control offers. Do not adopt it if you need the accessibility and behaviour guarantees of a native control without testing them, or if your project cannot take the iOS 13 and Swift 5.9 floor. Before you wire it up, read the notes on value type semantics, because assigning a whole PinterestSegmentStyle reloads the control while mutating a property in place does not and leaves you needing an explicit call to reloadData(), and decide which of the two APIs you want, since the UIKit and SwiftUI implementations expose separate style types even though the available options match one to one.

Frequently asked questions

What is PinterestSegment and what makes it different from UISegmentedControl?

It is a Swift segment control that copies the Pinterest style, where an indicator capsule slides between titles and the selected title colour is revealed through a mask rather than drawn as a filled background. The system UISegmentedControl has no such animation or mask effect.

How do I install PinterestSegment?

Through Swift Package Manager, either with File and Add Package Dependencies in Xcode using the repository URL, or by adding .package(url: "https://github.com/TBXark/PinterestSegment.git", from: "2.0.0") to your Package.swift. CocoaPods is also supported with pod 'PinterestSegment', '~> 2.0'. The requirements are iOS 13.0+, Swift 5.9+ and Xcode 15+.

Why does changing a style property not update the control?

PinterestSegmentStyle is a value type, so mutating a property in place does not write back to the control's stored copy. Assigning a whole style, segment.style = newStyle, reloads the control automatically, but mutating a property such as segment.style.titleFont requires an explicit call to segment.reloadData() afterwards.

Can I use PinterestSegment in a SwiftUI app?

Yes, with a native SwiftUI implementation that does not bridge UIKit. The selection is a Binding and data changes drive the view, and the picker is PinterestSegmentPicker, with separate items and onSelectionChange variants. Its style type is PinterestSegmentPicker.Style, using Color and Font rather than the UIColor and UIFont based PinterestSegmentStyle.

How do I build the PinterestSegment demo app?

The demo is generated with XcodeGen, so run ./Scripts/demo.sh or make demo. The Makefile's demo-build target runs xcodegen generate with the spec at Demo/project.yml and then builds the PinterestSegmentDemo scheme, and make test runs the test scheme on a simulator that defaults to iPhone 16.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. tbxark/PinterestSegment on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tbxark-pinterestsegment.svg)](https://hysenlabs.com/projects/tbxark-pinterestsegment)

Was this useful?