Skip to content

artemkalinovsky/Kite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

159 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

swift workflow Swift 6 macOS iOS tvOS watchOS visionOS Linux Windows Android

Kite

Kite is named after the kite bird, known for its lightness, speed, and agile flight. This Swift Package aims to embody those qualities—offering a lightweight, fast, and flexible networking layer that runs on Apple platforms, Linux, Windows, and Android.

Features

  • async/await-first request execution
  • Small protocol-based request model
  • Built-in JSON and XML response deserializers
  • Raw-data and no-op deserializers for simple endpoints
  • Query-string, JSON body, auth-header, and multipart upload support
  • Zero external dependencies
  • Explicit error behavior for auth failures, decode failures, and non-2xx responses

Requirements

  • Swift 6
  • Apple platforms: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+, visionOS 1+
  • Linux: any Swift 6-supported distribution
  • Windows: any Swift 6-supported release
  • Android: experimental via the nightly Swift SDK for Android

Installation 📦

Swift Package Manager

In Xcode, choose:

File -> Add Package Dependencies... -> Up to Next Major Version starting at 5.0.0

Or add Kite to Package.swift:

.package(url: "https://github.com/artemkalinovsky/Kite.git", from: "5.0.0")

Example:

// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyPackage",
            targets: ["MyPackage"]
        )
    ],
    dependencies: [
        .package(url: "https://github.com/artemkalinovsky/Kite.git", from: "5.0.0")
    ],
    targets: [
        .target(
            name: "MyPackage",
            dependencies: ["Kite"]
        )
    ]
)

Quick Start 🧑‍💻

Suppose you want to fetch users from this JSON payload:

{
  "results": [
    {
      "name": {
        "first": "brad",
        "last": "gibson"
      },
      "email": "brad.gibson@example.com"
    }
  ]
}

Create the client:

import Kite

let apiClient = APIClient()

Define the response model:

struct User: Decodable {
    struct Name: Decodable {
        let first: String
        let last: String
    }

    let name: Name
    let email: String
}

Define the request:

import Foundation
import Kite

struct FetchRandomUsersRequest: DeserializeableRequestProtocol {
    var baseURL: URL { URL(string: "https://randomuser.me")! }
    var path: String { "api" }

    var deserializer: any ResponseDataDeserializer<[User]> {
        JSONDeserializer<User>.collectionDeserializer(keyPath: "results")
    }
}

Execute it:

let (users, _) = try await apiClient.execute(request: FetchRandomUsersRequest())

Request Defaults

HTTPRequestProtocol keeps the required surface deliberately small:

  • baseURL is the only required property.
  • path defaults to "".
  • method defaults to .get.
  • parameters defaults to nil.
  • headers defaults to [:].
  • multipartFormData defaults to nil.

Parameter encoding is determined by the request shape:

  • For .get requests, parameters are encoded as URL query items.
  • For non-GET requests without multipartFormData, parameters are encoded as a JSON body and Content-Type is set to application/json.
  • When multipartFormData is present, parameters are included as text form fields alongside the files in the same multipart body.

Deserializers

Kite ships with three built-in deserializer styles:

  • VoidDeserializer() for endpoints where you only care whether the request succeeded
  • RawDataDeserializer() when you want the raw response bytes
  • JSONDeserializer for Decodable models
  • XMLDeserializer for types that conform to XMLObjectDeserialization — built into Kite, no extra import needed

Examples:

let users = JSONDeserializer<User>.collectionDeserializer(keyPath: "results")
let profile = JSONDeserializer<User>.singleObjectDeserializer()
let feed = XMLDeserializer<FeedItem>.collectionDeserializer(keyPath: "response", "items", "item")

To use XMLDeserializer, conform your model to XMLObjectDeserialization:

import Kite

struct FeedItem: XMLObjectDeserialization {
    let title: String

    static func deserialize(_ node: XMLIndexer) throws -> Self {
        FeedItem(title: try node["title"].value())
    }
}

Authenticated Requests

Conform to AuthRequestProtocol when the endpoint requires an Authorization header:

import Foundation
import Kite

struct FetchProfileRequest: AuthRequestProtocol, DeserializeableRequestProtocol {
    let accessToken: String

    var baseURL: URL { URL(string: "https://api.example.com")! }
    var path: String { "profile" }

    var deserializer: any ResponseDataDeserializer<User> {
        JSONDeserializer<User>.singleObjectDeserializer()
    }
}

By default, Kite sends:

Authorization: Bearer <accessToken>

If your backend uses a different prefix, override accessTokenPrefix. If your backend expects a different authorization header name or casing, override authorizationHeaders.

Raw Data Requests

Use RawDataDeserializer when the endpoint does not return JSON or XML:

import Foundation
import Kite

struct DownloadAvatarRequest: DeserializeableRequestProtocol {
    var baseURL: URL { URL(string: "https://cdn.example.com")! }
    var path: String { "avatar.png" }

    var deserializer: any ResponseDataDeserializer<Data> {
        RawDataDeserializer()
    }
}

Multipart Uploads

Provide a [String: URL] dictionary through multipartFormData to upload files:

import Foundation
import Kite

struct UploadAvatarRequest: AuthRequestProtocol, DeserializeableRequestProtocol {
    let accessToken: String
    let imageURL: URL

    var baseURL: URL { URL(string: "https://api.example.com")! }
    var path: String { "upload" }
    var method: HTTPMethod { .post }
    var multipartFormData: [String: URL]? { ["file": imageURL] }

    var deserializer: any ResponseDataDeserializer<URL> {
        JSONDeserializer<URL>.singleObjectDeserializer(keyPath: "avatar_url")
    }
}

Kite builds the multipart body and sets the correct Content-Type boundary automatically.

Error Handling

Kite keeps failure modes explicit:

  • URLError(.userAuthenticationRequired) when an authenticated request resolves to an empty Authorization header
  • APIClientError.unacceptableStatusCode for non-2xx HTTP responses
  • JSONDeserializerError and XMLDeserializerError for decode failures

Example:

do {
    let (users, _) = try await apiClient.execute(request: FetchRandomUsersRequest())
    print(users)
} catch let error as APIClientError {
    print(error.localizedDescription)
} catch {
    print(error.localizedDescription)
}

Project Status

Kite is production-ready. Pull requests, questions, and suggestions are welcome.

Apps Using Kite

License 📄

Kite is released under the MIT license. See LICENSE for details.

About

A Swift 6 networking library with async/await, JSON/XML deserialization, and auth header support — running on iOS/macOS/tvOS/watchOS/visionOS/Linux/Windows/Android!

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages