Skip to content

Commit 28a1dfd

Browse files
authored
Merge pull request #23 from uhooi/feature/support_swift_concurrency
Support Swift Concurrency
2 parents c0e67b9 + e77bfd7 commit 28a1dfd

File tree

14 files changed

+124
-63
lines changed

14 files changed

+124
-63
lines changed

.github/workflows/ci-examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [ main ]
88

99
env:
10-
DEVELOPER_DIR: /Applications/Xcode_13.0.app
10+
DEVELOPER_DIR: /Applications/Xcode_13.2.app
1111

1212
jobs:
1313
build:

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
runs-on: macos-11
1717
strategy:
1818
matrix:
19-
xcode: ["12.5.1", "13.0"]
19+
xcode: ["13.2"]
2020
subcommand: ["Build", "Test"]
2121
steps:
2222
- uses: actions/checkout@v2
@@ -36,7 +36,7 @@ jobs:
3636
container: swift:${{ matrix.swift }}
3737
strategy:
3838
matrix:
39-
swift: ["5.4.2", "5.5.0"]
39+
swift: ["5.5.2"]
4040
steps:
4141
- uses: actions/checkout@v2
4242
- name: Build and Test

Examples/Example/APIClient/JSONPlaceholderAPIClient.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ final class JSONPlaceholderAPIClient {
77
}
88

99
extension JSONPlaceholderAPIClient: JSONPlaceholderRepository {
10+
// Swift Concurrency
11+
func fetchAllUser() async throws -> [User] {
12+
return try await httpClient.request(FetchAllUserRequest())
13+
}
14+
15+
// Completion handler
1016
func fetchAllUser(_ completion: @escaping (Result<[User], Error>) -> Void) {
1117
httpClient.request(FetchAllUserRequest()) { result in
1218
completion(result)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
protocol JSONPlaceholderRepository {
2+
// Swift Concurrency
3+
func fetchAllUser() async throws -> [User]
4+
5+
// Completion handler
26
func fetchAllUser(_ completion: @escaping (Result<[User], Error>) -> Void)
37
}
48

Examples/Example/Views/ViewController.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,24 @@ final class ViewController: UIViewController {
1313
override func viewDidLoad() {
1414
super.viewDidLoad()
1515

16+
// Swift Concurrency
17+
Task {
18+
do {
19+
self.users = try await repository.fetchAllUser()
20+
print("Swift Concurrency:", users)
21+
} catch {
22+
print("Swift Concurrency:", error)
23+
}
24+
}
25+
26+
// Completion handler
1627
repository.fetchAllUser { result in
1728
switch result {
1829
case let .success(users):
1930
self.users = users
20-
print(users)
31+
print("Completion handler:", users)
2132
case let .failure(error):
22-
print(error)
33+
print("Completion handler:", error)
2334
}
2435
}
2536
}

Examples/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TEST_SDK := iphonesimulator
66
TEST_CONFIGURATION := Debug
77
TEST_PLATFORM := iOS Simulator
88
TEST_DEVICE ?= iPhone 13 Pro Max
9-
TEST_OS ?= 15.0
9+
TEST_OS ?= 15.2
1010
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'
1111

1212
.PHONY: build-debug

Package.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.5
22

33
import PackageDescription
44

55
let package = Package(
66
name: "HTTPClient",
77
platforms: [
8-
.iOS(.v9),
9-
.macOS(.v10_10),
10-
.tvOS(.v9),
11-
.watchOS(.v2),
8+
.iOS(.v13),
9+
.macOS(.v10_15),
10+
.tvOS(.v13),
11+
.watchOS(.v6),
1212
],
1313
products: [
1414
.library(

README.md

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ Communicate via HTTP easily in Swift.
1313

1414
## Table of Contents
1515

16+
- [Requirement](#Requirement)
1617
- [Installation](#installation)
1718
- [How to use](#how-to-use)
1819
- [Contribution](#contribution)
1920
- [Stats](#stats)
2021

22+
## Requirement
23+
24+
- Xcode 13.2+ (Swift 5.5.2+)
25+
2126
## Installation
2227

2328
### Swift Package Manager (Recommended)
@@ -108,7 +113,7 @@ You can just import `HTTPClient` to use it.
108113

109114
```swift
110115
protocol VersatileRepository {
111-
func registerUser(name: String, description: String, completion: @escaping (Result<UserID, Error>) -> Void)
116+
func registerUser(name: String, description: String) async throws -> UserID
112117
}
113118
```
114119

@@ -122,28 +127,21 @@ You can just import `HTTPClient` to use it.
122127
}
123128

124129
extension VersatileAPIClient: VersatileRepository {
125-
func registerUser(name: String, description: String, completion: @escaping (Result<UserID, Error>) -> Void) {
130+
func registerUser(name: String, description: String) async throws -> UserID {
126131
let requestBody = RegisterUserRequestBody(name: name, description: description)
127-
httpClient.request(RegisterUserRequest(), requestBody: requestBody) { result in
128-
switch result {
129-
case let .success(responseBody):
130-
completion(.success(responseBody.convertToUserID()))
131-
case let .failure(error):
132-
completion(.failure(error))
133-
}
134-
}
132+
let responseBody = try await httpClient.request(RegisterUserRequest(), requestBody: requestBody)
133+
return responseBody.convertToUserID()
135134
}
136135
}
137136
```
138137

139138
```swift
140-
VersatileAPIClient.shared.registerUser(name: "Uhooi", description: "Green monster.") { result in
141-
switch result {
142-
case let .success(userID):
143-
// Do something.
144-
case let .failure(error):
145-
// Do error handling.
146-
}
139+
do {
140+
let userID = try await VersatileAPIClient.shared.registerUser(name: "Uhooi", description: "Green monster.")
141+
// Do something.
142+
} catch {
143+
// Do error handling.
144+
print(error)
147145
}
148146
```
149147

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
@available(iOS, introduced: 13.0, deprecated: 15.0, message: "Use the built-in API instead")
7+
extension URLSession {
8+
9+
func data(from request: URLRequest) async throws -> (Data, URLResponse) {
10+
try await withCheckedThrowingContinuation { continuation in
11+
self.dataTask(with: request) { data, response, error in
12+
if let error = error {
13+
return continuation.resume(throwing: error)
14+
}
15+
guard let data = data, let response = response else {
16+
return continuation.resume(throwing: URLError(.badServerResponse))
17+
}
18+
continuation.resume(returning: (data, response))
19+
}.resume()
20+
}
21+
}
22+
23+
}
24+

Sources/HTTPClient/HTTPClient.swift

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,46 @@ public final class HTTPClient {
2020

2121
// MARK: Other Public Methods
2222

23+
/// Send an HTTP request.
24+
/// - Parameters:
25+
/// - requestContents: Request contents.
26+
/// - Returns: Response body.
27+
/// - SeeAlso: ``request(_:requestBody:)``
28+
public func request<T: Request>(_ requestContents: T) async throws -> T.ResponseBody {
29+
let request = try createRequest(requestContents)
30+
return try await self.request(requestContents, request: request)
31+
}
32+
33+
/// Send an HTTP request.
34+
/// - Parameters:
35+
/// - requestContents: Request contents.
36+
/// - requestBody: Request body.
37+
/// - Returns: Response body.
38+
/// - SeeAlso: ``request(_:)``
39+
public func request<T: Request, U: Encodable>(_ requestContents: T, requestBody: U) async throws -> T.ResponseBody {
40+
var request: URLRequest
41+
request = try createRequest(requestContents)
42+
request.httpBody = try JSONEncoder().encode(requestBody)
43+
return try await self.request(requestContents, request: request)
44+
}
45+
2346
/// Send an HTTP request.
2447
/// - Parameters:
2548
/// - requestContents: Request contents.
2649
/// - completion: Completion handler.
2750
/// - SeeAlso: ``request(_:requestBody:completion:)``
51+
@available(iOS, deprecated: 13.0, message: "Consider using asynchronous alternative function")
52+
@available(macOS, deprecated: 10.15, message: "Consider using asynchronous alternative function")
53+
@available(tvOS, deprecated: 13.0, message: "Consider using asynchronous alternative function")
54+
@available(watchOS, deprecated: 6.0, message: "Consider using asynchronous alternative function")
2855
public func request<T: Request>(_ requestContents: T, completion: @escaping (Result<T.ResponseBody, Error>) -> Void) {
29-
let request: URLRequest
3056
do {
31-
request = try createRequest(requestContents)
32-
} catch let error {
57+
let request = try createRequest(requestContents)
58+
self.request(requestContents, request: request, completion: completion)
59+
} catch {
3360
completion(.failure(error))
3461
return
3562
}
36-
37-
self.request(requestContents, request: request, completion: completion)
3863
}
3964

4065
/// Send an HTTP request.
@@ -43,17 +68,20 @@ public final class HTTPClient {
4368
/// - requestBody: Request body.
4469
/// - completion: Completion handler.
4570
/// - SeeAlso: ``request(_:completion:)``
71+
@available(iOS, deprecated: 13.0, message: "Consider using asynchronous alternative function")
72+
@available(macOS, deprecated: 10.15, message: "Consider using asynchronous alternative function")
73+
@available(tvOS, deprecated: 13.0, message: "Consider using asynchronous alternative function")
74+
@available(watchOS, deprecated: 6.0, message: "Consider using asynchronous alternative function")
4675
public func request<T: Request, U: Encodable>(_ requestContents: T, requestBody: U, completion: @escaping (Result<T.ResponseBody, Error>) -> Void) {
47-
var request: URLRequest
4876
do {
77+
var request: URLRequest
4978
request = try createRequest(requestContents)
5079
request.httpBody = try JSONEncoder().encode(requestBody)
51-
} catch let error {
80+
self.request(requestContents, request: request, completion: completion)
81+
} catch {
5282
completion(.failure(error))
5383
return
5484
}
55-
56-
self.request(requestContents, request: request, completion: completion)
5785
}
5886

5987
// MARK: Other Private Methods
@@ -80,6 +108,16 @@ public final class HTTPClient {
80108
return request
81109
}
82110

111+
private func request<T: Request>(_ requestContents: T, request: URLRequest) async throws -> T.ResponseBody {
112+
let (data, response) = try await URLSession.shared.data(from: request)
113+
if let requestError = validateResponse(response) {
114+
throw requestError
115+
}
116+
let jsonDecoder = JSONDecoder()
117+
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
118+
return try jsonDecoder.decode(T.ResponseBody.self, from: data)
119+
}
120+
83121
private func request<T: Request>(_ requestContents: T, request: URLRequest, completion: @escaping (Result<T.ResponseBody, Error>) -> Void) {
84122
URLSession.shared.dataTask(with: request) { data, response, error in
85123
if let error = error {
@@ -100,7 +138,7 @@ public final class HTTPClient {
100138
let responseBody = try jsonDecoder.decode(T.ResponseBody.self, from: data)
101139
completion(.success(responseBody))
102140
return
103-
} catch let error {
141+
} catch {
104142
completion(.failure(error))
105143
return
106144
}

0 commit comments

Comments
 (0)