Problem: How to easily reuse/share/discover custom SwiftUI views ?
Solution: Add your custom SwiftUI views to the Xcode view library !
Important: This tutorial is only available for iOS 14+
Because it will use the LibraryContentProvider
I've created a custom segmented control
I would like to reuse it easily and share with my team
Here the code of the this SwiftUI view
struct SegmentedControlView: View {
let titles: [String]
let onSelection: ((_ index: Int) -> Void)?
@State private var titleFrames: [CGRect]
@State private var selectedIndex = 0 {
willSet { updateSelectionIfIndexIsDifferent(using: newValue) }
}
private let spacingBetweenSections: CGFloat = 0
private let horizontalPadding: CGFloat = 32
private let selectedBackgroundHeight: CGFloat = 51
private let selectedIndicatorHeight: CGFloat = 5
private let minSectionCount = 2
private let maxSectionCount = 4
init(titles: [String], onSelection: @escaping (Int) -> Void) {
guard (minSectionCount...maxSectionCount).contains(titles.count) else {
fatalError("Expected between \(minSectionCount) and \(maxSectionCount) sections in the segmented control, got instead \(titles.count) section(s)")
}
self.titles = titles
self.onSelection = onSelection
self.titleFrames = [CGRect](repeating: .zero, count: titles.count)
}
var body: some View {
HStack(spacing: spacingBetweenSections) {
ForEach(titles.indices, id: \.self) { index in
Button(action: {
selectedIndex = index
}, label: {
Text(titles[index])
.foregroundColor(index == selectedIndex ? .primary : .secondary)
.frame(maxWidth: .infinity)
.frame(height: segmentedControlHeight())
.background(
ProxyFrameView(onChange: { frame in // Get the frame of the view
self.setFrame(at: index, with: frame) // Update the frame for the selected section
})
)
})
}
}
.frame(width: segmentedControlWidth())
.background(
VStack(spacing: 0) { // Represent the selected view
Rectangle()
.fill(.white)
.frame(width: sectionWidth(), height: selectedBackgroundHeight)
Capsule()
.fill(.red)
.frame(width: sectionWidth(), height: selectedIndicatorHeight)
}
.offset(x: alignToLeading())
.offset(x: setFrameToSelectedTitle())
)
.border(.gray.opacity(0.4), width: 1)
.background(Color(UIColor.secondarySystemBackground))
.animation(.spring(), value: selectedIndex)
}
// MARK: - Views
@ViewBuilder
func ProxyFrameView(onChange: @escaping (CGRect) -> Void) -> some View {
GeometryReader { // Use geometry reader with background modifier to get the view width
Color.clear
.preference(
key: FramePreferenceKey.self,
value: $0.frame(in: .global) // Get the frame of a section view
)
.onPreferenceChange(FramePreferenceKey.self) { frame in
onChange(frame)
}
}
}
// MARK: - Helpers
private func segmentedControlHeight() -> CGFloat {
selectedBackgroundHeight + selectedIndicatorHeight
}
private func segmentedControlWidth() -> CGFloat {
screenWidth() - horizontalPadding
}
private func updateSelectionIfIndexIsDifferent(using index: Int) {
if index != selectedIndex {
onSelection?(index)
}
}
private func screenWidth() -> CGFloat {
UIScreen.main.bounds.width
}
private func setFrame(at index: Int, with frame: CGRect) {
titleFrames[index] = frame
}
private func sectionWidth() -> CGFloat {
(screenWidth() - horizontalPadding) / CGFloat(titles.count)
}
private func alignToLeading() -> CGFloat {
let halfOfTheSelectedTitleWidth = sectionWidth() / 2
let halfOfTheSegmentedControlWidth = (screenWidth() - horizontalPadding) / 2
let xOffsetToAlignToLeading = halfOfTheSelectedTitleWidth - halfOfTheSegmentedControlWidth - spacingBetweenSections
return xOffsetToAlignToLeading
}
private func setFrameToSelectedTitle() -> CGFloat {
titleFrames[selectedIndex].minX - titleFrames[0].minX
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SegmentedControlView(titles: ["First", "Second"], onSelection: {_ in })
.previewLayout(.sizeThatFits)
SegmentedControlView(titles: ["First", "Second", "Third"], onSelection: {_ in })
.previewLayout(.sizeThatFits)
SegmentedControlView(titles: ["First", "Second", "Third", "Fourth"], onSelection: {_ in })
.previewLayout(.sizeThatFits)
}
}
struct FramePreferenceKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue = CGRect.zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
OK It cool
But how can I access/share this view easily ?
Just need to add it to the views library !
Create a new class where you can add a new library view
In the bar menu
Go in Editor -> Create Library Item
A class Previews_SegmentedView_LibraryContent
Is added to the bottom of your current file
struct Previews_SegmentedView_LibraryContent: LibraryContentProvider { // 1
var views: [LibraryItem] { // 2
LibraryItem(Text("Hello, World!")) // 3
}
}
- It creates for me a new struct called
Previews_SegmentedView_LibraryContent
that conforms to theLibraryContentProvider
protocol - The
views
property will returns all views added to the library - The item to add to the library is represent by a
LibraryItem
.Text("Hello, World!")
is the view that will be added to the view library
Check if the view Text("Hello, World!")
has been created
Display the library with keyboard shortcut
⌘ + ⇧ + L
Search for "text" and check that the custom view is here
Double click on it, to display it in your code
You can also add it with drag and drop on the live preview
The view will be created
Text("Hello, World!")
The live preview updates automatically
Because "Hello, World!" string is not a simple placeholder
It's filled as tokenised value
We can edit/validate it
OK now replace the text by the custom segmented control
LibraryItem(
SegmentedControlView(
titles: ["First", "Second"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
default:
print("Second segmented control selected update the view model")
}
}
))
Looking at the library
Xcode create a name for us "Segmented Control View"
Using the class name SegmentedControlView
Let's customize this name with the title "Segmented control - 2 sections"
LibraryItem(
SegmentedControlView(
titles: ["First", "Second"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
default:
print("Second segmented control selected update the view model")
}
}
),
title: "Segmented control - 2 sections" 👈
)
Much better
Now I add the category of this view
Here it's a control view
LibraryItem(
SegmentedControlView(
titles: ["First", "Second"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
default:
print("Second segmented control selected update the view model")
}
}
),
title: "Segmented control - 2 sections",
category: .control 👈
)
Now the icon as change
Blue color represent the control category
There is others parametersvisible
and matchingSignature
If visible is false
The view will not be added to the library
Can be usefull if want to access your view using a code completion
Using the matchingSignature
For example "sc2s" (to match the name Segmented control - 2 sections)
I try it
LibraryItem(
SegmentedControlView(
titles: ["First", "Second"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
default:
print("Second segmented control selected update the view model")
}
}
),
visible: false, 👈
title: "Segmented control - 2 sections",
category: .control,
matchingSignature: "sc2s" 👈
)
But it doesn't works
When I tap "sc2s", no completion for adding my view
I May be don't use it properly
If anybody know how use it
Tell us in the comment below
So I will delete those 2 parameters for now
So they will be set to their default valuesvisible
will set to true
matchingSignature
will set to nil
OK we have our segmented control to the library
But it's only for 2 sections
Let's add the 3 and 4 sections
struct Previews_SegmentedtView_LibraryContent: LibraryContentProvider {
@LibraryContentBuilder
var views: [LibraryItem] {
...
LibraryItem(
SegmentedControlView(
titles: ["First", "Second", "Third"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
case 1:
print("Second segmented control selected update the view model")
default:
print("Third segmented control selected update the view model")
}
}
),
title: "Segmented control - 3 sections",
category: .control
)
LibraryItem(
SegmentedControlView(
titles: ["First", "Second", "Third", "Fourth"],
onSelection: { index in
switch index {
case 0:
print("First segmented control selected update the view model")
case 1:
print("Second segmented control selected update the view model")
case 2:
print("Third segmented control selected update the view model")
default:
print("Fourh segmented control selected update the view model")
}
}
),
title: "Segmented control - 4 sections",
category: .control
)
}
}
All views are present in the library 😎
Let's try to create those custom views using the Xcode library
Create a new SwiftUI view and add a VStack
Add all segmented control using the Xcode library views
Run the live preview
All segmented controls are working fine
I hope you enjoy this article
Xcode view library is a powerful tool
It allows you to reuse custom views
Help other developer discover/learn about already created views
Share custom views with your team
Those views library can be access at any time
You don't need to be in a working state
If you are for example in a refactoring
And the project don't build
Those custom views are still accessible
Also custom views created in an SPM package
Will also be scan by the system
And will be available in the library
That's it !! 🎉