Skip to content

Commit 82f3e80

Browse files
committed
add Actions, Dispatcher, Stores
1 parent e16f4d2 commit 82f3e80

26 files changed

+798
-612
lines changed

Images/Flux/Views.key

1.53 MB
Binary file not shown.

Images/favorite.png

-43.6 KB
Loading

Images/repository.png

-31.8 KB
Loading

Images/search.png

-57.8 KB
Loading

Images/structure.png

-1.43 KB
Loading

Images/user_repository.png

-56.6 KB
Loading

README.md

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,55 @@
1-
# iOSDesignPatternSamples (MVVM)
1+
# iOSDesignPatternSamples (Flux)
22

3-
This is Github user search demo app that made with MVVM design pattern.
3+
This is Github user search demo app that made with Flux design pattern.
44

55
## Application Structure
66

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

9+
## Flux
10+
11+
### Repository
12+
13+
- [RepositoryAction](./iOSDesignPatternSamples/Sources/Common/Flux/Repository/RepositoryAction.swift)
14+
- [RepositoryStore](./iOSDesignPatternSamples/Sources/Common/Flux/Repository/RepositoryStore.swift)
15+
16+
### User
17+
18+
- [UserAction](./iOSDesignPatternSamples/Sources/Common/Flux/User/UserAction.swift)
19+
- [UserStore](./iOSDesignPatternSamples/Sources/Common/Flux/User/UserStore.swift)
20+
21+
922
## ViewControllers
1023

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

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

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

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

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

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

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

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

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

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

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

40-
- [RepositoryViewModel](./iOSDesignPatternSamples/Sources/UI/Repository/RepositoryViewModel.swift)
41-
4250
## How to add / remove favorites
4351

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

4654
## Run
4755

iOSDesignPatternSamples.xcodeproj/project.pbxproj

+67-30
Large diffs are not rendered by default.

iOSDesignPatternSamples/Sources/Common/AppDelegate.swift

-11
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
1717
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
1818
// Override point for customization after application launch.
1919

