├── .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 | RxController 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 | ![Platform](https://raw.githubusercontent.com/RxSwiftCommunity/RxController/master/images/viewmodel.jpg) 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 | ![RxFlow](https://raw.githubusercontent.com/RxSwiftCommunity/RxController/master/images/rxflow.jpg) 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 | ![subviews_and_child_controller](https://raw.githubusercontent.com/RxSwiftCommunity/RxController/master/images/subviews_and_child_controller.jpg) 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 | ![Platform](https://raw.githubusercontent.com/RxSwiftCommunity/RxController/master/images/child_view_controllers.jpg) 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 | ![Child view models](https://raw.githubusercontent.com/RxSwiftCommunity/RxController/master/images/child_view_models.jpg) 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 --------------------------------------------------------------------------------