Contents

nerzh/swift-extensions-pack

SwiftExtensionsPack is a collection of practical Swift extensions and small utilities for everyday application code: JSON and Codable helpers, date and string conversions, collection helpers, synchronized containers, HTTP utilities, hex/base64 conversion, AES-GCM, HMAC, SHA, Ed25

Contents

Features

  • JSON conversion helpers: String -> Dictionary/Array/Model, Dictionary -> JSON/Model, Encodable -> JSON.
  • AnyValue, a Codable and Equatable JSON-like value container.
  • String helpers for byte count, chunking, date parsing, UTF-8 pointers, hex, base64, and Unicode scalars.
  • Date helpers for formatting, parsing, time zones, date components, and timestamps.
  • Float and Double helpers for non-scientific string output, decimal accuracy, and rounding.
  • Synchronized containers: SafeArray, SafeDictionary, SafeValue, SendableValue, @Atomic, @AtomicOptional.
  • LimiterAsync for limiting async operations per time interval.
  • Crypto helpers: random bytes/data, SHA-256/384/512, HMAC, AES-256-GCM, Ed25519, hex, bytes, bits, and endian conversion.
  • Basic HTTP requests, flat and Rails-style query params, and multipart form-data.
  • Common helpers: SEPCommonError, shell commands on macOS/Linux, autoReleasePool, isNumeric, debug printing, **, and runtime reflection.

Installation

Swift Package Manager

Add the package to Package.swift:

dependencies: [
    .package(
        url: "https://github.com/nerzh/swift-extensions-pack.git",
        from: "2.0.0"
    )
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: [
            .product(
                name: "SwiftExtensionsPack",
                package: "swift-extensions-pack"
            )
        ]
    )
]

In Xcode, use File -> Add Package Dependencies... and enter the repository URL.

Import

import SwiftExtensionsPack

Quick Start

import Foundation
import SwiftExtensionsPack

struct User: Codable {
    let id: Int
    let name: String
}

let json = #"{"id":1,"name":"Alice"}"#
let user = json.toModel(User.self)

let now = Date()
let textDate = now.toString(dateFormat: "yyyy-MM-dd HH:mm:ss", secondsFromGMT: 0)
let parsedDate = textDate.toDate(dateFormat: "yyyy-MM-dd HH:mm:ss", secondsFromGMT: 0)

let chunks = "HelloWorld".chunks(5) // ["Hello", "World"]
let hex = "Hello".toHexadecimal // "48656c6c6f"
let original = hex.fromHexadecimal() // "Hello"

let array = SafeArray([1, 2, 3])
array.append(4)
let even = array.filter { $0.isMultiple(of: 2) }

let digest = SEPCrypto.HMAC.sha512.digest(
    string: "password_string",
    key: "mnemonic_string"
)

JSON and Codable

String to JSON or model

struct Profile: Decodable {
    let id: Int
    let name: String
}

let objectJson = #"{"id":7,"name":"Oleh"}"#

let profile1 = objectJson.decode(to: Profile.self)
let profile2 = objectJson.toModel(Profile.self)
let profile3: Profile = try objectJson.toModel(Profile.self)

let dict = objectJson.toDictionary()
let anyObject = try objectJson.toJsonObject()

let arrayJson = #"[1,2,3]"#
let array = arrayJson.toArray()

| API | Description | | --- | --- | | String.decode(to:) | Decodes a UTF-8 JSON string with JSONDecoder and returns an optional model. | | String.toModel(:) -> T? | Decodes a UTF-8 JSON string and returns an optional model. | | String.toModel(:) throws -> T | Throwing model decoding helper. | | String.toDictionary() | Parses a JSON object into [String: Any]?. | | String.toArray() | Parses a JSON array into [Any]?. | | String.toJsonObject() | Parses JSON and returns Any, throwing on failure. |

Encodable to JSON

struct Token: Encodable {
    let value: String
}

let token = Token(value: "abc")

let optionalJson = token.toJson
let unsafeJson = token.toJsonUnsafe
let throwingJson = try token.toJsonThrowable()

