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 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:

Ghost Content API Documentation
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
Boost coding productivity with Cursor’s @Doc feature. Learn how to index external documentation directly in your workspace, eliminating tab-switching and keeping you in flow.

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!

String Catalog

String Catalog - App Localization on Autopilot

Push to GitHub, and we'll automatically localize your app for 40+ languages, saving you hours of manual work.

Tagged in: