Xcode Library customization with SPM plugin
9 min read • ––– views
Cover Source: Apple
With Xcode Library you can search and discover SwiftUI views and modifiers. You can find it by clicking on Plus sign in the top right or by pressing ⌘ + ⇧ + L shortcut:
If you don't see Views or Modifiers tabs, try to enable Canvas in Editor > Canvas menu or by pressing ⌥ + ⌘ + ⏎.
It also contains tabs for Snippets, Media, and SF Symbols. You can double-click on any item or just drag-n-drop it to the code editor. Moreover, you can extend the library with your custom views and modifiers:
import DeveloperToolsSupport
import SwiftUI
struct LibraryContent: @preconcurrency LibraryContentProvider {
@MainActor @LibraryContentBuilder
var views: [LibraryItem] {
LibraryItem(TitleView(), category: .control)
}
@MainActor @LibraryContentBuilder
func modifiers(base: Text) -> [LibraryItem] {
LibraryItem(base.customModifier(), category: .effect)
}
}
We use LibraryContentProvider protocol to provide views and/or modifiers for the library. Every LibraryItem must contains a code snippet, optionally you can specify a category to place it in a corresponding section.
Xcode Library extends the discoverability of your components and makes it easier to reuse them across the project. It also may be useful for third-party Swift packages with UI components. One downside is that you need to write and update the code manually. Initially I thought about using Swift Macro to simplify Library customization:
import SwiftUI
struct TitleView: View {
var body: some View {
Text("Title")
}
}
#Preview {
TitleView()
}
#LibraryContent(category: .control) {
TitleView()
}
I like this approach because it's declarative, flexible and doesn't require additional boilerplate. But unfortunately, Xcode Library doesn't see the code inside Swift Macro.
Another way to generate code is to use Swift Package Manager plugin. There are two types of plugins:
- Build tools. They can be integrated into the build process, but can't modify the package source code;
- Commands. They can be run from Xcode interface or from the command line, and can modify the package source code.
The second type is suitable for our task. Let's create a new Swift Package with the following structure:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Components",
platforms: [.iOS(.v18), .macOS(.v15)],
products: [
.library(name: "Components", targets: ["Components"]),
],
targets: [
.target(name: "Components"),
]
)
In Source/Components
create a title view:
import SwiftUI
public struct TitleView: View {
public init() {}
public var body: some View {
Text("Title")
}
}
How to get all views from the package and generate a code snippet with them? We can use regular expressions, but it's not a very flexible solution. Apple provides SwiftSyntax package to parse the source code and extract the necessary information. Source code is represented as a tree of nodes for inspecting and modifying.
Command plugins often perform their work by invoking to command line tools as subprocesses. Moreover, current plugins can import only standard libraries. That's why we need to create two targets: one for the plugin and another for the executable.
/* Previous configurations */
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "600.0.0"),
],
targets: [
.target(name: "Components"),
.plugin(
name: "GenerateLibraryContent",
capability: .command(
intent: .custom(
verb: "generate-library-content",
description: "Generate LibraryContent"),
permissions: [.writeToPackageDirectory(reason: "Generate LibraryContent")]),
dependencies: [
.target(name: "generate-library-content")
]),
.executableTarget(
name: "generate-library-content",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
]),
]
For the command plugin we need to specify the intent
and permissions
. The intent
is a intended use case of the plugin. The permissions
is a list of permissions that the plugin requires. In our case, we need to write to the package directory to generate the code snippet. And here is a files structure:
Components
├── Package.swift
├── Plugins
│ ├── LibraryContentPlugin
│ │ ├── LibraryContentPlugin.swift
├── Sources
│ ├── Components
│ │ ├── TitleView.swift
│ ├── generate-library-content
│ │ ├── GenerateLibraryContent.swift
In LibraryContentPlugin.swift
we need to implement the CommandPlugin
protocol:
import Foundation
import PackagePlugin
@main
struct LibraryContentPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
}
}
If you right-click on Components
package in Xcode, you'll see a new menu item GenerateLibraryContent
that runs the plugin:
Clicking on it will shows a menu with input selection:
That's why we need to handle selected targets in the plugin:
func performCommand(context: PluginContext, arguments: [String]) async throws {
var argumentExtractor = ArgumentExtractor(arguments)
let targetNames = argumentExtractor.extractOption(named: "target")
if targetNames.isEmpty {
return
}
for target in try context.package.targets(named: targetNames) {
// Working with tragets
}
}
PluginContext
provides information about the package and the environment in which the plugin is running, and ArgumentExtractor
helps to extract options from arguments.
Next, we need to check the target and work only with generic targets (not a test nor an executable):
guard let target = target as? SwiftSourceModuleTarget else {
continue
}
guard target.kind == .generic else {
continue
}
Now it's time to run the executable with arguments for source code and output file:
let tool = try context.tool(named: "generate-library-content")
let toolExec = URL(fileURLWithPath: tool.url.path())
let process = try Process.run(toolExec, arguments: ["--input", target.directoryURL.path,
"--output", target.directoryURL.appending(path: "LibraryContent.swift").path])
process.waitUntilExit()
if process.terminationReason == .exit && process.terminationStatus == 0 {
print("LibraryContent.swift has been generated successfully")
}
else {
let problem = "\(process.terminationReason):\(process.terminationStatus)"
Diagnostics.error("Failed to generate LibraryContent.swift: \(problem)")
}
In the executable we need to parse the source code and extract the necessary information. For argument parsing we'll use Swift Argument Parser framework, it's a convenience tool for creating command-line interfaces:
import Foundation
import ArgumentParser
@main
struct GenerateLibraryContent: ParsableCommand {
@Option(help: "Target directory URL")
var input: String
@Option(help: "The URL for generated file")
var output: String
func run() throws {
let fileNames = try FileManager.default.contentsOfDirectory(atPath: input)
for fileName in fileNames {
let contents = FileManager.default.contents(atPath: input + "/" + fileName)
let source = String(data: contents!, encoding: .utf8)!
print(source)
}
}
}
To simplify the logic we'll check files in a root directory, but in a real project you need to check all subfolders. The next step is to parse the source code with SwiftSyntax
:
import SwiftSyntax
import SwiftParser
func run() throws {
/* Previous code */
let sourceFile = Parser.parse(source: source)
let visitor = ViewStructVisitor(viewMode: .fixedUp)
visitor.walk(sourceFile)
}
final class ViewStructVisitor: SyntaxVisitor {
}
in ViewStructVisitor
we'll search for structs that support View
protocol:
final class ViewStructVisitor: SyntaxVisitor {
var viewNodes: [StructDeclSyntax] = []
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
if let inheritanceClause = node.inheritanceClause {
for inheritance in inheritanceClause.inheritedTypes {
if inheritance.type.description.trimmingCharacters(in: .whitespacesAndNewlines) == "View" {
viewNodes.append(node)
}
}
}
return .visitChildren
}
}
In viewNodes
we save StructDeclSyntax
nodes. It's a representation of struct declaration. Now we're ready to generate the code snippet for the library content. I'll skip over the details of the implementation. Honestly, SwiftSyntax is a bit complex and and hard to understand. Many APIs have no documentation, open source examples use old interfaces, and LLM tools generate wrong code. Here is my final code:
import SwiftSyntaxBuilder
func makeLibraryContent(nodes: [StructDeclSyntax]) -> SourceFileSyntax {
SourceFileSyntax {
ImportDeclSyntax(path: .init { ImportPathComponentSyntax(name: "DeveloperToolsSupport") })
ImportDeclSyntax(path: .init { ImportPathComponentSyntax(name: "SwiftUI") })
.with(\.trailingTrivia, .newlines(2))
let attributes = AttributeListSyntax {
.attribute(AttributeSyntax(attributeName: TypeSyntax("preconcurrency")))
}
let attributedTypeSyntax = AttributedTypeSyntax(specifiers: [],
attributes: attributes,
baseType: TypeSyntax("LibraryContentProvider"))
let inheritanceClause = InheritanceClauseSyntax {
InheritedTypeSyntax(type: attributedTypeSyntax)
}
StructDeclSyntax(name: "LibraryContent", inheritanceClause: inheritanceClause) {
let attributes = AttributeListSyntax {
AttributeListSyntax.Element.attribute(AttributeSyntax(attributeName: TypeSyntax("MainActor")))
AttributeListSyntax.Element.attribute(AttributeSyntax(attributeName: TypeSyntax("LibraryContentBuilder")))
}
.with(\.trailingTrivia, .newline)
let accessorBlock = AccessorBlockSyntax(accessors: AccessorBlockSyntax.Accessors(
CodeBlockItemListSyntax {
nodes.map(makeLibraryItem)
})
)
let bindings = PatternBindingListSyntax {
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: .identifier("views")),
typeAnnotation: TypeAnnotationSyntax(type: ArrayTypeSyntax(element: TypeSyntax("LibraryItem"))),
accessorBlock: accessorBlock
)
}
VariableDeclSyntax(attributes: attributes,
bindingSpecifier: .keyword(.var),
bindings: bindings)
.with(\.leadingTrivia, .newlines(2))
}
}
}
func makeLibraryItem(node: StructDeclSyntax) -> FunctionCallExprSyntax {
let viewExpression = FunctionCallExprSyntax(callee: DeclReferenceExprSyntax(baseName: node.name))
let categoryExpression = MemberAccessExprSyntax(name: "control")
let callee = DeclReferenceExprSyntax(baseName: .identifier("LibraryItem"))
return FunctionCallExprSyntax(callee: callee) {
LabeledExprSyntax(label: nil, expression: viewExpression)
LabeledExprSyntax(label: "category", expression: categoryExpression)
}
}
I'm sure the code is not perfect, but it works. It generates SourceFileSyntax
with the following content:
import DeveloperToolsSupport
import SwiftUI
struct LibraryContent: @preconcurrency LibraryContentProvider {
@MainActor @LibraryContentBuilder
var views: [LibraryItem] {
LibraryItem(TitleView(), category: .control)
}
}
My view example has only one initializer without parameters, but with the power of SwiftSyntax we can insprect any kinds of declarations and generate the necessary code with default values if needed.
The last step here is to save it to output file:
let finalCode = makeLibraryContent(nodes: viewNodes).formatted().description
let url = URL(fileURLWithPath: output)
try finalCode.write(to: url, atomically: true, encoding: .utf8)
If you run the plugin from the Xcode interface, you'll see a warning:
Don't be afraid, click on Allow
and the plugin will generate the code snippet. Another way to run it is via command line:
> swift package generate-library-content --target Components --allow-writing-to-package-directory
Building for debugging...
[7/7] Applying generate-library-content-tool
Build of product 'generate-library-content' complete! (2.19s)
LibraryContent.swift has been generated successfully
Finally, generated file is added to the package and Xcode Library automatically update Views section:
You may notice there is no documentation or examples in the right section. Xcode Library doesn't support this feature for custom components 🥲.
Final thoughts
Swift Package Manager plugins are a powerful tool to extend the functionality of your packages. Big advantage is installation, you don't need to install any additional tools via Homebrew or Mint. But what's require improvements and additional documentation is SwiftSyntax. I used Swift AST Explorer to visualize the syntax tree and understand the structure of nodes:
And just search on Github repositories to explore open-source examples.
Also, Xcode Library is one of the underrated and forgotten Xcode features. It may replace some documenation and simplify code reusing. With LibraryContentProvider
code snippets will be up-to-date, because they use real views and will fail build process in case of any break changes in view interfaces. I hope Apple will improve Xcode Library in next releases.
As usual you can find the final project on Github. Feel free to ask questions or share your feedback on X. Thanks for reading!