| API | Description | | --- | --- | | Encodable.toJson | Encodes a value into String?. | | Encodable.toJsonUnsafe | Encodes with try! and force unwrap. Use only when failure is impossible in your context. | | Encodable.toJsonThrowable() | Encodes and throws if encoding or UTF-8 conversion fails. |

Dictionary to JSON or model

struct Settings: Decodable {
    let retries: Int
    let debug: Bool
}

let raw: [String: Any] = [
    "retries": 3,
    "debug": true
]

let json = try raw.toJSON(options: [.prettyPrinted])
let data = try raw.toJSONData()
let settings = try raw.toModel(Settings.self)

| API | Description | | --- | --- | | Dictionary.toJSON(options:) | Serializes a dictionary into a JSON string. | | Dictionary.toJSONData(options:) | Serializes a dictionary into JSON Data. | | Dictionary.toModel(:) | Serializes a dictionary to JSON and decodes a model. | | mergeOptionalDictionary(:_:) | Merges two optional dictionaries. Values from the right dictionary win. |

AnyValue

AnyValue is a Codable and Equatable enum for JSON-like values.

let rawValue: [String: Any] = [
    "id": 1,
    "name": "Alice",
    "isActive": true,
    "tags": ["ios", "swift"]
]

let value = rawValue.toAnyValue()

let json = value.toJSON()
let dict = value.toDictionary()
let any = value.toAny()

Supported cases: string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float, float32, float64, double, decimal, bool, object, array, and nil.

let fromArray = ([1, "two", true] as [Any]).toAnyValue()
let fromDict = (["id": 1, "name": "Bob"] as [String: Any]).toAnyValue()
let fromJsonObject = #"{"id":1}"#.toAnyValue()

struct User: Decodable {
    let id: Int
}

let user = try (["id": 1] as [String: Any]).toAnyValue().toModel(User.self)

Dictionary.toAnyValue() has overloads for common value types: Any, Any?, String, String?, Int, Int?, Float, Float?, [Any], and [Any]?.

String

General helpers

let bytes = "Hello".bytes()
let parts = "abcdef".chunks(2) // ["ab", "cd", "ef"]

"hello".getPointer { (pointer: UnsafePointer<UInt8>, length: Int) in
    print(length)
}

| API | Description | | --- | --- | | bytes(:) | Returns the number of bytes in the selected encoding. UTF-8 is used by default. | | chunks(:) | Splits a string into fixed-size chunks. | | getPointer(_:) | Gives access to the UTF-8 buffer rebound to the requested pointer type. |

Date parsing

let date = "2026-05-02 12:30:00".toDate(
    dateFormat: "yyyy-MM-dd HH:mm:ss",
    secondsFromGMT: 0
)

let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let anotherDate = "02.05.2026".toDate(formatter)

| API | Description | | --- | --- | | toDate(_:secondsFromGMT:) | Parses a date with an existing DateFormatter. | | toDate(dateFormat:secondsFromGMT:) | Creates a DateFormatter from a format string and parses a date. |

Hex, base64, and Unicode

let hex = "Hello".toHexadecimal
let text = hex.fromHexadecimal()

let data = "48656c6c6f".dataFromHex
let strictData = try "48656c6c6f".dataFromHexThrowing()

let withPrefix = "ff".add0x // "0xff"
let noPrefix = "0xff".remove0x // "ff"
let normalized = "0x000abc".hexClear // "0xabc"

let encoded = "hello".base64Encoded()
let decoded = encoded?.base64Decoded()

let symbol = "2705".hexToCharacter()
let number = try "ff".hexToUInt

| API | Description | | --- | --- | | String(hexadecimal:encoding:) | Creates a string from a hex representation. | | hexadecimalToData / dataFromHex | Converts a hex string into Data?. | | dataFromHexThrowing() | Throwing hex-to-Data conversion. | | toHexadecimal | Encodes a UTF-8 string into hex. | | fromHexadecimal(encoding:) | Decodes hex back into a string. | | Data(hexString:) | Creates Data from a hex string. | | Data.toHexadecimal | Encodes bytes into a hex string. | | isHexNumber | Checks that all characters are hexadecimal digits. | | hexToUInt | Throwing property backed by UInt(self, radix: 16). | | hexToCharacter() | Converts a hex Unicode scalar into Character. | | addHexZeroX / add0x | Adds 0x if the string does not already have it. | | removeHexZeroX / remove0x / delete0x | Removes the 0x prefix. | | addFirstZeroToHexIfNeeded | Adds a leading zero for odd-length hex strings without 0x. | | hexClear | Collapses leading zeroes after 0x. | | dataFromHexOrBase64() | Reads the string as either hex or base64. | | base64Encoded() | Encodes a string as base64. | | base64Decoded() | Decodes base64 into a string. | | isBase64() | Checks the string with a base64 regular expression. |

