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