-
Notifications
You must be signed in to change notification settings - Fork 3
GitHub Search
In this page, we will show you how to build an application with MVC. Let's make a simple GitHub repository searching application.
First, define data structures we need.
These are events what user can make.
enum GitHubSearchEvent {
case changeSearchText(String)
case selectRepository(Repository)
}
These are actions (model commands) what controller can make.
enum GitHubSearchAction {
case updateQuery(String?)
case updateRepositories([Repository])
case repositoriesError(Error)
}
And these are states that model can have.
enum RepositoriesState {
case some([Repository])
case error(Error)
}
struct GitHubSearchState {
let query: String?
let repositories: RepositoriesState
}
Oh, the definition of Repository
is here
struct Repository {
let name: String
let fullName: String
let htmlURL: String
}
And this is an REST client for GitHub.
import UIKit
import RxSwift
import Alamofire
import RxAlamofire
public struct GitHubAPI {
let manager: SessionManager
func searchRepo(_ key: String) -> Observable<[Repository]> {
let URL = Foundation.URL(string: "https://api.github.com/search/repositories")!
return manager.rx.json(.get, URL, parameters: ["q": key]).map { data in
if let data = data as? Dictionary<String, NSObject> {
let items = data["items"]
if let items = items as? [Dictionary<String, NSObject>] {
return items.map{ item in
let name = item["name"]?.description ?? ""
let fullName = item["full_name"]?.description ?? ""
let htmlURL = item["html_url"]?.description ?? ""
return Repository(name: name, fullName: fullName, htmlURL: htmlURL)
}
}
}
return []
}
}
}
And now, let's make a controller.
import Foundation
import RxSwift
import RxCocoa
import Alamofire
import RxMVC
protocol GitHubSearchControllerDelegate: class {
func openURL(_ url: URL)
}
struct GitHubSearchController: FlatMapController {
typealias Event = GitHubSearchEvent
typealias Action = GitHubSearchAction
var flatMapType = FlatMapType.latest
weak var delegate: GitHubSearchControllerDelegate?
init(delegate: GitHubSearchControllerDelegate) {
self.delegate = delegate
}
func flatMapEventToAction(_ event: Event) -> Observable<Action> {
switch event {
case .changeSearchText(let text):
let query = text.trimmingCharacters(in: CharacterSet.whitespaces)
if query == "" {
return Observable.of(Action.updateQuery(nil), Action.updateRepositories([]))
} else {
return Observable.just(Action.updateQuery(query)).concat(
GitHubAPI(manager: SessionManager.default).searchRepo(query).map { repositories in
return Action.updateRepositories(repositories)
}.asDriver(onErrorRecover: { (error) -> Driver<Action> in
return Driver.just(Action.repositoriesError(error))
}))
}
case .selectRepository(let repository):
if let url = URL(string: repository.htmlURL) {
self.delegate?.openURL(url)
}
return Observable.empty()
}
}
}
FlatMapController
is a kind of Controller that converts event into Action stream.
GitHubSearchController
accepts an event.
func flatMapEventToAction(event: Event) -> Observable<Action> {
switch event {
In case of ChangeSearchText
, it trims the text and converts empty string into nil.
If text presents, update query and send search request to GitHub
case .ChangeSearchText(let text):
let query = text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
if query == "" {
return Observable.of(Action.UpdateQuery(nil), Action.UpdateRepositories([]))
} else {
return Observable.just(Action.UpdateQuery(query)).concat(
GitHubAPI(manager: Manager.sharedInstance).searchRepo(query).map { repositories in
return Action.UpdateRepositories(repositories)
}.asDriver(onErrorRecover: { (error) -> Driver<Action> in
return Driver.just(Action.RepositoriesError(error))
}))
}
In case of SelectRepository
delegates URL opening to delegate. In iOS, delegate will be UIViewController.
case .SelectRepository(let repository): if let url = NSURL(string: repository.htmlURL) { self.delegate?.openURL(url) } return Observable.empty() }
Controller is just a struct. So It should delegate some actions to special objects.
And set flatMapType
to FlatMapType.Latest
to ignore the result of previous requests.
var flatMapType = FlatMapType.Latest
Model is pretty simple. It uses ReducerModel
that accepts (previousState, action)
pair
and returns newState
. (Like redux's reducer)
import Foundation
import RxMVC
struct GitHubSearchModel: ReducerModel {
typealias State = GitHubSearchState
typealias Action = GitHubSearchAction
let initialState = GitHubSearchState(query: nil, repositories: RepositoriesState.Some([]))
func reduce(state: State, with action: Action) -> State {
switch action {
case .UpdateQuery(let query):
return State(query: query, repositories: state.repositories)
case .UpdateRepositories(let repositories):
return State(query: state.query, repositories: RepositoriesState.Some(repositories))
case .RepositoriesError(let error):
return State(query: state.query, repositories: RepositoriesState.Error(error))
}
}
}
We will make view and user interactable as single struct.
import Foundation
import RxSwift
import RxCocoa
import RxMVC
import JLToast
struct GitHubSearchView: View, UserInteractable {
typealias State = GitHubSearchState
typealias Event = GitHubSearchEvent
let searchTextField: UITextField
let tableView: UITableView
func interact() -> Observable<Event> {
return [
searchTextField.rx_text.asDriver().map{text in Event.ChangeSearchText(text)}.throttle(0.5).asObservable(),
tableView.rx_modelSelected(Repository).map{repository in Event.SelectRepository(repository)}
].toObservable().merge()
}
func update(stateStream: Observable<State>) -> Disposable {
return CompositeDisposable(disposables: [
stateStream.map{state in state.query ?? ""}.distinctUntilChanged().bindTo(searchTextField.rx_text),
stateStream
.map {state -> [Repository] in
switch(state.repositories) {
case .Some(let items):
return items as Array<Repository>
case .Error:
return []
}
}
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) { (row, element, cell) in
cell.textLabel?.text = element.name
cell.detailTextLabel?.text = element.fullName
},
stateStream
.map {state -> ErrorType? in
switch (state.repositories) {
case .Error(let error):
return error
default:
return nil
}
}
.asDriver(onErrorJustReturn: nil)
.filter { error in error != nil }
.map { error in error! }
.throttle(1.0)
.asObservable()
.subscribeNext {error in
let error = error as NSError
JLToast.makeText(error.localizedDescription).show()
}])
}
}
Accepts events from views and convert them into event objects
throttling the user interaction, so we will not make useless requests.
func interact() -> Observable<Event> {
return [
searchTextField.rx_text.asDriver().map{text in Event.ChangeSearchText(text)}.throttle(0.5).asObservable(),
tableView.rx_modelSelected(Repository).map{repository in Event.SelectRepository(repository)}
].toObservable().merge()
}
and render
func update(stateStream: Observable<State>) -> Disposable {
return CompositeDisposable(disposables: [
stateStream.map{state in state.query ?? ""}.distinctUntilChanged().bindTo(searchTextField.rx_text),
stateStream
.map {state -> [Repository] in
switch(state.repositories) {
case .Some(let items):
return items as Array<Repository>
case .Error:
return []
}
}
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) { (row, element, cell) in
cell.textLabel?.text = element.name
cell.detailTextLabel?.text = element.fullName
},
stateStream
.map {state -> ErrorType? in
switch (state.repositories) {
case .Error(let error):
return error
default:
return nil
}
}
.asDriver(onErrorJustReturn: nil)
.filter { error in error != nil }
.map { error in error! }
.throttle(1.0)
.asObservable()
.subscribeNext {error in
let error = error as NSError
JLToast.makeText(error.localizedDescription).show()
}])
}
View controller is just a context
for RxMVC. Just makes M, V, C and combine them.
import UIKit
import RxSwift
import RxMVC
class GitHubSearchViewController: UIViewController, GitHubSearchControllerDelegate {
@IBOutlet weak var searchTextField: UITextField!
@IBOutlet weak var tableView: UITableView!
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let model = GitHubSearchModel()
let view = GitHubSearchView(searchTextField: searchTextField, tableView: tableView)
let controller = GitHubSearchController(delegate: self)
let userInteractable = view
combineModel(model,
withView: view,
controller: controller,
andUserInteractable: userInteractable)
.addDisposableTo(disposeBag)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Controller delegate functions
func openURL(url: NSURL) {
UIApplication.sharedApplication().openURL(url)
}
}
It's pretty long, But we can predict side-effects!
You can make predictable applications by using RxMVC.
See more examples from source.