Skip to content

Commit

Permalink
add Actions, Dispatcher, Stores
Browse files Browse the repository at this point in the history
  • Loading branch information
marty-suzuki committed Jan 23, 2018
1 parent d302996 commit fefa8a7
Show file tree
Hide file tree
Showing 26 changed files with 798 additions and 612 deletions.
Binary file added Images/Flux/Views.key
Binary file not shown.
Binary file modified Images/favorite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/repository.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/user_repository.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,55 @@
# iOSDesignPatternSamples (MVVM)
# iOSDesignPatternSamples (Flux)

This is Github user search demo app that made with MVVM design pattern.
This is Github user search demo app that made with Flux design pattern.

## Application Structure

![](./Images/structure.png)

## Flux

### Repository

- [RepositoryAction](./iOSDesignPatternSamples/Sources/Common/Flux/Repository/RepositoryAction.swift)
- [RepositoryStore](./iOSDesignPatternSamples/Sources/Common/Flux/Repository/RepositoryStore.swift)

### User

- [UserAction](./iOSDesignPatternSamples/Sources/Common/Flux/User/UserAction.swift)
- [UserStore](./iOSDesignPatternSamples/Sources/Common/Flux/User/UserStore.swift)


## ViewControllers

### [SearchViewController](./iOSDesignPatternSamples/Sources/UI/Search/SearchViewController.swift)
Search Github user and show user result list

![](./Images/search.png)

- [SearchViewModel](./iOSDesignPatternSamples/Sources/UI/Search/SearchViewModel.swift)
- [SearchViewDataSource](./iOSDesignPatternSamples/Sources/UI/Search/SearchViewDataSource.swift) <- Adapt UITableViewDataSource and UITableViewDelegate

### [FavoriteViewController](./iOSDesignPatternSamples/Sources/UI/Favorite/FavoriteViewController.swift)
Show local on memory favorite repositories

![](./Images/favorite.png)

- [FavoriteViewModel](./iOSDesignPatternSamples/Sources/UI/Favorite/FavoriteViewModel.swift)
- [FavoriteViewDataSource](./iOSDesignPatternSamples/Sources/UI/Favorite/FavoriteViewDataSource.swift) <- Adapt UITableViewDataSource and UITableViewDelegate

### [UserRepositoryViewController](./iOSDesignPatternSamples/Sources/UI/UserRepository/UserRepositoryViewController.swift)
Show Github user's repositories

![](./Images/user_repository.png)

- [UserRepositoryViewModel](./iOSDesignPatternSamples/Sources/UI/UserRepository/UserRepositoryViewModel.swift)
- [UserRepositoryViewDataSource](./iOSDesignPatternSamples/Sources/UI/UserRepository/UserRepositoryViewDataSource.swift) <- Adapt UITableViewDataSource and UITableViewDelegate

### [RepositoryViewController](./iOSDesignPatternSamples/Sources/UI/Repository/RepositoryViewController.swift)
Show a repository and add / remove local on memory favorites

![](./Images/repository.png)

- [RepositoryViewModel](./iOSDesignPatternSamples/Sources/UI/Repository/RepositoryViewModel.swift)

## How to add / remove favorites

You can add / remove favorite repositories in RepositoryViewController, but an Array of favorite repository is hold by FavoriteViewController.
You can add / remove favorite repositories in RepositoryViewController. Array of favorite repository is hold by RepositoryStore, therefore you can use its reference everywhere!

## Run

Expand Down
97 changes: 67 additions & 30 deletions iOSDesignPatternSamples.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

11 changes: 0 additions & 11 deletions iOSDesignPatternSamples/Sources/Common/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.

if let viewControllers = (window?.rootViewController as? UITabBarController)?.viewControllers,
let searchVC = viewControllers.flatMap({
($0 as? UINavigationController)?.topViewController as? SearchViewController
}).first,
let favoriteVC = viewControllers.flatMap({
($0 as? UINavigationController)?.topViewController as? FavoriteViewController
}).first {
searchVC.favoritesInput = favoriteVC.favoritesInput
searchVC.favoritesOutput = favoriteVC.favoritesOutput
}

