Welcome to Beaver

Join the chat at https://gitter.im/BeaverFramework/Lobby Build status

A framework to help you build your application fast and clean!

Introduction

Beaver is a framework which includes everything you need to create your iOS applications in Swift. It aims to set standards in order to make iOS development easier, more scalable and fully testable.

Features

What Beaver can help you with:

On the other hand, Beaver can’t help you make your code shorter by doing some obscure magic for you. Beaver is not a library solving specific problems, it is a framework guiding you to make the right choices when developing your application so it can scale and stay easy to maintain.

Architecture

Let’s see what an App built with Beaver is made of:

What is a module made of?

Unidirectional data flow

Beaver’s architecture implements a strict unidirectional data flow. The flow begins with a ui action, which is dispatched to the store by the view controller. The store asks a state update to the reducer. The reducer applies the application’s business logic based on the current state and the received action. The store updates the state, and propagates it throughout the application to refresh the views, or to give the presenters the opportunity to dispatch a routing action. If that’s the case, the store asks a state update for this routing action to the reducer, and propagates the new state throughout the application, causing the concerned presenters to present a view.

Unidirection Data Flow

Project structure

A fresh Beaver project is structured like so:

$ tree
NewProject/
├── App
│   ├── AppDelegate.swift
│   ├── AppPresenter.swift
│   ├── AppReducer.swift
│   └── Info.plist
├── AppTests
│   └── Info.plist
├── Cakefile
├── Module
│   ├── Core
│   │   ├── Cakefile.rb
│   │   ├── Core
│   │   │   ├── AppAction.swift
│   │   │   ├── AppState.swift
│   │   │   ├── HomeAction.swift
│   │   │   ├── HomeState.swift
│   │   │   └── Info.plist
│   │   ├── CoreTests
│   │   │   └── Info.plist
│   │   └── Podfile.rb
│   └── Home
│       ├── Cakefile.rb
│       ├── Home
│       │   ├── HomeAction.swift
│       │   ├── HomePresenter.swift
│       │   ├── HomeReducer.swift
│       │   ├── HomeViewController.swift
│       │   └── Info.plist
│       ├── HomeTests
│       │   └── Info.plist
│       └── Podfile.rb
└── Podfile

Show me some code

Let’s write a very simple module showing a list of cells, and presenting another module when the user taps one of them.

First, our module needs to define its routing events. That’s how we’ll be able to interact with it. For now, let’s define the start and stop actions.

public protocol HomeAction: Beaver.Action {
}

public enum HomeRoutingAction: HomeAction {
    case start
    case stop
}

Then, our module needs a state. We want to show movies in a list, so let’s define an array of movie titles.

public struct HomeState: Beaver.State {
    public var movies: [String]?
    
    public init() {
    }
}

Note that these two classes are defined public because they need to be accessible by the rest of the app. They are built with the Core framework for that purpose.

The next step is to write our reducer. It will build the state with the data we need to show.

public struct HomeReducer: Beaver.ChildReducing {
    public typealias ActionType = HomeAction
    public typealias StateType = HomeState

    public init() {
    }

    public func handle(action: HomeAction,
                       state: HomeState,
                       completion: @escaping (HomeState) -> ()) -> HomeState {
        var newState = state

        switch ExhaustiveAction<HomeRoutingAction, HomeUIAction>(action) {
        case .routing(.start):
            newState.movies = (0...10).map { "Movie \($0)" }
            
        case .routing(.stop):
            newState.movies = nil

        case .ui:
            break
        }
        
        return newState
    }
}

When starting, the AppPresenter will send a start routing event to our module, which will result in our reducer generating 10 movie titles. In the other hand, when stoping, we reset to an empty state.

The ExhaustiveAction permits to exhaustively write one case per action in order to make sure that no action is left unhandled.

Also note that this class is built with the Home framework and declared public so that the AppReducer is able to access to it from the App target.

Let’s write our view now. The architecture of Beaver is made so the UIViewController is made very simple. It basically only handles the UI logic, dispatches UI events throughout the module and build the UIViews.

final class HomeViewController: Beaver.ViewController<HomeState, AppState, HomeUIAction>, UITableViewDataSource {
    let tableView: UITableView = ...

    override func stateDidUpdate(oldState: HomeState?,
                                 newState: HomeState,
                                 completion: @escaping () -> ()) {
        if oldState != newState {
            tableView.reloadData()
        }
        completion()
    }
        
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return state.movies?.count ?? 0
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        cell.textLabel?.text = state.movies?[indexPath.row].title
        
        return cell
    }
}

This controller basically only gets the movie titles from the state, and shows them in a table view.

Note that this class is declared internal and built with the Home module so it can’t be accessed by other modules.

Now let’s write our presenter, so we can actually show our view to the screen.

public final class HomePresenter: Beaver.Presenting, Beaver.ChildStoring {
    public typealias StateType = HomeState
    public typealias ParentStateType = AppState

    public let store: ChildStore<HomeState, AppState>

    public let context: Context

    public init(store: ChildStore<HomeState, AppState>,
                context: Context) {
        self.store = store
        self.context = context
    }
}

extension HomePresenter {
    public func stateDidUpdate(oldState: HomeState?,
                               newState: HomeState,
                               completion: @escaping () -> ()) {

        switch (oldState?.movies, newState.movies) {
        case (.none, .some):
            let homeController = HomeViewController(store: store)
            context.present(controller: homeController, completion: completion)

        case (.some, .none):
            context.dismiss(completion: completion)
        }
    }
}

