NgNetworkModule

1.0.4

iOS Networking Module
netguru/ng-ios-network-module

What's New

Release 1.0.4

2023-02-28T11:48:51Z

Sample App simplifications and refactoring

Netguru Networking Module

Welcome to the Netguru Networking Module.
NgNetworkModule is simple, highly configurable iOS and MacOS module to execute network calls and parse received answers.

Main Features

  • Pure Swift
  • Intuitive, simple API
  • Protocol-oriented implementation
  • Extensive Unit Tests coverage
  • Support for reactive programming (Combine)
  • Support for modern Swift concurrency (async/await)
  • Extendable with configurable actions

Integration

Requirements

  • iOS 13+
  • macOS 10.15+

Adding module to your app

NgNetworkModule is available through Swift Package Manager.

  • When added as a package dependency:
dependencies: [
        .package(
            url: "https://github.com/netguru/ng-ios-network-module.git",
            from: "1.0.3"
        )
    ]
  • When added as a project dependency:
1. Select File > Swift Packages > Add Package Dependency.
2. Enter the repository URL.
3. Wait until the dependencies are resolved.

Using NgNetworkModule

Available API variants:

The module is available in 3 variants:

  • Network Module Core - a base module, leveraging callback-based API to execute a network request and return an answer in a callback.
import NgNetworkModuleCore
  • Reactive API (Combine) - a reactive API emitting an answer as an event, or an error. Requires importing NgNetworkModuleCore to work!
import ReactiveNgNetworkModule
  • Concurrent API (async/await) - an API utilising modern Swift concurrency. Requires importing NgNetworkModuleCore to work!
import ConcurrentNgNetworkModule

Creating network module:

import NgNetworkModuleCore
(optionally) import ReactiveNgNetworkModule / ConcurrentNgNetworkModule
...
let baseURL = URL(string: "https://your.base.url")!
let requestBuilder = DefaultRequestBuilder(baseURL: baseURL)
let networkModule = DefaultNetworkModule(requestBuilder: requestBuilder)

Creating and using automatic request / response parsing:

In order to parse correctly, the request description must conform to NetworkRequest protocol:

struct SampleGetNetworkRequest: NetworkRequest {
    let path = "/welcome"
    let method = NetworkRequestType.get
}

or

struct SamplePostNetworkRequest: NetworkRequest {
    let method = NetworkRequestType.post
    let path = "/welcome"
    let body: Encodable? = ["foo": "bar"]
}

or

struct SamplePostNetworkRequest2: NetworkRequest {
    let method = NetworkRequestType.post
    let path = "/welcome2"
    let bodyData: Data? = Data()
}

Having defined such a request, you may use convenience API to encode and call it, (optionally) decoding received response:

struct ExpectedResponse: Decodable {}
...
let request = SampleGetRequest()
...
networkModule.performAndDecode(request: request, responseType: ExpectedResponse.self)
    switch result {
    case let .success(response):
        // Handle decoded response here
    case let .failure(error):
        // Handle error here
    }
}

Available APIs

  • Default API - a classic API to execute a network request and return an answer in a callback:
networkModule.perform(request: networkRequest) { result in
networkModule.perform(urlRequest: urlRequest) { result in
networkModule.performAndDecode(urlRequest: urlRequest, responseType: ExpectedResponse.self) { result in
networkModule.performAndDecode(request: networkRequest, responseType: ExpectedResponse.self) { result in
    switch result {
    case let .success(response):
        // Handle response here
    case let .failure(error):
        // Handle error here
    }
}
  • Reactive API (Combine) - a reactive API emitting an answer as an event, or an error:
    No need to add .receive(on: DispatchQueue.main) as it is guaranteed by the NgNetworkModuleCore design.
perform(request: networkRequest)
perform(urlRequest: urlRequest)
performAndDecode(request: networkRequest, responseType: ExpectedResponse.self)
performAndDecode(urlRequest: urlRequest, responseType: ExpectedResponse.self)
        .sink(
            receiveCompletion: { completion in
                switch completion {
                case .finished:
                    // Handle completion here (optionally)
                case let .failure(error):
                    // Handle error here
                }
            },
            receiveValue: { response in
                // Handle response here
            })
        .store(in: &cancellables)
  • Concurrent API (async/await) - an API utilising modern Swift concurrency:
do {
    let response = try await networkModule.perform(request: networkRequest)
    let response = try await networkModule.perform(urlRequest: urlRequest)
    let response = try await networkModule.performAndDecode(urlRequest: urlRequest, responseType: ExpectedResponse.self)
    let response = try await networkModule.performAndDecode(request: networkRequest, responseType: ExpectedResponse.self)
    // Handle response here
} catch {
    // Handle error here
}

Advanced use cases

  • Adding authentication token to the request:
let accessTokenProvider = ... // Implement AuthenticationTokenProvider or just use String
let accessTokenHeaderName = "Authorization"
addTokenAction = AddAuthenticationTokenNetworkModuleAction(
	authenticationTokenProvider: accessTokenProvider, 
	authenticationTokenHeaderFieldName: accessTokenHeaderName
)
...
let networkModule = DefaultNetworkModule(
    requestBuilder: requestBuilder,
    actions: [addTokenAction] // <-- Include additional Actions here
)
...
networkModule.perform(request: networkRequest) { ... }
  • Defining custom pre/post execution request actions:
/// An Action extracting an updated Access Token from a received Network Response:
struct RetrieveAccessTokenAction: NetworkModuleAction {
    private let accessTokenHeaderName = "Access-token" 
        
    func performBeforeExecutingNetworkRequest(request: NetworkRequest?, urlRequest: inout URLRequest) {
        // Noop
    }

    func performAfterExecutingNetworkRequest(request: NetworkRequest?, networkResponse: NetworkResponse) {
        guard let headerFields = networkResponse.networkResponse.allHeaderFields,
           let accessToken = headerFields[accessTokenHeaderName] as? String else {
            return
        }
        // Save retrieved access token
    }
}

Adding the Action to the Network Module:

let action = RetrieveAccessTokenAction()
let networkModule = DefaultNetworkModule(
	requestBuilder: requestBuilder,
	actions: [action] // <-- Include additional Actions here
)

Error handling

  • NetworkError.requestParsingFailed
    Description: The request builder failed to parse the NetworkRequest structure into an URLRequest.
    Solution: Ensure that the non-trivial request properties (especially parameters, body or bodyData) are properly defined.
  • NetworkError.responseParsingFailed
    Description: Although the request succeeded, the NetworkModule failed to decoded received response into a provided data type.
    Solution: Ensure an expected response type matches what is returned from the backend.
  • NetworkError.noResponseData
    Description: Although the request succeeded, the received response contained no data to decode.
    Solution: Ensure the endpoint you try to call is supposed to return a non-empty response.
  • NetworkError.serverError(code:message:)
    Description: The request caused a server error.
    Solution: Report the error to the backend team.
  • NetworkError.notFound
    Description: The requested endpoint cannot be found.
    Solution: Ensure the request path is correct.
  • NetworkError.forbidden
    Description: The requested endpoint requires authentication.
    Solution: Ensure that you included a valid Access Token in the request.
  • NetworkError.unauthorized
    Description: The provided access token does not allow you to access the requested resource.
    Solution: Reach out to the backend team and verify access level for the resource.
  • NetworkError.invalidRequest(code:message:)
    Description: Any other request-related error (4XX error code).
    Solution: Ensure the outgoing request is according to the backend specification.
  • NetworkError.cancelled
    Description: The request has been cancelled by the user.
    Solution: Not treated as an error!

Sample content

Running sample application

There is a sample application showcasing the NgNetworkModule versatility. It can be found in NetworkModuleSampleApp folder and opened using the Xcode project file. The app uses SwiftFormat dependency (obtained from the NgNetworkModule) to ensure proper code formatting.
The application is based on free API service for Final Space animated series. It allows you to go through the list of episodes, see the episode details and characters that appeared there. It is written 100% in SwiftUI, targeting iOS 16. Points of interests:

  • HomeScene - a main app view, allowing to select which of the APIs exposed by the NgNetworkModule (classic, reactive or async/await) should be used to obtain data from the Final Space API service.
  • EpisodeListViewModel - a view model for EpisodeListScene, showcasing obtaining, parsing and providing episodes list data to the view.
  • EpisodeViewModel - a view model for EpisodeScene, showcasing obtaining, parsing and providing data necessary to render episode details view (mostly character data).

Running sample playground

Alternatively, within NetworkModuleSampleApp there is a Playground folder containing a playground file also showcasing the NgNetworkModule API.
It uses several, internal source files, e.g. FetchCharacterRequest and FetchCharacterResponse to increase code readability.

Contributing

If you would like to help us improve the module, don't hesitate to contribute!

Authors & project maintainers

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License.

Related repositories

Description

  • Swift Tools 5.4.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sat Oct 19 2024 10:39:18 GMT-0900 (Hawaii-Aleutian Daylight Time)