Date

let now = Date()

let string = now.toString(dateFormat: "yyyy-MM-dd HH:mm:ss", secondsFromGMT: 0)
let localString = now.toString(dateFormat: "dd-MM-yyyy HH:mm:ss")

let day = now.getDay()
let month = now.getMonth()
let year = now.getYear()
let hours = now.getHours(secondsFromGMT: 0)

let ms = now.toMillis()
let seconds = now.toSeconds()

| API | Description | | --- | --- | | toString(:secondsFromGMT:) | Formats a date with an existing DateFormatter. | | toString(dateFormat:secondsFromGMT:) | Formats a date with a format string. | | stringWithTimeZone(:secondsFromGMT:) | Formats a date with a required time zone offset. | | stringWithTimeZone(dateFormat:secondsFromGMT:) | Same as above, but creates the formatter from a format string. | | dateWithTimeZone(_:secondsFromGMT:) | Rebuilds a date using a formatter and time zone offset. | | dateWithTimeZone(dateFormat:secondsFromGMT:) | Same as above, but creates the formatter from a format string. | | getDay(secondsFromGMT:) | Returns the day of month as UInt?. | | getMonth(secondsFromGMT:) | Returns the month as UInt?. | | getYear(secondsFromGMT:) | Returns the year as UInt?. | | getHours(secondsFromGMT:) | Returns the hour as UInt?. | | getMinutes(secondsFromGMT:) | Returns minutes as UInt?. | | getSeconds(secondsFromGMT:) | Returns seconds as UInt?. | | toMillis() | Returns a Unix timestamp in milliseconds. | | toSeconds() | Returns a Unix timestamp in seconds. |

Numbers

let d = 1.23e-5
d.toString() // "0.0000123"
d.accurancy()
d.round(toDecimalPlaces: 4)

let f: Float = 12.3400
f.toString()
f.round(toDecimalPlaces: 2, rule: .down)

| API | Types | Description | | --- | --- | --- | | toString() | Double, Float | Returns a string without unnecessary .0 and with scientific notation expanded when supported. | | accurancy() | Double, Float | Returns the number of digits after 0. for small values. The API name is intentionally accurancy(). | | round(toDecimalPlaces:rule:) | Double, Float | Rounds to a selected number of decimal places with a FloatingPointRoundingRule. |

Power operator:

let intPower = 2 ** 10 // 1024
let doublePower = 2.0 ** 0.5

Collections

Sequence

let joined = [1, 2, 3].join(", ") // "1, 2, 3"
let unique = [1, 1, 2, 3].uniq()

struct Row {
    let id: Int
    let name: String
}

let rows = [
    Row(id: 1, name: "A"),
    Row(id: 1, name: "B"),
    Row(id: 2, name: "C")
]

let uniqueById = rows.uniq { $0.id }

| API | Available on | Description | | --- | --- | --- | | join(:) | Sequence where Element: LosslessStringConvertible | Joins elements using a separator. | | uniq() | Sequence where Element: Hashable | Returns unique elements using Set. | | uniq(:) | Sequence | Returns unique elements using a hashable key. |

SafeArray

SafeArray<Element> is a class wrapper around Array<Element> with NSLock around operations. It is useful for simple shared-state scenarios where individual reads and writes must be synchronized.

let values = SafeArray<Int>()
values.append(1)
values.append(contentsOf: [2, 3])

let count = values.count
let doubled = values.map { $0 * 2 }
let filtered = values.filter { $0 > 1 }

values.removeAll { $0.isMultiple(of: 2) }

