Skip to content

0xWDG/SimpleNetworking

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

81 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Simple Networking

This class is meant for simple network tasks.

Requirements

  • Swift 5.9+ (Xcode 15+)
  • iOS 13+, macOS 10.15+, tvOS, watchOS, Linux

Installation

Install using Swift Package Manager

dependencies: [
    .package(url: "https://github.com/0xWDG/SimpleNetworking.git", branch: "main"),
],
targets: [
    .target(name: "MyTarget", dependencies: [
        .product(name: "SimpleNetworking", package: "SimpleNetworking"),
    ]),
]

And import it:

import SimpleNetworking

Usage

Declare networking variable (preferred)

import SimpleNetworking

let networking = SimpleNetworking.shared

We use networking as the variable name, but you can use any name you like. Please note in the examples below we use networking as the variable name. If you use a different name, please replace networking with your variable name. Or use SimpleNetworking.shared instead of networking.

Setup (optional)

networking.set(serverURL: "https://wesleydegroot.nl")

Set user-agent (optional)

networking.set(userAgent: "STRING")

Set authentication (optional)

networking.set(authorization: "STRING")

Set post type (optional)

networking.set(postType: .json) // .plain, .json, .graphQL

GET data Async/Await

Task {
    let response = await networking.request(
        path: "/",
        method: .get
    )

    print(response.string)
}

POST data Async/Await

Task {
    let response = await networking.request(
        path: "/",
        method: .post(
            [
                "postfield1": "poststring1",
                "postfield2": "poststring2"
            ]
        )
    )

    print(response.string)
}

Upload Files Async/Await

Upload a single file with multipart/form-data encoding:

Task {
    // Create a file upload from data
    let fileData = "Hello, World!".data(using: .utf8)!
    let fileUpload = SimpleNetworking.FileUpload(
        data: fileData,
        name: "file",
        filename: "document.txt",
        mimeType: "text/plain"
    )
    
    // Create upload data with optional parameters
    let uploadData = SimpleNetworking.MultipartUploadData(
        file: fileUpload,
        parameters: ["description": "My document"]
    )
    
    // Perform the upload
    let response = await networking.upload(
        path: "/upload",
        uploadData: uploadData
    )
    
    print(response.string)
}

Upload a file from a file URL:

Task {
    do {
        // Create a file upload from a file URL
        let fileURL = URL(fileURLWithPath: "/path/to/file.jpg")
        let fileUpload = try SimpleNetworking.FileUpload(
            fileURL: fileURL,
            name: "photo"
            // filename and mimeType are auto-detected from the file
        )
        
        let uploadData = SimpleNetworking.MultipartUploadData(
            file: fileUpload
        )
        
        let response = await networking.upload(
            path: "/upload",
            uploadData: uploadData
        )
        
        print(response.string)
    } catch {
        print("Error uploading file: \(error)")
    }
}

Upload multiple files:

Task {
    let file1 = SimpleNetworking.FileUpload(
        data: "Content 1".data(using: .utf8)!,
        name: "file1",
        filename: "document1.txt"
    )
    
    let file2 = SimpleNetworking.FileUpload(
        data: "Content 2".data(using: .utf8)!,
        name: "file2",
        filename: "document2.txt"
    )
    
    let uploadData = SimpleNetworking.MultipartUploadData(
        files: [file1, file2],
        parameters: [
            "user_id": "123",
            "description": "Multiple files"
        ]
    )
    
    let response = await networking.upload(
        path: "/upload",
        uploadData: uploadData
    )
    
    print(response.string)
}

GET data (closure based)

networking.request(
    path: "/",
    method: .get
) { networkResponse in
    print(networkResponse)
}

POST data (closure based)

networking.request(
    path: "/",
    method: .post(
        [
            "postfield1": "poststring1",
            "postfield2": "poststring2"
        ]
    )
) { networkResponse in
    print(networkResponse)
}

