├── .github
└── workflows
│ └── build_check.yml
├── .gitignore
├── Example
├── .gitignore
├── Podfile
├── Podfile.lock
├── README.md
├── RxController.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── RxController-Example.xcscheme
├── RxController
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.xib
│ ├── Controller
│ │ ├── Child
│ │ │ ├── FirstNameViewController.swift
│ │ │ ├── FirstNameViewModel.swift
│ │ │ ├── InfoViewController.swift
│ │ │ ├── InfoViewModel.swift
│ │ │ ├── LastNameViewController.swift
│ │ │ ├── LastNameViewModel.swift
│ │ │ ├── NameViewController.swift
│ │ │ ├── NameViewModel.swift
│ │ │ ├── NumberViewController.swift
│ │ │ └── NumberViewModel.swift
│ │ ├── MainViewController.swift
│ │ ├── MainViewModel.swift
│ │ └── Recursion
│ │ │ ├── FirendsViewModel.swift
│ │ │ ├── FriendsViewController.swift
│ │ │ ├── ProfileViewController.swift
│ │ │ └── ProfileViewModel.swift
│ ├── Flow
│ │ ├── AppFlow.swift
│ │ ├── ChildFlow.swift
│ │ ├── FriendsFlow.swift
│ │ ├── MainFlow.swift
│ │ └── ProfileFlow.swift
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ └── icon.png
│ ├── Info.plist
│ └── Library
│ │ ├── BaseViewController.swift
│ │ ├── BaseViewModel.swift
│ │ └── SelectionTableViewCell.swift
└── Tests
│ ├── Info.plist
│ └── Tests.swift
├── LICENSE
├── Package.swift
├── README.md
├── RxController.podspec
├── RxController
├── Assets
│ └── .gitkeep
└── Classes
│ ├── .gitkeep
│ ├── Log.swift
│ ├── RxControllerEvent.swift
│ ├── RxControllerEventBinder.swift
│ ├── RxControllerEventRouter.swift
│ ├── RxFlow+Extension.swift
│ ├── RxViewController.swift
│ └── RxViewModel.swift
├── _Pods.xcodeproj
├── document
├── chapter1-introduction.md
├── chapter2-rxflow.md
├── chapter3-viewcontroller.md
├── chapter4-viewmodel.md
├── chapter5-view.md
├── chapter6-cell.md
└── chapter7-manager.md
├── images
├── child_view_controllers.jpg
├── child_view_models.jpg
├── logo.jpg
├── rxflow.jpg
├── subviews_and_child_controller.jpg
└── viewmodel.jpg
└── rxtree
├── .gitignore
├── Package.swift
├── Sources
└── rxtree
│ ├── Extensions
│ ├── Array+Extension.swift
│ ├── FileManager+Extension.swift
│ ├── Int+Extension.swift
│ ├── String+Extension.swift
│ └── URL+Extension.swift
│ ├── Keyword.swift
│ ├── Node.swift
│ ├── RxTree.swift
│ ├── Scanner.swift
│ └── main.swift
├── Tests
├── LinuxMain.swift
└── rxtreeTests
│ ├── XCTestManifests.swift
│ └── rxtreeTests.swift
└── build_for_xcode.sh
/.github/workflows/build_check.yml:
--------------------------------------------------------------------------------
1 | name: build_check
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: macOS-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Pod install
17 | run: pod install --project-directory=Example --repo-update
18 | - name: Run test
19 | run: set -o pipefail && xcodebuild -workspace Example/RxController.xcworkspace -scheme RxController -sdk iphonesimulator build CODE_SIGNING_REQUIRED=NO | xcpretty -c
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
26 | # Carthage/Checkouts
27 |
28 | Carthage/Build
29 |
30 | # We recommend against adding the Pods directory to your .gitignore. However
31 | # you should judge for yourself, the pros and cons are mentioned at:
32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
33 | #
34 | # Note: if you ignore the Pods directory, make sure to uncomment
35 | # `pod install` in .travis.yml
36 | #
37 | # Pods/
38 |
39 | .idea/
40 |
41 | # Swift Package Manager
42 | .build
43 | Package.resolved
44 | .swiftpm
45 |
46 | # rxtree
47 | .rxtree-version
48 |
--------------------------------------------------------------------------------
/Example/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/
2 | *.xcworkspace/
3 | .idea/
4 |
5 | # rxtree
6 | rxtree
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 | platform :ios, '10.0'
3 |
4 | target 'RxController_Example' do
5 | pod 'RxController', :path => '../'
6 | pod 'RxDataSourcesSingleSection'
7 | pod 'RxBinding'
8 | pod 'SnapKit'
9 | pod 'Fakery'
10 |
11 | target 'RxController_Tests' do
12 | inherit! :search_paths
13 | end
14 |
15 | post_install do |installer|
16 | system('cd ../rxtree && swift build && cp .build/debug/rxtree `pwd`/../Example')
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Differentiator (5.0.0)
3 | - Fakery (5.0.0)
4 | - Reusable (4.1.1):
5 | - Reusable/Storyboard (= 4.1.1)
6 | - Reusable/View (= 4.1.1)
7 | - Reusable/Storyboard (4.1.1)
8 | - Reusable/View (4.1.1)
9 | - RxBinding (0.5):
10 | - RxCocoa (~> 6)
11 | - RxSwift (~> 6)
12 | - RxCocoa (6.0.0):
13 | - RxRelay (= 6.0.0)
14 | - RxSwift (= 6.0.0)
15 | - RxController (1.3):
16 | - RxCocoa (~> 6)
17 | - RxFlow (~> 2.12)
18 | - RxSwift (~> 6)
19 | - RxDataSources (5.0.0):
20 | - Differentiator (~> 5.0)
21 | - RxCocoa (~> 6.0)
22 | - RxSwift (~> 6.0)
23 | - RxDataSourcesSingleSection (0.3):
24 | - Reusable (~> 4)
25 | - RxCocoa (~> 6)
26 | - RxDataSources (~> 5)
27 | - RxSwift (~> 6)
28 | - RxFlow (2.12.0):
29 | - RxCocoa (>= 6.0.0)
30 | - RxSwift (>= 6.0.0)
31 | - RxRelay (6.0.0):
32 | - RxSwift (= 6.0.0)
33 | - RxSwift (6.0.0)
34 | - SnapKit (5.0.1)
35 |
36 | DEPENDENCIES:
37 | - Fakery
38 | - RxBinding
39 | - RxController (from `../`)
40 | - RxDataSourcesSingleSection
41 | - SnapKit
42 |
43 | SPEC REPOS:
44 | trunk:
45 | - Differentiator
46 | - Fakery
47 | - Reusable
48 | - RxBinding
49 | - RxCocoa
50 | - RxDataSources
51 | - RxDataSourcesSingleSection
52 | - RxFlow
53 | - RxRelay
54 | - RxSwift
55 | - SnapKit
56 |
57 | EXTERNAL SOURCES:
58 | RxController:
59 | :path: "../"
60 |
61 | SPEC CHECKSUMS:
62 | Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
63 | Fakery: 8146918b8dd6df98564dca10cbe8bd05354b8cc4
64 | Reusable: 53a9acf5c536f229b31b5865782414b508252ddb
65 | RxBinding: e3c76d02d0ee3f1a306a0fb8e8ef6f2eda65a375
66 | RxCocoa: 3f79328fafa3645b34600f37c31e64c73ae3a80e
67 | RxController: 0dd505aff2189fd61a4fa2dd053f3049c6f480b5
68 | RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf
69 | RxDataSourcesSingleSection: e646e523ad92109293b22e745b55dcb38bea7a58
70 | RxFlow: b407eb6b5d956041a9e0930469346e104911a470
71 | RxRelay: 8d593be109c06ea850df027351beba614b012ffb
72 | RxSwift: c14e798c59b9f6e9a2df8fd235602e85cc044295
73 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
74 |
75 | PODFILE CHECKSUM: 0e048f42166325ac7ea57547d12ebbc2fc47fb5f
76 |
77 | COCOAPODS: 1.10.1
78 |
--------------------------------------------------------------------------------
/Example/README.md:
--------------------------------------------------------------------------------
1 | # Example of RxController
2 |
3 | The example app shows how to develop an iOS app with the MVVM-C design pattern using RxController(RxSwift, RxCocoa and RxFlow).
4 | This documentation introduces the structure of this demo app, in order to helps you to understand RxController and use it in your project.
5 |
6 | To extend `RxViewController` and `RxViewModel`, the subclass `BaseViewController` and `BaseViewModel` is prepared.
7 |
8 | The app has the following tabs.
9 | Each tab shows a scene with some components of RxController.
10 |
11 | ### Child View Controller Demo
12 |
13 | This part shows how to use `BaseViewController` and `BaseViewModel` to create a standard view controller under the standard of RxController.
14 |
15 | ```
16 | InfoViewController
17 | ├── NameViewController
18 | │ ├── FirstNameViewController
19 | │ ├── LastNameViewController
20 | ├── NumberViewController
21 | ```
22 |
23 | In the root parent view controller `InfoViewController`, a random name including first and last name, a random telephone number,
24 | and a update button are shown.
25 | It has two child view controllers, `NameViewController` and `NumberViewController`.
26 | The name and number in the child view controllers(`NameViewController` and `NumberViewController`) can be updated while the update button is clicked in the parent view controller `InfoViewController`,
27 | because they can exchange data with events which are defined in the view model `InfoViewModel`.
28 | Benefited by the events, when the update button in the childViewControllers is clicked,
29 | the corresponding label in the parent view controller can be updated.
30 |
31 | As same as the system constituted by `InfoViewController`, `FirstNameViewController`, `LastNameViewController`,
32 | the sub system constituted by `NameViewController`, `FirstNameViewController`, `LastNameViewController` have some events in the view model `NameViewModel`.
33 | These events help data exchanging among the view models of the sub system.
34 |
35 | By default, events from `InfoViewModel` cannot be transported to `FirstNameViewModel` or `LastNameViewModel`.
36 | Also, the events from `FirstNameViewModel` or `LastNameViewModel` cannot be transported to `InfoViewModel`.
37 | The `forward` methods introduced in the event router can helps data changing from `InfoViewModel` to `FirstNameViewModel` via `NameViewModel`.
38 |
39 | ### Recursion Test
40 |
41 | This tab shows how to create flows and jump to a new view controller or a new flow with `RxController` and `RxFlow`.
42 | The following output of `rxtree` shows that `ProfileFlow` and `FriendsFlow` initialize each other recursively.
43 |
44 | ```
45 | AppFlow
46 | ├── MainFlow
47 | │ ├── ChildFlow
48 | │ ├── ProfileFlow
49 | │ │ ├── FriendsFlow
50 | │ │ │ ├── ProfileFlow
51 | │ │ │ │ ├── FriendsFlow
52 | │ │ │ │ │ ├── ProfileFlow
53 | │ │ │ │ │ │ ├── FriendsFlow
54 | │ │ │ │ │ │ │ ├── ProfileFlow
55 | │ │ │ │ │ │ │ │ ├── FriendsFlow
56 | │ │ │ │ │ │ │ │ │ ├── ProfileFlow
57 | │ │ │ │ │ │ │ │ │ ├── FriendsViewController
58 | │ │ │ │ │ │ │ │ ├── ProfileViewController
59 | │ │ │ │ │ │ │ ├── FriendsViewController
60 | │ │ │ │ │ │ ├── ProfileViewController
61 | │ │ │ │ │ ├── FriendsViewController
62 | │ │ │ │ ├── ProfileViewController
63 | │ │ │ ├── FriendsViewController
64 | │ │ ├── ProfileViewController
65 |
66 | ```
67 |
--------------------------------------------------------------------------------
/Example/RxController.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/RxController.xcodeproj/xcshareddata/xcschemes/RxController-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
99 |
105 |
106 |
107 |
108 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Example/RxController/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/01/2019.
6 | // Copyright (c) 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import SnapKit
10 | import RxSwift
11 | import RxFlow
12 | @_exported import RxBinding
13 |
14 | @UIApplicationMain
15 | class AppDelegate: UIResponder, UIApplicationDelegate {
16 |
17 | private let coordinator = FlowCoordinator()
18 | private let disposeBag = DisposeBag()
19 | private let window = UIWindow()
20 |
21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
22 | coordinator.rx.didNavigate.subscribe(onNext: {
23 | print("did navigate to \($0) -> \($1)")
24 | }).disposed(by: disposeBag)
25 |
26 | coordinate {
27 | (AppFlow(window: $0), AppStep.main)
28 | }
29 | return true
30 | }
31 |
32 | private func coordinate(to: (UIWindow) -> (Flow, Step)) {
33 | let (flow, step) = to(window)
34 | coordinator.coordinate(flow: flow, with: OneStepper(withSingleStep: step))
35 | window.makeKeyAndVisible()
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Example/RxController/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/FirstNameViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstNameViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/06/03.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 |
13 | struct title {
14 | static let marginLeft = 30
15 | }
16 |
17 | struct firstName {
18 | static let marginTop = 10
19 | }
20 |
21 | struct lastNameLabel {
22 | static let marginTop = 10
23 | }
24 |
25 | struct update {
26 | static let width = 150
27 | static let marginTop = 10
28 | static let marginRight = 10
29 | }
30 |
31 | }
32 |
33 | class FirstNameViewController: BaseViewController {
34 |
35 | private lazy var titleLabel: UILabel = {
36 | let label = UILabel()
37 | label.text = "FirstNameChildViewController"
38 | label.textColor = .cyan
39 | return label
40 | }()
41 |
42 | private lazy var firstNameLabel: UILabel = {
43 | let label = UILabel()
44 | label.textColor = .red
45 | return label
46 | }()
47 |
48 | private lazy var lastNameLabel = UILabel()
49 |
50 | private lazy var updateButton: UIButton = {
51 | let button = UIButton()
52 | button.setTitle("Update Name", for: .normal)
53 | button.backgroundColor = .lightGray
54 | button.layer.cornerRadius = 5
55 | button.layer.masksToBounds = true
56 | button.rx.tap.bind { [unowned self] in
57 | self.viewModel.updateFirstName()
58 | }.disposed(by: disposeBag)
59 | return button
60 | }()
61 |
62 | override func viewDidLoad() {
63 | super.viewDidLoad()
64 |
65 | view.backgroundColor = .white
66 | }
67 |
68 | override func subviews() -> [UIView] {
69 | return [
70 | titleLabel,
71 | firstNameLabel,
72 | lastNameLabel,
73 | updateButton
74 | ]
75 | }
76 |
77 | override func bind() -> [Disposable] {
78 | return [
79 | viewModel.firstName ~> firstNameLabel.rx.text,
80 | viewModel.lastName ~> lastNameLabel.rx.text
81 | ]
82 | }
83 |
84 | override func createConstraints() {
85 |
86 | titleLabel.snp.makeConstraints {
87 | $0.left.equalToSuperview().offset(Const.title.marginLeft)
88 | $0.top.equalToSuperview()
89 | }
90 |
91 | firstNameLabel.snp.makeConstraints {
92 | $0.left.equalTo(titleLabel)
93 | $0.top.equalTo(titleLabel.snp.bottom).offset(Const.firstName.marginTop)
94 | }
95 |
96 | lastNameLabel.snp.makeConstraints {
97 | $0.centerY.equalTo(firstNameLabel)
98 | $0.left.equalTo(firstNameLabel.snp.right).offset(Const.lastNameLabel.marginTop)
99 | }
100 |
101 | updateButton.snp.makeConstraints {
102 | $0.width.equalTo(Const.update.width)
103 | $0.top.equalTo(firstNameLabel.snp.bottom).offset(Const.update.marginTop)
104 | $0.right.equalToSuperview().offset(-Const.update.marginRight)
105 | }
106 |
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/FirstNameViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstNameViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/06/03.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import Fakery
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class FirstNameViewModel: BaseViewModel {
14 |
15 | private let faker = Faker(locale: "nb-NO")
16 |
17 | private let firstNameRelay = BehaviorRelay(value: nil)
18 | private let lastNameRelay = BehaviorRelay(value: nil)
19 |
20 | override func prepareForParentEvents() {
21 | bindParentEvents(to: firstNameRelay, with: NameEvent.firstName)
22 | bindParentEvents(to: lastNameRelay, with: NameEvent.lastName)
23 | }
24 |
25 | var firstName: Observable {
26 | firstNameRelay.asObservable()
27 | }
28 |
29 | var lastName: Observable {
30 | lastNameRelay.asObservable()
31 | }
32 |
33 | func updateFirstName() {
34 | parentEvents.accept(NameEvent.firstName.event(faker.name.lastName()))
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/InfoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoViewController.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/01/2019.
6 | // Copyright (c) 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 |
13 | struct nameTitle {
14 | static let marginLeft = 10
15 | static let marginTop = 100
16 | }
17 |
18 | struct numberTitle {
19 | static let marginRight = 10
20 | }
21 |
22 | struct update {
23 | static let width = 150
24 | static let marginTop = 10
25 | }
26 |
27 | struct name{
28 | static let height = 360
29 | static let marginTop = 30
30 | }
31 |
32 | struct number {
33 | static let height = 100
34 | static let marginTop = 30
35 | }
36 |
37 | }
38 |
39 | class InfoViewController: BaseViewController {
40 |
41 | private lazy var nameTitleLabel = UILabel()
42 |
43 | private lazy var numberTitleLabel: UILabel = {
44 | let label = UILabel()
45 | label.textAlignment = .right
46 | return label
47 | }()
48 |
49 | private lazy var updateButton: UIButton = {
50 | let button = UIButton()
51 | button.setTitle("Update All", for: .normal)
52 | button.backgroundColor = .lightGray
53 | button.layer.cornerRadius = 5
54 | button.layer.masksToBounds = true
55 | button.rx.tap.bind { [unowned self] in
56 | self.viewModel.updateAll()
57 | }.disposed(by: disposeBag)
58 | return button
59 | }()
60 |
61 | private lazy var nameView = UIView()
62 | private lazy var numberView = UIView()
63 |
64 | private lazy var nameViewController = NameViewController(viewModel: .init())
65 | private lazy var numberViewController = NumberViewController(viewModel: .init())
66 |
67 | override func viewDidLoad() {
68 | super.viewDidLoad()
69 | view.backgroundColor = .white
70 |
71 | addChild(nameViewController, to: nameView)
72 | addChild(numberViewController, to: numberView)
73 | }
74 |
75 | override func subviews() -> [UIView] {
76 | return [
77 | nameTitleLabel,
78 | numberTitleLabel,
79 | updateButton,
80 | nameView,
81 | numberView
82 | ]
83 | }
84 |
85 | override func bind() -> [Disposable] {
86 | return [
87 | viewModel.name ~> nameTitleLabel.rx.text,
88 | viewModel.number ~> numberTitleLabel.rx.text
89 | ]
90 | }
91 |
92 | override func createConstraints() {
93 |
94 | nameTitleLabel.snp.makeConstraints {
95 | $0.left.equalToSuperview().offset(Const.nameTitle.marginLeft)
96 | $0.top.equalToSuperview().offset(Const.nameTitle.marginTop)
97 | }
98 |
99 | numberTitleLabel.snp.makeConstraints {
100 | $0.centerY.equalTo(nameTitleLabel)
101 | $0.right.equalToSuperview().offset(-Const.numberTitle.marginRight)
102 | }
103 |
104 | updateButton.snp.makeConstraints {
105 | $0.width.equalTo(Const.update.width)
106 | $0.top.equalTo(numberTitleLabel.snp.bottom).offset(Const.update.marginTop)
107 | $0.right.equalTo(numberTitleLabel)
108 | }
109 |
110 | nameView.snp.makeConstraints {
111 | $0.height.equalTo(Const.name.height)
112 | $0.left.right.equalToSuperview()
113 | $0.top.equalTo(updateButton.snp.bottom).offset(Const.name.marginTop)
114 | }
115 |
116 | numberView.snp.makeConstraints {
117 | $0.height.equalTo(Const.number.height)
118 | $0.left.right.equalToSuperview()
119 | $0.top.equalTo(nameView.snp.bottom).offset(Const.number.marginTop)
120 | }
121 |
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/InfoViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/09.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 | import RxCocoa
11 | import RxController
12 | import Fakery
13 |
14 | struct InfoEvent {
15 | static let name = RxControllerEvent.identifier()
16 | static let number = RxControllerEvent.identifier()
17 | }
18 |
19 | class InfoViewModel: BaseViewModel {
20 |
21 | private let faker = Faker(locale: "nb-NO")
22 |
23 | override init() {
24 | super.init()
25 |
26 | viewDidLoad.subscribe(onNext: { print("viewDidLoad") }).disposed(by: disposeBag)
27 | viewWillAppear.subscribe(onNext: { print("viewWillAppear") }).disposed(by: disposeBag)
28 | viewWillDisappear.subscribe(onNext: { print("viewWillDisappear") }).disposed(by: disposeBag)
29 | viewDidAppear.subscribe(onNext: { print("viewDidAppear") }).disposed(by: disposeBag)
30 | viewDidDisappear.subscribe(onNext: { print("viewDidDisappear") }).disposed(by: disposeBag)
31 | }
32 |
33 | var name: Observable {
34 | events.value(of: InfoEvent.name)
35 | }
36 |
37 | var number: Observable {
38 | events.value(of: InfoEvent.number)
39 | }
40 |
41 | func updateAll() {
42 | events.accept(InfoEvent.name.event(faker.name.name()))
43 | events.accept(InfoEvent.number.event(faker.phoneNumber.cellPhone()))
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/LastNameViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LastNameViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/06/03.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 |
13 | struct title {
14 | static let marginLeft = 30
15 | }
16 |
17 | struct firstName {
18 | static let marginTop = 10
19 | }
20 |
21 | struct lastNameLabel {
22 | static let marginTop = 10
23 | }
24 |
25 | struct update {
26 | static let width = 150
27 | static let marginTop = 10
28 | static let marginRight = 10
29 | }
30 |
31 | }
32 |
33 | class LastNameViewController: BaseViewController {
34 |
35 | private lazy var titleLabel: UILabel = {
36 | let label = UILabel()
37 | label.text = "LastNameChildViewController"
38 | label.textColor = .cyan
39 | return label
40 | }()
41 |
42 | private lazy var firstNameLabel = UILabel()
43 |
44 | private lazy var lastNameLabel: UILabel = {
45 | let label = UILabel()
46 | label.textColor = .red
47 | return label
48 | }()
49 |
50 | private lazy var updateButton: UIButton = {
51 | let button = UIButton()
52 | button.setTitle("Update Name", for: .normal)
53 | button.backgroundColor = .lightGray
54 | button.layer.cornerRadius = 5
55 | button.layer.masksToBounds = true
56 | button.rx.tap.bind { [unowned self] in
57 | self.viewModel.updateLastName()
58 | }.disposed(by: disposeBag)
59 | return button
60 | }()
61 |
62 | override func viewDidLoad() {
63 | super.viewDidLoad()
64 |
65 | view.backgroundColor = .white
66 | }
67 |
68 | override func subviews() -> [UIView] {
69 | return [
70 | titleLabel,
71 | firstNameLabel,
72 | lastNameLabel,
73 | updateButton
74 | ]
75 | }
76 |
77 | override func bind() -> [Disposable] {
78 | return [
79 | viewModel.firstName ~> firstNameLabel.rx.text,
80 | viewModel.lastName ~> lastNameLabel.rx.text
81 | ]
82 | }
83 |
84 | override func createConstraints() {
85 |
86 | titleLabel.snp.makeConstraints {
87 | $0.left.equalToSuperview().offset(Const.title.marginLeft)
88 | $0.top.equalToSuperview()
89 | }
90 |
91 | firstNameLabel.snp.makeConstraints {
92 | $0.left.equalTo(titleLabel)
93 | $0.top.equalTo(titleLabel.snp.bottom).offset(Const.firstName.marginTop)
94 | }
95 |
96 | lastNameLabel.snp.makeConstraints {
97 | $0.centerY.equalTo(firstNameLabel)
98 | $0.left.equalTo(firstNameLabel.snp.right).offset(Const.lastNameLabel.marginTop)
99 | }
100 |
101 | updateButton.snp.makeConstraints {
102 | $0.width.equalTo(Const.update.width)
103 | $0.top.equalTo(firstNameLabel.snp.bottom).offset(Const.update.marginTop)
104 | $0.right.equalToSuperview().offset(-Const.update.marginRight)
105 | }
106 |
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/LastNameViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LastNameViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/06/03.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import Fakery
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class LastNameViewModel: BaseViewModel {
14 |
15 | private let faker = Faker(locale: "nb-NO")
16 |
17 | private let firstNameRelay = BehaviorRelay(value: nil)
18 | private let lastNameRelay = BehaviorRelay(value: nil)
19 |
20 | override func prepareForParentEvents() {
21 | bindParentEvents(to: firstNameRelay, with: NameEvent.firstName)
22 | bindParentEvents(to: lastNameRelay, with: NameEvent.lastName)
23 | }
24 |
25 | var firstName: Observable {
26 | firstNameRelay.asObservable()
27 | }
28 |
29 | var lastName: Observable {
30 | lastNameRelay.asObservable()
31 | }
32 |
33 | func updateLastName() {
34 | parentEvents.accept(NameEvent.lastName.event(faker.name.lastName()))
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/NameViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NameViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/16.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 | struct title {
13 | static let marginLeft = 10
14 | }
15 |
16 | struct name {
17 | static let marginTop = 10
18 | }
19 |
20 | struct number {
21 | static let marginRight = 10
22 | }
23 |
24 | struct update {
25 | static let width = 150
26 | static let marginTop = 10
27 | }
28 |
29 | struct firstName {
30 | static let height = 100
31 | static let marginTop = 30
32 | }
33 |
34 | struct lastName {
35 | static let height = 100
36 | static let marginTop = 30
37 | }
38 |
39 | }
40 |
41 | class NameViewController: BaseViewController {
42 |
43 | private lazy var titleLabel: UILabel = {
44 | let label = UILabel()
45 | label.text = "NameMidChildViewController"
46 | label.textColor = .blue
47 | return label
48 | }()
49 |
50 | private lazy var nameLabel: UILabel = {
51 | let label = UILabel()
52 | label.textColor = .red
53 | return label
54 | }()
55 |
56 | private lazy var numberLabel: UILabel = {
57 | let label = UILabel()
58 | label.textAlignment = .right
59 | return label
60 | }()
61 |
62 | private lazy var updateButton: UIButton = {
63 | let button = UIButton()
64 | button.setTitle("Update Name", for: .normal)
65 | button.backgroundColor = .lightGray
66 | button.layer.cornerRadius = 5
67 | button.layer.masksToBounds = true
68 | button.rx.tap.bind { [unowned self] in
69 | self.viewModel.updateName()
70 | }.disposed(by: disposeBag)
71 | return button
72 | }()
73 |
74 | private lazy var firstNameView = UIView()
75 | private lazy var lastNameView = UIView()
76 |
77 | private lazy var firstNameViewController = FirstNameViewController(viewModel: .init())
78 | private lazy var lastNameViewController = LastNameViewController(viewModel: .init())
79 |
80 | override func viewDidLoad() {
81 | super.viewDidLoad()
82 | view.backgroundColor = .white
83 |
84 | addChild(firstNameViewController, to: firstNameView)
85 | addChild(lastNameViewController, to: lastNameView)
86 | }
87 |
88 | override func viewWillAppear(_ animated: Bool) {
89 | super.viewWillAppear(animated)
90 |
91 | viewModel.updateName()
92 | }
93 |
94 | override func subviews() -> [UIView] {
95 | return [
96 | titleLabel,
97 | nameLabel,
98 | numberLabel,
99 | updateButton,
100 | firstNameView,
101 | lastNameView
102 | ]
103 | }
104 |
105 | override func bind() -> [Disposable] {
106 | return [
107 | viewModel.name ~> nameLabel.rx.text,
108 | viewModel.number ~> numberLabel.rx.text
109 | ]
110 | }
111 |
112 | override func createConstraints() {
113 |
114 | titleLabel.snp.makeConstraints {
115 | $0.left.equalToSuperview().offset(Const.title.marginLeft)
116 | $0.top.equalToSuperview()
117 | }
118 |
119 | nameLabel.snp.makeConstraints {
120 | $0.left.equalTo(titleLabel)
121 | $0.top.equalTo(titleLabel.snp.bottom).offset(Const.name.marginTop)
122 | }
123 |
124 | numberLabel.snp.makeConstraints {
125 | $0.centerY.equalTo(nameLabel)
126 | $0.right.equalToSuperview().offset(-Const.number.marginRight)
127 | }
128 |
129 | updateButton.snp.makeConstraints {
130 | $0.width.equalTo(Const.update.width)
131 | $0.top.equalTo(numberLabel.snp.bottom).offset(Const.update.marginTop)
132 | $0.right.equalTo(numberLabel)
133 | }
134 |
135 | firstNameView.snp.makeConstraints {
136 | $0.height.equalTo(Const.firstName.height)
137 | $0.left.right.equalToSuperview()
138 | $0.top.equalTo(updateButton.snp.bottom).offset(Const.firstName.marginTop)
139 | }
140 |
141 | lastNameView.snp.makeConstraints {
142 | $0.height.equalTo(Const.lastName.height)
143 | $0.left.right.equalToSuperview()
144 | $0.top.equalTo(firstNameView.snp.bottom).offset(Const.lastName.marginTop)
145 | }
146 |
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/NameViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NameViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/16.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import Fakery
10 | import RxCocoa
11 | import RxController
12 | import RxSwift
13 |
14 |
15 | struct NameEvent {
16 | static let firstName = RxControllerEvent.identifier()
17 | static let lastName = RxControllerEvent.identifier()
18 | }
19 |
20 | class NameViewModel: BaseViewModel {
21 |
22 | private let faker = Faker(locale: "nb-NO")
23 |
24 | private let nameRelay = BehaviorRelay(value: nil)
25 |
26 | override func prepareForParentEvents() {
27 | bindParentEvents(to: nameRelay, with: InfoEvent.name)
28 | }
29 |
30 | var name: Observable {
31 | Observable.merge(
32 | nameRelay.asObservable(),
33 | Observable
34 | .combineLatest(
35 | events.unwrappedValue(of: NameEvent.firstName),
36 | events.unwrappedValue(of: NameEvent.lastName)
37 | )
38 | .map { $0 + " " + $1 }
39 | )
40 | }
41 |
42 | var number: Observable {
43 | parentEvents.value(of: InfoEvent.number)
44 | }
45 |
46 | func updateName() {
47 | let firstName = faker.name.firstName()
48 | let lastName = faker.name.lastName()
49 | parentEvents.accept(InfoEvent.name.event(firstName + " " + lastName))
50 | events.accept(NameEvent.firstName.event(firstName))
51 | events.accept(NameEvent.lastName.event(lastName))
52 | }
53 |
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/NumberViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/16.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 |
13 | struct title {
14 | static let marginLeft = 10
15 | }
16 |
17 | struct number {
18 | static let marginTop = 10
19 | }
20 |
21 | struct update {
22 | static let width = 150
23 | static let marginTop = 10
24 | static let marginRight = 10
25 | }
26 |
27 | }
28 |
29 | class NumberViewController: BaseViewController {
30 |
31 | private lazy var titleLabel: UILabel = {
32 | let label = UILabel()
33 | label.text = "NumberChildViewController"
34 | label.textColor = .blue
35 | return label
36 | }()
37 |
38 | private lazy var numberLabel: UILabel = {
39 | let label = UILabel()
40 | label.textAlignment = .right
41 | label.textColor = .red
42 | return label
43 | }()
44 |
45 | private lazy var updateButton: UIButton = {
46 | let button = UIButton()
47 | button.setTitle("Update Number", for: .normal)
48 | button.backgroundColor = .lightGray
49 | button.layer.cornerRadius = 5
50 | button.layer.masksToBounds = true
51 | button.rx.tap.bind { [unowned self] in
52 | self.viewModel.updateNumber()
53 | }.disposed(by: disposeBag)
54 | return button
55 | }()
56 |
57 | override func viewDidLoad() {
58 | super.viewDidLoad()
59 |
60 | view.backgroundColor = .white
61 | }
62 |
63 | override func viewWillAppear(_ animated: Bool) {
64 | super.viewWillAppear(animated)
65 |
66 | viewModel.updateNumber()
67 | }
68 |
69 | override func subviews() -> [UIView] {
70 | return [
71 | titleLabel,
72 | numberLabel,
73 | updateButton
74 | ]
75 | }
76 |
77 | override func bind() -> [Disposable] {
78 | return [
79 | viewModel.number ~> numberLabel.rx.text
80 | ]
81 | }
82 |
83 | override func createConstraints() {
84 |
85 | titleLabel.snp.makeConstraints {
86 | $0.left.equalToSuperview().offset(Const.title.marginLeft)
87 | $0.top.equalToSuperview()
88 | }
89 |
90 | numberLabel.snp.makeConstraints {
91 | $0.left.equalTo(titleLabel)
92 | $0.top.equalTo(titleLabel.snp.bottom).offset(Const.number.marginTop)
93 | }
94 |
95 | updateButton.snp.makeConstraints {
96 | $0.width.equalTo(Const.update.width)
97 | $0.top.equalTo(numberLabel.snp.bottom).offset(Const.update.marginTop)
98 | $0.right.equalToSuperview().offset(-Const.update.marginRight)
99 | }
100 |
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Child/NumberViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/16.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import Fakery
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class NumberViewModel: BaseViewModel {
14 |
15 | private let faker = Faker(locale: "nb-NO")
16 |
17 | private let numberRelay = BehaviorRelay(value: nil)
18 |
19 | override func prepareForParentEvents() {
20 | bindParentEvents(to: numberRelay, with: InfoEvent.number)
21 | }
22 |
23 | var number: Observable {
24 | numberRelay.asObservable()
25 | }
26 |
27 | func updateNumber() {
28 | parentEvents.accept(InfoEvent.number.event(faker.phoneNumber.cellPhone()))
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/MainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | enum MainTabType: Int {
12 | case child = 0
13 | case recursion
14 |
15 | var tabTitle: String? {
16 | switch self {
17 | case .child:
18 | return "Child"
19 | case .recursion:
20 | return "Recursion"
21 | }
22 | }
23 |
24 | var navigationTitle: String? {
25 | switch self {
26 | case .child:
27 | return "Child View Controller Demo"
28 | case .recursion:
29 | return "Recursion Test"
30 | }
31 | }
32 | }
33 |
34 | class MainViewController: UITabBarController {
35 |
36 | private let disposeBag = DisposeBag()
37 | private let viewModel: MainViewModel
38 |
39 | init(viewModel: MainViewModel) {
40 | self.viewModel = viewModel
41 | super.init(nibName: nil, bundle: nil)
42 | }
43 |
44 | required init?(coder aDecoder: NSCoder) {
45 | fatalError("init(coder:) has not been implemented")
46 | }
47 |
48 | override var viewControllers: [UIViewController]? {
49 | didSet {
50 | guard let viewControllers = viewControllers, viewControllers.count > 0 else {
51 | return
52 | }
53 | for (index, viewController) in viewControllers.enumerated() {
54 | let type = MainTabType(rawValue: index)
55 | viewController.tabBarItem = UITabBarItem(title: type?.tabTitle, image: nil, tag: index)
56 | }
57 | updateNavigationBar(with: .child)
58 | }
59 | }
60 |
61 | override func viewDidLoad() {
62 | super.viewDidLoad()
63 |
64 | delegate = self
65 | }
66 |
67 | private func updateNavigationBar(with type: MainTabType) {
68 | title = type.navigationTitle
69 | }
70 |
71 | }
72 |
73 | extension MainViewController: UITabBarControllerDelegate {
74 |
75 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
76 | guard let tabType = MainTabType(rawValue: tabBarController.selectedIndex) else {
77 | return
78 | }
79 | updateNavigationBar(with: tabType)
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | class MainViewModel: BaseViewModel {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Recursion/FirendsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirendsViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxDataSourcesSingleSection
11 | import RxSwift
12 | import Fakery
13 |
14 | class FriendsViewModel: BaseViewModel {
15 |
16 | private let nameRealy = BehaviorRelay(value: nil)
17 | private let firindsRelay = BehaviorRelay<[String]>(value: [])
18 |
19 | init(name: String) {
20 | super.init()
21 | nameRealy.accept(name)
22 | let faker = Faker(locale: "nb-NO")
23 | firindsRelay.accept((0...Int.random(in: 5...10)).map { _ in faker.name.name() })
24 | }
25 |
26 | var title: Observable {
27 | nameRealy.filter { $0 != nil }.map {
28 | "\($0!)'s friends"
29 | }
30 | }
31 |
32 | var friendSection: Observable> {
33 | firindsRelay
34 | .map {
35 | $0.map { Selection(title: $0, accessory: .disclosureIndicator) }
36 | }
37 | .map {
38 | SingleSection.create($0)
39 | }
40 | }
41 |
42 | func pick(at index: Int) {
43 | guard 0.. {
12 |
13 | private lazy var tableView: UITableView = {
14 | let tableView = UITableView()
15 | tableView.register(cellType: SelectionTableViewCell.self)
16 | tableView.rx.itemSelected.bind { [unowned self] in
17 | self.viewModel.pick(at: $0.row)
18 | }.disposed(by: disposeBag)
19 | return tableView
20 | }()
21 |
22 | private lazy var dataSource = SelectionTableViewCell.tableViewSingleSectionDataSource()
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | view.backgroundColor = .white
28 | }
29 |
30 | override func subviews() -> [UIView] {
31 | return [
32 | tableView
33 | ]
34 | }
35 |
36 | override func bind() -> [Disposable] {
37 | return [
38 | viewModel.title ~> rx.title,
39 | viewModel.friendSection ~> tableView.rx.items(dataSource: dataSource)
40 | ]
41 | }
42 |
43 | override func createConstraints() {
44 | tableView.snp.makeConstraints {
45 | $0.left.right.bottom.equalToSuperview()
46 | $0.top.equalTo(view.safeAreaInsets.top)
47 | }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Recursion/ProfileViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | private struct Const {
12 |
13 | struct name {
14 | static let marginTop = 130
15 | }
16 |
17 | struct friends {
18 | static let marginTop = 30
19 | }
20 |
21 | }
22 |
23 | class ProfileViewController: BaseViewController {
24 |
25 | private lazy var nameLabel = UILabel()
26 |
27 | private lazy var friendsButton: UIButton = {
28 | let button = UIButton()
29 | button.setTitle("Friends", for: .normal)
30 | button.backgroundColor = .darkGray
31 | button.rx.tap.bind { [unowned self] in
32 | self.viewModel.showFriends()
33 | }.disposed(by: disposeBag)
34 | return button
35 | }()
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | view.backgroundColor = .white
41 | }
42 |
43 | override func subviews() -> [UIView] {
44 | return [
45 | nameLabel,
46 | friendsButton
47 | ]
48 | }
49 |
50 | override func bind() -> [Disposable] {
51 | return [
52 | viewModel.name ~> nameLabel.rx.text
53 | ]
54 | }
55 |
56 | override func createConstraints() {
57 |
58 | nameLabel.snp.makeConstraints {
59 | $0.centerX.equalToSuperview()
60 | $0.top.equalToSuperview().offset(Const.name.marginTop)
61 | }
62 |
63 | friendsButton.snp.makeConstraints {
64 | $0.centerX.equalToSuperview()
65 | $0.top.equalTo(nameLabel.snp.bottom).offset(Const.friends.marginTop)
66 | }
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/Example/RxController/Controller/Recursion/ProfileViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import Fakery
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class ProfileViewModel: BaseViewModel {
14 |
15 | private let nameRelay = BehaviorRelay(value: nil)
16 |
17 | init(name: String? = nil) {
18 | super.init()
19 | nameRelay.accept(name ?? Faker(locale: "nb-NO").name.name())
20 | }
21 |
22 | var name: Observable {
23 | nameRelay.asObservable()
24 | }
25 |
26 | func showFriends() {
27 | guard let name = nameRelay.value else {
28 | return
29 | }
30 | steps.accept(ProfileStep.friends(name))
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Example/RxController/Flow/AppFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppFlow.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/04/09.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxFlow
10 |
11 | enum AppStep: Step {
12 | case main
13 | }
14 |
15 | class AppFlow: Flow {
16 |
17 | var root: Presentable {
18 | return rootWindow
19 | }
20 |
21 | private let rootWindow: UIWindow
22 |
23 | private lazy var navigationController = UINavigationController()
24 |
25 | init(window: UIWindow) {
26 | rootWindow = window
27 | rootWindow.backgroundColor = .white
28 | rootWindow.rootViewController = navigationController
29 | }
30 |
31 | func navigate(to step: Step) -> FlowContributors {
32 | guard let appStep = step as? AppStep else {
33 | return .none
34 | }
35 | switch appStep {
36 | case .main:
37 | let mainFlow = MainFlow()
38 | Flows.use(mainFlow, when: .ready ) { [unowned self] in
39 | self.navigationController.viewControllers = [$0]
40 | }
41 | return .flow(mainFlow, with: MainStep.start)
42 | }
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/Example/RxController/Flow/ChildFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChildFlow.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxFlow
10 |
11 | enum ChildStep: Step {
12 | case start
13 | }
14 |
15 | class ChildFlow: Flow {
16 |
17 | var root: Presentable {
18 | infoViewController
19 | }
20 |
21 | private let infoViewController = InfoViewController(viewModel: InfoViewModel())
22 |
23 | func navigate(to step: Step) -> FlowContributors {
24 | guard let childStep = step as? ChildStep else {
25 | return .none
26 | }
27 | switch childStep {
28 | case .start:
29 | return .viewController(infoViewController)
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Example/RxController/Flow/FriendsFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FriendsFlow.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/12/5.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxFlow
10 |
11 | enum FriendsStep: Step {
12 | case start
13 | case profile(String)
14 | }
15 |
16 | class FriendsFlow: Flow {
17 |
18 | var root: Presentable {
19 | friendsViewController
20 | }
21 |
22 | private let friendsViewController: FriendsViewController
23 |
24 | init(name: String) {
25 | friendsViewController = FriendsViewController(viewModel: .init(name: name))
26 | }
27 |
28 | private var navigationController: UINavigationController? {
29 | friendsViewController.navigationController
30 | }
31 |
32 | func navigate(to step: Step) -> FlowContributors {
33 | guard let friendsStep = step as? FriendsStep else {
34 | return .none
35 | }
36 | switch friendsStep {
37 | case .start:
38 | return .viewController(friendsViewController)
39 | case .profile(let name):
40 | let profileFlow = ProfileFlow(name: name)
41 | Flows.use(profileFlow, when: .ready) {
42 | self.navigationController?.pushViewController($0, animated: true)
43 | }
44 | return .flow(profileFlow, with: ProfileStep.start)
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Example/RxController/Flow/MainFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainFlow.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxFlow
10 |
11 | enum MainStep: Step {
12 | case start
13 | }
14 |
15 | class MainFlow: Flow {
16 |
17 | var root: Presentable {
18 | return mainViewController
19 | }
20 |
21 | private let mainViewModel = MainViewModel()
22 | private lazy var mainViewController = MainViewController(viewModel: mainViewModel)
23 |
24 | private var navigationController: UINavigationController? {
25 | return mainViewController.navigationController
26 | }
27 |
28 | func navigate(to step: Step) -> FlowContributors {
29 | guard let step = step as? MainStep else {
30 | return .none
31 | }
32 | switch step {
33 | case .start:
34 | let childFlow = ChildFlow()
35 | let profileFlow = ProfileFlow()
36 | Flows.use(childFlow, profileFlow, when: .ready) {
37 | self.mainViewController.viewControllers = [$0, $1]
38 | }
39 | return .multiple(flowContributors: [
40 | .contribute(withNextPresentable: mainViewController, withNextStepper: mainViewModel),
41 | .flow(childFlow, with: ChildStep.start),
42 | .flow(profileFlow, with: ProfileStep.start)
43 | ])
44 | }
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Example/RxController/Flow/ProfileFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileFlow.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/12/5.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 |
9 | import RxFlow
10 |
11 | enum ProfileStep: Step {
12 | case start
13 | case friends(String)
14 | }
15 |
16 | class ProfileFlow: Flow {
17 |
18 | var root: Presentable {
19 | profileViewController
20 | }
21 |
22 | private let profileViewController: ProfileViewController
23 |
24 | init(name: String? = nil) {
25 | profileViewController = ProfileViewController(viewModel: .init(name: name))
26 | }
27 |
28 | private var navigationController: UINavigationController? {
29 | profileViewController.navigationController
30 | }
31 |
32 | func navigate(to step: Step) -> FlowContributors {
33 | guard let profileStep = step as? ProfileStep else {
34 | return .none
35 | }
36 | switch profileStep {
37 | case .start:
38 | return .viewController(profileViewController)
39 | case .friends(let name):
40 | let friendsFlow = FriendsFlow(name: name)
41 | Flows.use(friendsFlow, when: .ready) {
42 | self.navigationController?.pushViewController($0, animated: true)
43 | }
44 | return .flow(friendsFlow, with: FriendsStep.start)
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Example/RxController/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-60@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-60@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "1024x1024",
47 | "idiom" : "ios-marketing",
48 | "filename" : "icon.png",
49 | "scale" : "1x"
50 | }
51 | ],
52 | "info" : {
53 | "version" : 1,
54 | "author" : "xcode"
55 | }
56 | }
--------------------------------------------------------------------------------
/Example/RxController/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/Example/RxController/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/Example/RxController/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/Example/RxController/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/Example/RxController/Images.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/Example/RxController/Images.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/Example/RxController/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | RxController
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Example/RxController/Library/BaseViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewController.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 CocoaPods. All rights reserved.
7 | //
8 |
9 | import RxController
10 |
11 | class BaseViewController: RxViewController {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/Example/RxController/Library/BaseViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewModel.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 12/3/19.
6 | // Copyright © 2019 CocoaPods. All rights reserved.
7 | //
8 |
9 | import RxController
10 |
11 | class BaseViewModel: RxViewModel {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/Example/RxController/Library/SelectionTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectionTableViewCell.swift
3 | // RxController_Example
4 | //
5 | // Created by Meng Li on 2019/06/21.
6 | // Copyright © 2019 CocoaPods. All rights reserved.
7 | //
8 |
9 | import RxDataSourcesSingleSection
10 | import UIKit
11 |
12 | private struct Const {
13 | static let margin = 16
14 |
15 | struct Icon {
16 | static let size = 30
17 | }
18 |
19 | struct Title {
20 | static let marginRight = 40
21 | }
22 | }
23 |
24 | struct Selection {
25 |
26 | let identifier = UUID().uuidString
27 | var icon: UIImage?
28 | var title: String
29 | var subtitle: String?
30 | var accessory: UITableViewCell.AccessoryType = .none
31 |
32 | init(icon: UIImage?, title: String, subtitle: String, accessory: UITableViewCell.AccessoryType) {
33 | self.init(icon: icon, title: title, subtitle: subtitle)
34 | self.accessory = accessory
35 | }
36 |
37 | init(icon: UIImage?, title: String, subtitle: String) {
38 | self.icon = icon
39 | self.title = title
40 | self.subtitle = subtitle
41 | }
42 |
43 | init(icon: UIImage?, title: String) {
44 | self.icon = icon
45 | self.title = title
46 | }
47 |
48 | init(title: String, accessory: UITableViewCell.AccessoryType = .none) {
49 | self.title = title
50 | self.accessory = accessory
51 | }
52 |
53 | }
54 |
55 | extension Selection: Equatable {
56 | public static func == (lhs: Selection, rhs: Selection) -> Bool {
57 | return lhs.identifier == rhs.identifier
58 | }
59 | }
60 |
61 | class SelectionTableViewCell: UITableViewCell {
62 |
63 | private lazy var iconImageView = UIImageView()
64 |
65 | private lazy var titleLabel: UILabel = {
66 | let label = UILabel()
67 | label.textColor = .black
68 | label.textAlignment = .left
69 | return label
70 | }()
71 |
72 | private lazy var subtitleLabel: UILabel = {
73 | let label = UILabel()
74 | label.textColor = .darkGray
75 | label.textAlignment = .right
76 | label.font = UIFont.systemFont(ofSize: 15)
77 | return label
78 | }()
79 |
80 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
81 | super.init(style: style, reuseIdentifier: reuseIdentifier)
82 |
83 | backgroundColor = .clear
84 | selectionStyle = .none
85 |
86 | addSubview(iconImageView)
87 | addSubview(titleLabel)
88 | addSubview(subtitleLabel)
89 | createConstraints()
90 | }
91 |
92 | required init?(coder aDecoder: NSCoder) {
93 | fatalError("init(coder:) has not been implemented")
94 | }
95 |
96 | private func createConstraints() {
97 | iconImageView.snp.makeConstraints {
98 | $0.left.equalToSuperview().offset(Const.margin)
99 | $0.size.equalTo(Const.Icon.size)
100 | $0.centerY.equalToSuperview()
101 | }
102 |
103 | titleLabel.snp.makeConstraints {
104 | $0.left.equalTo(iconImageView.snp.right).offset(Const.margin)
105 | $0.centerY.equalToSuperview()
106 | }
107 |
108 | subtitleLabel.snp.makeConstraints {
109 | $0.left.equalTo(titleLabel.snp.right).offset(Const.margin)
110 | $0.right.equalToSuperview().offset(-Const.Title.marginRight)
111 | $0.centerY.equalToSuperview()
112 | }
113 | }
114 |
115 | }
116 |
117 | extension SelectionTableViewCell: Configurable {
118 |
119 | typealias Model = Selection
120 |
121 | func configure(_ selection: Selection) {
122 | if selection.icon == nil {
123 | iconImageView.snp.updateConstraints {
124 | $0.left.equalToSuperview().offset(0)
125 | $0.width.equalTo(0)
126 | }
127 | } else {
128 | iconImageView.snp.updateConstraints {
129 | $0.left.equalToSuperview().offset(Const.margin)
130 | $0.width.equalTo(Const.Icon.size)
131 | }
132 | }
133 |
134 | iconImageView.image = selection.icon
135 | titleLabel.text = selection.title
136 | subtitleLabel.text = selection.subtitle
137 | accessoryType = selection.accessory
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/Example/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/Tests/Tests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import RxController
3 |
4 | class Tests: XCTestCase {
5 |
6 | override func setUp() {
7 | super.setUp()
8 | // Put setup code here. This method is called before the invocation of each test method in the class.
9 | }
10 |
11 | override func tearDown() {
12 | // Put teardown code here. This method is called after the invocation of each test method in the class.
13 | super.tearDown()
14 | }
15 |
16 | func testExample() {
17 | // This is an example of a functional test case.
18 | XCTAssert(true, "Pass")
19 | }
20 |
21 | func testPerformanceExample() {
22 | // This is an example of a performance test case.
23 | self.measure() {
24 | // Put the code you want to measure the time of here.
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 MuShare
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "RxController",
7 | platforms: [
8 | .iOS(.v10)
9 | ],
10 | products: [
11 | .library(name: "RxController", targets: ["RxController"])
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")),
15 | .package(url: "https://github.com/RxSwiftCommunity/RxFlow.git", .upToNextMajor(from: "2.12.0"))
16 | ],
17 | targets: [
18 | .target(name: "RxController", dependencies: ["RxSwift", "RxCocoa", "RxFlow"], path: "RxController")
19 | ],
20 | swiftLanguageVersions: [.v5]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## Introduction
13 |
14 | RxController is a library for the development with MVVM-C based on **RxFlow** and **RxSwift**.
15 | If you are not familiar with them, please learn these frameworks at first:
16 |
17 | - RxSwift (https://github.com/ReactiveX/RxSwift)
18 | - RxCocoa (https://github.com/ReactiveX/RxSwift)
19 | - RxFlow (https://github.com/RxSwiftCommunity/RxFlow)
20 |
21 | RxController provides the the following basic view controller and view model classes.
22 |
23 | - RxViewController
24 | - RxViewModel
25 |
26 | These classes make it easy to transfer data among the flows, the parent view models and the child view models.
27 |
28 | ## Demo applications
29 |
30 | The following 2 open source apps based on RxController are prepared to make it easier for understanding this library.
31 |
32 | - Githuber, a simple github search app: https://github.com/MuShare/Githuber
33 | - Httper, a RESTful api test app: https://github.com/MuShare/Httper-iOS
34 |
35 | ## Recommended guideline
36 |
37 | We recommend to develop a MMVM-C based on **RxController**, **RxFlow** and **RxSwift** in this guideline.
38 | It is better to read the documentation of [RxController](https://github.com/RxSwiftCommunity/RxController), [RxFlow](https://github.com/RxSwiftCommunity/RxFlow) and [RxSwift](https://github.com/ReactiveX/RxSwift) at first, if you are not familiar with them.
39 |
40 | - Chapter 1: [Introduction](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter1-introduction.md)
41 | - Chapter 2: [Using RxFlow](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter2-rxflow.md)
42 | - Chapter 3: [View controller](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter3-viewcontroller.md)
43 | - Chapter 4: [View model](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter4-viewmodel.md)
44 | - Chapter 5: [View](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter5-view.md)
45 | - Chapter 6: [Table and collection view cell](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter6-cell.md)
46 | - Chapter 7: [Manager classes](https://github.com/RxSwiftCommunity/RxController/blob/master/document/chapter7-manager.md)
47 |
48 | ## Documentation
49 |
50 | RxController is available through [CocoaPods](https://cocoapods.org). To install
51 | it, simply add the following line to your Podfile:
52 |
53 | ```ruby
54 | pod 'RxController'
55 | ```
56 |
57 | ### Example
58 |
59 | The example app helps you to understand how to use RxController.
60 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
61 |
62 | ### Generic class of View Controller
63 |
64 | RxController provides a generic classes `RxViewController`.
65 | It avoids using an `Optional` or an `Implicit Unwrapping Option` type for the view model property in the view controller class.
66 |
67 | In the demo app, we define the view model class by extending the RxViewModel class, and the view controller class by extending the RxViewController generic class.
68 |
69 | ```Swift
70 | // View model class
71 | class InfoViewModel: RxViewModel {
72 |
73 | }
74 |
75 | // View controller class
76 | class InfoViewController: RxViewController {
77 |
78 | }
79 | ```
80 |
81 | Then, we can initialize the `InfoViewController` with a safe way as the following.
82 |
83 | ```Swift
84 | func navigate(to step: Step) -> FlowContributors {
85 | guard let appStep = step as? AppStep else {
86 | return .none
87 | }
88 | switch appStep {
89 | case .start:
90 | let infoViewController = InfoViewController(viewModel: InfoViewModel())
91 | navigationController.pushViewController(infoViewController, animated: false)
92 | return .viewController(infoViewController)
93 | }
94 | }
95 | ```
96 |
97 | Within RxViewController, the following standard methods are provided for building UI and binding data.
98 |
99 | - `open func subviews() -> [UIView]`
100 |
101 | The subview method return an array of some views.
102 | These views will be added to the root view of the view controller orderly.
103 |
104 | - `open func createConstraints()`
105 |
106 | Create constranint for subviews of root view.
107 |
108 | - `open func bind() -> [Disposable]`
109 |
110 | The bind method return an array of `Disposable`.
111 | The RxSwift style data binding can be listed in this method, without writting a `disposed(by:)` method.
112 |
113 | ### Access lifecycle from view model directly
114 |
115 | The following lifecycle signal can be accessed from view model directly.
116 |
117 | - `viewDidLoad: Observable`
118 | - `viewWillAppear: Observable`
119 | - `viewDidAppear: Observable`
120 | - `viewWillDisappear: Observable`
121 | - `viewIsAppearing`: Observable`
122 | - `viewDidDisappear: Observable`
123 | - `viewDidLayoutSubview: Observable`
124 | - `viewWillLayoutSubviews: Observable`
125 | - `viewSafeAreaInsetsDidChange: Observable`
126 |
127 | ### Exchange data among parent and child view models
128 |
129 | In a standard MVVM-C architecture using RxFlow, view models exchange data via a flow class using the `steps.accept()` method.
130 | With `RxChildViewModel`, we can exchange data among parent and child view models without the flow class.
131 |
132 | Use the following method to add a child view controller to the root view or a customized view of its parent controller.
133 |
134 | ```Swift
135 | /**
136 | Add a child view controller to the root view of the parent view controller.
137 |
138 | @param childController: a child view controller.
139 | */
140 | override open func addChild(_ childController: UIViewController)
141 |
142 | /**
143 | Add a child view controller to the a container view of the parent view controller.
144 | The edges of the child view controller is same as the container view by default.
145 |
146 | @param childController: a child view controller.
147 | @param containerView: a container view of childController.
148 | */
149 | open func addChild(_ childController: UIViewController, to containerView: UIView)
150 | ```
151 |
152 | To transfer data among view models, we define some events with a struct in the parent view model.
153 |
154 | ```Swift
155 | struct InfoEvent {
156 | static let name = RxControllerEvent.identifier()
157 | static let number = RxControllerEvent.identifier()
158 | }
159 | ```
160 |
161 | 
162 |
163 | As shown in the graph, the events can only be transfered among a parent view model and its first generation child view models.
164 | For example, the `InfoEvent` we defined above, is enabled among `InfoViewModel`, `NameViewModel` and `NumberViewModel`.
165 |
166 | Send a event from the parent view model (`InfoViewModel `).
167 |
168 | ```Swift
169 | events.accept(InfoEvent.name.event("Alice"))
170 | ```
171 |
172 | Send a event from the child view model (`NameViewModel` and `NumberViewModel`).
173 |
174 | ```Swift
175 | parentEvents.accept(event: InfoEvent.name.event("Alice"))
176 | ```
177 |
178 | Receive a event in the parent view model (`InfoViewModel `).
179 |
180 | ```Swift
181 | var name: Observable {
182 | return events.value(of: InfoEvent.name)
183 | }
184 | ```
185 |
186 | Receive a event in the child view model (`NameViewModel` and `NumberViewModel`).
187 |
188 | ```Swift
189 | var name: Observable {
190 | return parentEvents.value(of: InfoEvent.name)
191 | }
192 | ```
193 |
194 | Pay attention to that **subscribing the `RxControllerEvent` in the `init` method of the view model is not effective**.
195 | It necessary to subscribe or bind the `RxControllerEvent` in the `prepareForParentEvents` methods.
196 |
197 | ```Swift
198 | override func prepareForParentEvents() {
199 | // Subscribe an event.
200 | parentEvents.unwrappedValue(of: ParentEvent.sample, type: EventData.self).subscribe(onNext: {
201 | // ...
202 | }.disposed(by: disposeBag))
203 |
204 | // Bind an event or a parent event to a relay directly.
205 | bindParentEvents(to: data, with: ParentEvent.sample)
206 |
207 | // Bind an observable type to an event or a parent event directly.
208 | bindToEvent(from: data, with: Event.sample)
209 |
210 | }
211 | ```
212 |
213 | ### Event router in the view model
214 |
215 | In the graph above, if an event needs to be transfered from `InfoViewModel` to `FirstNameViewModel`, the mid view model `NameViewModel` should be used as a router to forward data.
216 | To simply the data forwarding in the router view model, the `forward` methods are provided in the `RxViewModel`.
217 |
218 | ```swift
219 | // Forward a parent event to an event
220 | func forward(parentEvent: ,toEvent:)
221 |
222 | // Forward a parent event to an event with a `flatMapLatest` closure
223 | func forward(parentEvent: ,toEvent: ,flatMapLatest:)
224 |
225 | // Forward an event to a parent event
226 | func forward(toEvent: ,parentEvent:)
227 |
228 | // Forward an event to a parent event with a `flatMapLatest` closure
229 | func forward(toEvent: ,parentEvent: ,flatMapLatest:)
230 | ```
231 |
232 | ### Send a step to the flow from a child view model
233 |
234 | In a general way, the method `steps.accpet()` of RxFlow cannot be revoked from a child view model, because we didn't return the instances of the child view controller and child view model in the `navigate(to)` method of a flow.
235 |
236 | With RxController, it is able to send a step to the flow from a child view model directly.
237 |
238 | ```Swift
239 | steps.accept(DemoStep.stepname)
240 | ```
241 |
242 | ## RxTree
243 |
244 | RxController provides a command line tool `rxtree` to print the relationship among flows and view controllers,
245 | just like using the `tree` command.
246 |
247 | ```shell
248 | ➜ ./rxtree MainFlow
249 | MainFlow
250 | ├── ProjectFlow
251 | │ ├── RequestFlow
252 | │ │ ├── AddProjectViewController
253 | │ │ ├── RequestViewController
254 | │ │ ├── ResultViewController
255 | │ │ ├── SaveToProjectViewController
256 | │ ├── ProjectIntroductionViewController
257 | │ ├── ProjectNameViewController
258 | │ ├── ProjectViewController
259 | │ ├── ProjectsViewController
260 | ├── RequestFlow
261 | │ ├── AddProjectViewController
262 | │ ├── RequestViewController
263 | │ ├── ResultViewController
264 | │ ├── SaveToProjectViewController
265 | ├── SettingsFlow
266 | │ ├── IPAddressViewController
267 | │ ├── PingViewController
268 | │ ├── SettingsViewController
269 | │ ├── WhoisViewController
270 | ├── AddProjectViewController
271 | ```
272 |
273 | ### Install RxTree with CocoaPods
274 |
275 | `rxtree` relays on the design of RxController.
276 | Once RxController updated, the old version of `rxtree` may be noneffective.
277 | For this reason, it is recommend to be installed with `post_install` of CocoaPods.
278 |
279 | ```ruby
280 | post_install do |installer|
281 | system("bash #{Pathname(installer.sandbox.root)}/RxController/rxtree/build_for_xcode.sh")
282 | end
283 | ```
284 |
285 | Once `pod install` or `pod update` is executed, the corresponding version of `rxtree` will be installed at the same time.
286 |
287 | ### Use RxTree
288 |
289 | The executed file `rxtree` will be copied to the root directory of the project.
290 | A **root node** which can be a subclass of `Flow` or a subclass of `RxViewController` must be selected as the root of the tree.
291 |
292 | ```shell
293 | ./rxtree MainFlow
294 | ```
295 |
296 | To prevent recustion calling, the default max levels of `rxtree` is 10.
297 | It means that only 10 levels of flows and view controllers will be listed by default.
298 | To change the value of max levels, use the paramter `maxLevels`.
299 |
300 | ```shell
301 | ./rxtree MainFlow --maxLevels 5
302 | ```
303 |
304 | ## Author
305 |
306 | lm2343635, lm2343635@126.com
307 |
308 | ## License
309 |
310 | RxController is available under the MIT license. See the LICENSE file for more info.
311 |
--------------------------------------------------------------------------------
/RxController.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint RxController.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'RxController'
11 | s.version = '1.3.2'
12 | s.summary = 'A library for developing with MVVM-C based on RxFlow and RxSwift.'
13 |
14 | # This description is used to generate tags and improve search results.
15 | # * Think: What does it do? Why did you write it? What is the focus?
16 | # * Try to keep it short, snappy and to the point.
17 | # * Write the description between the DESC delimiters below.
18 | # * Finally, don't worry about the indent, CocoaPods strips it!
19 |
20 | s.description = <<-DESC
21 | RxController is a library developing with MVVM-C based on RxFlow and RxSwift.
22 | RxController provides the basic view controller and view model classes.
23 | These classes make it easy to transfer data among the flows, the parent view models and the child view models.
24 | DESC
25 |
26 | s.homepage = 'https://github.com/RxSwiftCommunity/RxController'
27 | s.license = { :type => 'MIT', :file => 'LICENSE' }
28 | s.author = { 'Meng Li' => 'lm2343635@126.com' }
29 | s.source = { :git => 'https://github.com/RxSwiftCommunity/RxController.git', :tag => s.version.to_s }
30 |
31 | s.ios.deployment_target = '9.0'
32 | s.swift_versions = '5.1'
33 | s.source_files = 'RxController/Classes/**/*'
34 | s.preserve_paths = 'rxtree/**/*'
35 |
36 | s.dependency 'RxSwift', '~> 6'
37 | s.dependency 'RxCocoa', '~> 6'
38 | s.dependency 'RxFlow', '~> 2.12'
39 |
40 | end
41 |
--------------------------------------------------------------------------------
/RxController/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/RxController/Assets/.gitkeep
--------------------------------------------------------------------------------
/RxController/Classes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/RxController/Classes/.gitkeep
--------------------------------------------------------------------------------
/RxController/Classes/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/01/2019.
6 | // Copyright (c) 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | struct Log {
29 |
30 | static func debug(_ message: String, tag: String = "", tags: [String] = []) {
31 | output(tag: tag, tags: tags, message: message)
32 | }
33 |
34 | private static func createTagString(_ tag:String, _ tags: [String]) -> String {
35 | if !tag.isEmpty {
36 | return "[\(tag)] "
37 | }
38 |
39 | if tags.isEmpty {
40 | return " "
41 | } else {
42 | return "[" + tags.joined(separator: "][") + "] "
43 | }
44 | }
45 |
46 | private static func output(tag: String, tags:[String], message: String) {
47 | #if DEBUG
48 | NSLog("RxController: " + createTagString(tag, tags) + message)
49 | #endif
50 | }
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/RxController/Classes/RxControllerEvent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxControllerEvent.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/16/2019.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 | import RxCocoa
28 | import RxSwift
29 |
30 | public struct RxControllerEvent {
31 |
32 | public struct Identifier {
33 | var id: String
34 |
35 | static let none = Identifier(id: "none")
36 |
37 | public func event(_ value: Any?) -> RxControllerEvent {
38 | return RxControllerEvent(identifier: self, value: value)
39 | }
40 | }
41 |
42 | var identifier: Identifier
43 | var value: Any?
44 |
45 | init(identifier: Identifier, value: Any?) {
46 | self.identifier = identifier
47 | self.value = value
48 | }
49 |
50 | static let none = RxControllerEvent(identifier: .none, value: nil)
51 | static let steps = RxControllerEvent.identifier()
52 |
53 | public static func identifier() -> Identifier {
54 | return Identifier(id: UUID().uuidString)
55 | }
56 |
57 | }
58 |
59 | extension ObservableType where Element == RxControllerEvent {
60 |
61 | public func value(of identifier: RxControllerEvent.Identifier, type: T.Type = T.self) -> Observable {
62 | return observe(on: MainScheduler.asyncInstance)
63 | .filter { $0.identifier.id == identifier.id }
64 | .map { $0.value as? T }
65 | }
66 |
67 | public func unwrappedValue(of identifier: RxControllerEvent.Identifier, type: T.Type = T.self) -> Observable {
68 | return value(of: identifier)
69 | .filter { $0 != nil }
70 | .map { $0! }
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/RxController/Classes/RxControllerEventBinder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxControllerEventBinder.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 08/07/2019.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import RxCocoa
27 | import RxSwift
28 |
29 | public protocol RxControllerEventBinder: class {
30 | var events: PublishRelay { get }
31 | var parentEvents: PublishRelay { get }
32 | var disposeBag: DisposeBag { get }
33 | }
34 |
35 | extension RxControllerEventBinder {
36 |
37 | public func bindToEvents(from observable: O, with identifier: RxControllerEvent.Identifier) {
38 | observable
39 | .subscribe(onNext: { [unowned self] in
40 | self.events.accept(identifier.event($0))
41 | })
42 | .disposed(by: disposeBag)
43 | }
44 |
45 | public func bindToParentEvents(from observable: O, with identifier: RxControllerEvent.Identifier) {
46 | observable
47 | .subscribe(onNext: { [unowned self] in
48 | self.parentEvents.accept(identifier.event($0))
49 | })
50 | .disposed(by: disposeBag)
51 | }
52 |
53 | public func bindEvents(to relay: BehaviorRelay, with identifier: RxControllerEvent.Identifier) {
54 | events.value(of: identifier)
55 | .bind(to: relay)
56 | .disposed(by: disposeBag)
57 | }
58 |
59 | public func bindEvents(to relay: PublishRelay, with identifier: RxControllerEvent.Identifier) {
60 | events.value(of: identifier)
61 | .bind(to: relay)
62 | .disposed(by: disposeBag)
63 | }
64 |
65 | public func bindEvents(to observer: O, with identifier: RxControllerEvent.Identifier) where O.Element == T? {
66 | events.value(of: identifier, type: T.self)
67 | .bind(to: observer)
68 | .disposed(by: disposeBag)
69 | }
70 |
71 | public func bindEvents(to relay: BehaviorRelay, with identifier: RxControllerEvent.Identifier) {
72 | events.unwrappedValue(of: identifier)
73 | .bind(to: relay)
74 | .disposed(by: disposeBag)
75 | }
76 |
77 | public func bindEvents(to relay: PublishRelay, with identifier: RxControllerEvent.Identifier) {
78 | events.unwrappedValue(of: identifier)
79 | .bind(to: relay)
80 | .disposed(by: disposeBag)
81 | }
82 |
83 | public func bindEvents(to observer: O, with identifier: RxControllerEvent.Identifier) where O.Element == T {
84 | events.unwrappedValue(of: identifier, type: T.self)
85 | .bind(to: observer)
86 | .disposed(by: disposeBag)
87 | }
88 |
89 | public func bindParentEvents(to relay: BehaviorRelay, with identifier: RxControllerEvent.Identifier) {
90 | parentEvents.value(of: identifier)
91 | .bind(to: relay)
92 | .disposed(by: disposeBag)
93 | }
94 |
95 | public func bindParentEvents(to relay: PublishRelay, with identifier: RxControllerEvent.Identifier) {
96 | parentEvents.value(of: identifier)
97 | .bind(to: relay)
98 | .disposed(by: disposeBag)
99 | }
100 |
101 | public func bindParentEvents(to observer: O, with identifier: RxControllerEvent.Identifier) where O.Element == T? {
102 | parentEvents.value(of: identifier, type: T.self)
103 | .bind(to: observer)
104 | .disposed(by: disposeBag)
105 | }
106 |
107 | public func bindParentEvents(to relay: BehaviorRelay, with identifier: RxControllerEvent.Identifier) {
108 | parentEvents.unwrappedValue(of: identifier)
109 | .bind(to: relay)
110 | .disposed(by: disposeBag)
111 | }
112 |
113 | public func bindParentEvents(to relay: PublishRelay, with identifier: RxControllerEvent.Identifier) {
114 | parentEvents.unwrappedValue(of: identifier)
115 | .bind(to: relay)
116 | .disposed(by: disposeBag)
117 | }
118 |
119 | public func bindParentEvents(to observer: O, with identifier: RxControllerEvent.Identifier) where O.Element == T {
120 | parentEvents.unwrappedValue(of: identifier, type: T.self)
121 | .bind(to: observer)
122 | .disposed(by: disposeBag)
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/RxController/Classes/RxControllerEventRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxControllerEventRouter.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 08/06/2019.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import RxCocoa
27 | import RxSwift
28 |
29 | public protocol RxControllerEventRouter: class {
30 | var events: PublishRelay { get }
31 | var parentEvents: PublishRelay { get }
32 | var disposeBag: DisposeBag { get }
33 | }
34 |
35 | struct RxControllerEventForwarder {
36 | let input: PublishRelay
37 | let inputIdentifier: RxControllerEvent.Identifier
38 | let output: PublishRelay
39 | let outputIdentifier: RxControllerEvent.Identifier
40 |
41 | func forward() -> Disposable {
42 | return input.value(of: inputIdentifier).subscribe(onNext: {
43 | self.output.accept(self.outputIdentifier.event($0))
44 | })
45 | }
46 |
47 | func forward(flatMapLatest: @escaping ((T?) -> O)) -> Disposable {
48 | return input.value(of: inputIdentifier, type: T.self)
49 | .flatMapLatest(flatMapLatest)
50 | .subscribe(onNext: {
51 | self.output.accept(self.outputIdentifier.event($0))
52 | })
53 | }
54 | }
55 |
56 | extension RxControllerEventRouter {
57 |
58 | public func forward(
59 | parentEvent parentEventIdentifier: RxControllerEvent.Identifier,
60 | toEvent eventIdentifier: RxControllerEvent.Identifier
61 | ) {
62 | let forwarder = RxControllerEventForwarder(
63 | input: parentEvents, inputIdentifier: parentEventIdentifier,
64 | output: events, outputIdentifier: eventIdentifier
65 | )
66 | return forwarder.forward().disposed(by: disposeBag)
67 | }
68 |
69 | public func forward(
70 | parentEvent parentEventIdentifier: RxControllerEvent.Identifier,
71 | toEvent eventIdentifier: RxControllerEvent.Identifier,
72 | flatMapLatest: @escaping (T?) -> O
73 | ) {
74 | let forwarder = RxControllerEventForwarder(
75 | input: parentEvents, inputIdentifier: parentEventIdentifier,
76 | output: events, outputIdentifier: eventIdentifier
77 | )
78 | forwarder.forward(flatMapLatest: flatMapLatest).disposed(by: disposeBag)
79 | }
80 |
81 | public func forward(
82 | event eventIdentifier: RxControllerEvent.Identifier,
83 | toParentEvent parentEventIdentifier: RxControllerEvent.Identifier
84 | ) {
85 | let forwarder = RxControllerEventForwarder(
86 | input: events, inputIdentifier: eventIdentifier,
87 | output: parentEvents, outputIdentifier: parentEventIdentifier
88 | )
89 | forwarder.forward().disposed(by: disposeBag)
90 | }
91 |
92 | public func forward(
93 | event eventIdentifier: RxControllerEvent.Identifier,
94 | toParentEvent parentEventIdentifier: RxControllerEvent.Identifier,
95 | flatMapLatest: @escaping (T?) -> O
96 | ) {
97 | let forwarder = RxControllerEventForwarder(
98 | input: events, inputIdentifier: eventIdentifier,
99 | output: parentEvents, outputIdentifier: parentEventIdentifier
100 | )
101 | forwarder.forward(flatMapLatest: flatMapLatest).disposed(by: disposeBag)
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/RxController/Classes/RxFlow+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxFlow+Extension.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 2019/01/30.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import UIKit
27 | import RxFlow
28 |
29 | extension FlowContributors {
30 |
31 | public static func viewController(_ viewController: RxViewController) -> FlowContributors {
32 | return .one(flowContributor: .viewController(viewController))
33 | }
34 |
35 | public static func viewController(_ viewController: UIViewController, with viewModel: Stepper) -> FlowContributors {
36 | return .one(flowContributor: .viewController(viewController, with: viewModel))
37 | }
38 |
39 | public static func flow(_ flow: Flow, with step: Step) -> FlowContributors {
40 | return .one(flowContributor: .flow(flow, with: step))
41 | }
42 |
43 | }
44 |
45 | extension FlowContributor {
46 |
47 | public static func viewController(_ viewController: RxViewController) -> FlowContributor {
48 | return .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel)
49 | }
50 |
51 | public static func viewController(_ viewController: UIViewController, with viewModel: Stepper) -> FlowContributor {
52 | return .contribute(withNextPresentable: viewController, withNextStepper: viewModel)
53 | }
54 |
55 | public static func flow(_ flow: Flow, with step: Step) -> FlowContributor {
56 | return .contribute(withNextPresentable: flow, withNextStepper: OneStepper(withSingleStep: step))
57 | }
58 |
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/RxController/Classes/RxViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxViewController.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/09/2019.
6 | // Copyright (c) 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import UIKit
27 | import RxSwift
28 | import RxCocoa
29 |
30 | protocol RxViewControllerProtocol {
31 | var rxViewModel: RxViewModel { get }
32 | }
33 |
34 | open class RxViewController: UIViewController, RxViewControllerProtocol {
35 |
36 | public let disposeBag = DisposeBag()
37 | public let mainScheduler = MainScheduler.instance
38 | public let viewModel: ViewModel
39 |
40 | var rxViewModel: RxViewModel {
41 | viewModel
42 | }
43 |
44 | public init(viewModel: ViewModel) {
45 | self.viewModel = viewModel
46 | super.init(nibName: nil, bundle: nil)
47 |
48 | rx.methodInvoked(#selector(viewDidLoad))
49 | .map { _ in }
50 | .bind(to: viewModel.viewDidLoadSubject)
51 | .disposed(by: disposeBag)
52 |
53 | rx.methodInvoked(#selector(viewWillAppear(_:)))
54 | .map { _ in }
55 | .bind(to: viewModel.viewWillAppearSubject)
56 | .disposed(by: disposeBag)
57 |
58 | rx.methodInvoked(#selector(viewDidAppear))
59 | .map { _ in }
60 | .bind(to: viewModel.viewDidAppearSubject)
61 | .disposed(by: disposeBag)
62 |
63 | rx.methodInvoked(#selector(viewWillDisappear(_:)))
64 | .map { _ in }
65 | .bind(to: viewModel.viewWillDisappearSubject)
66 | .disposed(by: disposeBag)
67 |
68 | rx.methodInvoked(#selector(viewDidDisappear(_:)))
69 | .map { _ in }
70 | .bind(to: viewModel.viewDidDisappearSubject)
71 | .disposed(by: disposeBag)
72 |
73 | rx.methodInvoked(#selector(viewWillLayoutSubviews))
74 | .map { _ in }
75 | .bind(to: viewModel.viewWillLayoutSubviewsSubject)
76 | .disposed(by: disposeBag)
77 |
78 | rx.methodInvoked(#selector(viewDidLayoutSubviews))
79 | .map { _ in }
80 | .bind(to: viewModel.viewDidLayoutSubviewsSubject)
81 | .disposed(by: disposeBag)
82 |
83 | if #available(iOS 11.0, *) {
84 | rx.methodInvoked(#selector(viewSafeAreaInsetsDidChange))
85 | .map { _ in }
86 | .bind(to: viewModel.viewSafeAreaInsetsDidChangeSubject)
87 | .disposed(by: disposeBag)
88 | }
89 |
90 | if #available(iOS 13.0, *) {
91 | rx.methodInvoked(#selector(viewIsAppearing))
92 | .map { _ in }
93 | .bind(to: viewModel.viewIsAppearingSubject)
94 | .disposed(by: disposeBag)
95 | }
96 | }
97 |
98 | required public init?(coder aDecoder: NSCoder) {
99 | fatalError("RxController does not support to initialized from storyboard or xib!")
100 | }
101 |
102 | deinit {
103 | Log.debug("[DEINIT View Controller] \(type(of: self))")
104 | }
105 |
106 | open override func viewDidLoad() {
107 | super.viewDidLoad()
108 |
109 | subviews().forEach { view.addSubview($0) }
110 | createConstraints()
111 | bind().forEach { $0.disposed(by: disposeBag) }
112 | }
113 |
114 | open func subviews() -> [UIView] {
115 | Log.debug("[WARNING] \(type(of: self)).subview() has not been overrided")
116 | return []
117 | }
118 |
119 | open func createConstraints() {
120 | Log.debug("[WARNING] \(type(of: self)).createConstraints() has not been overrided.")
121 | }
122 |
123 | open func bind() -> [Disposable] {
124 | Log.debug("[WARNING] \(type(of: self)).bind() has not been overrided.")
125 | return []
126 | }
127 |
128 | /**
129 | Add a child view controller to the root view of the parent view controller.
130 | @param childController: a child view controller.
131 | */
132 | override open func addChild(_ childController: UIViewController) {
133 | super.addChild(childController)
134 |
135 | view.addSubview(childController.view)
136 | childController.didMove(toParent: self)
137 |
138 | guard let childController = childController as? RxViewControllerProtocol else { return }
139 | viewModel.addChild(childController.rxViewModel)
140 | }
141 |
142 | /**
143 | Add a child view controller to the a container view of the parent view controller.
144 | The edges of the child view controller is same as the container view by default.
145 |
146 | @param childController: a child view controller.
147 | @param containerView: a container view of childController.
148 | */
149 | open func addChild(_ childController: UIViewController, to containerView: UIView) {
150 | super.addChild(childController)
151 | // Add child view controller to a container view of the parent view controller.
152 | containerView.addSubview(childController.view)
153 | childController.didMove(toParent: self)
154 |
155 | // Create constraints for the root view of the child view controller.
156 | childController.view.translatesAutoresizingMaskIntoConstraints = false
157 | childController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
158 | childController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
159 | childController.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
160 | childController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
161 |
162 | guard let childController = childController as? RxViewControllerProtocol else { return }
163 | viewModel.addChild(childController.rxViewModel)
164 | }
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/RxController/Classes/RxViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RxViewModel.swift
3 | // RxController
4 | //
5 | // Created by Meng Li on 04/09/2019.
6 | // Copyright (c) 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 | import RxSwift
28 | import RxCocoa
29 | import RxFlow
30 |
31 | open class RxViewModel: NSObject, Stepper {
32 |
33 | public let viewDidLoadSubject = PublishSubject()
34 | public let viewWillAppearSubject = PublishSubject()
35 | public let viewIsAppearingSubject = PublishSubject()
36 | public let viewDidAppearSubject = PublishSubject()
37 | public let viewWillDisappearSubject = PublishSubject()
38 | public let viewDidDisappearSubject = PublishSubject()
39 | public let viewWillLayoutSubviewsSubject = PublishSubject()
40 | public let viewDidLayoutSubviewsSubject = PublishSubject()
41 | public let viewSafeAreaInsetsDidChangeSubject = PublishSubject()
42 |
43 | public let steps = PublishRelay()
44 | public let events = PublishRelay()
45 | public let disposeBag = DisposeBag()
46 | private var cachedEvents = [RxControllerEvent]()
47 |
48 | public override init() {
49 | super.init()
50 |
51 | events.subscribe(onNext: { [unowned self] in
52 | guard $0.identifier.id != RxControllerEvent.steps.id else { return }
53 | let cachedEventIds = self.cachedEvents.map { $0.identifier.id }
54 | if let index = cachedEventIds.firstIndex(of: $0.identifier.id) {
55 | self.cachedEvents[index] = $0
56 | } else {
57 | self.cachedEvents.append($0)
58 | }
59 | }).disposed(by: disposeBag)
60 |
61 | let stepEvents: Observable = events.unwrappedValue(of: RxControllerEvent.steps)
62 | stepEvents.subscribe(onNext: { [unowned self] in
63 | self.steps.accept($0)
64 | }).disposed(by: disposeBag)
65 |
66 | steps.subscribe(onNext: { [unowned self] in
67 | if let parentEvents = self._parentEvents {
68 | parentEvents.accept(RxControllerEvent.steps.event($0))
69 | }
70 | }).disposed(by: disposeBag)
71 | }
72 |
73 | deinit {
74 | Log.debug("[DEINIT View Model] \(type(of: self))")
75 | }
76 |
77 | open func prepareForParentEvents() {}
78 |
79 | public func addChild(_ viewModel: RxViewModel) {
80 | viewModel._parentEvents = events
81 | republishEvents()
82 | }
83 |
84 | public func addChildren(_ viewModels: RxViewModel...) {
85 | viewModels.forEach {
86 | $0._parentEvents = events
87 | }
88 | republishEvents()
89 | }
90 |
91 | private func republishEvents() {
92 | cachedEvents.forEach {
93 | events.accept($0)
94 | }
95 | }
96 |
97 | weak var _parentEvents: PublishRelay? {
98 | didSet {
99 | prepareForParentEvents()
100 | }
101 | }
102 |
103 | public var parentEvents: PublishRelay {
104 | guard let events = _parentEvents else {
105 | Log.debug("[WARNING] parentEvents have NOT been prepared in \(type(of: self))! Override prepareForParentEvents() if you subscribed parentEvents.")
106 | return PublishRelay()
107 | }
108 | return events
109 | }
110 |
111 | public var viewDidLoad: Observable {
112 | viewDidLoadSubject.asObservable()
113 | }
114 |
115 | public var viewWillAppear: Observable {
116 | viewWillAppearSubject.asObservable()
117 | }
118 |
119 | public var viewDidAppear: Observable {
120 | viewDidAppearSubject.asObservable()
121 | }
122 |
123 | public var viewWillDisappear: Observable {
124 | viewWillDisappearSubject.asObservable()
125 | }
126 |
127 | public var viewDidDisappear: Observable {
128 | viewDidDisappearSubject.asObservable()
129 | }
130 |
131 | public var viewWillLayoutSubviews: Observable {
132 | viewWillLayoutSubviewsSubject.asObservable()
133 | }
134 |
135 | public var viewDidLayoutSubviews: Observable {
136 | viewDidLayoutSubviewsSubject.asObservable()
137 | }
138 |
139 | @available(iOS 11.0, *)
140 | public var viewSafeAreaInsetsDidChange: Observable {
141 | viewSafeAreaInsetsDidChangeSubject.asObservable()
142 | }
143 |
144 | @available(iOS 13.0, *)
145 | public var viewIsAppearing: Observable {
146 | viewIsAppearingSubject.asObservable()
147 | }
148 | }
149 |
150 | extension RxViewModel: RxControllerEventBinder {}
151 |
152 | extension RxViewModel: RxControllerEventRouter {}
153 |
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------
/document/chapter1-introduction.md:
--------------------------------------------------------------------------------
1 | # Chapter 1: Introduction
2 |
3 | This guideline introduces how to develop a MVVM-C app based the RxController library.
4 | We will call MVVM directly in this guideline, it represents MVVM-C design pattern.
5 | The content in this guideline is just suggestions for developing a MVVM-C app.
6 |
7 | ### 1.1 Purpose
8 |
9 | RxSwift and RxFlow help developers to develop an app with MVVM.
10 | However, they do not provide a standard coding rule.
11 | The code style, especially the code of exchanging data among view models and controller presentation, will become very flexible.
12 | This guideline proposes a standard code style using RxController for teamwork development.
13 |
14 | This guideline follows the 3 principles:
15 |
16 | - **Standardization** : All of the code should follow a standard code style.
17 | - **Readability** : The pull request should be easy to read and understand.
18 | - **Modularization** : Module development is recommended, and the API of the module should be simple and easy to read.
19 |
20 | The readability is based on the enough understanding of [RxController](https://github.com/xflagstudio/RxController), [RxFlow](https://github.com/RxSwiftCommunity/RxFlow) and [RxSwift](https://github.com/ReactiveX/RxSwift).
21 | For module development, the simple API is required, while the code in the module is not required to follow the principles.
22 |
23 | ### 1.2 Libraries
24 |
25 | This MVVM design pattern of this guideline depends on the following libraries:
26 |
27 | - RxSwift (https://github.com/ReactiveX/RxSwift)
28 | - RxCocoa (https://github.com/ReactiveX/RxSwift)
29 | - RxFlow (https://github.com/RxSwiftCommunity/RxFlow)
30 | - RxController (this repo)
31 |
32 | RxSwift and RxCocoa are basic FRP libraries.
33 | RxFlow is a coordinator library.
34 | This library, RxController, provides some wrapper methods for RxFlow, the basic view controller and view model classes, and event based data exchanging among view models.
35 |
36 | The following libraries helps us to develop with MVVM easily.
37 |
38 | - SnapKit (https://github.com/SnapKit/SnapKit):
39 | Using storyboard requires to load view controller from storyboard, that is not convenient to manage the references of view controllers in the flow class.
40 | For this reason, we recommend to use SnapKit to create UI manually rather than using Storyboard.
41 |
42 | - RxDataSources (https://github.com/RxSwiftCommunity/RxDataSources)
43 | - RxDataSourcesSingleSection (https://github.com/xflagstudio/RxDataSourcesSingleSection)
44 | RxDataSources provides advanced table and collection view data sources.
45 | It makes the development of animated table and collection collection easy, because it contains algorithm for calculating differences.
46 | However, it requires to build a data source with multiple sections for table and collection view.
47 | Mostly, we don't need multiple sections in our apps.
48 | To simplify the code for the single section data source, we use the RxDataSourcesSingleSection library.
49 |
50 | - RxBinding (https://github.com/RxSwiftCommunity/RxBinding)
51 | RxBinding provides `~>`, `<~>` and `~` operators for data binding using RxSwift, to replace the `bind(to:)` and `disposed(by:)` method in RxSwift.
52 |
53 | **The operators of RxBinding is recommended to be used in data binding for the view model only.**
54 |
55 | ```swift
56 | viewModel.name ~> nameLabel.rx.text
57 | ```
58 |
59 | ### 1.3 Code Style
60 |
61 | #### Indent
62 |
63 | - Continuous methods invoking
64 |
65 | Sometimes, methods are invoked continuously like `users.filter().map().reduce()`.
66 | If thees methods and thier closures are too long, line feed is needed.
67 | When a new line is started, we should pay attention to the indent.
68 |
69 | If the last line is ended with an uncompleted clousre like `user.map {`, the new line should not be started with indent.
70 | In addition, the indent of the line with the end brace `{` of this closure should be same with the line with the start brace `{`.
71 |
72 | ```swift
73 | user.filter { $0.id != userId }.map {
74 | UserItem(user: user)
75 | }
76 | ```
77 |
78 | If the last line is ended without an uncompleted closure, a property or a method, the new line should be started with indent.
79 |
80 | ```swift
81 | user.filter { $0.id != userId }
82 | .map { UserItem(user: user) }
83 | ```
84 |
85 | #### Line feed
86 |
87 | A line which contains more than 127 characters cannot be shown completely in the Github code viewer.
88 | Depends on you browser, the number of the visible characters may be different.
89 |
90 | When a line contains more than 127 characters, we can divided it into multiple line.
91 | A new line is recommended when a method is invoked or a closure content is started.
92 |
93 | ```swift
94 | var name: Observable {
95 | user.map { $0.name }.distinctUntilChanged().take(1).map {
96 | R.string.localizable.user_profile_full_name($0)
97 | }
98 | }
99 | ```
100 |
101 | However, for the continuous semantic structure like `user_profile_full_name($0)`, a new line is not recommended although its line contains more than 127 characters.
102 |
103 | #### Omit if possible
104 |
105 | - Omit `return` for computed property.
106 |
107 | ```swift
108 | var name: Binder {
109 | Binder(base) { view, name in
110 | view.name = name
111 | }
112 | }
113 | ```
114 |
115 | Even it is able to omit `return` for a method with a single line, such a style is not recommended, because it makes harder to distingush a method that returns a value and which does not returns a value.
116 |
117 | - Omit `import Foundation` and `import UIKit`
118 |
119 | In the Swift project, `import Foundation` and `import UIKit` is not necessary.
120 | To simplify the code, they are not recommended to be imported.
121 |
122 | - Omit `self` keyword if possible
123 |
124 | ```swift
125 | viewModel.title ~> titleLabel.rx.text
126 | ```
127 |
128 | For some situations, the `self` keyword cannot be omitted.
129 | For example, when the properties are used with in a clousre, the `self` property should be captured with `[unowned self]` or `[weak self]` to avoid memoery leak.
130 | If there are too many `self` keywoard within our code, it is difficult for us to concentrate on those situations we need pay attention to.
131 | For this reason, unnecessary `self` keywords are recommended to be omitted.
132 |
133 | - Omit `class`, `struct` and `enum` keyword if possible.
134 |
135 | ```swift
136 | label.lineBreakMode = .byWordWrapping
137 | ```
138 |
139 | - Omit decimal point if possible
140 |
141 | ```swift
142 | label.font = .boldSystemFont(ofSize: 48)
143 | ```
144 |
145 | - Omit the closure name of a method, if the only one closure parameter of this method is used.
146 |
147 | ```swift
148 | UIView.animate(withDuration: 1) {
149 | // Do something ...
150 | }
151 | ```
152 |
153 | **If multiple closure parameters are used, all of the closure names should be written.**
154 |
155 | ```swift
156 | UIView.animate(withDuration: 1, animations: {
157 | // Do something ...
158 | }, completion: {
159 | // Do something ...
160 | })
161 | ```
162 |
163 | - Omit the parameter name of a closure, if the closure contains only one parameter.
164 |
165 | ```swift
166 | tableView.rx.itemSelected.bind { [unowned self] in
167 | self.viewModel.pick(at: $0.row)
168 | }.disposed(by: disposeBag)
169 | ```
170 |
171 | Omit multiple parameter names of a clousre is not recommended.
172 |
--------------------------------------------------------------------------------
/document/chapter2-rxflow.md:
--------------------------------------------------------------------------------
1 | # Chapter 2: Using RxFlow
2 |
3 | This chapter introduces the basic usage of RxFlow in our guideline.
4 |
5 | 
6 |
7 | In RxFlow, a step enum is a way to express a state that can lead to a navigation.
8 | A flow class which holds the references of some view controllers and flows, defines a navigation area in your application.
9 | **Each flow is corresponding to a step, the names of them are same.**
10 |
11 | ## 2.1 Start app with RxFlow
12 |
13 | An `AppFlow`, which is corresponding to `AppStep` is recommended as a start flow in our guideline.
14 |
15 | ```swift
16 | import UIKit
17 | import RxFlow
18 |
19 | enum AppStep: Step {
20 | case start
21 | }
22 |
23 | class AppFlow: Flow {
24 |
25 | var root: Presentable {
26 | return rootWindow
27 | }
28 |
29 | private let rootWindow: UIWindow
30 |
31 | private lazy var navigationController = UINavigationController()
32 |
33 | init(window: UIWindow) {
34 | rootWindow = window
35 | rootWindow.rootViewController = navigationController
36 | }
37 |
38 | func navigate(to step: Step) -> FlowContributors {
39 | guard let appStep = step as? AppStep else {
40 | return .none
41 | }
42 | switch appStep {
43 | case .start:
44 | let infoViewController = InfoViewController(viewModel: InfoViewModel())
45 | navigationController.pushViewController(infoViewController, animated: false)
46 | return .viewController(infoViewController)
47 | }
48 | }
49 |
50 | }
51 |
52 | ```
53 |
54 | Then we navigate to the `AppFlow` in the AppDelegate class.
55 |
56 | ```swift
57 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
58 | coordinator.rx.didNavigate.subscribe(onNext: {
59 | print("did navigate to \($0) -> \($1)")
60 | }).disposed(by: disposeBag)
61 |
62 | coordinate {
63 | (AppFlow(window: $0), AppStep.start)
64 | }
65 | return true
66 | }
67 |
68 | private func coordinate(to: (UIWindow) -> (Flow, Step)) {
69 | let (flow, step) = to(window)
70 | coordinator.coordinate(flow: flow, with: OneStepper(withSingleStep: step))
71 | window.makeKeyAndVisible()
72 | }
73 | ```
74 |
75 | ## 2.2 Screen transition
76 |
77 | RxFlow controls screen transition among view controllers.
78 | A step enum defines a navigation area, each step defines a transition between two view controllers.
79 |
80 | For example, a `SigninFlow` contains a `SignViewController` ans `SignupViewController`.
81 | It containts the following 3 steps:
82 | - `start`: When this flow is loaded, the `SignViewController` will be shown.
83 | - `signup`: When the `Sign Up` button in the `SignViewController` is clicked, the `SignupViewController` will be presented with a modal.
84 | - `signupIsComplete`: If user submit the form in the `SignupViewController`, the `SignupViewController` will be dismissed.
85 |
86 | ```swift
87 | enum SigninStep {
88 | case start
89 | case signup
90 | case signupIsComplete
91 | }
92 | ```
93 |
94 | As same as `AppFlow`, we return `signinViewController` for `root`, and implement `navigate(to step:)` method.
95 | **In a step, returning with `.viewController(vc)` is required for navigating to a new view controller.**
96 | Otherwise, the `steps` relay cannot be used in the view model of this new view controller.
97 | In this example, we return with `.viewController(vc)` in the `.start` step and `.signinStep`.
98 |
99 | ```swift
100 | class SigninFlow: Flow {
101 |
102 | var root: Presentable {
103 | return signinViewController
104 | }
105 |
106 | private lazy var signinViewController = SigninViewController()
107 |
108 | func navigate(to step: Step) -> FlowContributors {
109 | guard let sininStep = step as? SigninStep else {
110 | return .none
111 | }
112 | switch sininStep {
113 | case .start:
114 | return .viewController(signinViewController)
115 | case .signup:
116 | let signupViewController = SignupViewController(viewModel: .init())
117 | signinViewController.present(signupViewController, animated: true)
118 | return .viewController(signupViewController)
119 | case .signupIsComplete:
120 | guard let signupViewController = signinViewController.presentedViewController as? SignupViewController else {
121 | return .none
122 | }
123 | signupViewController.dismiss(animated: true)
124 | return .none
125 | }
126 | }
127 |
128 | }
129 | ```
130 |
131 | Using a step for multiple screen transitions is difficult to manage.
132 | For this reason, **One-To-One relationship bwtween a step and a screen transition is recommended.**
133 |
134 | To back to the `SigninViewController` from `SignupViewController`, we invoke `steps.accept(SigninStep.signupIsComplete)` in the view model of `SignupViewController`.
135 |
136 | ```swift
137 | func signup() {
138 | // Sign up related code here...
139 |
140 | // Back to signin
141 | steps.accept(SigninStep.signupIsComplete)
142 | }
143 | ```
144 |
145 | **All screen transitions should be managed in the flows.**
146 |
147 | ## 2.3 Present navigation controller in a step
148 |
149 | If a navigation controller is presented in a step, you should pay attention to the memory leak issue.
150 | A navigation controller has a root view controller named `rootViewController` which is a subclass of RxController.
151 | **DO NOT** return `.viewController(rootViewController)` directly in this step.
152 | Because he navigation controller is not managed by the RxFlow, a memory leak issue will be caused when this flow is ended.
153 |
154 | **Return `.navigationController(navigationController)` in the step, if a navigation controller with a root view controller, which is a subclass of RxController, is presented.**
155 |
156 | ```swift
157 | case .childOnNavigation:
158 | guard let menuViewController = navigationController.topViewController as? MenuViewController else {
159 | return .none
160 | }
161 | let infoViewController = InfoViewController(viewModel: InfoViewModel())
162 | let navigationController = UINavigationController(rootViewController: infoViewController)
163 | menuViewController.present(navigationController, animated: true)
164 | return .navigationController(navigationController)
165 | ```
166 |
--------------------------------------------------------------------------------
/document/chapter3-viewcontroller.md:
--------------------------------------------------------------------------------
1 | # Chapter 3: View controller and view model
2 |
3 | This chapter introduces the rule of view controller.
4 |
5 | ## 3.1 Using RxViewController
6 |
7 | RxController provides a basic view controller `RxViewController` (generic classes) and a basic view model `RxViewModel`.
8 |
9 | We recommend to create a BaseViewController which extends RxViewController and a BaseViewModel which extends RxViewController.
10 | Developers can customized something in the BaseViewController and BaseViewModel class.
11 |
12 | ```swift
13 | class BaseViewController: RxViewController {
14 | // Customize somthing here...
15 | }
16 |
17 | class BaseViewModel: RxViewModel {
18 | // Customize somthing here...
19 | }
20 | ```
21 |
22 | **All the view controller classes should extend the BaseViewController.**
23 |
24 | **All the view model classes should extend the BaseViewModel**
25 |
26 | ## 3.2 Structure of view controller
27 |
28 | The code in the view controller should follow the order:
29 |
30 | #### Define views
31 |
32 | **Views should be defined with `private lazy var`.**
33 |
34 | ```swift
35 | private lazy var nameLabel: UILabel = {
36 | let label = UILabel()
37 | label.textColor = UIColor(hex: 0x222222)
38 | label.font = UIFont.systemFont(ofSize: 14)
39 | return label
40 | }()
41 | ```
42 |
43 | **A closure should be used when some properties of this view need to be set.**
44 | Otherwise, we omit the type and invoke the init method directly.
45 |
46 | ```swift
47 | private lazy var nameLabel = UILabel()
48 | ```
49 |
50 | **The order of the views should be same as the design.**
51 | The left views and top views should be in the top of the right views and the bottom views.
52 |
53 | **When a parent view is used as a container view, it should be defined after its subviews.**
54 |
55 | ```swift
56 | private lazy var nameLabel = UILabel()
57 |
58 | private lazy var iconImage = UIImageView()
59 |
60 | private lazy var containerView: UILabel = {
61 | let view = UIView()
62 | view.addSubview(nameLabel)
63 | view.addSubview(iconImage)
64 | return view
65 | }()
66 | ```
67 |
68 | **The view definition closure does not contains the definition of subviews.**
69 | The following code is not recommended.
70 |
71 | ```swift
72 | // NOT recommended.
73 | private lazy var containerView: UILabel = {
74 | let view = UIView()
75 | let nameLabel = UILabel()
76 | view.addSubview(nameLabel)
77 | return view
78 | }()
79 | ```
80 |
81 | **The view definition closure contains the properties and methods of this view only.**
82 |
83 | ### Define child view controllers
84 |
85 | We define child view controllers using `private lazy var` after the definition of views.
86 |
87 | ```swift
88 | private lazy var childViewController = ChildViewController(viewModel: init())
89 | ```
90 |
91 | ### Define data source of RxDataSources
92 |
93 | We define the data source of RxDataSources using `private lazy var` after the definition of child view controllers.
94 |
95 | ```swift
96 | private lazy var dataSource = DemoTableViewCell.tableViewSingleSectionDataSource()
97 | ```
98 |
99 | ### Define other private properties
100 |
101 | Private properties are not recommended in the view controller.
102 | The state properties are recommened to define in the view model class.
103 | However, if needed, define them here.
104 |
105 | ### Init method
106 |
107 | Set some properties of the view controller in `init` method if needed.
108 |
109 | ```swift
110 | override init(viewModel: DemoViewModel) {
111 | super.init(viewModel: viewModel)
112 |
113 | modalPresentationStyle = .overCurrentContext
114 | modalTransitionStyle = .crossDissolve
115 | }
116 |
117 | required init?(coder aDecoder: NSCoder) {
118 | fatalError("init(coder:) has not been implemented")
119 | }
120 | ```
121 |
122 | Just invoke `fatalError` method in `required init?(coder aDecoder: NSCoder)` because we do not support storyboard.
123 |
124 | ### Override properties.
125 |
126 | ```swift
127 | override var shouldAutorotate: Bool {
128 | return false
129 | }
130 |
131 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
132 | return .portrait
133 | }
134 | ```
135 |
136 | ### viewDidLoad method
137 |
138 | The following steps should be contained in the `viewDidLoad` method.
139 |
140 | - Set navigation bar and navigation items.
141 | - Set peroperties of root view.
142 | - Add subviews.
143 | - Create constraints.
144 | - Add child view controllers.
145 | - Bind data.
146 | - Others.
147 |
148 | ```swift
149 | override func viewDidLoad() {
150 | super.viewDidLoad()
151 |
152 | // Set navigation bar and navigation items.
153 | navigationItem.leftBarButtonItem = closeBarButtonItem
154 |
155 | // Set peroperties of root view.
156 | view.backgroundColor = .white
157 | // Add subviews.
158 | view.addSubview(titleLabel)
159 | view.addSubview(tableView)
160 | // Create constraints.
161 | createConstraints()
162 |
163 | //Add child view controllers.
164 | addChild(topViewController, to: headerView)
165 |
166 | // Bind data.
167 | disposeBag ~ [
168 | viewModel.title ~> titleLabel.rx.text,
169 | viewModel.cartSection ~> tableView.rx.items(dataSource: dataSource)
170 | ]
171 |
172 | // Others.
173 | // Do something here.
174 | }
175 | ```
176 |
177 | We use the operator `~>` and `<~>` of RxBinding to bind data.
178 | **The order of the bind code is recommended be same as the order of the definition of the views.**
179 |
180 | If a view controller does not extend the RxViewController or its subclass, there is no data binding code in the `viewDidLoad` method.
181 | If some lines of code for setting data of the subviews are needed, they are recommended to be written in the same position as data binding.
182 |
183 | ### Other lifecycle methods.
184 |
185 | Add other lifecycle methods here if needed.
186 | **The life cycle methods should follow the order.**
187 |
188 | - viewDidLoad
189 | - viewDidAppear
190 | - viewDidDisappear
191 | - viewWillAppear
192 | - viewWillDisappear
193 | - viewDidLayoutSubviews
194 | - viewWillLayoutSubviews
195 |
196 | ```swift
197 | override func viewDidLoad() {
198 | super.viewDidLoad()
199 | // ...
200 | }
201 |
202 | override func viewDidAppear(_ animated: Bool) {
203 | super.viewDidAppear(animated)
204 | // ...
205 | }
206 |
207 | override func viewDidDisappear(_ animated: Bool) {
208 | super.viewDidDisappear(animated)
209 | // ...
210 | }
211 |
212 | override func viewWillAppear(_ animated: Bool) {
213 | super.viewWillAppear(animated)
214 | // ...
215 | }
216 |
217 | override func viewWillDisappear(_ animated: Bool) {
218 | super.viewWillDisappear(animated)
219 | // ...
220 | }
221 |
222 | override func viewDidLayoutSubviews() {
223 | super.viewDidLayoutSubviews()
224 | // ...
225 | }
226 |
227 | override func viewWillLayoutSubviews() {
228 | super.viewWillLayoutSubviews()
229 | // ...
230 | }
231 | ```
232 |
233 | ### Create constraints method.
234 |
235 | A contraints method contains the SnapKit constraint creators of all the views in this view controller.
236 |
237 | ```swift
238 | private func createConstraints() {
239 |
240 | titleLabel.snp.makeConstraints {
241 | $0.centerX.equalToSuperview()
242 | $0.top.equalToSuperview().offset(Const.Title.marginTop)
243 | }
244 |
245 | closeButton.snp.makeConstraints {
246 | $0.size.equalTo(Const.Close.size)
247 | $0.centerY.equalTo(titleLabel.snp.centerY)
248 | $0.right.equalToSuperview().offset(-Const.Close.marginRight)
249 | }
250 |
251 | }
252 | ```
253 |
254 | ### Other methods.
255 |
256 | The private methods should be above the internal methods.
257 |
258 | ## 3.3 Using child view controller or customized view
259 |
260 | A view controller needs multiple child view controller or customized view to reduce the complexity.
261 | Generally, we consider to implements a group of subviews with relationships in a child view controller or a customized view.
262 |
263 | ### How to select
264 |
265 | **Cusztomized view is recommended for showing data only, or handling a simple action like tap.**
266 | We don't receommend to use RxSwift directly in a customized view.
267 | **To handle complex actions, a child view controller is receommened.**
268 |
269 | To make it easy to understand this problem, we can refer to the following grpah.
270 |
271 | 
272 |
273 | In this graph, both the count of subviews and the user interactions in the group are considered.
274 | In the situation with less subviews and less user interactions, compared to add the subviews into the parent view controller directly,
275 | using a child view controller or a customized view is not recommended.
276 | In the situation with less subviews and more user interactions, developers can choose the parent plan or child plan by themselves.
277 | The parent plan is recommended with the high relevancy degree between the user interactions and the parent view controller.
278 |
279 | ### Child view controller
280 |
281 | A child view controller also extends the BaseViewController, so it can take advantage of view model and RxSwift.
282 |
283 | 
284 |
285 | **Using a container view is recommended for a child view controller.**
286 | For example, to add the `childViewController1` into the parent view controller, a container view `containerView1` should be prepared at first.
287 | The constraints is applied to the containerView1.
288 | When we invoke the `addChild(childViewController1, to: containerView1)`, the root view of the child view controller will be added to `containerView1`.
289 | The edges of the root view is same as `containerView1`.
290 |
291 | A child view controller (`childViewController2`) may have its own child view controllers (`childViewController3`).
292 |
293 | ## 3.4 Create constraints with SnapKit
294 |
295 | **As a general rule, `makeConstraints` methods is recommened to be written in the `createConstraints` method.**
296 | Sometimes, we need to add and remove views dynamically.
297 | In this situation, `makeConstraints` methods may be written in other private methods.
298 |
299 | **The constants used in the closure should be define in a private enum `Const` before the view controller.**
300 | The demo code of `Const` enum:
301 |
302 | ```swift
303 | private enum Const {
304 | enum Title {
305 | static let marginTop = 11
306 | static let marginBottom = 17
307 | }
308 |
309 | enum Close {
310 | static let size = 24
311 | static let marginRight = 11
312 | }
313 | }
314 | ```
315 |
316 | **The type of the property is recommened to be omitted if possible**
317 |
318 | **The sub enum in the `Const` enum is corresponding to the subview, and it name is the prefix of the subview's name.**
319 |
320 | ```
321 | demoView -> enum Demo
322 | demoLabel -> enum Demo
323 | ```
324 |
325 | **The order of the subenums should be same as the definition of the corresponding views.**
326 | If there is a container view which contains multiple views, the following code is recommend.
327 |
328 | ```swift
329 | private enum Const {
330 | enum Container {
331 | static let marginTop = 11
332 |
333 | enum Name {
334 | marginTop = 11
335 | }
336 |
337 | enum Icon {
338 | marginTop = 11
339 | }
340 | }
341 |
342 | }
343 | ```
344 |
345 | **Making contraints in the clousre should follow the order.**
346 | We define 3 groups `size`, `center` and `margin` here.
347 |
348 | - size.
349 | - width
350 | - height
351 | - center
352 | - centerX
353 | - centerY
354 | - margin
355 | - left(or leading)
356 | - top
357 | - right(or trailing)
358 | - bottom
359 |
360 | Sometime, we may set multiple constraints of a same group in a single line.
361 | In this line, the multiple constraints should follow this order.
362 | For multiple lines, the first constraints should follow this order.
363 |
364 | ```swift
365 | iconImageView.snp.makeConstraints {
366 | $0.size.equalTo(Const.Name.size)
367 | $0.top.right.equalToSuperview()
368 | }
369 | ```
370 |
371 | **However, for multiple constraints of different groups, a single line is not recommended**
372 |
373 | ```swift
374 | nameLabel.snp.makeConstraints {
375 | $0.width.centerY.equalToSuperview()
376 | }
377 | ```
378 |
379 | Different groups of constraints should be separated to make it easier for understanding.
380 |
381 | ## 3.5 Reactive extension for view controller
382 |
383 | Sometimes, a few lines of code should be executed after updating an observable object in the view model.
384 | Pay attention that running on the main thread is required if updating UI is needed.
385 | For this, observing on the main scheduler is required before subscribing the observable object in the view controller.
386 |
387 | ```swift
388 | viewModel.user.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self] in
389 | // ...
390 | }).disposed(by: disposeBag)
391 | ```
392 |
393 | However, such a way makes the `viewDidLoad` method complicated.
394 | Although, a private method can solve this problem,
395 | **reactive extension is recommended to bind a complex observable object.**
396 |
397 | Prepare the internal method in a private extension of this view controller at first.
398 |
399 | ```swift
400 | private extension UserViewController {
401 |
402 | func updateUser(_ user: User) {
403 | // Update user here...
404 | }
405 |
406 | }
407 | ```
408 |
409 | Then, write a reactive extension for this view controller to bind the observable object.
410 |
411 | ```swift
412 | extension Reactive where Base: UserViewController {
413 |
414 | var user: Binder {
415 | return Binder(base) { viewController, user in
416 | viewController.updateUser(user)
417 | }
418 | }
419 |
420 | }
421 | ```
422 |
--------------------------------------------------------------------------------
/document/chapter4-viewmodel.md:
--------------------------------------------------------------------------------
1 | # Chapter 4: View model
2 |
3 | This chapter introduces the rule of view model.
4 |
5 | ## 4.1 Structure of view model
6 |
7 | The structure of view models is flexible than view controllers.
8 | The code in the view model should follow the order:
9 |
10 | ### Define the private store properties
11 |
12 | Private properties contain both general type (`Int`, `String`, `Bool`, `User`), and RxSwift type (`BehaviourRelay`, `PublishSubject`) .
13 | **The general type should be above the RxSwift type.**
14 |
15 | ```swift
16 | private let isVisable = true
17 | private let userRealy = BehaviorRelay(value: User())
18 | ```
19 |
20 | **The name of RxSwift properties should containts a short suffix like `Relay` or `Subject`**
21 |
22 | ```swift
23 | private let userRealy = BehaviorRelay(value: User())
24 | private let iconRealy = PublishRelay()
25 | private let userSubject = BehaviorSubject()
26 | private let iconSubject = PublishSubject()
27 | ```
28 |
29 | ### Define the child view model if needed
30 |
31 | Define child view models here if needed with `private lazy var`.
32 |
33 | ```swift
34 | private lazy var childViewModel = ChildViewModel()
35 | ```
36 |
37 | ### Init/Deinit method
38 |
39 | Append `init` and `deinit` methods here if needed.
40 | **`init` method in recommended to be above `deinit` method.**
41 |
42 | ```swift
43 | override init() {
44 | super.init()
45 | // Do something here.
46 | }
47 |
48 | deinit {
49 | // Do something here.
50 | }
51 |
52 | ```
53 |
54 | ### Override methods and properties.
55 |
56 | ```swift
57 | override func prepareForParentEvents() {
58 | // Do something here.
59 | }
60 | ```
61 | ### Private computed peroperties.
62 |
63 | The type (`Int`, `String`, `Bool`, `User`) should be above RxSwift type.
64 |
65 | ```swift
66 | private var sex: Bool {
67 | return userRelay.value.sex
68 | }
69 |
70 | private var name: Observable {
71 | return userRelay.map { $0.name }
72 | }
73 | ```
74 |
75 | ### Private methods.
76 |
77 | ```swift
78 | private func setup() {
79 |
80 | }
81 | ```
82 |
83 | ### Internal computed properties.
84 |
85 | The type (`Int`, `String`, `Bool`, `User`) should be above RxSwift type.
86 |
87 | ```swift
88 | var sex: Bool {
89 | return userRelay.value.sex
90 | }
91 |
92 | var name: Observable {
93 | return userRelay.map { $0.name }
94 | }
95 | ```
96 |
97 | The computed observable properties is prepared for data binding in the `viewDidLoad` method of is view controller class.
98 |
99 | **The name of computed observable propertie is the prefix of the subview's name.**
100 |
101 | ```swift
102 | viewModel.name ~> nameLabel.rx.text
103 | ```
104 |
105 | ### Internal methods.
106 |
107 | In a general way, the internal methods of view model are prepared for its view controller class.
108 | Invoking the internal method in the flow or other view model is not recommended.
109 | Exchanging data among view models with `steps` and `events` is recommended, this will be introduced in the chapter 4.2.
110 |
111 | ```swift
112 | func pick(at index: Int) {
113 | // ...
114 | }
115 |
116 | func close() {
117 | steps.accept(UserStep.complete)
118 | }
119 |
120 | ```
121 |
122 | ## 4.2 Exchange data among view models
123 |
124 | We have 3 ways to exchange data among view models.
125 |
126 | #### Using `steps` of RxFlow
127 |
128 | A flow contains multiple view controllers.
129 | **The view models of these view controllers should exchange data using `steps` of RxFlow.**
130 |
131 | The type of the exchanging data is defined in the Step enum in the top of the Flow file.
132 |
133 | ```swift
134 | enum UserStep {
135 | case start
136 | case user(User)
137 | }
138 | ```
139 |
140 | The step `user(User)` contains a parameter.
141 | In the `UserListViewModel`, when the method `steps.accept(UserStep(user))` is invoked, the step will be executed.
142 | We can get the parameter `user` and initialize `UserViewModel` with the `user` parameter.
143 |
144 | ```swift
145 | case .user(let user):
146 | let userViewController = UserViewController(viewModel: .init(user: user))
147 | navigationController?.push(userViewController, animated: true)
148 | return .viewController(userViewController)
149 | ```
150 |
151 | Of course, the init method with a user parameter should be prepared in `UserViewModel`.
152 |
153 | ```swift
154 | class UserViewModel: BaseViewModel {
155 |
156 | init(user: User) {
157 | super.init()
158 |
159 | // Customized code.
160 | }
161 |
162 | }
163 | ```
164 |
165 | Compared to using an internal method,
166 | **the init method is recommended to pass the parameters to a new view model,**
167 | because the init method can ensure passing the necessary parameters from grammatical level.
168 |
169 | #### Using `events` of RxController
170 |
171 | Exchanging data with `steps` among a view model and its child view models requires that the flow holds the references of the view models.
172 | Many additional steps must be added with frequent data exchanging among child view models and the parent view model.
173 | For these reasons, **`events` is recommended to change data among a view model and its child view models.**
174 |
175 | **All events is recommended to be defined in the top of the common view model.**
176 |
177 | ```swift
178 | struct InfoEvent {
179 | static let name = RxControllerEvent.identifier()
180 | static let number = RxControllerEvent.identifier()
181 | }
182 | ```
183 |
184 | A **common** view model depends on the structure of view controllers and the business logic.
185 | It can be a parent view model or a child view model.
186 |
187 | 
188 |
189 | In this graph, there are 6 view models, and the arrows represent the relationship among them.
190 | There a 3 child view controller systems:
191 |
192 | | Systems | Parent view Model | Child view models |
193 | | ---- | ----- | ---- |
194 | | A | VM 1 | VM 3 |
195 | | B | VM 2 | VM 3, WM 4 |
196 | | C | VM 4 | VM 5, WM 6 |
197 |
198 | In the second and third systems (B and C), child view models have common parent view models.
199 | Parent view models is common view models.
200 | The events is recommended to be defined in the parent view model in such situations.
201 |
202 | However, the parent view controller in the first and the second systems (A and B) has a some child view model.
203 | Thus, the parent view models (VM 1 and VM2) may send some same objects to the child view model (VM 3).
204 | In this situation, the child view model (VM 3) is regarded as the common view models, and the events should be defined in the child view model.
205 |
206 | We have introduced the usage of `events` in the README of RxController.
207 | There are the ways to send and receive event in the parent view model and the child view models:
208 |
209 | - Send a event from the parent view model (`InfoViewModel `).
210 |
211 | ```Swift
212 | events.accept(InfoEvent.name.event("Alice"))
213 | ```
214 |
215 | - Send a event from the child view model (`NameViewModel` and `NumberViewModel`).
216 |
217 | ```Swift
218 | parentEvents.accept(event: InfoEvent.name.event("Alice"))
219 | ```
220 |
221 | - Receive a event in the parent view model (`InfoViewModel `).
222 |
223 | ```Swift
224 | var name: Observable {
225 | return events.value(of: InfoEvent.name)
226 | }
227 | ```
228 |
229 | - Receive a event in the child view model (`NameViewModel` and `NumberViewModel`).
230 |
231 | ```Swift
232 | var name: Observable {
233 | return parentEvents.value(of: InfoEvent.name)
234 | }
235 | ```
236 |
237 | #### Using single instance manager class
238 |
239 | If a same single instance needs to be shared among multiple view models, using a corresponding instance manager class is better than using `steps` and `events`.
240 |
241 | ```swift
242 | class UserManager {
243 | static let shared = UserManager()
244 |
245 | private init() {}
246 |
247 | let user = UserRepository.shared.observeMySelf()
248 | }
249 | ```
250 |
251 | For most of the data exchanging,
252 | **`steps` and `events` is recommended to used to exchange data among view models.**
253 |
254 | ## 4.3 Error Handling
255 |
256 | Using the `bind(to:)` method cannot handle the error.
257 | Without error handling in the view model, app will crash if an error comes.
258 |
259 | **Observable objects for data binding in the view controller class cannot contain error event.**
260 |
261 | ```swift
262 | private var userRelay = BehaviourRelay(value: nil)
263 |
264 | override init() {
265 | super.init()
266 |
267 | UserManager.share.getMySelf()
268 | .bind(to: userRelay)
269 | .disposed(by: disposeBag)
270 | }
271 |
272 | var name: Observable {
273 | return userRelay.map {
274 | $0.name
275 | }
276 | }
277 |
278 | ```
279 |
280 | In this situation, if an error comes, the Observable cannot bind to the `userRelay` due to the error.
281 | Of course, the name cannot be bind the the `nameLabel` in the view controller.
282 |
283 | To solve this problem, we have 3 ways:
284 |
285 | #### Subscribe the observable object
286 |
287 | `onError` is ignored automatically after subscribing the observable object.
288 |
289 | ```swift
290 | UserManager.share.getMySelf().subscribe(onNext: { [unowned self] in
291 | self.userRelay.accept($0)
292 | }).disposed(by: disposeBag)
293 | ```
294 |
295 | Of course, if the error should be handled in the view model, `onError` closure is needed.
296 |
297 | #### Transform the observable object to a driver object
298 |
299 | ```swift
300 | UserManager.share.getMySelf()
301 | .asDriver(onErrorJustReturn: nil)
302 | .drive(userRelay)
303 | .disposed(by: disposeBag)
304 | ```
305 |
306 | Driver does not contains error event.
307 | Ignore the error and provide a default value if a error event comes.
308 |
309 | #### Handling error in your manager classes
310 |
311 | The errors should be handled before using the method.
312 |
--------------------------------------------------------------------------------
/document/chapter5-view.md:
--------------------------------------------------------------------------------
1 | # Chapter 5: View
2 |
3 | This chapter introduces the rule of view including system views of UIKit and customized views.
4 |
5 | **The MVC design pattern + Reactive extension is recommended to develop a customized view.**
6 | We don't recommend to use RxSwift and MVVM directly in a customized view, because managing the `disposeBag` in the view may cause some memory leak issues.
7 |
8 | If the customized view is simple and only used in a single view controller, it is recommended to be written as a `private class` at the last of the view controller.
9 | Otherwise, creating a file for the customized view and implement with a `internal class` is recommended.
10 |
11 | ## 4.1 Structure of a customized view model
12 |
13 | We have introduced the difference between the view controller and the customized view model.
14 | We can simply understand the customized view model as a simple view controller without view model.
15 |
16 | The code in the customized view model should follow the order:
17 |
18 | ### Define views
19 |
20 | The rule of the view definition is same as view controller.
21 |
22 | ```swift
23 | private lazy var nameLabel: UILabel = {
24 | let label = UILabel()
25 | label.textColor = UIColor(hex: 0x222222)
26 | label.font = UIFont.systemFont(ofSize: 14)
27 | return label
28 | }()
29 | ```
30 |
31 | Internal subviews is not recommended in the customzied view.
32 | Internal methods and propertied is recommended as the wrapper of the methods and properties of the private subviews.
33 |
34 | ### Define other private properties
35 |
36 | Private properties stores the state of views.
37 |
38 | ```swift
39 | private var isButtonEnable = false
40 | ```
41 |
42 | ### Init method
43 |
44 | Without lifecycle like view controller, we can add subviews to the view and create constraints directly in the `init` method.
45 |
46 | ```swift
47 | class InfoView: UIView {
48 |
49 | private lazy var titleLabel: UILabel = {
50 | let label = UILabel()
51 | label.textColor = .red
52 | label.textAlignment = .center
53 | return label
54 | }()
55 |
56 | override init(frame: CGRect) {
57 | super.init(frame: frame)
58 |
59 | addSubview(titleLabel)
60 | createConstraints()
61 | }
62 |
63 | required init?(coder: NSCoder) {
64 | fatalError("init(coder:) has not been implemented")
65 | }
66 | }
67 | ```
68 |
69 | ### Override methods and properties.
70 |
71 | Override other methods and properties here.
72 | Override properties is above override methods.
73 |
74 | ```swift
75 | override var isHighlighted: Bool {
76 | didSet {
77 | if isHighlighted {
78 | backgroundView?.backgroundColor = UIColor(hex: 0xdddddd)
79 | } else {
80 | backgroundView?.backgroundColor = .white
81 | }
82 | }
83 | }
84 |
85 | override func configure(model: Model) {
86 | nameLabel.text = model.name
87 | }
88 | ```
89 |
90 | ### Create constraints method.
91 |
92 | The rule of the `createcConstraints` method is same as view controller.
93 |
94 | ### Other methods.
95 |
96 | The private methods, internal methods and public method should follow:
97 |
98 | ```swift
99 | private func a() {
100 |
101 | }
102 |
103 | func b() {
104 |
105 | }
106 |
107 | public func c() {
108 |
109 | }
110 | ```
111 |
112 | ### Computed properties.
113 |
114 | The private computed properties should be above the internal computed properties.
115 | The computed properties with only get method should be above the properties with both set and get methods.
116 | **Definition of computed properties should follow the orders:**
117 |
118 | - Private computed properties
119 | - Private computed properties with only get method
120 | - Private computed properties with both set and get methods
121 | - Internal computed properties
122 | - Internal computed properties with only get method
123 | - Internal computed properties with both set and get methods
124 | - Public computed properties
125 | - Public computed properties with only get method
126 | - Public computed properties with both set and get methods
127 |
128 | ```swift
129 | private var name: String? {
130 | return nameLabel.text
131 | }
132 |
133 | private var icon: UIImage? {
134 | set {
135 | iconImageView.image = newValue
136 | }
137 | get {
138 | return iconImageView.image
139 | }
140 | }
141 |
142 | var title: String? {
143 | return titleLabel.text
144 | }
145 |
146 | var avatar: UIImage? {
147 | set {
148 | avararImageView.image = newValue
149 | }
150 | get {
151 | return avararImageView.image
152 | }
153 | }
154 |
155 | public var subtitle: String? {
156 | return subtitleLabel.text
157 | }
158 |
159 | public var font: UIFont? {
160 | set {
161 | titleLabel.font = newValue
162 | }
163 | get {
164 | return title.font
165 | }
166 | }
167 | ```
168 |
169 | **The set method should be above get method.**
170 |
171 | We have introduced that internal subviews is not recommended in the customzied view.
172 | The internal computed properties with both set and get methods is recommended to be used as a wrapper of the properties of subviews.
173 |
174 | ### Stored properties with `didSet` method
175 |
176 | The stored properties should following:
177 |
178 | - Private stored properties.
179 | - Internal stored properties.
180 | - Public stored properties.
181 |
182 | The stored properties is used for saving the state of views or data models.
183 |
184 | ```swift
185 | var user: User? {
186 | didSet {
187 | guard let user = user else {
188 | return
189 | }
190 | nameLabel.text = user.name
191 | avatarImageView.image = user.avatar
192 | }
193 | }
194 | ```
195 |
196 | ## 4.2 Code in the closure to initialize a view
197 |
198 | We initialize subviews in a view controller or a customized view with closure if some properties need to be set.
199 | **The code in the closure should follow the orders.**
200 |
201 | - Part 1: Set properties
202 | - Set properties of this view.
203 | - Set properties of its subviews, although setting properties of subviews is not recommended.
204 | - Set properties of its sublayers
205 | - Part 2: Methods of this view.
206 | - Part 3: Add subviews or sublayers
207 | - Add subviews.
208 | - Add sublayers.
209 | - Part 4: RxSwift related code.
210 | - Subscribe.
211 |
212 | ```swift
213 | private lazy var closeButton: UIButton = {
214 | let button = UIButton()
215 | button.backgroundColor = .clear
216 | button.setImage(R.image.close(), for: .normal)
217 | button.addsubView(blurView)
218 | button.rx.tap.bind { [unowned self] in
219 | self.viewModel.close()
220 | }.disposed(by: disposeBag)
221 | return button
222 | }()
223 | ```
224 |
225 | ## 4.3 Reactive extension for view
226 |
227 | Invoking the internal methods and properties in the view controller directly is not recommended in our guideline.
228 | Because developing with MVVM based on RxSwift, all data should be transferred with the `Observable` object of RxSwift.
229 | For example, there is a property `name` in a customized view `NameView`.
230 |
231 | ```swift
232 | class NameView: UIView {
233 |
234 | // ...
235 |
236 | var name: String? {
237 | set {
238 | nameLabel.text = newValue
239 | }
240 | get {
241 | return nameLabel.text
242 | }
243 | }
244 | }
245 | ```
246 |
247 | We define an `Observable` object `name` in the view model class.
248 |
249 | ```swift
250 | var name: Observable {
251 | return user.map { $0.name }
252 | }
253 | ```
254 |
255 | We hope to bind `name` to the `nameView` directly rather than subscribing `name` and setting it in the `onNext` closure.
256 |
257 | ```swift
258 | viewModel.name ~> nameView.rx.name
259 | ```
260 |
261 | Here, like the reactive extension in the view controller,
262 | **reactive extension is recommended to bind data for a customized view.**
263 | For this purpose, it better to create a binder in the reactive extension .
264 |
265 | ```swift
266 | extension Reactive where Base: NameView {
267 |
268 | var name: Binder { view, name in
269 | view.name = name
270 | }
271 |
272 | }
273 | ```
274 |
275 | Compared to the data binding in the view controller,
276 | the customized view provides both internal/public properties or methods and reactive extension binder to update UI,
277 | while the view controller provides the reactive extension binder only.
278 |
--------------------------------------------------------------------------------
/document/chapter6-cell.md:
--------------------------------------------------------------------------------
1 | # Chapter 6: Table and collection view cell
2 |
3 | This chapter introduces the rule of table and collection view cell.
4 | As unlike the view controller and customized view, **The pure MVC design pattern is recommended to develop a cell.**
5 |
6 | ## 6.1 Structure of cell
7 |
8 | We can easily understand a table or collection view cell as a special view.
9 | Generally, a cell contains the following parts:
10 |
11 | ### Same parts as customzied view
12 |
13 | - [Define views](https://github.com/xflagstudio/RxController/blob/master/document/chapter5-view.md#define-views)
14 | - [Define other private properties](https://github.com/xflagstudio/RxController/blob/master/document/chapter5-view.md#define-other-private-properties)
15 | - [Init method](https://github.com/xflagstudio/RxController/blob/master/document/chapter5-view.md#init-method)
16 | - [Override methods and properties](https://github.com/xflagstudio/RxController/blob/master/document/chapter5-view.md#override-methods-and-properties)
17 | - [Create constraints method](https://github.com/xflagstudio/RxController/blob/master/document/chapter5-view.md#create-constraints-method)
18 |
19 | ### Private methods and computed properties
20 |
21 | Because a view controller does not hold the references of cells directly, the internal customized properties for data binding is not recommend in the code the cells.
22 |
23 | ```swift
24 | private var name: String? {
25 | return nameLabel.text
26 | }
27 |
28 | private func updateName() {
29 |
30 | }
31 |
32 | ```
33 |
34 | ### Define model and implement `Configurable` protocol
35 |
36 | This will be introduced in the next part [Using RxDataSources and RxDataSourcesSingleSection](https://github.com/xflagstudio/RxController/blob/master/document/chapter6-cell.md#62-using-rxdatasources-and-rxdatasourcessinglesection)
37 |
38 | ## 6.2 Using RxDataSources and RxDataSourcesSingleSection
39 |
40 | Native RxSwift library does not provide a easy way to manage complicated data source and animation.
41 | With RxDataSources, we can easily define sections and data source.
42 | RxDataSourcesSingleSection makes it easy to create a table view or a collection view with a single section.
43 | RxDataSourcesSingleSection also define a `Configurable` protocol which extends the `Reusable` protocol, to make the data binding of the cell more standardized.
44 | Here, we introduce the rule of the cell based on RxDataSourcesSingleSection and its `Configurable` protocol.
45 |
46 | Each cell has its own view model, we call it view object of the cell to distinguish it from the view model concept in MVVM.
47 | **The name of the view object is recommended to be same as the prefix of the cell. To prevent the memory leak issues, `struct` is recommended to define a view object rather than `class`.**
48 |
49 | ```swift
50 | class PersonTableViewCell: UITableViewCell {
51 |
52 | }
53 |
54 | struct Person {
55 | var name: String
56 | var address: Int
57 | }
58 | ```
59 |
60 | **The cell class should implement the `Configurable` protocol.**
61 | The `Model` of the `associatedtype` is view object.
62 | In the `configure` method, we bind data to the views with the view object.
63 |
64 | ```swift
65 | extension PersonTableViewCell: Configurable {
66 |
67 | typealias Model = Person
68 |
69 | func configure(_ model: Person) {
70 | nameLabel.text = model.name
71 | addressLabel.text = model.address
72 | }
73 |
74 | }
75 | ```
76 |
77 | ## 6.3 Table view cell height
78 |
79 | Calculating cell height is a troublesome problem for the table view cell.
80 | If the cell height is dynamical, using autolayout using SnapKit in the cell and automatic row height in the table view is recommended.
81 |
82 | ```swift
83 | private lazy var tableView: UITableView = {
84 | let tableView = UITableView()
85 | tableView.rowHeight = UITableView.automaticDimension
86 | tableView.register(cellType: DemoTableViewCell.self)
87 | return tableView
88 | }()
89 | ```
90 |
91 | In the cell class, the autolayout rule is different with which in the view controller or the customized view.
92 |
93 | ## 6.4 Tap action
94 |
95 | We can handle the tap action for a cell with the `didItemSelect` delegate method of `UITableViewDelegate` or `UICollectionViewDelegate`,
96 | or using some customized buttons.
97 |
98 | ### Using system defined delegate methods.
99 |
100 | Using a delegate method with an extension in the view controller is not recommended.
101 | RxCocoa provides reactive extension for most of the delegate methods.
102 | **Handling the delegate methods with reactive extension in the definition closure of a table view or a collection view is recommended.**
103 |
104 | ```swift
105 | private lazy var tableView: UITableView = {
106 | let tableView = UITableView()
107 | // Set the table view...
108 | tableView.rx.itemDidSelected.bind { [unowned self] in
109 | // Invoke a method in the view model.
110 | self.viewModel.pick(at: $0.row)
111 | }.disposed(by: disposeBag)
112 | return tableView
113 | }()
114 | ```
115 |
116 | An internal method in corresponding view model should be prepared.
117 |
118 | ```swift
119 | func pick(at index: Int) {
120 | // Do something here...
121 | }
122 | ```
123 |
124 | ### Customized buttons
125 |
126 | Sometimes, a single cell may contains multiple tap action for multiple customized buttons.
127 | In the view controller, the reactive extension is recommended in the definition closure of the customized buttons.
128 | However, for the reusable cells, managing the `disposeBag` and avoiding the multiple tap action binding are troublesome.
129 | As we said in the top of this chapter, **the pure MVC design pattern is recommended for handling events of reusable cells.**
130 | Here, we define the customized button and add tap action with `addTarget` method directly.
131 |
132 | ```swift
133 | private lazy var favoriteButton: UIButton = {
134 | let button = UIButton()
135 | button.backgroundColor = .white
136 | button.nameColor = .black
137 | button.addTarget(self, action: #selector(favoriteTapped), for: .touchUpInside)
138 | return button
139 | }()
140 | ```
141 |
142 | When the button is tapped, the objc method will be invoked.
143 |
144 | ```swift
145 | @objc
146 | private func favoriteTapped() {
147 | didFavoriteTapped?()
148 | }
149 | ```
150 | In this private method, an optional closure is invoked.
151 | This clousre is defined as an internal optional property.
152 |
153 | ```swift
154 | var didFavoriteTapped: (() -> Void)?
155 | ```
156 |
157 | The tapped closure should be setted in the dataSource of the tableView.
158 |
159 | ```swift
160 | private lazy var dataSource = FavoriteTableViewCell.tableViewSingleSectionDataSource(configureCell: { [unowned self] cell, indexPath, _ in
161 | cell.didFavoriteTapped = {
162 | self.viewModel.pick(at: indexPath.row)
163 | }
164 | })
165 | ```
166 |
167 | At last, a method in the view model will be invoked.
168 | This method is responsible for handling the specific business logic.
169 |
--------------------------------------------------------------------------------
/document/chapter7-manager.md:
--------------------------------------------------------------------------------
1 | # Chapter 7: Manager classes
2 |
3 | Using manager classes is recommended for common business logic and API calling.
4 | **A manager class should be defined with `final class` beacuse it should not be extened.**
5 |
6 | ```swift
7 | final class UserManager {
8 |
9 | static let shared = UserManager()
10 |
11 | private init() {
12 | // Do something here...
13 | }
14 |
15 | }
16 | ```
17 |
18 | **The singleton pattern is recommended for the manager class.**
19 | To hide disable the `init` method of the manager class, using a `private init()` method is recommended.
20 |
21 |
--------------------------------------------------------------------------------
/images/child_view_controllers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/child_view_controllers.jpg
--------------------------------------------------------------------------------
/images/child_view_models.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/child_view_models.jpg
--------------------------------------------------------------------------------
/images/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/logo.jpg
--------------------------------------------------------------------------------
/images/rxflow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/rxflow.jpg
--------------------------------------------------------------------------------
/images/subviews_and_child_controller.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/subviews_and_child_controller.jpg
--------------------------------------------------------------------------------
/images/viewmodel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RxSwiftCommunity/RxController/4a3e7b75bc9dd83c60655448d411b11ea7ffec01/images/viewmodel.jpg
--------------------------------------------------------------------------------
/rxtree/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
--------------------------------------------------------------------------------
/rxtree/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "rxtree",
8 | dependencies: [
9 | // Dependencies declare other packages that this package depends on.
10 | // .package(url: /* package url */, from: "1.0.0"),
11 | .package(url: "https://github.com/kylef/Commander.git", from: "0.9.1"),
12 | .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0")
13 | ],
14 | targets: [
15 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
16 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
17 | .target(
18 | name: "rxtree",
19 | dependencies: ["Commander", "Rainbow"]),
20 | .testTarget(
21 | name: "rxtreeTests",
22 | dependencies: ["rxtree"]),
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Extensions/Array+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Extension.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/22.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | extension Array where Element: Hashable {
29 |
30 | var uniques: Array {
31 | reduce(Set()) {
32 | var result = $0
33 | result.insert($1)
34 | return result
35 | }.reduce([Element]()) {
36 | $0 + [$1]
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Extensions/FileManager+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileManager+Extension.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | extension FileManager {
29 |
30 | func urls(for directory: FileManager.SearchPathDirectory, skipsHiddenFiles: Bool = true) -> [URL]? {
31 | let documentsURL = urls(for: directory, in: .userDomainMask)[0]
32 | let fileURLs = try? contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: skipsHiddenFiles ? .skipsHiddenFiles : [])
33 | return fileURLs
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Extensions/Int+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int+Extension.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/22.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | extension Int {
29 |
30 | var lineNumber: String {
31 | if self < 10 {
32 | return " \(self) "
33 | }
34 | if self < 100 {
35 | return " \(self) "
36 | }
37 | return "\(self) "
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Extensions/String+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Extension.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | extension String {
29 |
30 | func match(with pattern: String) -> [String] {
31 | let regex = try? NSRegularExpression(pattern: pattern, options: [])
32 | guard let results = regex?.matches(in: self, options: [], range: NSRange(location: 0, length: self.count)), !results.isEmpty else {
33 | return []
34 | }
35 | return results.map {
36 | (self as NSString).substring(with: $0.range)
37 | }
38 | }
39 |
40 | func matchFirst(with pattern: String) -> String? {
41 | return match(with: pattern).first
42 | }
43 |
44 | func last(separatedBy string: String) -> String? {
45 | return self.components(separatedBy: string).last
46 | }
47 |
48 | func first(separatedBy string: String) -> String? {
49 | return self.components(separatedBy: string).first
50 | }
51 |
52 | }
53 |
54 | extension String {
55 |
56 | func loadSwiftFiles() -> [URL] {
57 | guard let rootURL = URL(string: self) else {
58 | return []
59 | }
60 | return loadFiles(url: rootURL).filter {
61 | $0.absoluteString.hasSuffix(".swift")
62 | }
63 | }
64 |
65 | func loadFiles(isRecursion: Bool) -> [URL] {
66 | guard let rootURL = URL(string: self) else {
67 | return []
68 | }
69 | return loadFiles(url: rootURL, isRecursion: isRecursion)
70 | }
71 |
72 | private func loadFiles(url: URL, isRecursion: Bool = true) -> [URL] {
73 | do {
74 | let fileURLs = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
75 | return fileURLs.filter {
76 | !$0.absoluteString.hasSuffix(".git/") && !$0.absoluteString.hasSuffix("Pods/")
77 | }.map {
78 | $0.absoluteString.last == "/" && isRecursion ? loadFiles(url: $0) : [$0]
79 | }.reduce([], +)
80 | } catch {
81 | print("Error while enumerating files \(url.path): \(error.localizedDescription)")
82 | }
83 | return []
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Extensions/URL+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+Extension.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | extension URL {
29 |
30 | var content: String {
31 | var data = ""
32 | do {
33 | data = try String(contentsOf: self, encoding: .utf8)
34 | } catch {
35 | print("Error while reading file \(self.path): \(error.localizedDescription)")
36 | }
37 | return data
38 | }
39 |
40 | var lines: [String] {
41 | return content.components(separatedBy: .newlines)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Keyword.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Keyword.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/20.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | struct Keyword {
29 | let name: String
30 | let url: URL
31 | }
32 |
33 | extension Keyword: CustomStringConvertible {
34 |
35 | var description: String {
36 | name + " in " + url.absoluteString
37 | }
38 |
39 | }
40 |
41 | extension Array where Element == Keyword {
42 |
43 | var names: [String] {
44 | map { $0.name }
45 | }
46 |
47 | func first(name: String) -> Keyword? {
48 | let firstIndex = map { $0.name }.firstIndex(of: name)
49 | guard let index = firstIndex else {
50 | return nil
51 | }
52 | return self[index]
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/20.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | protocol Node {
29 | var level: Int { get }
30 | var className: String { get }
31 | var description: String { get }
32 | }
33 |
34 | struct Flow: Node {
35 | let level: Int
36 | let className: String
37 | let flows: [Flow]
38 | let viewControllers: [ViewController]
39 |
40 | var description: String {
41 | let flowsDescription = flows.map {
42 | $0.description
43 | }.reduce("", +)
44 | let viewControllerDescription = viewControllers.map {
45 | $0.description
46 | }.reduce("", +)
47 | var indent = ""
48 | if level > 0 {
49 | indent += (0.. 0 {
66 | indent += (0.. String {
38 | classNames.map {
39 | "(class \(Pattern.legalIdentifier): \($0)<\(Pattern.legalIdentifier)>)"
40 | }.reduce("") {
41 | $0.isEmpty ? $1 : $0 + "|" + $1
42 | }
43 | }
44 |
45 | static func stepCase(for name: String) -> String {
46 | "(case .\(name)[\\s\\S]*?return[\\s\\S]*?case)|(case .\(name)[\\s\\S]*?return[\\s\\S]*?\\})"
47 | }
48 | }
49 |
50 | class RxTree {
51 |
52 | private let flows: [Keyword]
53 | private let viewControllers: [Keyword]
54 | private let maxLevels: Int
55 |
56 | init?(directory: String, maxLevels: Int) {
57 | var currentDirectory = directory + "/"
58 | var projects: [String] = []
59 | repeat {
60 | if let last = currentDirectory.last(separatedBy: "/") {
61 | currentDirectory = String(currentDirectory.dropLast(last.count + 1))
62 | } else {
63 | currentDirectory = ""
64 | }
65 | projects = currentDirectory.loadFiles(isRecursion: false).compactMap {
66 | $0.absoluteString
67 | }.map {
68 | $0.last == "/" ? String($0.dropLast()) : $0
69 | }.compactMap {
70 | $0.last(separatedBy: "/")?.matchFirst(with: "(\(Pattern.legalIdentifier).xcodeproj)|(\(Pattern.legalIdentifier).xcworkspace)")
71 | }
72 |
73 | } while (projects.isEmpty && !currentDirectory.isEmpty)
74 | guard !currentDirectory.isEmpty else {
75 | return nil
76 | }
77 | let swiftFiles = currentDirectory.loadSwiftFiles()
78 |
79 | // Load all flows
80 | flows = swiftFiles.map { url in
81 | url.lines.compactMap {
82 | $0.matchFirst(with: "class \(Pattern.legalIdentifier): Flow")
83 | }.compactMap {
84 | $0.last(separatedBy: "class ")?.first(separatedBy: ":")
85 | }.map {
86 | Keyword(name: $0, url: url)
87 | }
88 | }.reduce([], +)
89 |
90 | let scanner = Scanner(urls: swiftFiles)
91 | let viewControllersPattern = Pattern.parentViewControllerClass(for: scanner.scanClassForRxViewController())
92 | // Load all view controllers
93 | viewControllers = swiftFiles.map { url in
94 | url.lines.compactMap {
95 | $0.matchFirst(with: viewControllersPattern)
96 | }.compactMap {
97 | $0.last(separatedBy: "class ")?.first(separatedBy: ":")
98 | }.map {
99 | Keyword(name: $0, url: url)
100 | }
101 | }.reduce([], +)
102 |
103 | self.maxLevels = maxLevels
104 | }
105 |
106 | func list(root: String) -> Node? {
107 | if flows.names.contains(root) {
108 | return listFlow(root: root, lastLevel: 0)
109 | }
110 | if viewControllers.names.contains(root) {
111 | return listViewController(root: root, isChildViewController: false, lastLevel: 0)
112 | }
113 | return nil
114 | }
115 |
116 | // Search class name by property name from lines of code
117 | private func searchClassName(for name: String, in lines: [String]) -> String? {
118 | var className: String? = nil
119 | // Search class name from code with pattern `xxxFlow = XxxFlow`
120 | if className == nil {
121 | className = lines.first {
122 | $0.contains(name + " = ")
123 | }?.last(separatedBy: " = ")?.first(separatedBy: "(")
124 | }
125 | // Search class name from lines except all step cases with pattern `xxxFlow: XxxFlow`
126 | if className == nil {
127 | className = lines.compactMap {
128 | $0.matchFirst(with: "\(name): \(Pattern.legalIdentifier)")
129 | }.first?.last(separatedBy: ": ")
130 | }
131 | return className
132 | }
133 |
134 | }
135 |
136 | // List flow related methods.
137 | extension RxTree {
138 |
139 | private func listFlow(root: String, lastLevel: Int) -> Flow? {
140 | guard let rootFlow = flows.first(name: root) else {
141 | return nil
142 | }
143 |
144 | let content = rootFlow.url.content
145 | guard let stepName = content.matchFirst(with: Pattern.stepEnum) else {
146 | return nil
147 | }
148 | // Find all steps in the flow.
149 | let steps = stepName.components(separatedBy: "\n").compactMap {
150 | $0.matchFirst(with: Pattern.stepCase)?.last(separatedBy: "case ")
151 | }.compactMap {
152 | content.matchFirst(with: Pattern.stepCase(for: $0))
153 | }
154 |
155 | let linesOfSteps = steps.map {
156 | $0.components(separatedBy: "/")
157 | }.reduce([], +)
158 | let linesExceptSteps = rootFlow.url.lines.filter {
159 | !linesOfSteps.contains($0)
160 | }
161 |
162 | // Find class names of sub view controllers
163 | let subViewControllers = steps.compactMap { step in
164 | step.match(with: Pattern.viewController).compactMap {
165 | $0.last(separatedBy: ".viewController(")?.first(separatedBy: ",")?.first(separatedBy: ")")
166 | }.compactMap {
167 | searchClassName(for: $0, step: step, linesExceptSteps: linesExceptSteps, rootFlow: rootFlow)
168 | }
169 | }.reduce([], +).uniques.sorted().filter {
170 | viewControllers.names.contains($0)
171 | }.compactMap {
172 | lastLevel < maxLevels ? listViewController(root: $0, isChildViewController: false, lastLevel: lastLevel + 1) : nil
173 | }
174 |
175 | // Find class names of sub flows
176 | let subFlows = steps.compactMap { step in
177 | step.match(with: Pattern.flow).compactMap {
178 | $0.last(separatedBy: ".flow(")?.first(separatedBy: ",")
179 | }.compactMap {
180 | searchClassName(for: $0, step: step, linesExceptSteps: linesExceptSteps, rootFlow: rootFlow)
181 | }
182 | }.reduce([], +).uniques.sorted().filter {
183 | flows.names.contains($0)
184 | }.compactMap { className -> Flow? in
185 | guard lastLevel < maxLevels, let subFlow = listFlow(root: className, lastLevel: lastLevel + 1) else {
186 | return nil
187 | }
188 | return Flow(
189 | level: lastLevel + 1,
190 | className: className,
191 | flows: subFlow.flows,
192 | viewControllers: subFlow.viewControllers
193 | )
194 | }
195 |
196 | return Flow(level: lastLevel, className: root, flows: subFlows, viewControllers: subViewControllers)
197 | }
198 |
199 | private func searchClassName(for name: String, step: String, linesExceptSteps: [String], rootFlow: Keyword) -> String? {
200 | // Search flow name from `case` to `return` at first.
201 | let classNameInStep = step.components(separatedBy: "\n").dropLast().first {
202 | $0.contains(name + " = ")
203 | }?.last(separatedBy: " = ")?.first(separatedBy: "(")
204 | if classNameInStep != nil {
205 | return classNameInStep
206 | }
207 | // Search flow name from lines except step.
208 | let classNameExceptStep = searchClassName(for: name, in: linesExceptSteps)
209 | if classNameExceptStep != nil {
210 | return classNameExceptStep
211 | }
212 | // Class name not found, print warning.
213 | print("[Warning] Class name not found for \(name), check the code in \(rootFlow.url.absoluteString).\n".yellow)
214 | return nil
215 | }
216 |
217 | }
218 |
219 | extension RxTree {
220 |
221 | private func listViewController(root: String, isChildViewController: Bool, lastLevel: Int) -> ViewController? {
222 | guard let rootViewController = viewControllers.first(name: root) else {
223 | return nil
224 | }
225 | let content = rootViewController.url.content
226 | let lines = content.components(separatedBy: "\n")
227 | let childViewControllers = content.match(with: Pattern.addChild).compactMap {
228 | $0.last(separatedBy: "(")?.first(separatedBy: ",")
229 | }.compactMap {
230 | searchClassName(for: $0, in: lines)
231 | }.compactMap { className -> ViewController? in
232 | guard let childViewController = listViewController(root: className, isChildViewController: false, lastLevel: lastLevel + 1) else {
233 | return nil
234 | }
235 | return ViewController(
236 | level: lastLevel + 1,
237 | className: className,
238 | isChildViewController: true,
239 | viewControllers: childViewController.viewControllers
240 | )
241 | }
242 | return ViewController(
243 | level: lastLevel,
244 | className: root,
245 | isChildViewController: isChildViewController,
246 | viewControllers: childViewControllers
247 | )
248 | }
249 |
250 | }
251 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/Scanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Scanner.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/26.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Foundation
27 |
28 | class Scanner {
29 |
30 | private let lines: [String]
31 |
32 | init(urls: [URL]) {
33 | lines = urls.map { $0.lines }.reduce([], +)
34 | }
35 |
36 | // Scan all sub classes of RxViewController
37 | func scanClassForRxViewController() -> [String] {
38 | ["RxViewController"] + scanViewController(with: "RxViewController")
39 | }
40 |
41 | private func scanViewController(with parent: String) -> [String] {
42 | let subViewControllers = lines.compactMap {
43 | $0.match(with: "class \(Pattern.legalIdentifier)<\(Pattern.legalIdentifier): \(Pattern.legalIdentifier)>: \(parent)<\(Pattern.legalIdentifier)>")
44 | }.reduce([], +).compactMap {
45 | $0.first(separatedBy: "<")?.last(separatedBy: " ")
46 | }
47 | if subViewControllers.isEmpty {
48 | return []
49 | }
50 | return subViewControllers + subViewControllers.map {
51 | scanViewController(with: $0)
52 | }.reduce([], +)
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/rxtree/Sources/rxtree/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // rxtree
4 | //
5 | // Created by Meng Li on 2019/11/19.
6 | // Copyright © 2019 MuShare. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | import Commander
27 | import Foundation
28 |
29 | let main = command(
30 | Argument("root", description: "Root node, a flow or a view controller."),
31 | Option("dir", default: "", description: "Directory to scan the Xcode project."),
32 | Option("maxLevels", default: "10", description: "Max levels.")
33 | ) { root, dir, maxLevels in
34 | guard let maxLevels = Int(maxLevels), maxLevels > 0 else {
35 | print("maxLevels should be a number greater than 0.")
36 | return
37 | }
38 | let rootDir = dir.isEmpty ? FileManager.default.currentDirectoryPath : dir
39 | guard let rxtree = RxTree(directory: rootDir, maxLevels: maxLevels) else {
40 | print("Xcode project not found.".red)
41 | return
42 | }
43 | if let node = rxtree.list(root: root) {
44 | print(node.description)
45 | } else {
46 | print("Node not found for name: \(root)".red)
47 | }
48 | }
49 |
50 | main.run()
51 |
--------------------------------------------------------------------------------
/rxtree/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import rxtreeTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += rxtreeTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/rxtree/Tests/rxtreeTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(rxtreeTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/rxtree/Tests/rxtreeTests/rxtreeTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import class Foundation.Bundle
3 |
4 | final class rxtreeTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 |
10 | // Some of the APIs that we use below are available in macOS 10.13 and above.
11 | guard #available(macOS 10.13, *) else {
12 | return
13 | }
14 |
15 | let fooBinary = productsDirectory.appendingPathComponent("rxtree")
16 |
17 | let process = Process()
18 | process.executableURL = fooBinary
19 |
20 | let pipe = Pipe()
21 | process.standardOutput = pipe
22 |
23 | try process.run()
24 | process.waitUntilExit()
25 |
26 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
27 | let output = String(data: data, encoding: .utf8)
28 |
29 | XCTAssertEqual(output, "Hello, world!\n")
30 | }
31 |
32 | /// Returns path to the built products directory.
33 | var productsDirectory: URL {
34 | #if os(macOS)
35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
36 | return bundle.bundleURL.deletingLastPathComponent()
37 | }
38 | fatalError("couldn't find the products directory")
39 | #else
40 | return Bundle.main.bundleURL
41 | #endif
42 | }
43 |
44 | static var allTests = [
45 | ("testExample", testExample),
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/rxtree/build_for_xcode.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT_DIR=$(cd $(dirname $0); pwd)
4 |
5 | cd "$ROOT_DIR/../../../"
6 |
7 | if [ ! -f .rxtree-version ]; then
8 | echo "0.0" > .rxtree-version
9 | fi
10 |
11 | VERSION=$(cat Podfile.lock | grep RxController\ \( | sed -e 's/ - RxController (//g' | sed -e 's/)://g')
12 |
13 | if [ $(cat .rxtree-version) = ${VERSION} ] && [ -f rxtree ]; then
14 | echo "rxtree ${VERSION} existed, skip."
15 | else
16 | echo "Building rxtree $VERSION..."
17 | echo $VERSION > .rxtree-version
18 | cd $ROOT_DIR
19 | swift build
20 | cp .build/debug/rxtree "$ROOT_DIR/../../../"
21 | fi
--------------------------------------------------------------------------------