TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | May 2016 |
SPMSupports SPM | ✗ |
Maintained by Rui Peres.
Reactor provides a Model layer with minimum configuration. It makes use of the following elements to achieve that:
Reactor then uses flows (represented by the ReactorFlow<T>
), that are typically seen in applications. For example:
This particular flow is provided out of the box by Reactor. In the future we will provide others.
A flow is nothing more than a stream of events, in our case, that is composed by different pieces ( network, parsing and persistence ).
ReactorFlow<T>
. For Reactor to work, you need to make sure your Model objects comply with the Mappable
protocol. This protocol allows you to encode and decode an object. This is necessary for parsing the object (coming from the network) and storing it on disk.
Let’s use the Author
struct as an example (this can be found in the Unit tests). The first step is to make the Author
struct compliant with the Mappable
protocol:
struct Author {
let name: String
}
extension Author: Mappable {
static func mapToModel(object: AnyObject) -> Result<Author, MappedError> {
guard
let dictionary = object as? [String: AnyObject],
let name = dictionary["name"] as? String
else { return Result(error: MappedError.Custom("Invalid dictionary @ \(Author.self)\n \(object)"))}
let author = Author(name: name)
return Result(value: author)
}
func mapToJSON() -> AnyObject {
return ["name": self.name]
}
}
Note: The above implementation, is just an example, you are free to use whatever means you prefer.
The first function mapToModel
is what allows a model object to be created (JSON mapToJSON
is the inverse (Model
The second step would be:
let baseURL = NSURL(string: "https://myApi.com")!
let configuration = FlowConfiguration(persistenceConfiguration: .Enabled(withPath: "path_to_persistence"))
let flow: ReactorFlow<Author> = createFlow(baseURL, configuration: configuration)
let reactor: Reactor<Author> = Reactor(flow: flow)
Now that you have the reactor
ready, it exposes two functions:
func fetch(resource: Resource) -> SignalProducer<T, Error>
func fetchFromNetwork(resource: Resource) -> SignalProducer<T, Error>
We find that these are the two most common scenarios:
The final piece is the Resource
, which is nothing more than a struct that encapsulates how the request will be made:
For extra flexibility, you can use the CoreConfiguration
and FlowConfiguration
protocols.
The CoreConfiguration
protocols define how the Reactor behaves:
public protocol CoreConfiguration {
/// When enabled, you should pass the path where it will be stored
/// Otherwise it's disabled
var persistenceConfiguration: PersistenceConfiguration { get }
/// If the `saveToPersistenceFlow`, should be part of the flow.
/// Should be `false` when the flow shouldn't
/// wait for `saveToPersistenceFlow` to finish (for example it takes
/// a long time).
/// Note: if you set it as `false` and it fails, the failure will be
/// lost, because it's not part of the flow, but injected instead .
/// `true` by default.
var shouldWaitForSaveToPersistence: Bool { get }
}
The FlowConfiguration
protocol define how the Reactor Flow is created:
public protocol FlowConfiguration {
/// If persistence should be used.
/// `true` by default.
var usingPersistence: Bool { get }
/// If reachability should be used.
/// `true` by default.
var shouldCheckReachability: Bool { get }
/// If the parser should be strict or prune the bad objects.
/// Pruning will simply remove objects that are not parseable, instead
/// of erroring the flow. Strict on the other hand as soon as it finds
/// a bad object will error the entire flow.
/// Note: if you receive an entire batch of bad objects, it will default to
/// an empty array. Witch leads to not knowing if the server has no results or
/// all objects are badly formed.
/// `true` by default.
var shouldPrune: Bool { get }
}
The FlowConfiguration
protocol is used in the following methods:
public func createFlow<T where T: Mappable>(baseURL: NSURL, configuration: FlowConfigurable) -> ReactorFlow<T>
public func createFlow<T where T: Mappable>(connection: Connection, configuration: FlowConfigurable) -> ReactorFlow<T>
public func createFlow<T where T: SequenceType, T.Generator.Element: Mappable>(baseURL: NSURL, configuration: FlowConfigurable) -> ReactorFlow<T>
public func createFlow<T where T: SequenceType, T.Generator.Element: Mappable>(baseURL: NSURL, configuration: FlowConfigurable) -> ReactorFlow<T>
These are convenient methods, that provide a ready to use ReactorFlow
. It’s important to note, that if you would like to use a custom persistence (CoreData, Realm, SQLite, etc), you should create a ReactorFlow
on your own. The reason why, is because the default Persistence class (InDiskPersistence.swift
) takes a path, where the data will be saved. This might not make sense with other approaches (please check Using 3rd Party Dependencies section).
If it doesn’t make sense to persist data, you can:
let baseURL = NSURL(string: "https://myApi.com")!
let configuration = FlowConfiguration(persistenceConfiguration: .Disabled)
let flow: ReactorFlow<Foo> = createFlow(baseURL, configuration: configuration)
let reactor: Reactor<Foo> = Reactor(flow: flow)
As for the mapToJSON
function, you can simply return an NSNull
:
func mapToJSON() -> AnyObject {
return NSNull()
}
In order to make most of Reactor, keep the following in mind (these are ReactorFlow<T>
’s properties):
var networkFlow: Resource -> SignalProducer<T, Error>
var loadFromPersistenceFlow: Void -> SignalProducer<T, Error>
var saveToPersistenceFlow: T -> SignalProducer<T, Error>
All three properties are mutable (var
) on purpose, so you can extend specific behaviours. For example, you might be interested in knowing why loadFromPersistenceFlow
is failing and log it. With the default flow, this is not possible to do, because if loadFromPersistenceFlow
fails, the network flow will kick in and the error is lost.
A way to accomplish this, is by creating a default flow and then extending it:
let reactorFlow: ReactorFlow<Author> = ...
let extendedPersistence = reactorFlow.loadFromPersistenceFlow().on(failure: { error in print(error) })
reactorFlow.loadFromPersistenceFlow = { extendedPersistence }
You can further decompose the flow, since all the core pieces are exposed in the public API. More specifically:
Network
and the Connection
protocolParser
InDiskPersistenceHandler<T>
The default flow provided by Reactor (Intro) is something you are welcome to use, but not tied to. Keep in mind the following when creating your own flows:
The Reactor<T>
’s fetch
function invariant:
loadFromPersistenceFlow
will always be called first. If it fails, fetchFromNetwork
is called.The Reactor<T>
’s fetchFromNetwork
function invariant:
networkFlow
will always be called first, if it succeeds it will be followed by saveToPersistenceFlow
.Reactor plays quite well with other dependencies and requires minimum effort from your side. In the previous section, we saw the three essencial pieces of a ReactorFlow
:
var networkFlow: Resource -> SignalProducer<T, Error>
var loadFromPersistenceFlow: Void -> SignalProducer<T, Error>
var saveToPersistenceFlow: T -> SignalProducer<T, Error>
As mentioned, we encourage you to modify them to suit your needs. With 3rd party dependencies, you have to do exactly that. As an example, these could be the steps you would go through in order to make Alamofire compatible:
NSError
used by the approaches previously mentioned into an Error
. You can use the mapError
operator. You should then transform it into an Error.Network
.NSData -> SignalProducer<T, Error>
for the parser. Composition then becomes easy: alamofireCall().flatMap(.Latest, transformation: parse)
(a concrete example here).Mappable
protocol and the parse
function provided by Reactor. Once you have that, you can follow this.With all this in place, the final piece is:
let persistenceHandler = InDiskPersistenceHandler<MyModel>(persistenceFilePath: persistencePath)
let loadFromPersistence = persistenceHandler.load
let saveToPersistence = persistenceHandler.save
let reactorFlow: ReactorFlow<MyModel> = ReactorFlow(network: myNetworkFlow, loadFromPersistenceFlow: loadFromPersistence, saveToPersistence: saveToPersistence)
The createFlow
family methods follow this approach internally, so you should check them out.
Other 3rd party dependencies will follow the same approach:
ReactorFlow
as it suits you. Another benefit of this approach, is that it enforces a clear separation and decoupling of your code. If you have persistence coupled with your network, it will be quite difficult to use Reactor.
Reactor is licensed under the MIT License, Version 2.0. View the license file
Copyright © 2015 MailOnline
Header image by Henrique Macedo.