Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to NetworkingKit are documented in this file.

## 2.3.5 - 2026-07-19

### Changed

- Make every App-layer request-base example bind its own concrete client directly, rather than exposing a generic `SharedNetworkClient` constraint.

## 2.3.4 - 2026-07-19

### Added
Expand Down
2 changes: 1 addition & 1 deletion Examples/NetworkingKitDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct ContentView: View {
}
.multilineTextAlignment(.center)

Text("NetworkingKit on \(platformName)\nAppNetworkClient · AppRequest · REST · GraphQL · Metrics")
Text("NetworkingKit on \(platformName)\nAppNetworkClient · AppNetworkRequest · REST · GraphQL · Metrics")
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Expand Down
15 changes: 6 additions & 9 deletions Examples/NetworkingKitDemo/DemoViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,11 @@ struct AppNetworkErrorLocalizer: NetworkErrorLocalizing {
}
}

/// An app-specific generic base type that binds a request family to one shared client.
class AppNetworkRequest<ClientType: SharedNetworkClient>: @unchecked Sendable {
typealias Client = ClientType
var client: ClientType { .shared }
}

/// The request base for endpoints served by `AppNetworkClient`.
class AppRequest: AppNetworkRequest<AppNetworkClient>, @unchecked Sendable {}
class AppNetworkRequest: @unchecked Sendable {
typealias Client = AppNetworkClient
var client: AppNetworkClient { .shared }
}

// MARK: - REST

Expand All @@ -205,7 +202,7 @@ struct RESTCharacter: Codable, Sendable {
let status: String
}

final class GetCharacterRequest: AppRequest, RestfulRequest, @unchecked Sendable {
final class GetCharacterRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = RESTCharacter
private let id: String

Expand All @@ -224,7 +221,7 @@ struct GraphQLCharacterPayload: Codable, Sendable {
let character: Character?
}

final class FetchCharacterProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
final class FetchCharacterProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<GraphQLCharacterPayload>
private let id: String

Expand Down
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,16 @@ final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable {

Use a base class to avoid repeating the client in every request. Keep this base class free of `NetworkRequest` conformance so a REST or GraphQL subclass can receive the defaults from its own request protocol. Requests inheriting from a class must also be classes; Swift structures cannot inherit from classes.

`NetworkRequest` binds both a concrete `Client` type and a `Response` type. `AppNetworkRequest` fixes the client family without erasing it to `any NetworkClient`; the concrete REST or GraphQL request declares only its `Response`. This makes it impossible to accidentally use a request from one backend family with another backend's client. The base class should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`.
`NetworkRequest` binds both a concrete `Client` type and a `Response` type. `AppNetworkRequest` directly fixes `AppNetworkClient` without erasing it to `any NetworkClient`; the concrete REST or GraphQL request declares only its `Response`. This makes it impossible to accidentally use a request from one backend family with another backend's client. When an app has multiple backend clients, define one equivalent request base class per client. The base class should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`.

```swift
class AppNetworkRequest<ClientType: SharedNetworkClient>: @unchecked Sendable {
typealias Client = ClientType
class AppNetworkRequest: @unchecked Sendable {
typealias Client = AppNetworkClient

var client: ClientType {
var client: AppNetworkClient {
.shared
}
}

class AppRequest: AppNetworkRequest<AppNetworkClient>, @unchecked Sendable {}
```

### 3. Define a REST request
Expand All @@ -137,7 +135,7 @@ struct User: Decodable, Sendable {
let name: String
}

final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }
Expand All @@ -153,7 +151,7 @@ For a JSON request body, return any `Encodable & Sendable` value from `body`. Th
For successful endpoints with no response body, such as `204 No Content`, use `EmptyResponse` as the response type.

```swift
final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
final class DeleteUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = EmptyResponse
var path: String { "/users/123" }
var method: HTTPMethod { .delete }
Expand All @@ -174,7 +172,7 @@ struct UserProfile: Decodable, Sendable {
let email: String
}

final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String {
"""
Expand Down
16 changes: 7 additions & 9 deletions README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,16 @@ final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable {
### 2. 创建 App Request 基类

```swift
class AppNetworkRequest<ClientType: SharedNetworkClient>: @unchecked Sendable {
typealias Client = ClientType
class AppNetworkRequest: @unchecked Sendable {
typealias Client = AppNetworkClient

var client: ClientType {
var client: AppNetworkClient {
.shared
}
}

class AppRequest: AppNetworkRequest<AppNetworkClient>, @unchecked Sendable {}
```

采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`NetworkRequest` 同时绑定具体的 `Client` 类型与 `Response` 类型;`AppNetworkRequest` 固定 Client 类型但不使用 `any NetworkClient` 抹除它,具体 REST 或 GraphQL 请求只声明自身的 `Response`。这样能在编译期避免某个后端的 Request 被错误地绑定到另一个后端的 Client。`AppRequest` 不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。
采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`NetworkRequest` 同时绑定具体的 `Client` 类型与 `Response` 类型;`AppNetworkRequest` 直接固定为 `AppNetworkClient`,但不使用 `any NetworkClient` 抹除它,具体 REST 或 GraphQL 请求只声明自身的 `Response`。这样能在编译期避免某个后端的 Request 被错误地绑定到另一个后端的 Client。有多个后端 Client 时,为每个 Client 定义一个等价的请求基类。`AppNetworkRequest` 不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。

### 3. REST 请求

Expand All @@ -131,7 +129,7 @@ struct User: Decodable, Sendable {
let name: String
}

final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }
Expand All @@ -146,7 +144,7 @@ final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
对于 `204 No Content` 等成功但无 body 的接口,请使用 `EmptyResponse` 作为响应类型:

```swift
final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
final class DeleteUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = EmptyResponse
var path: String { "/users/123" }
var method: HTTPMethod { .delete }
Expand All @@ -164,7 +162,7 @@ struct UserProfile: Decodable, Sendable {
let name: String
}

final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String { "query { user { id name } }" }
}
Expand Down
12 changes: 5 additions & 7 deletions Tests/NetworkingKitTests/AppLayerExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@ private final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable {
private init() {}
}

private class AppNetworkRequest<ClientType: SharedNetworkClient>: @unchecked Sendable {
typealias Client = ClientType
var client: ClientType { .shared }
private class AppNetworkRequest: @unchecked Sendable {
typealias Client = AppNetworkClient
var client: AppNetworkClient { .shared }
}

private class AppRequest: AppNetworkRequest<AppNetworkClient>, @unchecked Sendable {}

private struct User: Codable, Sendable { let id: String }
private struct UserProfile: Codable, Sendable { let id: String }

private final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
private final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }
Expand All @@ -55,7 +53,7 @@ private final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Senda
var contentType: String? { nil }
}

private final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
private final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String { "query { user { id } }" }
}
Loading