Skip to content

Latest commit

 

History

History
164 lines (124 loc) · 5.53 KB

File metadata and controls

164 lines (124 loc) · 5.53 KB

Getting started

简体中文 · Documentation index

This guide creates the smallest maintainable networking layer: a client for one base URL, an App-level request protocol, REST and GraphQL requests, and two ways to execute them.

1. Model base URL boundaries

Create one NetworkClient for one base URL. Account, content, and payments need separate clients when they use different base URLs. Different credentials or shared request policies on the same URL belong in client profiles. Production, staging, and test remain configurations of the same client type.

import Foundation
import NetworkingKit

enum AccountEnvironment {
    case production
    case staging

    var baseURL: URL {
        switch self {
        case .production: URL(string: "https://api.example.com")!
        case .staging: URL(string: "https://staging-api.example.com")!
        }
    }
}

final class AccountAPIClient: SharedNetworkClient, @unchecked Sendable {
    static let shared = AccountAPIClient(environment: .production)

    let baseURL: URL
    let session: URLSession
    let defaultProfile: NetworkClientProfile

    init(environment: AccountEnvironment) {
        baseURL = environment.baseURL
        session = URLSession(configuration: .default)
        defaultProfile = NetworkClientProfile(
            configuration: NetworkConfiguration(
                timeoutInterval: 15,
                retryPolicy: RetryPolicy(maxAttempts: 3)
            )
        )
    }

    func makeDecoder() -> JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }
}

SharedNetworkClient is useful when normal App requests use one shared instance. A client can instead conform only to NetworkClient and be injected into requests when tests or a feature need separate instances.

2. Bind an App request protocol to a concrete client

NetworkRequest has two associated types: Client identifies the backend configuration and Response identifies decoded data. Bind the client once with an App protocol; each business request supplies only its response type.

protocol AccountRequest: NetworkRequest where Client == AccountAPIClient {}

extension AccountRequest {
    var client: AccountAPIClient { .shared }
}

For a different base URL, define another client and another request protocol. For different shared behavior on this base URL, keep the same client and select another client profile.

3. Define REST requests

RestfulRequest adds path, method, query items, a body, and content type. Keep endpoint-specific information here; do not add common headers or token code.

struct User: Codable, Sendable {
    let id: String
    let name: String
}

struct GetUserRequest: AccountRequest, RestfulRequest {
    typealias Response = User

    let id: String
    var path: String { "/v1/users/\(id)" }
    var method: HTTPMethod { .get }
    var queryItems: [URLQueryItem]? { [URLQueryItem(name: "include", value: "roles")] }
    var body: (any Encodable & Sendable)? { nil }
    var contentType: String? { nil }
}

struct UpdateUserBody: Codable, Sendable { let name: String }

struct UpdateUserRequest: AccountRequest, RestfulRequest {
    typealias Response = User

    let id: String
    let name: String
    var path: String { "/v1/users/\(id)" }
    var method: HTTPMethod { .put }
    var queryItems: [URLQueryItem]? { nil }
    var body: (any Encodable & Sendable)? { UpdateUserBody(name: name) }
    var contentType: String? { nil } // Defaults to application/json for a JSON body.
}

Use EmptyResponse for successful endpoints that intentionally return no body, such as 204 No Content.

4. Define GraphQL requests

GraphQLRequest provides /graphql, POST, and JSON request headers. Override those defaults only when the server is different.

struct UserProfile: Decodable, Sendable {
    let id: String
    let name: String
    let email: String
}

struct FetchProfileRequest: AccountRequest, GraphQLRequest {
    typealias Response = GraphQLResponse<UserProfile>

    let id: String
    var query: String {
        "query Profile($id: ID!) { user(id: $id) { id name email } }"
    }
    var variables: [String: AnyEncodable]? {
        ["id": AnyEncodable(id)]
    }
    var operationName: String? { "Profile" }
}

GraphQL may return usable data and server errors together. Treat errors as product-level information rather than assuming any HTTP-success response means the operation succeeded completely.

5. Execute a request

Use Swift Concurrency for new code:

let user = try await GetUserRequest(id: "42").execute()

let graphQL = try await FetchProfileRequest(id: "42").execute()
let profile = graphQL.data
let serverErrors = graphQL.errors

For a screen that already owns Combine cancellation, use the publisher form. Work starts upon subscription and cancellation cancels the underlying request.

GetUserRequest(id: "42")
    .executePublisher()
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { completion in print(completion) },
        receiveValue: { user in print(user.name) }
    )
    .store(in: &cancellables)

Next steps