Nomosi 0.1.13

Nomosi 0.1.13

Maintained by Mario Iannotta.



Nomosi 0.1.13

Nomosi: Declarative plug and play network services for your iOS apps

Version License Platform Carthage compatible

Why

Today every app is connected to some backend(s), usually that's achieved through a network layer, generally a singleton, that has the responsibility to take an input, perform a network request, parse the response and return a result.

In complex projects this approach could cause the network layer to be a massive and unmaintainable file with more than 20.000 LOC. Yes, that's a true story.

The idea behind Nomosi is to split the network layer into different services where every service represents a remote resource.

Each service is indipendent and atomic making things like module-based app development, client api versioning, working in large teams, testing and maintain the codebase a lot easier.

Features

Declarative functional syntax

The core object of Nomosi is a Service, declared as Service<Response: ServiceResponse> aka a generic class where the placeholder Response conforms the protocol ServiceResponse.

In this way instead of having a singleton that handle tons of requests, you'll have different services and it's immediatly clear what you should expect from each service.

After setting the required properties (url, method, etc..), by calling the load() function a new request will be performed. It is also possible to chain multiple actions like onSuccess, onFailure, addingObserver in a fancy functional way.

Example:

/**
  The service class: a resource "blueprint", here it is possible to set endpoint, cache policy, log level etc...
*/
class AService<AServiceResponse>: Service<Response> {

    init() {
        super.init()
        basePath = "https://api.a-backend.com/v1/resources/1234"
        cachePolicy = .inRam(timeout: 60*5)
        log = .minimal
        decorateRequest { [weak self] completion in
            // here you can decorate the request as you wish,
            // for example you can place here the token refresh logic
            // it is possible to pass a ServiceError to the completion block or nil
            completion(nil)
        }
    }

}

/** 
  The service response, since it conforms `Decodable`, there's no need to implement the parse function.
*/
struct AServiceResponse: Decodable {
    var aPropertyOne: String?
    var aPropertyTwo: String?
}

// callback-based approach
AService()
    .load()
    .onSuccess { response in
        // response is an instance of `AServiceResponse`: Type-safe swift superpower!
    }
    .onFailure { error in
        // handle error
    }
}

// async/await-based approach
do {
    let response = try await AService().load()
    // response is an instance of `AServiceResponse`: Type-safe swift superpower!
    print(response)
} catch {
    print(error)
}

Type-safe by design

Leveraging Swift's type system and latest features, with Nomosi you won't ever need to handle JSON and mixed data content directly. You can forget about third party libraries such as Marshal and SwiftyJSON.

Easy to decorate (eg: token refresh) and/or invalidate requests

Handling tokens and requests validation could be tricky. That's why the closure decorateRequest has been introduced.

The closure is called just before the network task is started and, using the completion block, it's possible to invalidate or decorate the request that is about to be performed.

Example:

class TokenProtectedService<ServiceResponse>: Service<Response> {

    init() {
        super.init()
        basePath = "https://api.aBackend.com/v1/resources/1234"
        decorateRequest { [weak self] completion in
            AuthManager.shared.retrieveToken { token in
                if let token = token {
                    self?.headers["Authorization"] = token
                    completion(nil)
                } else {
                    completion(ServiceError(code: 100, reason: "Unable to retrieve the token"))
                }
            }
        }
    }
    
}

Straightforward cache configuration with the layer of your choice (`URLCache` by default)

Cache is handled with the protocol CacheProvider.

URLCache already conforms this protocol and with the podspec Nomosi/CoreDataCache you can use CoreData as persistent storage.

If you want to use another persistent layer library (Realm, CouchBase, etc...) you have to implement just three methods:

func removeExpiredCachedResponses()

func loadIfNeeded(request: URLRequest,
                  cachePolicy: CachePolicy,
                  completion: ((_ data: Data?) -> Void))

func storeIfNeeded(request: URLRequest,
                  response: URLResponse,
                  data: Data,
                  cachePolicy: CachePolicy,
                  completion: ((_ success: Bool) -> Void))

Discard invalid or redundant requests

Nomosi ensure that every performed request is valid and unique.

For exampe, if you call two time the load() method on the same service, only one request will be performed, you'll receive a reduntant request error for the second one.

Mock support

Mock are handled with the protocol MockProvider defined as it follows:

protocol MockProvider {

    var isMockEnabled: Bool { get }
    var mockedData: DataConvertible? { get }
    var mockBundle: Bundle? { get }

}

By default mock are retieved by searching for files in the bundle named ServiceName.mock.

Example

// UserService.swift
class UserService<User>: Service<Response> {

    init(userID: Int) {
        super.init()
        basePath = "https://api.aBackend.com/v1/users/\(userID)"
    }

}
// User.swift
struct User {
    let name: String
    let surname: String
    let website: String
}
// UserService.mock
{
    "name": "Mario",
    "surname": "Iannotta",
    "website": "http://www.marioiannotta.com"
}

Develop and attach thirdy part components

Any class that conforms the protocol ServiceObserver can be notified when a request starts and ends; all the UI components such as loader and fancy buttons are built using this protocol.

Prebaked UI Components

Installing Nomosi/UI you can use different prebaked components, such as:

  • NetworkActivityIndicatorHandler to handle the network activity indicator in the status bar.
  • RemoteImageService to load, efficiently cache and display remote images in imageviews using custom loaders and placeholders.
  • ServiceOverlayView to handle loaders while performing requests and display any occurred errors alongside the retry button.
  • ServiceObserverButton to perform custom animations (resize, show loader, hide content etc...) on buttons while performing requests.


For an extensive overview about how all of that works, you can take a look at the service flow chart.

Installation

Cocoapods

pod 'Nomosi'

Carthage

github "MarioIannotta/Nomosi"

License

Nomosi is available under the MIT license. See the LICENSE file for more info.

TODOs:

  • Document all the public stuff
  • Support async/await (aka Swift concurrency)
  • Add unit tests
  • Add ssl pinning support
  • CoreData CacheProvider
  • Download requests
  • Upload requests
  • Add a way to mock services
  • Providing a generic interface for the cache so it's possible to use any storage layer by implementing just the methods loadIfNeeded and storeIfNeeded
  • UIImageView.Placeholder doesn't seems to work fine with cell reuse
  • Add status bar activity indicator
  • Split pod in podspec (Core + UI)
  • Provide a dictionary as body
  • Http status code range validation