Permalink
Browse files

Write the first draft of GitHubSearch tutorial

  • Loading branch information...
devxoul committed May 17, 2017
1 parent c823298 commit 494ba39931260306d827400c320d30b6b6a1e72a
View
@@ -0,0 +1,10 @@
# Table of Contents
* [View on GitHub](https://github.com/ReactorKit/ReactorKit)
* [Introduction](../README.md)
* [Tutorial](Tutorial/README.md)
* [GitHub Search](Tutorial/GitHubSearch/README.md)
* [Building User Interface](Tutorial/GitHubSearch/1-BuildingUserInterface.md)
* [Creating Reactor](Tutorial/GitHubSearch/2-CreatingReactor.md)
* [Defining View](Tutorial/GitHubSearch/3-DefiningView.md)
* [Implementing Reactor](Tutorial/GitHubSearch/4-ImplementingReactor.md)
@@ -0,0 +1,20 @@
# Building User Interface
First you have to build a user interface. This tutorial premises that you have a basic knowledge of building user interface. GitHubSearch application has a single UISearchBar and an UITableView. User types the query to the search bar then the search results will be displayed in the table view.
![github-search](https://cloud.githubusercontent.com/assets/931655/26028397/76671e92-385a-11e7-972f-5005160eb690.png)
Here is the code of `GitHubSearchViewController`:
**GitHubSearchViewController.swift**
```swift
import UIKit
final class GitHubSearchViewController: UIViewController {
@IBOutlet var searchBar: UISearchBar!
@IBOutlet var tableView: UITableView!
}
```
Don't forget to add a prototype cell to the table view. In this tutorial the cell identifier of the property cell is `"cell"`.
@@ -0,0 +1,148 @@
# Creating Reactor
## Empty Reactor
If you have finished building an user interface, it's time to create a first reactor. Reactor is an UI independent layer which manages the state of a view.
Create a new swift file named **`GitHubSearchViewReactor.swift`** and define a empty reactor.
**GitHubSearchViewReactor.swift**
```swift
import ReactorKit
final class GitHubSearchViewReactor: Reactor {
enum Action {
}
enum Mutation {
}
struct State {
}
let initialState = State()
}
```
## Action, Mutation and State
You'll find that `GitHubSearchViewReactor` conforms to the protocol `Reactor`. This protocol requires three types: `Action`, `Mutation` and `State`. Action represents an user input such as refresh or search. State defines a view state such as search results. Mutation is an operation that manipulates the state.
GitHubSearch will call search API each time user changes the query text. This user interaction can be represented as an action: `updateQuery(String?)`.
```swift
enum Action {
// user changes the query text
case updateQuery(String?)
}
```
Let's assume that the search result is an array of repository names. The table view needs this value to draw user interface so we can call this a view state. Let's add a property to a struct `State`.
```swift
struct State {
// search result (array of repository names)
var repos: [String]
}
```
The state can only be changed via `Mutation`. In general mutations correspond to each property of the state. In this case we have a single property so we can define a single mutation as `setRepos([String])`.
```swift
enum Mutation {
// update State.repos
case setRepos([String])
}
```
## Reactor Flow
When a user changes the text in the search bar, `Action.updateQuery` will be sent to the reactor. Then the reactor will convert the action to the `Mutation.setRepos` asynchronously. Then the mutation will change the current state and this state will be sent back to the view. Here is the complete flow of a reactor:
![flow](https://cloud.githubusercontent.com/assets/931655/25098066/2de21a28-23e2-11e7-8a41-d33d199dd951.png)
## Implementing `mutate()` and `reduce()`
There are two functions between each steps: Action-Mutation and Mutation-State. `mutate()` function converts an action to a mutation and `reduce()` function generates a new state from a mutation. Here are the function definitions:
```swift
// converts Action to Mutation
func mutate(action: Action) -> Observable<Mutation>
// generates a new State from an old State and a Mutation
func reduce(state: State, mutation: Mutation) -> State
```
We'll implement `mutate()` with dummy data first. This function gets called each time the reactor receives actions. The code below filters the action with `switch-case` statement and returns a `Mutation.setRepos` with dummy data.
```swift
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .updateQuery(query): // when user updates the search query
if let query = query {
let dummyRepos = ["\(query)1", "\(query)2", "\(query)3"] // dummy result
return Observable.just(Mutation.setRepos(dummyRepos))
} else {
return Observable.just(Mutation.setRepos([])) // empty result
}
}
}
```
`reduce()` function gets called each time the reactor emits mutations from `mutate()`. The code below filters the mutation with `switch-case` and returns a new state.
```swift
func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case let .setRepos(repos):
return State(repos: repos) // returns a new state
}
}
```
And this is the complete code:
**GitHubSearchViewReactor.swift**
```swift
import ReactorKit
final class GitHubSearchViewReactor: Reactor {
enum Action {
// user changes the query text
case updateQuery(String?)
}
enum Mutation {
// update State.repos
case setRepos([String])
}
struct State {
// search result (array of repository names)
var repos: [String]
}
let initialState = State(repos: [])
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .updateQuery(query): // when user updates the search query
if let query = query {
let dummyRepos = ["\(query)1", "\(query)2", "\(query)3"] // dummy result
return Observable.just(Mutation.setRepos(dummyRepos))
} else {
return Observable.just(Mutation.setRepos([])) // empty result
}
}
}
func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case let .setRepos(repos):
return State(repos: repos) // returns a new state
}
}
}
```
@@ -0,0 +1,49 @@
# Defining View
Now we have a working reactor. In this chapter we'll bind the reactor to a view. In ReactorKit, normal views, cells and view controllers are treated as a view. ReactorKit has a protocol named `View` for views.
A protocol `View` requires a dispose bag property and a binding method. This is a basic implementation of a `View`.
**GitHubSearchViewController.swift**
```swift
import UIKit
import ReactorKit
import RxSwift
class GitHubSearchViewController: UIViewController, View {
var disposeBag = DisposeBag()
func bind(reactor: GitHubSearchViewReactor) {
// define action and state bindings here
}
}
```
A view will automatically have a property `reactor` if the view conforms to the protocol `View`. The method `bind()` is called just after the new reactor is assigned. You can assign a new reactor anywhere but if you're using storyboard it's recommended to do it after the `super.viewDidLoad()`.
```swift
override func viewDidLoad() {
super.viewDidLoad()
reactor = GitHubSearchViewReactor() // this makes `bind()` get called
}
```
Define action and state bindings in `bind()`. Thanks to RxCocoa you can easily bind UI elements with action and state.
```swift
func bind(reactor: GitHubSearchViewReactor) {
// Action
searchBar.rx.text
.map { Reactor.Action.updateQuery($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// State
reactor.state.map { $0.repos }
.bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
cell.textLabel?.text = repo
}
.disposed(by: disposeBag)
}
```
@@ -0,0 +1,48 @@
# Implementing Reactor
We implemented `GitHubSearchViewReactor` with dummy data. In this chapter we'll use GitHub search API with URLSession. What we have to change is just a `mutate()` function implementation.
```swift
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .updateQuery(query):
if let query = query, !query.isEmpty {
return URLSession.shared.rx.json(url: url)
.map { json -> [String] in
guard let dict = json as? [String: Any] else { return [] }
guard let items = dict["items"] as? [[String: Any]] else { return [] }
let repos = items.flatMap { $0["full_name"] as? String }
return repos
}
.catchErrorJustReturn([])
.map { Mutation.setRepos($0) }
} else {
return Observabe.just(Mutation.setRepos([])) // empty result
}
}
}
```
The reactor sends API requests each time the query changes. It's important to cancel previous request to prevent from unnecessary API requests. In general it's the responsibility of a `flatMapLatest()` but it is not available in the `mutate()`. In this case we can use `takeUntil()` with the action subject.
```swift
return URLSession.shared.rx.json(url: url)
.map { json -> [String] in
guard let dict = json as? [String: Any] else { return [] }
guard let items = dict["items"] as? [[String: Any]] else { return [] }
let repos = items.flatMap { $0["full_name"] as? String }
return repos
}
.catchErrorJustReturn([])
.map { Mutation.setRepos($0) }
// dispose when the reactor emits next .updateQuery action
.takeUntil(self.action.filter {
if case .updateQuery = $0 {
return true
} else {
return false
}
})
```
It's done!
@@ -0,0 +1,3 @@
# GitHub Search
In this tutorial we'll make a simplified version of [GitHubSearch example](https://github.com/ReactorKit/ReactorKit/tree/master/Examples/GitHubSearch). You can learn how to implement the basic `View` and `Reactor`. This tutorial is for beginners so it doesn't contain advance usage. Check [Examples](https://github.com/ReactorKit/ReactorKit#examples) section on README for more examples.
@@ -0,0 +1,3 @@
# Tutorial
* [GitHub Search](GitHubSearch/README.md)
View

0 comments on commit 494ba39

Please sign in to comment.