Upload Files (closure based)

let fileData = "Hello, World!".data(using: .utf8)!
let fileUpload = SimpleNetworking.FileUpload(
    data: fileData,
    name: "file",
    filename: "document.txt"
)

let uploadData = SimpleNetworking.MultipartUploadData(
    file: fileUpload,
    parameters: ["key": "value"]
)

networking.upload(
    path: "/upload",
    uploadData: uploadData
) { networkResponse in
    print(networkResponse.string)
}

Property Wrappers

SimpleNetworking provides property wrappers for a declarative approach to defining HTTP requests.

Available Property Wrappers

  • @Get("/path") - GET requests
  • @Post("/path") - POST requests with optional data
  • @Put("/path") - PUT requests with optional data
  • @Delete("/path") - DELETE requests with optional data
  • @Upload("/path") - File upload requests
  • @SimpleNetworkingWrapper(.method, "/path") - Generic wrapper for any HTTP method

Standalone Usage

The property wrapper types can be used as standalone values without the @ syntax:

import SimpleNetworking

// Configure the shared instance (optional)
SimpleNetworking.shared.set(serverURL: "https://api.example.com")

// Create wrappers as standalone values
let getUsers = SimpleNetworking.Get("/users")
let createUser = SimpleNetworking.Post("/users")
let updateUser = SimpleNetworking.Put("/users/123")
let deleteUser = SimpleNetworking.Delete("/users/123")

// Execute requests
let usersResponse = await getUsers.execute()
print(usersResponse.string)

let createResponse = await createUser.execute(["name": "John Doe", "email": "john@example.com"])
print(createResponse.string)

let updateResponse = await updateUser.execute(["name": "Jane Doe"])
print(updateResponse.string)

let deleteResponse = await deleteUser.execute()
print(deleteResponse.statuscode)

File Upload (Standalone)

import SimpleNetworking

// Create an upload wrapper as a standalone value
let uploadFile = SimpleNetworking.Upload("/upload")

// Prepare the file
let fileData = "Hello, World!".data(using: .utf8)!
let fileUpload = SimpleNetworking.FileUpload(
    data: fileData,
    name: "file",
    filename: "document.txt",
    mimeType: "text/plain"
)

let uploadData = SimpleNetworking.MultipartUploadData(
    file: fileUpload,
    parameters: ["user_id": "123"]
)

// Execute the upload
let response = await uploadFile.execute(uploadData)
print(response.string)

Generic Wrapper (Standalone)

import SimpleNetworking

// Use the generic wrapper with any HTTP method
let getRequest = SimpleNetworking.SimpleNetworkingWrapper(.get, "/users")
let response = await getRequest.execute()

// With data for POST/PUT/DELETE
let postRequest = SimpleNetworking.SimpleNetworkingWrapper(.post(nil), "/users")
let postResponse = await postRequest.execute(["name": "John"])

Using as Property Wrappers with @ Syntax

Property wrappers are primarily designed to be used with the @ syntax on properties in your classes or structs:

import SimpleNetworking

class UserService {
    @Get("/users")
    var users: SimpleNetworking.NetworkResponse?
    
    @Post("/users")
    var createUser: SimpleNetworking.NetworkResponse?
    
    @Put("/users/123")
    var updateUser: SimpleNetworking.NetworkResponse?
    
    @Delete("/users/123")
    var deleteUser: SimpleNetworking.NetworkResponse?
    
    func fetchUsers() async {
        let response = await $users.execute()
        print(response.string ?? "No data")
    }
    
    func addUser(name: String, email: String) async {
        let userData = ["name": name, "email": email]
        let response = await $createUser.execute(userData)
        print(response.string ?? "No data")
    }
}

Note: Property wrappers use SimpleNetworking.shared by default, so any configuration you set on the shared instance will apply to all property wrappers.

When using property wrappers in a struct (rather than a class), methods that call execute() on the projected value must be marked as mutating because the execution updates the wrapped value. For example:

struct UserService {
    @Get("/users")
    var users: SimpleNetworking.NetworkResponse?
    
    mutating func fetchUsers() async {
        let response = await $users.execute()
        print(response.string ?? "No data")
    }
}

networkResponse

With networkResponse you can get the following data:

  • response: URLResponse? // URLResponse
  • statuscode: Int // HTTP status code
  • error: Error? // Error
  • data: Data? // Received data
  • string: String? // Received data as string
  • dictionary: [String: Any]? // Received data as dictionary (only works if data is JSON)
  • cookies: [HTTPCookie]? // Received cookies
  • request: URLRequest? // Sent request
  • headers: [HTTPHeader] // Received headers
  • decoded -> T? // Decoded data
  • cURL: String // as cURL command
  • asHTTPRequest: String // as HTTP Request

Bonus 1: JSON Decoding

Codable, decoding strategy = useDefaultKeys

struct MyCodable: Codable {
    let value1: String
    let value2: String
}

// Decode the response
let data: MyCodable? = networkResponse.decoded()

Codable, decoding strategy = convertFromSnakeCase

struct MyCodable: Codable {
    let snakeCase: String
    let caseSnake: String
}

// Decode the response
let data: MyCodable? = networkResponse.decoded(.convertFromSnakeCase)

Bonus: Websocket

import SimpleNetworking

networking.connect(to: "https://api.github.com/users/0xWDG") { data in
    print(data)
}

Add HTTP Cookie

let cookie = HTTPCookie.init(properties: [
    .name: "my cookie",
    .value: "my value",
    .domain: "wesleydegroot.nl"
    .path: "/"
])

networking.add(cookie: cookie)

Add HTTP Header

networking.add(header: .init(name: "my header", value: "my value"))

// Or if you want to declare it first
let header = SimpleNetworking.HTTPHeader(name: "my header", value: "my value")

networking.add(header: header)

Mocking

SimpleNetworking can be mocked (since version 1.0.3), so you can test your code without actually making a network request.

networking.set(mockData: [
    "https://wesleydegroot.nl": .init(
        data: "OVERRIDE", // Can be Data or String
        response: .init( // NSURLResponse, Can be nil
            url: .init(string: "https://wesleydegroot.nl")!,
            mimeType: "text/html",
            expectedContentLength: 8,
            textEncodingName: "utf-8"
        ),
        statusCode: 200, // Int: If omitted, 200 is used
        error: nil
    ),
    "/only/an/path": .init(
        data: "OVERRIDE", // Can be Data or String
        response: .init( // NSURLResponse, Can be nil
            url: .init(string: "https://wesleydegroot.nl/only/an/path")!,
            mimeType: "text/html",
            expectedContentLength: 8,
            textEncodingName: "utf-8"
        ),
        statusCode: 200, // Int: If omitted, 200 is used
        error: nil
    )
])

Debugging

/// Debug: NSURLRequest
networking.debug.requestURL = false

/// Debug: sent HTTP Headers
networking.debug.requestHeaders = false

/// Debug: sent Cookies
networking.debug.requestCookies = false

/// Debug: sent Body
networking.debug.requestBody = false

/// Debug: received HTTP Headers
networking.debug.responseHeaders = false

/// Debug: received Body
networking.debug.responseBody = false

/// Debug: received JSON (if any)
networking.debug.responseJSON = false

Contact

πŸ¦‹ @0xWDG 🐘 mastodon.social/@0xWDG 🐦 @0xWDG 🧡 @0xWDG 🌐 wesleydegroot.nl πŸ€– Discord

Interested learning more about Swift? Check out my blog.

About

Simple networking is a Swift library for handling network requests. It is built on top of URLSession and provides a simple and easy-to-use interface for making network requests. the key features of SimpleNetworking are simple, mockable, reliable.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Contributors

Languages