SwiftUI - Create custom views Library

Photo by Iñaki del Olmo on Unsplash

SwiftUI - Create custom views Library

Alexandre Alcuvilla's photo
Alexandre Alcuvilla
·Nov 21, 2022·

7 min read

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 image.png

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 !
image.png

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
  }
}
  1. It creates for me a new struct called Previews_SegmentedView_LibraryContent that conforms to the LibraryContentProvider protocol
  2. The views property will returns all views added to the library
  3. 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 image.png 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 image.png

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 image.png

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
image.png

There is others parameters
visible 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 image.png 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 values
visible 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 😎 image.png


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 Simulator Screen Recording - iPhone 11 - 2022-11-18 at 22.42.26.gif


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 !! 🎉

 
Share this