20-
if let viewControllers = (window?.rootViewController as? UITabBarController)?.viewControllers,
21-
let searchVC = viewControllers.flatMap({
22-
($0 as? UINavigationController)?.topViewController as? SearchViewController
23-
}).first,
24-
let favoriteVC = viewControllers.flatMap({
25-
($0 as? UINavigationController)?.topViewController as? FavoriteViewController
26-
}).first {
27-
searchVC.favoritesInput = favoriteVC.favoritesInput
28-
searchVC.favoritesOutput = favoriteVC.favoritesOutput
29-
}
30-
3120
return true
3221
}
3322

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Dispatcher.Repository.swift
3+
// iOSDesignPatternSamples
4+
//
5+
// Created by marty-suzuki on 2017/09/12.
6+
// Copyright © 2017年 marty-suzuki. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FluxCapacitor
11+
import GithubKit
12+
13+
extension Dispatcher {
14+
enum Repository: DispatchValue {
15+
typealias RelatedStoreType = RepositoryStore
16+
typealias RelatedActionType = RepositoryAction
17+
18+
case isRepositoryFetching(Bool)
19+
case addRepositories([GithubKit.Repository])
20+
case removeAllRepositories
21+
case selectedRepository(GithubKit.Repository?)
22+
case lastPageInfo(PageInfo?)
23+
case repositoryTotalCount(Int)
24+
25+
case addFavorite(GithubKit.Repository)
26+
case removeFavorite(GithubKit.Repository)
27+
case removeAllFavorites
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// RepositoryAction.swift
3+
// iOSDesignPatternSamples
4+
//
5+
// Created by marty-suzuki on 2017/09/12.
6+
// Copyright © 2017年 marty-suzuki. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FluxCapacitor
11+
import GithubKit
12+
import RxSwift
13+
14+
final class RepositoryAction: Actionable {
15+
typealias DispatchValueType = Dispatcher.Repository
16+
17+
private let session: ApiSession
18+
private var disposeBag = DisposeBag()
19+
20+
init(session: ApiSession = .shared) {
21+
self.session = session
22+
}
23+
24+
func fetchRepositories(withUserId id: String, after: String?) {
25+
invoke(.isRepositoryFetching(true))
26+
let request = UserNodeRequest(id: id, after: after)
27+
session.rx.send(request)
28+
.subscribe(onNext: { [weak self] in
29+
self?.invoke(.lastPageInfo($0.pageInfo))
30+
self?.invoke(.addRepositories($0.nodes))
31+
self?.invoke(.repositoryTotalCount($0.totalCount))
32+
}, onDisposed: { [weak self] in
33+
self?.invoke(.isRepositoryFetching(false))
34+
})
35+
.disposed(by: disposeBag)
36+
}
37+
38+
func selectRepository(_ repository: Repository) {
39+
invoke(.selectedRepository(repository))
40+
}
41+
42+
func clearSelectedRepository() {
43+
invoke(.selectedRepository(nil))
44+
}
45+
46+
func addFavorite(_ repository: Repository) {
47+
invoke(.addFavorite(repository))
48+
}
49+
50+
func removeFavorite(_ repository: Repository) {
51+
invoke(.removeFavorite(repository))
52+
}
53+
54+
func pageInfo(_ pageInfo: PageInfo) {
55+
invoke(.lastPageInfo(pageInfo))
56+
}
57+
58+
func clearPageInfo() {
59+
invoke(.lastPageInfo(nil))
60+
}
61+
62+
func addRepositories(_ repositories: [Repository]) {
63+
invoke(.addRepositories(repositories))
64+
}
65+
66+
func removeAllRepositories() {
67+
invoke(.removeAllRepositories)
68+
}
69+
70+
func repositoryTotalCount(_ count: Int) {
71+
invoke(.repositoryTotalCount(count))
72+
}
73+
74+
func isRepositoriesFetching(_ isFetching: Bool) {
75+
invoke(.isRepositoryFetching(isFetching))
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//
2+
// RepositoryStore.swift
3+
// iOSDesignPatternSamples
4+
//
5+
// Created by marty-suzuki on 2017/09/12.
6+
// Copyright © 2017年 marty-suzuki. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FluxCapacitor
11+
import GithubKit
12+
import RxSwift
13+
import RxCocoa
14+
15+
final class RepositoryStore: Storable {
16+
typealias DispatchValueType = Dispatcher.Repository
17+
18+
let isRepositoryFetching: Observable<Bool>
19+
fileprivate let _isRepositoryFetching = BehaviorRelay<Bool>(value: false)
20+
21+
let favorites: Observable<[Repository]>
22+
fileprivate let _favorites = BehaviorRelay<[Repository]>(value: [])
23+
24+
let repositories: Observable<[Repository]>
25+
fileprivate let _repositories = BehaviorRelay<[Repository]>(value: [])
26+
27+
let selectedRepository: Observable<Repository?>
28+
fileprivate let _selectedRepository = BehaviorRelay<Repository?>(value: nil)
29+
30+
let lastPageInfo: Observable<PageInfo?>
31+
fileprivate let _lastPageInfo = BehaviorRelay<PageInfo?>(value: nil)
32+
33+
let repositoryTotalCount: Observable<Int>
34+
fileprivate let _repositoryTotalCount = BehaviorRelay<Int>(value: 0)
35+
36+
init(dispatcher: Dispatcher) {
37+
self.isRepositoryFetching = _isRepositoryFetching.asObservable()
38+
self.favorites = _favorites.asObservable()
39+
self.repositories = _repositories.asObservable()
40+
self.selectedRepository = _selectedRepository.asObservable()
41+
self.lastPageInfo = _lastPageInfo.asObservable()
42+
self.repositoryTotalCount = _repositoryTotalCount.asObservable()
43+
44+
register { [weak self] in
45+
guard let me = self else { return }
46+
switch $0 {
47+
case .isRepositoryFetching(let value):
48+
me._isRepositoryFetching.accept(value)
49+
case .addRepositories(let value):
50+
me._repositories.accept(me._repositories.value + value)
51+
case .removeAllRepositories:
52+
me._repositories.accept([])
53+
case .selectedRepository(let value):
54+
me._selectedRepository.accept(value)
55+
case .lastPageInfo(let value):
56+
me._lastPageInfo.accept(value)
57+
case .repositoryTotalCount(let value):
58+
me._repositoryTotalCount.accept(value)
59+
60+
case .addFavorite(let value):
61+
if me._favorites.value.index(where: { $0.url == value.url }) == nil {
62+
me._favorites.accept(me._favorites.value + [value])
63+
}
64+
case .removeFavorite(let value):
65+
if let index = self?._favorites.value.index(where: { $0.url == value.url }) {
66+
var favorites = me._favorites.value
67+
favorites.remove(at: index)
68+
me._favorites.accept(favorites)
69+
}
70+
case .removeAllFavorites:
71+
me._favorites.accept([])
72+
}
73+
}
74+
}
75+
}
76+
77+
extension RepositoryStore: ValueCompatible {}
78+
79+
extension Value where Base == RepositoryStore {
80+
var isRepositoryFetching: Bool {
81+
return base._isRepositoryFetching.value
82+
}
83+
84+
var favorites: [Repository] {
85+
return base._favorites.value
86+
}
87+
88+
var repositories: [Repository] {
89+
return base._repositories.value
90+
}
91+
92+
var selectedRepository: Repository? {
93+
return base._selectedRepository.value
94+
}
95+
96+
var lastPageInfo: PageInfo? {
97+
return base._lastPageInfo.value
98+
}
99+
100+
var repositoryTotalCount: Int {
101+
return base._repositoryTotalCount.value
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// Dispatcher.User.swift
3+
// iOSDesignPatternSamples
4+
//
5+
// Created by marty-suzuki on 2017/09/12.
6+
// Copyright © 2017年 marty-suzuki. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FluxCapacitor
11+
import GithubKit
12+
13+
extension Dispatcher {
14+
enum User: DispatchValue {
15+
typealias RelatedStoreType = UserStore
16+
typealias RelatedActionType = UserAction
17+
18+
case isUserFetching(Bool)
19+
case addUsers([GithubKit.User])
20+
case userTotalCount(Int)
21+
case removeAllUsers
22+
case selectedUser(GithubKit.User?)
23+
case lastPageInfo(PageInfo?)
24+
case lastSearchQuery(String)
25+
case fetchError(Error)
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// UserAction.swift
3+
// iOSDesignPatternSamples
4+
//
5+
// Created by marty-suzuki on 2017/09/12.
6+
// Copyright © 2017年 marty-suzuki. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FluxCapacitor
11+
import GithubKit
12+
import RxSwift
13+
14+
final class UserAction: Actionable {
15+
typealias DispatchValueType = Dispatcher.User
16+
17+
private let session: ApiSession
18+
private var disposeBag = DisposeBag()
19+
20+
init(session: ApiSession = .shared) {
21+
self.session = session
22+
}
23+
24+
func fetchUsers(withQuery query: String, after: String?) {
25+
invoke(.lastSearchQuery(query))
26+
if query.isEmpty { return }
27+
disposeBag = DisposeBag()
28+
invoke(.isUserFetching(true))
29+
let request = SearchUserRequest(query: query, after: after)
30+
session.rx.send(request)
31+
.subscribe(onNext: { [weak self] in
32+
self?.invoke(.addUsers($0.nodes))
33+
self?.invoke(.lastPageInfo($0.pageInfo))
34+
self?.invoke(.userTotalCount($0.totalCount))
35+
}, onError: { [weak self] in
36+
self?.invoke(.fetchError($0))
37+
}, onDisposed: { [weak self] in
38+
self?.invoke(.isUserFetching(false))
39+
})
40+
.disposed(by: disposeBag)
41+
}
42+
43+
func selectUser(_ user: User) {
44+
invoke(.selectedUser(user))
45+
}
46+
47+
func clearSelectedUser() {
48+
invoke(.selectedUser(nil))
49+
}
50+
51+
func addUsers(_ users: [User]) {
52+
invoke(.addUsers(users))
53+
}
54+
55+
func removeAllUsers() {
56+
invoke(.removeAllUsers)
57+
}
58+
59+
func pageInfo(_ pageInfo: PageInfo) {
60+
invoke(.lastPageInfo(pageInfo))
61+
}
62+
63+
func clearPageInfo() {
64+
invoke(.lastPageInfo(nil))
65+
}
66+
67+
func userTotalCount(_ count: Int) {
68+
invoke(.userTotalCount(count))
69+
}
70+
71+
func isUserFetching(_ isFetching: Bool) {
72+
invoke(.isUserFetching(isFetching))
73+
}
74+
}

0 commit comments

Comments
 (0)