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!

Cursor AI with iOS Development Book.

Cursor AI for iOS Dev

Autocompletion from Cursor Tab, multifile edits with Cursor Composer, & chat with easy inline edits. Learn to use this AI editor for Swift and SwiftUI!

Tagged in: