Networking is a lightweight and powerful HTTP network framework written in Swift by Viktor Gidlöf. It uses await / async
and URLSession
for network calls and can be used as a network layer for any REST API on iOS, macOS, tvOS and watchOS.
- Easy to build server configurations and requests for any REST API
- Clear request and response logging
- URL query and JSON parameter encoding
- Authentication with Basic and Bearer token
- Download files with progress
- Simple and clean syntax
- Await / async
Platform | Min. Swift Version | Installation |
---|---|---|
iOS 13.0+ | 5.4 | CocoaPods, Swift Package Manager |
macOS 10.15+ | 5.4 | CocoaPods, Swift Package Manager |
tvOS 13.0+ | 5.4 | CocoaPods, Swift Package Manager |
watchOS 6.0+ | 5.4 | CocoaPods, Swift Package Manager |
Networking is built around three core components:
The Network.Service
is the main component of the framework that makes the actual requests to a backend.
It is initialized with a server configuration that determines the API base url and any custom HTTP headers based on request parameters.
Start by creating a requestable object. Typically an enum
that conforms to Requestable
:
enum GitHubUserRequest: Requestable {
case user(String)
// 1.
var endpoint: EndpointType {
switch self {
case .user(let username):
return Endpoint.user(username)
}
}
// 2.
var encoding: Request.Encoding { .query }
// 3.
var httpMethod: HTTP.Method { .get }
}
- Define what endpoint type the request should use. More about endpoint types below.
- Define what type of encoding the request will use.
- Define the HTTP method to use.
The EndpointType
can be defined as an enum
that contains all the possible endpoints for an API:
enum Endpoint {
case user(String)
case repos(String)
// ...
}
extension Endpoint: EndpointType {
var path: String {
switch self {
case .user(let username):
return "users/\(username)"
case .repos(let username):
return "users/\(username)/repos"
// ...
}
}
}
Then simply create a server configuration and a new network service and make a request:
let serverConfig = ServerConfig(baseURL: "https://api.github.com")
let networkService = Network.Service(server: serverConfig)
let user = GitHubUserRequest.user("brillcp")
do {
let cancellable = try networkService.request(user)
// The response data type is inferred in the result object
.sink { [weak self] (result: Result<GitHubUser, Error>) in
switch result {
case .success(let user):
// Handle the data
case .failure(let error):
// Handle error
}
}
catch {
// Handle error
}
Every request is logged to the console by default. This is an example of an outgoing request log:
⚡️ Outgoing request to api.github.com @ 2022-12-05 16:58:25 +0000
GET /users/brillcp?foo=bar
Header: {
Content-Type: application/json
}
Body: {}
Parameters: {
foo=bar
}
This is how the incoming responses are logged:
♻️ Incoming response from api.github.com @ 2022-12-05 16:58:32 +0000
~ /users/brillcp?foo=bar
Status-Code: 200
Localized Status-Code: no error
Content-Type: application/json; charset=utf-8
There is also a way to log the pure JSON response for requests in the console. By passing logRespose: true
when making a request, the response JSON will be logged in the console. That way it is easy to debug when modeling an API:
let cancellable = try networkService.request(user, logResponse: true)
Some times an API requires that requests are authenticated. Networking currently supports basic authentication and bearer token authentication.
It involves creating a server configuration with a token provider object. The TokenProvider
object can be any type of data storage, UserDefaults
, Keychain
, CoreData
or other.
The point of the token provider is to persist an authentication token on the device and then use that token to authenticate requests.
The following implementation demonstrates how a bearer token can be retrieved from the device using UserDefaults
, but as mentioned, it can be any persistant storage:
final class TokenProvider {
private static let tokenKey = "com.example.ios.jwt.key"
private let defaults: UserDefaults
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
}
extension TokenProvider: TokenProvidable {
var token: Result<String, TokenProvidableError> {
guard let token = defaults.string(forKey: Self.tokenKey) else { return .failure(.missing) }
return .success(token)
}
func setToken(_ token: String) {
defaults.set(token, forKey: Self.tokenKey)
}
func reset() {
defaults.set(nil, forKey: Self.tokenKey)
}
}
In order to use this authentication token just implement the authorization
property on the requests that require authentication:
enum AuthenticatedRequest: Requestable {
// ...
var authorization: Authorization { .bearer }
}
This will automatically add a "Authorization: Bearer [token]"
HTTP header to the request before sending it. Then just provide the token provider object when initializing a server configuration:
let server = ServerConfig(baseURL: "https://api.github.com", tokenProvider: TokenProvider())
Adding parameters to a request is done by implementing the parameters
property on a request:
enum Request: Requestable {
case getData(String)
// ...
var parameters: HTTP.Parameters {
switch self {
case .getData(let username):
return [
"page": 1,
"username": username
]
}
}
}
Depedning on the encoding
method, the parameters will either be encoded in the url query, in the HTTP body as JSON or as a string.
The encoding
property on a request will encode the given parameters either in the url query or the HTTP body.
var encoding: Request.Encoding { .query } // Encode parameters in the url: `.../users?page=1&username=viktor`
var encoding: Request.Encoding { .json } // Encode parameters as JSON in the HTTP body: `{"page":"1,"name":"viktor"}"`
var encoding: Request.Encoding { .body } // Encode parameters as a string in the HTTP body: `"page=1&name=viktor"`
Making post requests to a backend API is done by setting the httpMethod
property to .post
and provide parameters:
enum PostRequest: Requestable {
case postData(String)
// ...
var httpMethod: HTTP.Method { .post }
var parameters: HTTP.Parameters {
switch self {
case .postData(let username):
return ["page": 1, "username": username]
}
}
}
If you have a custom data model that conforms to Codable
you can use .asParameters()
to convert the data model object to HTTP Parameters
:
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "Günther", age: 69)
let parameters = user.asParameters()
print(parameters) // ["name": "Günther", "age": "69"]
This is useful if you have any data model objects that you want to send as parameters in any requests.
Sometimes it can be useful to just check for a HTTP status code when a response comes back. Use response
to send a request and get back the status code in the response:
let usersRequest = ...
let responseCode = try await networkService.response(usersRequest)
print(responseCode == .ok)
Networking supports all the status codes defined in the HTTP protocol, see here.
You can download files and track progress asynchronously using the downloader.download()
. This function returns a tuple containing the file’s download URL and an AsyncStream<Float>
to observe the progress of the download. The AsyncStream will yield progress updates from 0.0 to 1.0 as the download progresses. When the download completes, the final destination URL is provided.
let url = ...
do {
let downloader = networkService.downloader(url: url!)
let (fileURL, progressStream) = try await downloader.download()
// Track download progress
for await progress in progressStream {
// The download progress: 0.0 ... 1.0
print("Download progress: \(progress * 100)%")
}
// The final destination URL
print("Download completed at: \(fileURL)")
} catch {
// Handle error
print("Download failed: \(error)")
}
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. Once you have your Swift package set up, adding Networking as a dependency is as easy as adding it to the dependencies value of your Package.swift.
dependencies: [
.package(url: "https://github.com/brillcp/Networking.git", .upToNextMajor(from: "0.9.0"))
]
CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Networking into your Xcode project using CocoaPods, specify it in your Podfile:
pod 'Networking-Swift'
The sample project is a small application that demonstrates some of the functionality of the framework. Start by cloning the repo:
git clone https://github.com/brillcp/Networking.git
Open the workspace Networking-Example.xcworkspace
and run.
-
Create an issue if you:
- Are struggling or have any questions
- Want to improve the framework
-
Create a PR if you:
- Find a bug
- Find a documentation typo
Networking is released under the MIT license. See LICENSE for more details.