Files
LCEssentials/Documentation/Extensions.md
2026-08-29 19:23:23 -03:00

7.0 KiB

LCEssentials — Extensions

Foundation, value-type, string, collection, numeric, date, and crypto helpers, plus the LCEssentials namespace itself. UIKit extensions live in UIKit.md; SwiftUI helpers in SwiftUI.md.

Every section is a collapsible block — click a heading to expand it.

Contents


API & Networking

API is an actor wrapping URLSession for typed JSON requests and multipart uploads. This is a summary — the full guide is in API.md (all parameters, error model, client certificates, custom body types, and the rationale vs. a hand-rolled URLSession).

API — typed async requests & uploads

API.shared

The shared actor instance. Every call is await; nothing runs on the main thread unless you hop there yourself.

struct User: Decodable, Sendable { let id: Int; let name: String }

let user: User = try await API.shared.request(
    url: "https://api.example.com/users/{id}",
    method: .get,
    pathParams: ["id": "42"]
)

request(url:method:body:pathParams:headers:debug:timeoutInterval:networkServiceType:persistConnection:)

Sends a request and decodes the JSON response into T: Decodable & Sendable. Non-2xx responses throw an NSError whose code is the HTTP status and whose localizedFailureReason is the response body. When T == String the raw body is returned without JSON decoding.

struct CreateUser: Encodable, Sendable { let name: String }

// JSON body
let created: User = try await API.shared.request(
    url: "https://api.example.com/users",
    method: .post,
    body: jsonBody(CreateUser(name: "Ana"))
)

// form-url-encoded body
let token: Token = try await API.shared.request(
    url: "https://api.example.com/oauth/token",
    method: .post,
    body: .form(["grant_type": "password", "username": "ana"])
)

upload(url:method:form:pathParams:headers:debug:timeoutInterval:networkServiceType:) and its onProgress: overload

Uploads a multipart/form-data body serialised to a temp file (removed afterwards) and streamed from disk, so large files never fully load into memory.

var form = MultipartForm()
form.field("caption", "Sunset")
form.file("photo", data: jpegData, filename: "p.jpg")
form.file("video", url: localVideoURL)          // streamed from disk

let result: UploadResult = try await API.shared.upload(
    url: "https://api.example.com/media",
    form: form,
    onProgress: { fraction in
        Task { @MainActor in progressView.progress = Float(fraction) }
    }
)

Body types — HTTPBody

jsonBody(_:) wraps any Encodable & Sendable; FormURLEncodedBody (a.k.a. .form([:])) percent-escapes every value and never drops one; RawBody lets you supply the bytes and Content-Type yourself. Conform your own type to HTTPBody and API accepts it with no change.

struct CSVBody: HTTPBody {
    let rows: [[String]]
    func encoded() throws -> (data: Data, contentType: String) {
        let text = rows.map { $0.joined(separator: ",") }.joined(separator: "\n")
        return (Data(text.utf8), "text/csv; charset=UTF-8")
    }
}

await API.shared.setupCertification(certData:password:)

Registers a client certificate (.p12) for mutual-TLS on subsequent requests.

let p12 = try Data(contentsOf: certURL)
await API.shared.setupCertification(certData: p12, password: "cert-pw")

Strings & Text

Collections & Sequences

Numbers & Geometry

Date, Data & Files

Encoding & Errors

Crypto

RIPEMD_160 — RIPEMD-160 digest

static func hash(_ message: Data) -> Data

Returns the 20-byte RIPEMD-160 digest of message. Pure Swift, no system dependency. Mainly useful for Bitcoin-style address hashing (RIPEMD160(SHA256(x))).

let digest = RIPEMD_160.hash(Data("abc".utf8))
digest.count                                   // 20
digest.map { String(format: "%02x", $0) }.joined()
// "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"
LCECryptoKitManager — OTP / peppered-login bridge (needs the LCECryptoKit binary)

A thin facade over the optional LCECryptoKit binary product (enabled by the LCE_ENABLE_CRYPTO_BINARY build flag). When the binary is not linked every method is a no-op returning nil / "" / false, so calling code still compiles and runs.

init() / init(privateKey:)

Create a manager. The privateKey (a.k.a. "hash key") is only needed by the *WithKey methods.

let crypto = LCECryptoKitManager()
let keyed  = LCECryptoKitManager(privateKey: serverHashKey)

static func generateKey() -> String

Generates a random AES key string.

let key = LCECryptoKitManager.generateKey()

func encodeTP(email:password:) -> String? / func decodeOTP(_:) -> String?

Encode an email+password pair into an OTP seed hash, and decode it back.

let hash = crypto.encodeTP(email: "ana@x.com", password: "s3cr3t")
let back = crypto.decodeOTP(hash ?? "")

func encodeOTPWithKey(email:password:) -> String? / func decodeOTPWithKey(_:) -> Bool

Same as above but bound to the instance's privateKey; decodeOTPWithKey returns whether the hash validates against that key rather than the decoded value.

let keyed = LCECryptoKitManager(privateKey: serverHashKey)
let hash  = keyed.encodeOTPWithKey(email: "ana@x.com", password: "s3cr3t")
let ok    = keyed.decodeOTPWithKey(hash ?? "")     // Bool

static func generateSalt() -> String

Random salt for the salted/iterated/peppered login flow.

let salt = LCECryptoKitManager.generateSalt()

static func computeClientHash(email:password:salt:) -> String

Client-side hash of the credentials with the given salt — sent to the server instead of the raw password.

let clientHash = LCECryptoKitManager.computeClientHash(
    email: "ana@x.com", password: "s3cr3t", salt: salt
)

static func computeLoginBearerToken(userId:clientHash:) -> String?

Derives the login bearer token from a user id and the client hash.

let token = LCECryptoKitManager.computeLoginBearerToken(userId: "42", clientHash: clientHash)

static func otpEncode(_:) -> String? / static func otpDecode(_:) -> String?

One-time-pad encode/decode of an arbitrary string.

let enc = LCECryptoKitManager.otpEncode("secret-value")
let dec = LCECryptoKitManager.otpDecode(enc ?? "")     // "secret-value"

Core — the LCEssentials namespace