Main API groups:

| Group | APIs | | --- | --- | | Initialization | init(), init(:), init(repeating:count:), init(unsafeUninitializedCapacity:initializingWith:) | | Size and state | count, isEmpty, capacity, underestimatedCount, first, last, lazy, description, debugDescription, customMirror | | Operators and iteration | +, +=, makeIterator(), formIndex(:offsetBy:), formIndex(:offsetBy:limitedBy:) | | Append and insert | append(:), append(contentsOf:), insert(:at:), insert(contentsOf:at:), reserveCapacity(:) | | Removal | remove(at:), removeFirst(), removeFirst(:), removeLast(), removeLast(:), removeSubrange(:), removeAll(keepingCapacity:), removeAll(where:), popLast() | | Replacement and ordering | replaceSubrange, swapAt, partition(by:), reverse(), reversed(), shuffle(), shuffle(using:), shuffled(), shuffled(using:), sort(), sort(by:), sorted(), sorted(by:) | | Slices | Range subscript, prefix, suffix, dropFirst, dropLast, drop(while:), split | | Search and checks | first(where:), firstIndex(where:), last(where:), lastIndex(where:), contains(where:), allSatisfy, starts(with:by:), elementsEqual, lexicographicallyPrecedes, randomElement(), randomElement(using:), min, max | | Transformations | map, filter, compactMap, flatMap, reduce, reduce(into:), forEach, enumerated, joined | | Storage | withUnsafeBufferPointer, withUnsafeMutableBufferPointer, withUnsafeBytes, withUnsafeMutableBytes, withContiguousStorageIfAvailable, withContiguousMutableStorageIfAvailable | | Diff and newer APIs | applying(:), difference(from:by:), firstRange(of:), trimPrefix(while:), trimmingPrefix(while:) | | Protocol conformances | Sequence, Encodable, Decodable, Equatable, Hashable, CustomReflectable when Element satisfies the required constraints. |

SafeArray synchronizes each individual call. If you receive an ArraySlice, iterator, or reference-type element and mutate it outside the wrapper, that mutation is no longer synchronized by SafeArray.

SafeArrayPrtcl is a public protocol with associatedtype Element, lock: NSLock, and array: Array<Element>. It is used as a shared contract for array-like wrappers in operator overloads.

SafeDictionary

SafeDictionary<Key, Value> is a class wrapper around Dictionary<Key, Value> with NSLock around operations.

let cache = SafeDictionary<String, Int>()
cache["hits"] = 1
cache.updateValue(2, forKey: "hits")

let hits = cache["hits"]
let keys = Array(cache.keys)
let filtered = cache.filter { $0.value > 1 }

Main API groups:

| Group | APIs | | --- | --- | | Initialization | init(), init(minimumCapacity:), init(uniqueKeysWithValues:), init(:uniquingKeysWith:), init(grouping:by:) | | Subscripts | subscript(key:), subscript(key:default:), range subscript | | Size and state | count, isEmpty, capacity, first, keys, values, indices, lazy, underestimatedCount, description, debugDescription | | Mutation | updateValue, merge, merging, remove(at:), removeValue(forKey:), removeAll(keepingCapacity:), popFirst(), reserveCapacity(:) | | Slices | prefix, suffix, dropFirst, dropLast, drop(while:), split | | Search and checks | first(where:), firstIndex(where:), contains(where:), allSatisfy, starts(with:by:), elementsEqual, lexicographicallyPrecedes, min(by:), max(by:) | | Transformations | map, mapValues, compactMap, compactMapValues, filter, flatMap, reduce, reduce(into:), forEach, enumerated, sorted(by:), reversed, shuffled | | Storage and newer APIs | withContiguousStorageIfAvailable, trimmingPrefix(while:) | | Protocol conformances | Encodable, Decodable, Equatable, Hashable when Key and Value satisfy the required constraints. |

Synchronized Values

SafeValue

SafeValue<Value> stores a value behind NSLock. Read with value and mutate with change.

let counter = SafeValue(0)

counter.change { value in
    value += 1
}

let current = counter.value

SendableValue

