Project Name | Stars | Downloads | Repos Using This | Packages Using This | Most Recent Commit | Total Releases | Latest Release | Open Issues | License | Language |
---|---|---|---|---|---|---|---|---|---|---|
Reduxmoviedb | 140 | a year ago | 23 | mit | Swift | |||||
๐ฅ See the upcoming movies! ReSwift + RxSwift ๐ Hacktoberfest ๐ | ||||||||||
Thewarholoutloud | 88 | 4 years ago | mit | JavaScript | ||||||
An inclusive audio guide for The Andy Warhol Museum | ||||||||||
Revill | 14 | 2 years ago | 6 | mit | Swift | |||||
๐ฎ Favorite your games filter and see the upcoming games and ! Swift + Combine = ๐ Hacktoberfest ๐ ๐พ | ||||||||||
Spotify Clone | 14 | 5 years ago | JavaScript | |||||||
A Spotify Clone made with React Native :mega: | ||||||||||
Deliveroo | 11 | 3 months ago | 1 | mit | JavaScript | |||||
iOS/Android React Native App of Deliveroo. Select a Restaurant, Pick the items you want and place an order! | ||||||||||
Hubapp Frontend | 8 | 7 years ago | gpl-3.0 | JavaScript | ||||||
๐บ An iOS app for the Impact Hub Zurich built with React Native, Redux & ImmutableJS. | ||||||||||
Beaver | 8 | 5 years ago | mit | Swift | ||||||
A framework to help you build your iOS application fast and clean | ||||||||||
Pokeapp Redux Ios | 5 | 5 years ago | apache-2.0 | Swift | ||||||
Example of architecture of an iOS app using Layout, ReSwift (Redux), GraphQL and RxSwift | ||||||||||
Get In Mobile App | 5 | 5 years ago | 1 | mit | JavaScript | |||||
GetIn's React-Native mobile applications for IOS and Android. Uses server from GetInBackend repository. | ||||||||||
My Store | 3 | 5 years ago | HTML | |||||||
:white_check_mark: An e-Commerce made with React Native |
Beaver
A framework to help you build your application fast and clean!
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.
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.
Let's see what an App built with Beaver is made of:
At least one Module per feature.
The App State is the data structure of the application. It contains the modules' data and global context.
The App Reducer is the core logic of the application. It performs updates on the current state based on an event. It is also entirely stateless.
The Routing Events are dispatched to ensure intermodule communications. It's also triggering modules' presentations and dismissals. They are the only public API of the modules.
The App Store stores the app state and maintains it up to date. It also dispatches the events and state updates to its children and subscribers.
The App Presenter subscribes to the app store and presents the modules based on the current app state.
What is a module made of?
The State is a subset of the app state. It is the data structure of the module.
The UI Events are dispatched to ensure the module internal UI communications. It's basically triggering business logic on user interactions.
The Child Store is the child of the app store. It holds the module's state. It is the interface to dispatch routing events to the rest of the app and ui event internally.
The Reducer is a subset of the app reducer. It is responsible of the internal logic of the module.
The Presenter subscribes to the child store and presents the view controllers based on the current state. It also dispatches routing events to interact with other modules.
The ViewController subscribes to the child store state and builds the views based on the current state. It dispatches ui actions and lifecycle events.
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.
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
Core
is a dynamic framework containing all the shared classes between your modules.Home
is a dynamic framework containing a module implementation.Podfile
and Cakefile
are located at the root directory. These two configurations are reading each modules' Podfile.rb
and Cakefile.rb
in order generate the xcodeproj
and xcworkspace
files.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 UIView
s.
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
}
}
}
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.
Beaver comes with its command line tools, which you can install like so:
$ git clone [email protected]:Beaver/BeaverScript.git
$ make build
$ make install
The beaver
command permits to:
$ beaver init --project_path . --project_name NewProject --module_names ModuleOne,ModuleTwo
$ beaver add module --project_path . --module_name NewModule
$ beaver add action --project_path . --module_name ModuleOne --action_name NewAction --action_type ui
Creating a new project with beaver is very simple. Follow these few steps:
$ beaver init
It will to guide you to generate your project boiler plate.
$ xcake make
App.xcworkspace
You're all set!
You install Beaver via CocoaPods by adding it to your Podfile
:
pod 'Beaver', :git => '[email protected]:Beaver/Beaver.git'
And run pod install
.
You can install Beaver via Carthage by adding the following line to your Cartfile
:
github "Beaver/Beaver"