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:
GitHub - rryam/GhostingKit: Unofficial Swift SDK for Ghost API
Unofficial Swift SDK for Ghost API. Contribute to rryam/GhostingKit development by creating an account on GitHub.
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 executableOpening 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),](https://ghost.org/docs/content-api/)**
.**
Ghost’s RESTful Content API delivers published content to the world and can be accessed by any client to render a website. Read more on Ghost Docs.
I added the relevant link to Cursor’s settings, indexed the docs, and could reference them directly while writing code:
**[Exploring Cursor: Accessing External Documentation using @Doc](/posts/exploring-cursor-accessing-external-documentation-using-doc)**
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:
```swift
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.
How I am shipping with Cursor: pic.twitter.com/2YxxqKsjr0
— Rudrank Riyam (@rudrankriyam) October 18, 2024
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!