SendableValue<Value> uses a concurrent DispatchQueue: reads may run in parallel, while mutations are executed through barrier blocks.

let state = SendableValue([String: Int]())

await state.change { dict in
    dict["count"] = 1
}

let snapshot = await state.read()

try await state.change { dict in
    if dict.isEmpty {
        throw SEPCommonError("Empty state")
    }
}

Atomic and AtomicOptional

Property wrappers for object references (Value: AnyObject):

final class Store {
    @Atomic var items = NSMutableArray()
    @AtomicOptional var current: NSObject?
}

These wrappers synchronize reading and writing the reference itself. Mutating the referenced object still needs to be thread-safe or externally synchronized.

LimiterAsync

LimiterAsync limits the number of async operations per time interval.

let limiter = LimiterAsync(maxRequests: 5, per: 1.0)

try await limiter.run {
    let (data, _) = try await Net.sendRequest(
        url: "https://example.com/api",
        method: "GET"
    )
    print(data.count)
}

let acquired = await limiter.acquire()
if acquired {
    // Run the operation.
}

await limiter.stop()

| API | Description | | --- | --- | | init(maxRequests:per:) | Creates a limiter for maxRequests operations per TimeInterval. | | acquire() | Waits for an available slot and returns Bool. | | run( operation: () async throws -> T) | Waits for a slot and runs an async closure. | | run( operation: (Bool) async throws -> T) | Passes a cancellation flag into the closure. | | stop() | Cancels the internal timer task. |

Crypto

Random

let bytes = randomBytes(count: 32)
let data = randomData(count: 32)
let index = randomInt(min: 0, max: 10)
let unsigned = randomUInt(min: 1, max: 100)

Data, bytes, bits, and endian conversion

let data = Data([0x01, 0x02])
let bytes = data.bytes

let number: UInt32 = 0x01020304
let little = number.toBytes(endian: .littleEndian)
let big = number.toBytes(endian: .bigEndian)

let restored = UInt32(little)
let bits = number.toBits(endian: .bigEndian)

| API | Description | | --- | --- | | Data.bytes / Data.getBytes | Returns [UInt8]. | | ToBytesConvertable.toBytes(endian:) | Converts an integer into bytes. | | ToBytesConvertable.toBytes(endian:count:) | Converts an integer into a selected number of bytes. | | ToBytesConvertable.init( bytes:) | Creates an integer from little-endian bytes. | | ToBytesConvertable.init(:endian:) | Creates an integer from bytes in the selected endian order. | | ToBitsConvertable.toBits(endian:) | Returns a binary string. | | Endianness.bigEndian / .littleEndian | Byte order. |

ToBytesConvertable is implemented for UInt16, UInt32, UInt64, UInt, Int16, Int32, Int64, and Int. ToBitsConvertable is also implemented for UInt8 and Int8.

SHA

let digest = SEPCrypto.SHA.sha256.digest(data: Data("hello".utf8))
let bytes = digest.withUnsafeBytes { Array($0) }
let hex = Data(bytes).toHexadecimal

Available algorithms: SEPCrypto.SHA.sha256, .sha384, and .sha512.

HMAC

let dataDigest = SEPCrypto.HMAC.sha256.digest(
    data: Data("message".utf8),
    key: Data("secret".utf8)
)

let hexDigest = SEPCrypto.HMAC.sha512.digest(
    string: "message",
    key: "secret"
)

Available algorithms: SEPCrypto.HMAC.sha256, .sha384, and .sha512.

AES-256-GCM

let key = try "my password".toAESKey()

let encryptedHex: String = try "secret text".encryptAES256(key: key)
let decryptedText: String = try encryptedHex.decryptAES256(key: key)

let encryptedWithStringKey: String = try "secret text".encryptAES256(key: "password")
let decryptedWithStringKey: String = try encryptedWithStringKey.decryptAES256(key: "password")

For Data:

let encrypted = try SEPCrypto.encryptAES256GCM(
    data: Data("secret".utf8),
    key: key
)

let decrypted = try SEPCrypto.decryptAES256GCM(
    data: encrypted,
    key: key
)

