Exploring Cursor: Quickly Creating a Swift Package
This chapter is taken from my book, "AI Assisted Coding for iOS Development". You can buy it to learn further about Cursor:
Over the past few weeks, I have been working on numerous Swift packages, and thanks to some extra AI assistance, I managed to ship them faster than I normally would. I used Cursor, and this post is about my process of creating GhostingKit, an unofficial Swift SDK for the Ghost Content API.
Creating GhostingKit
GhostingKit is an unofficial library I created for interacting with the Ghost Content API. You can check out the project here:
Creating the Package
To get started, I used a few basic bash commands to create the package and make it executable:
mkdir GhostingKit # Create directory for the package
cd GhostingKit # Change into that directory
swift package init --type executable # Initialize as an executable package
swift build # Build the package
swift run # Run the executable
Opening in Cursor
Once the package was set up, I opened the project in Cursor. After installing the Cursor shell command, I used the terminal to open the project with a single line:
This opened the directory directly in Cursor, ready for editing.
cursor "$PWD"
Updating Package.swift
Next, I updated the Package.swift file to define the platforms I wanted to support. Since GhostingKit depends on async/await but does not use any latest SwiftUI syntax, I still set the minimum iOS version to 16+. Here is the relevant section of Package.swift
:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
/// Package definition for GhostingKit, a Swift library for interacting with the Ghost Content API.
///
/// This package provides a convenient way to integrate Ghost content into Swift applications,
/// supporting various Apple platforms including iOS, macOS, tvOS, watchOS, and visionOS.
let package = Package(
name: "GhostingKit",
platforms: [
.iOS(.v16),
.macOS(.v13),
.tvOS(.v16),
.watchOS(.v9),
.visionOS(.v1)
],
products: [
/// The main GhostingKit library product.
///
/// This static library can be integrated into Swift projects to access Ghost Content API functionality.
.library(
name: "GhostingKit",
type: .static,
targets: ["GhostingKit"]
)
],
targets: [
/// The main target for the GhostingKit library.
///
/// This target contains the core functionality for interacting with the Ghost Content API.
.target(name: "GhostingKit"),
/// The test target for GhostingKit.
///
/// This target contains unit tests to ensure the proper functioning of the GhostingKit library.
.testTarget(
name: "GhostingKitTests",
dependencies: ["GhostingKit"]
)
]
)
Adding Docs to Cursor
One of the best features of Cursor is the ability to link and index external documentation directly into the editor. For GhostingKit, I added the Ghost Content API docs using Cursor’s @Doc feature. Here is the link to the API reference:
I added the relevant link to Cursor’s settings, indexed the docs, and could reference them directly while writing code:
For example, I accessed the docs like this: @GhostAPI.
I want to create a package for Ghost Content API and here is the documentation. @GhostAPI
All I had to do was wait and see the magic unfold. In Cursor Rules, I added this simple lousy line:
write documentation in detail like apple
Since I prefer my packages to be compatible with Swift 6.0, I used the actor
instead of creating a class. Here is a snippet of the GhostingKit
actor:
import Foundation
/// An actor representing the Ghost Content API client.
///
/// The `GhostingKit` actor provides methods to interact with Ghost's RESTful Content API,
/// allowing read-only access to published content. It simplifies the process of fetching
/// posts, pages, tags, authors, tiers, and settings from a Ghost site.
///
/// - Important: This actor requires a valid API key and admin domain to function correctly.
///
/// - Note: The Content API is designed to be fully cacheable, allowing frequent data fetching without limitations.
public actor GhostingKit {
/// The base URL for the Ghost Content API.
private let baseURL: URL
/// The API key used for authentication.
private let apiKey: String
/// The API version to use for requests.
private let apiVersion: String
/// The URL session used for network requests.
private let urlSession: URLSession
/// Initializes a new instance of the GhostingKit actor.
///
/// - Parameters:
/// - adminDomain: The admin domain of the Ghost site (e.g., "example.ghost.io").
/// - apiKey: The Content API key for authentication.
/// - apiVersion: The API version to use (default is "v5.0").
/// - urlSession: The URL session to use for network requests (default is shared session).
///
/// - Important: Ensure you're using the correct admin domain and HTTPS protocol for consistent behavior.
public init(
adminDomain: String,
apiKey: String,
apiVersion: String = "v5.0",
urlSession: URLSession = .shared
) {
self.baseURL = URL(string: "https://\(adminDomain)/ghost/api/content/")!
self.apiKey = apiKey
self.apiVersion = apiVersion
self.urlSession = urlSession
}
/// Performs a GET request to the specified endpoint.
///
/// - Parameters:
/// - endpoint: The API endpoint to request (e.g., "posts", "pages", "tags").
/// - parameters: Additional query parameters for the request.
///
/// - Returns: The response data from the API.
///
/// - Throws: An error if the network request fails or returns an invalid response.
private func get(
_ endpoint: String,
parameters: [String: String] = [:]
) async throws -> Data {
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint), resolvingAgainstBaseURL: true)!
var queryItems = [URLQueryItem(name: "key", value: apiKey)]
queryItems += parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.addValue("v\(apiVersion)", forHTTPHeaderField: "Accept-Version")
let (data, response) = try await urlSession.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NSError(domain: "GhostingKit", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
}
return data
}
I will not sugar-coat it. Not everything worked perfectly from the start. I had to iterate on methods for handling posts, tags, pages, and authors. I still prefer doing some manual work.
Creating the Sample Project
With everything in place, I wanted to create the sample project quickly. When I quickly, I meant it.
I opened Composer again and asked it to create a sample project in SwiftUI using the two relevant files. The first one contained all the methods to call, and the second one had all the tests written in it, with a demo API domain and key.
However, not everything worked out of the box. The sample project could not fetch responses because the two files I mentioned were unavailable locally. Again, my mistake. After some manual adjustments, I got everything working as taste.
Within 10 minutes. Even though it is a tiny app, I was proud to complete it so quickly.
Moving Forward
I am pretty impressed by the speed at which I could create functional Swift packages, from initial setup to a working sample project. Cursor is not the Midas touch (yet), but I imagine something like AI agents that autonomously handle the manual work I did.
Again, Cursor is not intended as an Xcode replacement. Use it for what it is great for.
Happy tab tab tab!