Powering your RAC architecture

Related tags

Networking Reactor
Overview

Build Status Carthage compatible CocoaPods Swift 3.0 Platforms iOS

Intro

Most applications out there follow the same pattern:

  1. Is T persisted and has not expired?
  2. Yes: Use it
  3. No: Fetch T from the network 1. Do we have an internet connection?
    1. Yes: make the Request.
    2. Create T from the network response's data and persist it (send any error that might occur)
    3. Request failed: send an error
    4. No: send an error

If we look carefully the only thing that changes is the T. Reactor provides the whole infrastructure around T with minimum configuration, but with flexibility in mind. In order to achieve that, it uses:

Pros...
  • One of the biggest Pros of Reactor, is how intrinsically forces you to decouple your different components. If your persistence is coupled with your network, Reactor is not for you. 🌳
  • Since Reactor provides most of the insfrastructure out of the box, you can quickly create your entire Model layer. This is useful if you are creating a prototype or a demo. 🚀
  • It removes most of the boilerplate you usually need, when creating a project that follows the flow described in the Intro.
Cons...
  • If you have an unusual flow, that doesn't really fit the flow described in the Intro. ⛔️
  • After checking the Advance usage, Reactor doesn't give you enough flexibility. 😭 😭 If this is the case, please open an issue, so we see what we can do! 👍

How to use

Carthage

github "MailOnline/Reactor"

Cocoapods

# Since there is already a podfile named `Reactor`, we are using `MOReactor`.
pod 'MOReactor', '~> 0.9'

Basic setup

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 ➡️ Model). The second function mapToJSON is the inverse (Model ➡️ JSON).

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:

  1. When you get to a new screen, you try to get some data. In this case, Reactor checks the persistence first and if it fails it will fallback to the network.
  2. You want fresh data, so Reactor will use the network.

The final piece is the Resource, which is nothing more than a struct that encapsulates how the request will be made:

  • path
  • query
  • body
  • HTTP headers
  • HTTP method

Configuration

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>(connection: Connection, 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).

Without Persistence

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()
}

Advance Usage

Intro

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:

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:

  • the loadFromPersistenceFlow will always be called first. If it fails, fetchFromNetwork is called.

The Reactor<T>'s fetchFromNetwork function invariant:

  • the networkFlow will always be called first, if it succeeds it will be followed by saveToPersistenceFlow.

Using 3rd Party Dependencies

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:

  1. Wrap Alamofire with ReactiveCocoa. You can see an example of that here, here and here. This is a fairly trivial task and are plenty of examples out there.
  2. Make the 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.
  3. This will now depend if you have a parser in place or not.
  4. If you do, then you just need to hook up your previously wrapped Alamofire request with it. Ideally you will have a function with the following signature: NSData -> SignalProducer<T, Error> for the parser. Composition then becomes easy: alamofireCall().flatMap(.Latest, transformation: parse) (a concrete example here).
  5. If you don't, you can make use of the 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:

  1. Wrap the dependency with ReactiveCocoa
  2. Make it compatible with flow signature.
  3. Create the ReactorFlow as it suits you.

License

Reactor is licensed under the MIT License, Version 2.0. View the license file

Copyright (c) 2015 MailOnline

Header image by Henrique Macedo.