| API | Description | | --- | --- | | SEPCrypto.encryptAES256GCM(data:key:nonce:) | Encrypts Data with AES-GCM and returns the combined sealed box. | | SEPCrypto.decryptAES256GCM(data:key:) | Decrypts a combined sealed box. | | String.encryptAES256(key: Data, nonce:) -> Data | Encrypts a string and returns Data. | | String.encryptAES256(key: Data, nonce:) -> String | Encrypts a string and returns a hex string. | | String.decryptAES256(key: Data) -> Data | Reads an encrypted hex string and returns Data. | | String.decryptAES256(key: Data) -> String | Reads an encrypted hex string and returns a UTF-8 string. | | String.encryptAES256(key: String, nonce:) | Hashes a string key with SHA-256 and encrypts. | | String.decryptAES256(key: String) | Hashes a string key with SHA-256 and decrypts. | | convertToAESKey() / toAESKey() | SHA-256 of a UTF-8 string, suitable as a 256-bit key. |

Ed25519

let seed = randomData(count: 32)
let pair = SEPCrypto.Ed25519.createKeyPair(seed32Byte: seed)

let message = Data("hello".utf8)
let signature = SEPCrypto.Ed25519.sign(
    message: message,
    publicKey32byte: pair.public,
    secretKey64byte: pair.secret
)

let verified = SEPCrypto.Ed25519.verify(
    signature: signature,
    message: message,
    len: message.count,
    publicKey: pair.public
)

| API | Description | | --- | --- | | createKeyPair(seed32Byte:) | Creates a 32-byte public key and a 64-byte secret key. | | createKeyPairHex(seed32Byte:) | Same as above, but returns hex strings. | | createPublicKey(secretKey:) | Rebuilds the public key from a secret key. | | sign(message:publicKey32byte:secretKey64byte:) | Creates a 64-byte signature. | | verify(signature:message:len:publicKey:) | Verifies a signature. | | edwardsToMontgomery(bytesData:) throws | Converts exactly 32 compressed Ed25519 bytes to a 32-byte little-endian Montgomery u-coordinate. Checks canonical y and rejects y = 1; does not validate the full point or subgroup. | | convertEd25519ToX25519(ed25519PrivateKey:) | Hashes an original 32-byte Ed25519 seed once and clamps the first 32 digest bytes to prepare an X25519 scalar. Do not pass an expanded secret or prepared scalar. | | getKeyExchange(privateKey:publicKey:) throws | Accepts a prepared 32-byte private scalar and the peer's original 32-byte Ed25519 public key. Returns a 32-byte raw shared secret; rejects invalid lengths, noncanonical y, y = 1, and all-zero output. |

Coordinate conversion and key exchange migration

edwardsToMontgomery and getKeyExchange now throw SEPCrypto.Ed25519.KeyError. Add try and propagate or handle errors at existing call sites. Their argument labels and Data return types are unchanged. Inputs of the wrong length are rejected rather than truncated, including 64-byte expanded secrets passed to getKeyExchange.

let aliceSeed = randomData(count: 32)
let bobSeed = randomData(count: 32)
let alice = SEPCrypto.Ed25519.createKeyPair(seed32Byte: aliceSeed)
let bob = SEPCrypto.Ed25519.createKeyPair(seed32Byte: bobSeed)

// For an API that expects an X25519 public key:
let bobMontgomery = try SEPCrypto.Ed25519.edwardsToMontgomery(bytesData: bob.public)

// This C-backed API expects the original Edwards public key, not bobMontgomery.
let aliceScalar = alice.secret.prefix(32)
let ab = try SEPCrypto.Ed25519.getKeyExchange(privateKey: aliceScalar, publicKey: bob.public)

// Alternatively, derive the scalar once from the original seed.
let bobScalar = SEPCrypto.Ed25519.convertEd25519ToX25519(ed25519PrivateKey: bobSeed)
let ba = try SEPCrypto.Ed25519.getKeyExchange(privateKey: bobScalar, publicKey: alice.public)
assert(ab == ba)

Both throwing methods support Data slices with nonzero startIndex. They check the coordinate encoding, but do not perform full point or subgroup validation. getKeyExchange also rejects an all-zero shared secret; this is not a full validation of the Edwards public key. Its output is a raw shared secret, to be processed by the key derivation step required by your protocol.