The presenter is a subscriber of the store. Therefore, we can handle presentation in the stateDidUpdate(oldState:newState:completion:) method.

We use the context to present and dismiss our controller. Context is a protocol that can have different implementations of present(controller:) and dismiss(). For example NavigationContext know how to push and pop a controller when ModalContext knows how to present and dismiss a controller as a modal.

Note that this presenter is built with the Home module and declared public so it can be accessed by the AppPresenter.

Now, we have the ability to show a list of movies, through our Home module, but nothing happen yet when tapping a cell. Let’s assume we have a MovieCard module defining the following routing actions.

public enum MovieCardRoutingAction: MovieCardAction {
    case start(title: String)
    case stop
}

When sending the action start(title:), it would present a controller showing the movie title. To do that, let’s begin by writing our module ui actions, so we can handle the tap of a cell.

enum HomeUIAction: HomeAction {
    case didTapOnMovieCell(title: String)
}

Note that this enum is declared internal and built in the Home module.

Now, let’s dispatch this action when the user taps a cell.

final class HomeViewController: Beaver.ViewController<HomeState, AppState, HomeUIAction>, UITableViewDataSource, UITableViewDelegate {
    ...

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let movies = state.movies, movies.count > indexPath.row else {
            fatalError("State inconsticency: the selected movie does not exist in state.")
        }

        let movie = movies[indexPath.row]

        controller.dispatch(action: .didTapOnMovieCell(title: movie.title))
    }

    ...
}

When this action is passing through our reducer, the state needs to be mutated in a way that will let our presenter know what to do. To achieve that, let’s add an attribute to our state.

public struct HomeState: Beaver.State {
    public var movies: [String]?

    public var selectedMovie: String?
    
    public init() {
    }
}

Now, let’s make our reducer build this new state when receiving the .didTapOnMovieCell(title:) action.

public func handle(action: HomeAction,
                   state: HomeState,
                   completion: @escaping (HomeState) -> ()) -> HomeState {
    var newState = state

    switch ExhaustiveAction<HomeRoutingAction, HomeUIAction>(action) {
    case .routing(.start):
        newState.movies = (0...10).map { "Movie \($0)" }
        
    case .routing(.stop):
        newState.movies = nil

    case .ui(.didTapOnMovieCell(let title)):
    	newState.selectedMovie = title
    }
    
    return newState
}

And finally, let’s make our presenter handle this new state.

extension HomePresenter {
    public func stateDidUpdate(oldState: HomeState?,
                               newState: HomeState,
                               completion: @escaping () -> ()) {

        if oldState?.movies != newState.movies {
            switch (oldState?.movies, newState.movies) {
            case (.none, .some):
                let homeController = HomeViewController(store: store)
                context.present(controller: homeController, completion: completion)

            case (.some, .none):
                context.dismiss(completion: completion)
            }
            return
    	}

    	if oldState?.selectedMovie != newState.selectedMovie {
            switch (oldState?.selectedMovie, newState.selectedMovie) {
            case (.none, let title):
                dispatch(AppAction.start(module: MovieCardRoutingAction.start(title: title)))

            case (.some, .none):
                break
            }	        
            completion()
            return
        }
    }
}

Why Beaver?

Writing an application at an early stage should be done right. In the mean time, we build applications to solve real life problems, and most often, we want to focus on these instead of architecture details. Beaver is here to help you start your project fast, but clean by generating for you all the boiler plate code that you need, including project’s and frameworks’ configurations.

The most difficult part when writing an application are the data flows. They can easily be made complex because of the product needs, but also because of the way we write our code. MVC, MVVM or VIPER don’t define a clear way to handle the state and the way it’s mutated while users use the application. What usually happens is that each developer implement data flows their own way, resulting in an inconsistent codebase, making the project hard to maintain. Beaver forces you to exhaustively handle all the cases of your flow in a strict unidirectional way which can be easily understood and maintained by any developer.

While the codebase is growing, developers tend to write generic code in order not to rewrite the wheel for each feature. They also tend to use singletons in order to access global states more easily. These two tendencies lead to strong coupling between classes and overdesign, making the whole system a lot less flexible. Beaver aims to solve this by providing a project structure that gives a place for every classes. Common classes belongs in the Core framework, feature business logic code belongs in the modules’ frameworks. Modules don’t know about each others, avoiding wrong coupling. Beaver also removes the need of singletons by providing a safe and easily accessible global state.

Command line tools

Beaver comes with its command line tools, which you can install like so:

$ git clone git@github.com:Beaver/BeaverScript.git
$ make build
$ make install

The beaver command permits to:

Getting started guide

For a new project

Creating a new project with beaver is very simple. Follow these few steps:

  1. Install the beaver command line tools, xcake and cocoapods.

  2. Run:
    $ beaver init
    

    It will to guide you to generate your project boiler plate.

  3. Go to your project directory, and run:
    $ xcake make
    
  4. Open App.xcworkspace

You’re all set!

For an existing project

Cocoapods

You install Beaver via CocoaPods by adding it to your Podfile:

pod 'Beaver', :git => 'git@github.com:Beaver/Beaver.git'

And run pod install.

Carthage

You can install Beaver via Carthage by adding the following line to your Cartfile:

github "Beaver/Beaver"

TODO