Comments
  • NSURLComponents path assignment deletes previous path

    NSURLComponents path assignment deletes previous path

    Hi,

    In first place, congrats Reactor is awesome :)

    Well, sometimes based on the environment, my baseURL has a path after the host, i mean for example, staging.api.com/v2/ and for the prod environment, api.com/

    When the NSURLComponents inits, in the staging url it assigns v2 to the path and for example, when i create a Resource to GET popcorns, i do Resource(path: "/popcorns", method: .GET) it will build an URL like staging.api.com/popcorns.

    I found that Resource assigns the path to the generated NSURLComponents which will delete the previous path value.

    My suggestion is that we should append the new value, instead of assign (if any exists).

    So in the Resource class replace components?.path = path with:

    if let componentsPath = components?.path {
        components?.path = componentsPath
            .stringByAppendingString(path)
            .stringByReplacingOccurrencesOfString("//", withString: "/")
     }
    

    Let me know if you agree and want that i create a PR with this.

    Cheers 🍻

    Enhancement 
    opened by portellaa 3
  • Manual Installation

    Manual Installation

    CocoaPods and Carthage are awesome tools and make our life really easier, but there are some devs who still don't know how to use them.

    It would be cool to add the Manual installation guide in your README.md. You can take a look at my iOS Readme Template to see how you can do it.

    opened by lfarah 1
  • Fix possible silent errors on Travis

    Fix possible silent errors on Travis

    Because we're piping xcodebuild through xcpretty we need to guarantee that we exit with same status code as xcodebuild otherwise we can have some false positives caused by xcpretty which always succeeds since he hasn't in consideration the exit code of xcodebuild.

    Reference: https://github.com/supermarin/xcpretty#usage.

    P.S. My apologies, I'm aware that I originated this issue 😞

    opened by dmcrodrigues 1
  • Configuration

    Configuration

    Closes #9

    • [x] Not using persistence
    • [x] Not using Reachability
    • [x] The entire flow should not fail when saveToPersistenceFlow fails, but the network succeeds.
    • [x] The flow using saveToPersistenceFlow as a side effect and not as part of the flow. Right now the entire flows waits until saveToPersistenceFlow is done, we could potentially save some milliseconds.
    • [x] Currently the parser will discard bad objects (when parsing an array) and keeps going. It would be good to allow the user to fail the entire flow when the parser can't parse a single element.
    opened by RuiAAPeres 0
  • ReactorConfiguration

    ReactorConfiguration

    It should be possible to use a ReactorConfiguration in order to configure certain behaviours, for example:

    • Not using persistence
    • Not using Reachability
    • The flow not failing when saveToPersistenceFlow fails, but the network succeeds. 🎉
    • The flow using saveToPersistenceFlow as a side effect and not as part of the flow. Right now the entire flows waits until saveToPersistenceFlow is done, we could potentially save some milliseconds.😅
    • Currently the parser will discard bad objects (when parsing an array) and keeps going. It would be good to allow the user to fail the entire flow when the parser can't parse a single element. ✨

    I envision this with as a simple ReactorConfiguration struct being passed to the Reactor<T> initializer.

    Work in Progress Feature 
    opened by RuiAAPeres 0
  • Experimentations (possible v3.0)

    Experimentations (possible v3.0)

    Intro

    With version 2.0 for every resource we would access (via Reactor's fetch), it would be "loaded from" and "saved to" to the same place. This was due to how InDiskPersistence was created (with a persistencePath). Having a resource as input for the fetch, is wrong as well, because it implies that there is a different behaviour, depending on the resource, in terms of persistence. The correct way, would be to fix the resource when the Reactor is created.

    Example

    A ViewModel that accesses different sections: Sports, Fashion and World news. With 2.0 we would need a reactor for each section. With this new approach, we only need one instance.

    Changes

    From a code perspective:

    1. The InDiskPersistenceHandler, doesn't have any property now. It could actually be converted to free functions.
    2. The ReactorFlow is no longer responsible for deciding if persistence should be used or not.
    3. The Reactor methods, now have the responsibility to decide if it should be persisted and if so, what input they should receive.
    opened by RuiAAPeres 0
Releases(3.0)
Owner
Rui Peres
Software Engineer 🛠 | Ultra Trail Runner 🏃‍♂️🏔 "Technology as a means to an end 🚀"
Rui Peres
GitHub iOS client in RxSwift and MVVM-C clean architecture

GitHub iOS client in RxSwift and MVVM-C clean architecture. FlutterHub - Flutter version available at an early stage KotlinHub - Android version is co

Khoren Markosyan 2.7k Jan 7, 2023
A communication channel from your Mac to your watch.

Stargate A communication channel from your Mac to your watch. Providing a convenient wrapper around MMWormhole and PeerKit, Stargate leverages Multipe

Contentful 135 Jun 29, 2022
StatusBarOverlay will automatically show a "No Internet Connection" bar when your app loses connection, and hide it again. It supports apps which hide the status bar and The Notch

StatusBarOverlay StatusBarOverlay will automatically show a "No Internet Connection" bar when your app loses connection, and hide it again. It support

Idle Hands Apps 160 Nov 2, 2022
Another network wrapper for URLSession. Built to be simple, small and easy to create tests at the network layer of your application.

Another network wrapper for URLSession. Built to be simple, small and easy to create tests at the network layer of your application. Install Carthage

Ronan Rodrigo Nunes 89 Dec 26, 2022
This generic SOAP client allows you to access web services using a your iOS app, Mac OS X app and AppleTV app.

This generic SOAP client allows you to access web services using a your iOS app, Mac OS X app and Apple TV app. With this Framework you can create iPh

Prioregroup.com 479 Nov 22, 2022
Get your device ip address, router ip or wifi ssid

FGRoute FGRoute is written on C and Objective C (includes Swift support), it helps developers to get rid of dealing with WiFi interfaces. Example To r

Arthur Sahakyan 137 Dec 5, 2022
Setup your class structure in Xcode Interface Builder and save() in Parse Server.

ISParseBind With ISParseBind you can save, update and query PFObjects using the power of Xcode Interface Builder resources. https://www.youtube.com/wa

Weni 10 Mar 28, 2022
iOS client for the Brewfactory project - brew your own beer

BrewMobile iOS client for the Brewfactory project. Read the stories of upgrading BrewMobile to ReactiveCocoa & Swift on AllTheFlow. What is this? App

brewfactory 195 Dec 18, 2022
A remote for your IR devices for iOS and Mac!

Command your TV, Apple TV or Receiver with your Mac/iOS device through iTach. Screenshots On iOS: On Mac (notification center): How to use Buy a iTach

Michael Villar 19 Nov 4, 2022
The fastest iOS app to add a note to your email inbox

Major Key How often are you on the run, or hanging out with friends, only to suddenly think of this really important thing you need to do when you're

Felix Krause 190 Oct 9, 2022
An iOS app for communicating with your clightning node over the lightning network

An iOS app for communicating with your clightning node over the lightning network

William Casarin 18 Dec 14, 2022
A super fast & convenient object mapper tailored for your needs

A super fast & convenient object mapper tailored for your needs. Mapping objects to arrays or dictionaries can be a really cumbersome task, but those

Christoffer Winterkvist 246 Sep 9, 2022
VFNetwork is a protocol-oriented network layer that will help you assemble your requests in just a few steps.

Simple, Fast and Easy. Introduction VFNetwork is a protocol-oriented network layer that will help you assemble your requests in just a few steps. How

Victor Freitas 4 Aug 22, 2022
iOS app for monitoring and controlling your Tesla vehicles.

Teslawesome This is an unofficial iOS app for monitoring and controling your Tesla vehicles. The purpose of being open sourced is just for more visibi

Ivaylo Gashev 2 Oct 14, 2022
Best architecture for SwiftUI + CombineBest architecture for SwiftUI + Combine

Best architecture for SwiftUI + Combine The content of the presentation: First of the proposed architectures - MVP + C Second of the proposed architec

Kyrylo Triskalo 3 Sep 1, 2022
Mvi Architecture for SwiftUI Apps. MVI is a unidirectional data flow architecture.

Mvi-SwiftUI If you like to read this on Medium , you can find it here MVI Architecture for SwiftUI Apps MVI Architecture Model-View-Intent (MVI) is a

null 12 Dec 7, 2022
Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Tiago Henriques 0 Oct 2, 2022
PJFDataSource is a small library that provides a simple, clean architecture for your app to manage its data sources while providing a consistent user interface for common content states (i.e. loading, loaded, empty, and error).

PJFDataSource PJFDataSource is a small library that provides a simple, clean architecture for your app to manage its data sources while providing a co

Square 88 Jun 30, 2022
Visualize your dividend growth. DivRise tracks dividend prices of your stocks, gives you in-depth information about dividend paying stocks like the next dividend date and allows you to log your monthly dividend income.

DivRise DivRise is an iOS app written in Pure SwiftUI that tracks dividend prices of your stocks, gives you in-depth information about dividend paying

Kevin Li 78 Oct 17, 2022
Switshot is a game media manager helps you transfer your game media from Nintendo Switch to your phone, and manage your media just few taps.

Switshot is a game media manager helps you transfer your game media from Nintendo Switch to your phone, and manage your media just few taps.

Astrian Zheng 55 Jun 28, 2022