Coordinate conversion uses the public PublicKey.toX25519() API from bytehubio/ed25519 1.1.0 or later. Fixed independent libsodium vectors and their provenance are included in Ed25519ConversionTests.swift.

HTTP and Multipart

Query params

let simpleParams: [String: Any] = [
    "q": "swift extensions",
    "page": 1
]
let simple = Net.toQueryParams(simpleParams)

let railsParams: [String: Any] = [
    "user": [
        "name": "Alice",
        "roles": ["admin", "editor"]
    ]
]
let rails = Net.toRailsQueryParams(railsParams)

let full = Net.makeQueryParamsString(["page": 1])
let encoded = Net.urlEncode("hello world")

| API | Description | | --- | --- | | Net.makeQueryParamsString(:) | Returns a query string prefixed with ?. | | Net.paramsString(:) | Uses Rails-style query params. | | Net.toQueryParams(:) | Builds a flat query string. | | Net.toRailsQueryParams(:) | Builds recursive params like user[name]=... and tags=.... | | Net.urlEncode(:) | Percent-encodes with alphanumerics + .- allowed. |

Request

Callback API:

try Net.sendRequest(
    url: "https://example.com/api",
    method: "POST",
    headers: ["Content-Type": "application/x-www-form-urlencoded"],
    params: ["name": "Alice"]
) { data, response, error in
    if let error {
        throw error
    }
    print(data?.count ?? 0)
}

Async API:

let result = try await Net.sendRequest(
    url: "https://example.com/api",
    method: "GET",
    params: ["page": 1]
)

print(result.data.count)

For GET, params are appended to the URL. For other methods, the body is filled from body, params, or multipart form-data.

Multipart

let file = NetSessionFile(
    data: Data("content".utf8),
    fileName: "file.txt",
    mimeType: "text/plain"
)

let multipart = NetMultipartData()
multipart.append("title", "Document")
multipart.appendFile("file", file.data, file.fileName, mimeType: file.mimeType)
let body = multipart.finalizeBodyAndGetData()
let boundary = multipart.boundary

Rails-style multipart from a nested object:

let multipartObject: [String: Any] = [
    "user": [
        "name": "Alice",
        "avatar": file
    ]
]
let body = NetMultipartData().toRailsMultipartData(multipartObject)

| API | Description | | --- | --- | | NetSessionFilePrtcl | File protocol with data, fileName, and mimeType. | | NetSessionFile | Ready-to-use multipart file value. | | NetMultipartData.body | Accumulated NSMutableData. | | NetMultipartData.boundary | Multipart body boundary. | | append(::) | Adds a regular form field. | | appendFile(:::mimeType:) | Adds a file field. | | finalizeBodyAndGetData() | Adds the closing boundary and returns the body. | | toRailsMultipartData(:) | Recursively builds Rails-style multipart body. | | Net.sharedSession | Shared URLSession with URL cache disabled. | | Net.NetErrors | NotValidParams, SomeError, and BadData. |

Errors and Common Utilities

SEPCommonError and ErrorCommon

func load() throws {
    throw SEPCommonError("Something went wrong")
}

do {
    try load()
} catch {
    let wrapped = SEPCommonError(error)
    print(wrapped.localizedDescription)
}

ErrorCommon combines Error, LocalizedError, CustomStringConvertible, CustomDebugStringConvertible, and Decodable, and adds a mutable reason.

Initializer helpers:

let sourceError = NSError(
    domain: "SwiftExtensionsPackExample",
    code: 1
)

let e1 = SEPCommonError("Reason")
let e2 = SEPCommonError(sourceError, errorLevel: .debug)
let e3 = SEPCommonError(sourceError, exReason: "Additional context")
let e4 = SEPCommonError.error(sourceError)

ErrorCommonLevel: .release, .debug.

Common methods

let result = autoReleasePool {
    Data(count: 1024)
}

isNumeric(123) // true
isNumeric("123") // false

let timestamp = getTimestampMs()
let char = hexToCharacter("2705")

pe("debug only with ASDF prefix")
pp("debug only")