return true
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Dispatcher.Repository.swift
// iOSDesignPatternSamples
//
// Created by marty-suzuki on 2017/09/12.
// Copyright © 2017年 marty-suzuki. All rights reserved.
//

import Foundation
import FluxCapacitor
import GithubKit

extension Dispatcher {
enum Repository: DispatchValue {
typealias RelatedStoreType = RepositoryStore
typealias RelatedActionType = RepositoryAction

case isRepositoryFetching(Bool)
case addRepositories([GithubKit.Repository])
case removeAllRepositories
case selectedRepository(GithubKit.Repository?)
case lastPageInfo(PageInfo?)
case repositoryTotalCount(Int)

case addFavorite(GithubKit.Repository)
case removeFavorite(GithubKit.Repository)
case removeAllFavorites
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// RepositoryAction.swift
// iOSDesignPatternSamples
//
// Created by marty-suzuki on 2017/09/12.
// Copyright © 2017年 marty-suzuki. All rights reserved.
//

import Foundation
import FluxCapacitor
import GithubKit
import RxSwift

final class RepositoryAction: Actionable {
typealias DispatchValueType = Dispatcher.Repository

private let session: ApiSession
private var disposeBag = DisposeBag()

init(session: ApiSession = .shared) {
self.session = session
}

func fetchRepositories(withUserId id: String, after: String?) {
invoke(.isRepositoryFetching(true))
let request = UserNodeRequest(id: id, after: after)
session.rx.send(request)
.subscribe(onNext: { [weak self] in
self?.invoke(.lastPageInfo($0.pageInfo))
self?.invoke(.addRepositories($0.nodes))
self?.invoke(.repositoryTotalCount($0.totalCount))
}, onDisposed: { [weak self] in
self?.invoke(.isRepositoryFetching(false))
})
.disposed(by: disposeBag)
}

func selectRepository(_ repository: Repository) {
invoke(.selectedRepository(repository))
}

func clearSelectedRepository() {
invoke(.selectedRepository(nil))
}

func addFavorite(_ repository: Repository) {
invoke(.addFavorite(repository))
}

func removeFavorite(_ repository: Repository) {
invoke(.removeFavorite(repository))
}

func pageInfo(_ pageInfo: PageInfo) {
invoke(.lastPageInfo(pageInfo))
}

func clearPageInfo() {
invoke(.lastPageInfo(nil))
}

func addRepositories(_ repositories: [Repository]) {
invoke(.addRepositories(repositories))
}

func removeAllRepositories() {
invoke(.removeAllRepositories)
}

func repositoryTotalCount(_ count: Int) {
invoke(.repositoryTotalCount(count))
}

func isRepositoriesFetching(_ isFetching: Bool) {
invoke(.isRepositoryFetching(isFetching))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// RepositoryStore.swift
// iOSDesignPatternSamples
//
// Created by marty-suzuki on 2017/09/12.
// Copyright © 2017年 marty-suzuki. All rights reserved.
//

import Foundation
import FluxCapacitor
import GithubKit
import RxSwift
import RxCocoa

final class RepositoryStore: Storable {
typealias DispatchValueType = Dispatcher.Repository

let isRepositoryFetching: Observable<Bool>
fileprivate let _isRepositoryFetching = BehaviorRelay<Bool>(value: false)

let favorites: Observable<[Repository]>
fileprivate let _favorites = BehaviorRelay<[Repository]>(value: [])

let repositories: Observable<[Repository]>
fileprivate let _repositories = BehaviorRelay<[Repository]>(value: [])

let selectedRepository: Observable<Repository?>
fileprivate let _selectedRepository = BehaviorRelay<Repository?>(value: nil)

let lastPageInfo: Observable<PageInfo?>
fileprivate let _lastPageInfo = BehaviorRelay<PageInfo?>(value: nil)

let repositoryTotalCount: Observable<Int>
fileprivate let _repositoryTotalCount = BehaviorRelay<Int>(value: 0)

init(dispatcher: Dispatcher) {
self.isRepositoryFetching = _isRepositoryFetching.asObservable()
self.favorites = _favorites.asObservable()
self.repositories = _repositories.asObservable()
self.selectedRepository = _selectedRepository.asObservable()
self.lastPageInfo = _lastPageInfo.asObservable()
self.repositoryTotalCount = _repositoryTotalCount.asObservable()

register { [weak self] in
guard let me = self else { return }
switch $0 {
case .isRepositoryFetching(let value):
me._isRepositoryFetching.accept(value)
case .addRepositories(let value):
me._repositories.accept(me._repositories.value + value)
case .removeAllRepositories:
me._repositories.accept([])
case .selectedRepository(let value):
me._selectedRepository.accept(value)
case .lastPageInfo(let value):
me._lastPageInfo.accept(value)
case .repositoryTotalCount(let value):
me._repositoryTotalCount.accept(value)

case .addFavorite(let value):
if me._favorites.value.index(where: { $0.url == value.url }) == nil {
me._favorites.accept(me._favorites.value + [value])
}
case .removeFavorite(let value):
if let index = self?._favorites.value.index(where: { $0.url == value.url }) {
var favorites = me._favorites.value
favorites.remove(at: index)
me._favorites.accept(favorites)
}
case .removeAllFavorites:
me._favorites.accept([])
}
}
}
}

extension RepositoryStore: ValueCompatible {}

extension Value where Base == RepositoryStore {
var isRepositoryFetching: Bool {
return base._isRepositoryFetching.value
}

var favorites: [Repository] {
return base._favorites.value
}

var repositories: [Repository] {
return base._repositories.value
}

var selectedRepository: Repository? {
return base._selectedRepository.value
}

var lastPageInfo: PageInfo? {
return base._lastPageInfo.value
}

var repositoryTotalCount: Int {
return base._repositoryTotalCount.value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Dispatcher.User.swift
// iOSDesignPatternSamples
//
// Created by marty-suzuki on 2017/09/12.
// Copyright © 2017年 marty-suzuki. All rights reserved.
//

import Foundation
import FluxCapacitor
import GithubKit

extension Dispatcher {
enum User: DispatchValue {
typealias RelatedStoreType = UserStore
typealias RelatedActionType = UserAction

case isUserFetching(Bool)
case addUsers([GithubKit.User])
case userTotalCount(Int)
case removeAllUsers
case selectedUser(GithubKit.User?)
case lastPageInfo(PageInfo?)
case lastSearchQuery(String)
case fetchError(Error)
}
}
74 changes: 74 additions & 0 deletions iOSDesignPatternSamples/Sources/Common/Flux/User/UserAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// UserAction.swift
// iOSDesignPatternSamples
//
// Created by marty-suzuki on 2017/09/12.
// Copyright © 2017年 marty-suzuki. All rights reserved.
//

import Foundation
import FluxCapacitor
import GithubKit
import RxSwift

final class UserAction: Actionable {
typealias DispatchValueType = Dispatcher.User

private let session: ApiSession
private var disposeBag = DisposeBag()

init(session: ApiSession = .shared) {
self.session = session
}

func fetchUsers(withQuery query: String, after: String?) {
invoke(.lastSearchQuery(query))
if query.isEmpty { return }
disposeBag = DisposeBag()
invoke(.isUserFetching(true))
let request = SearchUserRequest(query: query, after: after)
session.rx.send(request)
.subscribe(onNext: { [weak self] in
self?.invoke(.addUsers($0.nodes))
self?.invoke(.lastPageInfo($0.pageInfo))
self?.invoke(.userTotalCount($0.totalCount))
}, onError: { [weak self] in
self?.invoke(.fetchError($0))
}, onDisposed: { [weak self] in
self?.invoke(.isUserFetching(false))
})
.disposed(by: disposeBag)
}

func selectUser(_ user: User) {
invoke(.selectedUser(user))
}

func clearSelectedUser() {
invoke(.selectedUser(nil))
}

func addUsers(_ users: [User]) {
invoke(.addUsers(users))
}

func removeAllUsers() {
invoke(.removeAllUsers)
}

func pageInfo(_ pageInfo: PageInfo) {
invoke(.lastPageInfo(pageInfo))
}

func clearPageInfo() {
invoke(.lastPageInfo(nil))
}

func userTotalCount(_ count: Int) {
invoke(.userTotalCount(count))
}

func isUserFetching(_ isFetching: Bool) {
invoke(.isUserFetching(isFetching))
}
}
Loading

0 comments on commit fefa8a7

Please sign in to comment.