Shell helpers are available on macOS/Linux:

let branch = try systemCommand("git branch --show-current")
let home = try getEnvironmentVar("HOME")

| API | Description | | --- | --- | | autoReleasePool(:) | Uses autoreleasepool where Objective-C is available, otherwise just runs the closure. | | isNumeric(:) | Checks Swift numeric types: Int, UInt, Float, Double, and Decimal. | | forceKillProcess(:) | Attempts to terminate, interrupt, and then kill a Process. macOS/Linux only. | | systemCommand(::timeOutNanoseconds:) | Runs a shell command through /usr/bin/env bash -lc. macOS/Linux only. | | SystemCommandExitError | Error thrown by systemCommand when the process exits with a non-zero status. | | getEnvironmentVar(:) | Reads an environment variable through shell. macOS/Linux only. | | pe(:) | Debug print with the ASDF: prefix, compiled only in DEBUG. | | pp(:) | Debug print, compiled only in DEBUG. | | * | Power operator for Int and Double. | | getTimestampMs() | Current Unix timestamp in milliseconds. | | hexToCharacter(_:) | Converts a hex Unicode scalar into Character. |

Runtime and Object Identity

Optional helpers

let optional: Int? = nil

isOptionalType(optional as Any) // true
isOptionalValue(optional) // true
optional.isNil // true

| API | Description | | --- | --- | | isOptionalType(:) | Checks whether the runtime type is optional. | | isOptionalValue(:) | Checks whether an optional value is nil. | | AnyOptional.isNil | Common protocol-based optional nil check. |

Reflection

struct Person {
    let name: String
    let age: Int?
    let tags: [String]
}

let info = getPropertiesInfo(Person(name: "Alice", age: nil, tags: ["dev"]))

for property in info {
    print(property.name, property.type, property.isOptional, property.wrappedType as Any)
}

getPropertiesInfo(_:) returns an array of tuples:

(
    name: String,
    value: Any?,
    type: Any.Type,
    isOptional: Bool,
    wrappedType: Any.Type?
)

Cases

enum Mode: Cases {
    case debug
}

Mode.debug.caseName // "debug"

ObjectIdentifiable

For classes:

final class Service: ObjectIdentifiable {}

let service = Service()
let id = service.objectId()

For structs:

struct Job: ObjectIdentifiableStruct {
    let _objectId = ObjectId()
    let name: String
}

let id = Job(name: "sync").objectId()

iOS-only APIs

Text size

Available only on iOS with UIKit:

let font = UIFont.systemFont(ofSize: 16)
let h = "Hello".height(200, font)
let w = "Hello".width(200, font)

| API | Description | | --- | --- | | String.height(:: ) | Calculates bounding height for a width and UIFont. | | String.width(:: ) | Calculates size through boundingRect. | | String.height(constrainedToWidth:) | Calculates height through a CoreText framesetter. |

CommonCrypto HMAC

When CommonCrypto is available, an additional HMAC API is compiled:

let hmac = "message".hmac(algorithm: .SHA256, key: "secret")

CryptoAlgorithm: .MD5, .SHA1, .SHA224, .SHA256, .SHA384, .SHA512.

| API | Description | | --- | --- | | CryptoAlgorithm.HMACAlgorithm | Returns CCHmacAlgorithm. | | CryptoAlgorithm.digestLength | Returns the digest length. | | String.hmac(algorithm:key:) | Returns a hex digest. |

Platforms

The Swift package declares:

  • iOS 13+
  • macOS 10.15+
  • Swift tools version 6.0

Platform notes:

  • CryptoKit is used on Apple platforms, while swift-crypto is used on Linux/Android.
  • AES-GCM APIs are marked @available(iOS 13.0, macOS 10.15, *).
  • Async Net.sendRequest is available on iOS 13+ and macOS 12+.
  • systemCommand, forceKillProcess, and getEnvironmentVar are available on Linux/macOS.
  • UIKit helpers are available only on iOS.
  • CommonCrypto HMAC helpers are compiled only when CommonCrypto is available.

License

See LICENSE.

Package Metadata

Repository: nerzh/swift-extensions-pack

Default branch: master

README: README.md