├── .gitignore ├── Demo ├── RxAutoBinding │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ └── LoginViewController.swift └── RxAutoBinding.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── RxUI.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxSwift", 6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift", 7 | "state": { 8 | "branch": null, 9 | "revision": "7e01c05f25c025143073eaa3be3532f9375c614b", 10 | "version": "6.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxSwift", 6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7e01c05f25c025143073eaa3be3532f9375c614b", 10 | "version": "6.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "RxUI", 6 | platforms: [ 7 | .iOS(.v11) 8 | ], 9 | products: [ 10 | .library(name: "RxUI", targets: ["RxUI"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.0.0") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "RxUI", 18 | dependencies: [ 19 | .product(name: "RxSwift", package: "RxSwift"), 20 | .product(name: "RxCocoa", package: "RxSwift") 21 | ], 22 | path: "Sources" 23 | ) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RxAutoBinding 4 | // 5 | // Created by Alexander Grebenyuk on 14.02.2021. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // RxAutoBinding 4 | // 5 | // Created by Alexander Grebenyuk on 14.02.2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RxAutoBinding 4 | // 5 | // Created by Alexander Grebenyuk on 14.02.2021. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | 12 | // TODO: setup local package correctly 13 | final class LoginViewController: UIViewController, RxView { 14 | private let titleLabel = UILabel() 15 | private let emailTextField = UITextField() 16 | private let passwordTextField = UITextField() 17 | private let loginButton = UIButton(type: .system) 18 | private let spinner = UIActivityIndicatorView() 19 | 20 | private let model = LoginViewModel() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | createView() 26 | 27 | disposeBag.insert( 28 | emailTextField.rx.text.bind(to: model.$email), 29 | passwordTextField.rx.text.bind(to: model.$password), 30 | loginButton.rx.tap.subscribe(onNext: model.login) 31 | ) 32 | 33 | bind(model) // Automatically registers for update 34 | } 35 | 36 | /// `refreshView` gets called automatically whenever viewModel sends `objectWillChange` event 37 | func refreshView() { 38 | titleLabel.text = model.loginButtonTitle 39 | model.isLoading ? spinner.startAnimating() : spinner.stopAnimating() 40 | loginButton.isEnabled = model.isLoginButtonEnabled 41 | } 42 | 43 | private func createView() { 44 | emailTextField.borderStyle = .roundedRect 45 | passwordTextField.borderStyle = .roundedRect 46 | loginButton.setTitle("Login", for: .normal) 47 | 48 | let stack = UIStackView(arrangedSubviews: [titleLabel, emailTextField, passwordTextField, loginButton, spinner]) 49 | stack.axis = .vertical 50 | stack.spacing = 8 51 | view.addSubview(stack) 52 | stack.translatesAutoresizingMaskIntoConstraints = false 53 | NSLayoutConstraint.activate([ 54 | stack.widthAnchor.constraint(equalToConstant: 200), 55 | stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100), 56 | stack.centerXAnchor.constraint(equalTo: view.centerXAnchor) 57 | ]) 58 | } 59 | } 60 | 61 | final class LoginViewModel: RxObservableObject { 62 | @RxPublished var email: String? 63 | @RxPublished var password: String? 64 | @RxPublished private(set) var isLoading = false 65 | 66 | var loginButtonTitle: String { 67 | "Welcome, \(email ?? "–")" 68 | } 69 | 70 | var isLoginButtonEnabled: Bool { 71 | isInputValid && !isLoading 72 | } 73 | 74 | var isInputValid: Bool { 75 | !(email ?? "").isEmpty && !(password ?? "").isEmpty 76 | } 77 | 78 | func login() { 79 | isLoading = true 80 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { 81 | self.isLoading = false 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxUI 2 | 3 | RxUI is inspired by SwiftUI. RxUI goal is to improve the developer experience of using RxSwift by allowing you to concentrate on the business logic instead of the low-level reactive code. 4 | 5 | - You can express your business logic in a natural way using plain Swift properties and methods 6 | - It makes it much easier to debug your views and view models. You can set breakpoints and query any of your view model state. 7 | - It’s beginner-friendly. You don’t need to learn `combineLatest`, `withLatestFrom` and other complex stateful operators to use it. You don't need `flatMap` to send a network request. 8 | - It is more efficient because you avoid creating massive observable chains 9 | 10 | > **WARNING** This is proof of concept. 11 | 12 | ## RxObservableObject 13 | 14 | You can think of `RxObservableObject` and `RxPublished` as analogs of SwiftUI `ObservableObject` and `Published`. 15 | 16 | ```swift 17 | final class LoginViewModel: RxObservableObject { 18 | @RxPublished var email = "" 19 | @RxPublished var password = "" 20 | @RxPublished private(set) var isLoading = false 21 | 22 | var title: String { 23 | "Welcome, \(email)" 24 | } 25 | 26 | var isLoginButtonEnabled: Bool { 27 | isInputValid && !isLoading 28 | } 29 | 30 | private var isInputValid: Bool { 31 | !email.isEmpty && !password.isEmpty 32 | } 33 | 34 | func login() { 35 | isLoading = true 36 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { 37 | self.isLoading = false 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | Each `RxObservableObject` has `objectWillChange` relay. The relay is generated automatically and is automatically bound to all properties marked with `@RxPublished` property wrapper. This all happens in runtime using reflection and associated objects. 44 | 45 | ## RxView 46 | 47 | `RxView` is an analog of a SwiftUI `View`. There is, however, one crucial difference. In `UIKit`, views are expensive, can't recreate them each time. The is reflected in `RxView` design. 48 | 49 | ```swift 50 | final class LoginViewController: UIViewController, RxView { 51 | private let model = LoginViewModel() 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | // ... add views on screen ... 57 | 58 | disposeBag.insert( 59 | emailTextField.rx.text.bind(to: model.$email), 60 | passwordTextField.rx.text.bind(to: model.$password), 61 | loginButton.rx.tap.subscribe(onNext: model.login) 62 | ) 63 | 64 | bind(model) // Automatically registers for update 65 | } 66 | 67 | // Called automatically when model changes, but no more frequently than 68 | // once per render cycle. 69 | func refreshView() { 70 | titleLabel.text = model.title 71 | model.isLoading ? spinner.startAnimating() : spinner.stopAnimating() 72 | loginButton.isEnabled = model.isLoginButtonEnabled 73 | } 74 | } 75 | ``` 76 | 77 | When you call `bind()` method that accepts `RxObservableObject` it automatically registers for its updates (`objectWillChange` property). When the object is changed, `refreshView()` is called automatically. `RxView` hooks into the display system such that `refreshView` called only once per one render cycle. 78 | 79 | # License 80 | 81 | RxUI is available under the MIT license. See the LICENSE file for more info. 82 | -------------------------------------------------------------------------------- /Sources/RxUI.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2021 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import RxSwift 7 | import RxCocoa 8 | 9 | // MARK: - RxObservableObject 10 | 11 | public protocol RxObservableObject: AnyObject { 12 | var objectWillChange: PublishRelay { get } 13 | } 14 | 15 | public extension RxObservableObject { 16 | var objectWillChange: PublishRelay { 17 | if let relay = objc_getAssociatedObject(self, &objectWillChangeAssociatedKey) as? PublishRelay { 18 | return relay 19 | } 20 | let relay = PublishRelay() 21 | registerPublishedProperties(objectWillChange: relay) 22 | objc_setAssociatedObject(self, &objectWillChangeAssociatedKey, relay, .OBJC_ASSOCIATION_RETAIN) 23 | return relay 24 | } 25 | } 26 | 27 | private extension RxObservableObject { 28 | func registerPublishedProperties(objectWillChange: PublishRelay) { 29 | let allPublished = Mirror(reflecting: self) 30 | .children 31 | .compactMap { $0.value as? RxPublishedProtocol } 32 | let disposeBag = getDisposeBag(for: self) 33 | for published in allPublished { 34 | published.publishedWillChange.bind(to: objectWillChange).disposed(by: disposeBag) 35 | } 36 | } 37 | } 38 | 39 | private func getDisposeBag(for object: AnyObject) -> DisposeBag { 40 | if let disposeBag = objc_getAssociatedObject(object, &disposeBagAssociatedKey) as? DisposeBag { 41 | return disposeBag 42 | } 43 | let disposeBag = DisposeBag() 44 | objc_setAssociatedObject(object, &disposeBagAssociatedKey, disposeBag, .OBJC_ASSOCIATION_RETAIN) 45 | return disposeBag 46 | } 47 | 48 | private var objectWillChangeAssociatedKey = "RxObservableObject.objectWillChange.AssociatedKey" 49 | private var disposeBagAssociatedKey = "RxObservableObject.disposeBag.AssociatedKey" 50 | 51 | // MARK: - RxPublished 52 | 53 | @propertyWrapper 54 | public struct RxPublished: RxPublishedProtocol { 55 | private let relay: BehaviorRelay 56 | var publishedWillChange: Observable { relay.map { _ in () } } 57 | 58 | public init(wrappedValue: Value) { 59 | relay = .init(value: wrappedValue) 60 | } 61 | 62 | public var wrappedValue: Value { 63 | set { relay.accept(newValue) } 64 | get { relay.value } 65 | } 66 | 67 | public var projectedValue: BehaviorRelay { relay } 68 | } 69 | 70 | protocol RxPublishedProtocol { 71 | var publishedWillChange: Observable { get } 72 | } 73 | 74 | // MARK: - RxView 75 | 76 | public protocol RxView: AnyObject { 77 | /// Gets called whenever the observable object changes. 78 | func refreshView() 79 | } 80 | 81 | public extension RxView { 82 | var disposeBag: DisposeBag { getDisposeBag(for: self) } 83 | } 84 | 85 | public extension RxView where Self: UIViewController { 86 | /// Observes `objectWillChange` and automatically called refresh. 87 | func bind(_ object: RxObservableObject) { 88 | bind(object, makeEmptyView(in: view)) 89 | } 90 | } 91 | 92 | public extension RxView where Self: UIView { 93 | /// Observes `objectWillChange` and automatically called refresh. 94 | func bind(_ object: RxObservableObject) { 95 | bind(object, makeEmptyView(in: self)) 96 | } 97 | } 98 | 99 | private extension RxView { 100 | /// Observes `objectWillChange` and automatically called refresh. 101 | func bind(_ object: RxObservableObject, _ emptyView: UIView) { 102 | refreshView() 103 | 104 | object.objectWillChange 105 | .subscribe(onNext: emptyView.setNeedsLayout) 106 | .disposed(by: disposeBag) 107 | 108 | emptyView.rx.sentMessage(#selector(UIView.layoutSubviews)) 109 | .subscribe(onNext: { [weak self] _ in self?.refreshView() }) 110 | .disposed(by: disposeBag) 111 | } 112 | } 113 | 114 | // The idea is to refresh the view with the new data only when the screen needs 115 | // to be re-rendered. We use a fake view to avoid re-rendering actualy stuff that 116 | // doesn't need re-layout. 117 | private func makeEmptyView(in container: UIView) -> UIView { 118 | let emptyView = UIView() 119 | emptyView.isHidden = true 120 | container.addSubview(emptyView) 121 | return emptyView 122 | } 123 | -------------------------------------------------------------------------------- /Demo/RxAutoBinding.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0C0A35EB25D99A5B00B05933 /* RxUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0A35EA25D99A5B00B05933 /* RxUI.swift */; }; 11 | 0C4B2AD125D964D000DDA57C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4B2AD025D964D000DDA57C /* AppDelegate.swift */; }; 12 | 0C4B2AD325D964D000DDA57C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4B2AD225D964D000DDA57C /* SceneDelegate.swift */; }; 13 | 0C4B2AD525D964D000DDA57C /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4B2AD425D964D000DDA57C /* LoginViewController.swift */; }; 14 | 0C4B2AD825D964D000DDA57C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C4B2AD625D964D000DDA57C /* Main.storyboard */; }; 15 | 0C4B2ADA25D964D300DDA57C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C4B2AD925D964D300DDA57C /* Assets.xcassets */; }; 16 | 0C4B2ADD25D964D300DDA57C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C4B2ADB25D964D300DDA57C /* LaunchScreen.storyboard */; }; 17 | 0C4B2AE725D9653C00DDA57C /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4B2AE625D9653C00DDA57C /* RxSwift */; }; 18 | 0C4B2AE925D9653C00DDA57C /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4B2AE825D9653C00DDA57C /* RxRelay */; }; 19 | 0C4B2AEB25D9653C00DDA57C /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4B2AEA25D9653C00DDA57C /* RxCocoa */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 0C0A35EA25D99A5B00B05933 /* RxUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RxUI.swift; path = ../../Sources/RxUI.swift; sourceTree = ""; }; 24 | 0C4B2ACD25D964D000DDA57C /* RxAutoBinding.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RxAutoBinding.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 0C4B2AD025D964D000DDA57C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 0C4B2AD225D964D000DDA57C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 27 | 0C4B2AD425D964D000DDA57C /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 28 | 0C4B2AD725D964D000DDA57C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 0C4B2AD925D964D300DDA57C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 0C4B2ADC25D964D300DDA57C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 0C4B2ADE25D964D300DDA57C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 0C4B2ACA25D964D000DDA57C /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | 0C4B2AEB25D9653C00DDA57C /* RxCocoa in Frameworks */, 40 | 0C4B2AE925D9653C00DDA57C /* RxRelay in Frameworks */, 41 | 0C4B2AE725D9653C00DDA57C /* RxSwift in Frameworks */, 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 0C4B2AC425D964D000DDA57C = { 49 | isa = PBXGroup; 50 | children = ( 51 | 0C4B2ACF25D964D000DDA57C /* RxAutoBinding */, 52 | 0C4B2ACE25D964D000DDA57C /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 0C4B2ACE25D964D000DDA57C /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 0C4B2ACD25D964D000DDA57C /* RxAutoBinding.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 0C4B2ACF25D964D000DDA57C /* RxAutoBinding */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 0C4B2AD025D964D000DDA57C /* AppDelegate.swift */, 68 | 0C4B2AD225D964D000DDA57C /* SceneDelegate.swift */, 69 | 0C4B2AD425D964D000DDA57C /* LoginViewController.swift */, 70 | 0C0A35EA25D99A5B00B05933 /* RxUI.swift */, 71 | 0C4B2AD625D964D000DDA57C /* Main.storyboard */, 72 | 0C4B2AD925D964D300DDA57C /* Assets.xcassets */, 73 | 0C4B2ADB25D964D300DDA57C /* LaunchScreen.storyboard */, 74 | 0C4B2ADE25D964D300DDA57C /* Info.plist */, 75 | ); 76 | path = RxAutoBinding; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | 0C4B2ACC25D964D000DDA57C /* RxAutoBinding */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = 0C4B2AE125D964D300DDA57C /* Build configuration list for PBXNativeTarget "RxAutoBinding" */; 85 | buildPhases = ( 86 | 0C4B2AC925D964D000DDA57C /* Sources */, 87 | 0C4B2ACA25D964D000DDA57C /* Frameworks */, 88 | 0C4B2ACB25D964D000DDA57C /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = RxAutoBinding; 95 | packageProductDependencies = ( 96 | 0C4B2AE625D9653C00DDA57C /* RxSwift */, 97 | 0C4B2AE825D9653C00DDA57C /* RxRelay */, 98 | 0C4B2AEA25D9653C00DDA57C /* RxCocoa */, 99 | ); 100 | productName = RxAutoBinding; 101 | productReference = 0C4B2ACD25D964D000DDA57C /* RxAutoBinding.app */; 102 | productType = "com.apple.product-type.application"; 103 | }; 104 | /* End PBXNativeTarget section */ 105 | 106 | /* Begin PBXProject section */ 107 | 0C4B2AC525D964D000DDA57C /* Project object */ = { 108 | isa = PBXProject; 109 | attributes = { 110 | LastSwiftUpdateCheck = 1240; 111 | LastUpgradeCheck = 1240; 112 | TargetAttributes = { 113 | 0C4B2ACC25D964D000DDA57C = { 114 | CreatedOnToolsVersion = 12.4; 115 | }; 116 | }; 117 | }; 118 | buildConfigurationList = 0C4B2AC825D964D000DDA57C /* Build configuration list for PBXProject "RxAutoBinding" */; 119 | compatibilityVersion = "Xcode 9.3"; 120 | developmentRegion = en; 121 | hasScannedForEncodings = 0; 122 | knownRegions = ( 123 | en, 124 | Base, 125 | ); 126 | mainGroup = 0C4B2AC425D964D000DDA57C; 127 | packageReferences = ( 128 | 0C4B2AE525D9653C00DDA57C /* XCRemoteSwiftPackageReference "RxSwift" */, 129 | ); 130 | productRefGroup = 0C4B2ACE25D964D000DDA57C /* Products */; 131 | projectDirPath = ""; 132 | projectRoot = ""; 133 | targets = ( 134 | 0C4B2ACC25D964D000DDA57C /* RxAutoBinding */, 135 | ); 136 | }; 137 | /* End PBXProject section */ 138 | 139 | /* Begin PBXResourcesBuildPhase section */ 140 | 0C4B2ACB25D964D000DDA57C /* Resources */ = { 141 | isa = PBXResourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 0C4B2ADD25D964D300DDA57C /* LaunchScreen.storyboard in Resources */, 145 | 0C4B2ADA25D964D300DDA57C /* Assets.xcassets in Resources */, 146 | 0C4B2AD825D964D000DDA57C /* Main.storyboard in Resources */, 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXResourcesBuildPhase section */ 151 | 152 | /* Begin PBXSourcesBuildPhase section */ 153 | 0C4B2AC925D964D000DDA57C /* Sources */ = { 154 | isa = PBXSourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 0C4B2AD525D964D000DDA57C /* LoginViewController.swift in Sources */, 158 | 0C0A35EB25D99A5B00B05933 /* RxUI.swift in Sources */, 159 | 0C4B2AD125D964D000DDA57C /* AppDelegate.swift in Sources */, 160 | 0C4B2AD325D964D000DDA57C /* SceneDelegate.swift in Sources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXSourcesBuildPhase section */ 165 | 166 | /* Begin PBXVariantGroup section */ 167 | 0C4B2AD625D964D000DDA57C /* Main.storyboard */ = { 168 | isa = PBXVariantGroup; 169 | children = ( 170 | 0C4B2AD725D964D000DDA57C /* Base */, 171 | ); 172 | name = Main.storyboard; 173 | sourceTree = ""; 174 | }; 175 | 0C4B2ADB25D964D300DDA57C /* LaunchScreen.storyboard */ = { 176 | isa = PBXVariantGroup; 177 | children = ( 178 | 0C4B2ADC25D964D300DDA57C /* Base */, 179 | ); 180 | name = LaunchScreen.storyboard; 181 | sourceTree = ""; 182 | }; 183 | /* End PBXVariantGroup section */ 184 | 185 | /* Begin XCBuildConfiguration section */ 186 | 0C4B2ADF25D964D300DDA57C /* Debug */ = { 187 | isa = XCBuildConfiguration; 188 | buildSettings = { 189 | ALWAYS_SEARCH_USER_PATHS = NO; 190 | CLANG_ANALYZER_NONNULL = YES; 191 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 192 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 193 | CLANG_CXX_LIBRARY = "libc++"; 194 | CLANG_ENABLE_MODULES = YES; 195 | CLANG_ENABLE_OBJC_ARC = YES; 196 | CLANG_ENABLE_OBJC_WEAK = YES; 197 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 198 | CLANG_WARN_BOOL_CONVERSION = YES; 199 | CLANG_WARN_COMMA = YES; 200 | CLANG_WARN_CONSTANT_CONVERSION = YES; 201 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 202 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 203 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 204 | CLANG_WARN_EMPTY_BODY = YES; 205 | CLANG_WARN_ENUM_CONVERSION = YES; 206 | CLANG_WARN_INFINITE_RECURSION = YES; 207 | CLANG_WARN_INT_CONVERSION = YES; 208 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 209 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 210 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 212 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 213 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 214 | CLANG_WARN_STRICT_PROTOTYPES = YES; 215 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 216 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | COPY_PHASE_STRIP = NO; 220 | DEBUG_INFORMATION_FORMAT = dwarf; 221 | ENABLE_STRICT_OBJC_MSGSEND = YES; 222 | ENABLE_TESTABILITY = YES; 223 | GCC_C_LANGUAGE_STANDARD = gnu11; 224 | GCC_DYNAMIC_NO_PIC = NO; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_OPTIMIZATION_LEVEL = 0; 227 | GCC_PREPROCESSOR_DEFINITIONS = ( 228 | "DEBUG=1", 229 | "$(inherited)", 230 | ); 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 238 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 239 | MTL_FAST_MATH = YES; 240 | ONLY_ACTIVE_ARCH = YES; 241 | SDKROOT = iphoneos; 242 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 243 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 244 | }; 245 | name = Debug; 246 | }; 247 | 0C4B2AE025D964D300DDA57C /* Release */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ALWAYS_SEARCH_USER_PATHS = NO; 251 | CLANG_ANALYZER_NONNULL = YES; 252 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 253 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 254 | CLANG_CXX_LIBRARY = "libc++"; 255 | CLANG_ENABLE_MODULES = YES; 256 | CLANG_ENABLE_OBJC_ARC = YES; 257 | CLANG_ENABLE_OBJC_WEAK = YES; 258 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 259 | CLANG_WARN_BOOL_CONVERSION = YES; 260 | CLANG_WARN_COMMA = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 263 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 264 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 265 | CLANG_WARN_EMPTY_BODY = YES; 266 | CLANG_WARN_ENUM_CONVERSION = YES; 267 | CLANG_WARN_INFINITE_RECURSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 270 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 271 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 274 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 275 | CLANG_WARN_STRICT_PROTOTYPES = YES; 276 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 277 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 278 | CLANG_WARN_UNREACHABLE_CODE = YES; 279 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 280 | COPY_PHASE_STRIP = NO; 281 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 282 | ENABLE_NS_ASSERTIONS = NO; 283 | ENABLE_STRICT_OBJC_MSGSEND = YES; 284 | GCC_C_LANGUAGE_STANDARD = gnu11; 285 | GCC_NO_COMMON_BLOCKS = YES; 286 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 287 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 288 | GCC_WARN_UNDECLARED_SELECTOR = YES; 289 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 290 | GCC_WARN_UNUSED_FUNCTION = YES; 291 | GCC_WARN_UNUSED_VARIABLE = YES; 292 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 293 | MTL_ENABLE_DEBUG_INFO = NO; 294 | MTL_FAST_MATH = YES; 295 | SDKROOT = iphoneos; 296 | SWIFT_COMPILATION_MODE = wholemodule; 297 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 298 | VALIDATE_PRODUCT = YES; 299 | }; 300 | name = Release; 301 | }; 302 | 0C4B2AE225D964D300DDA57C /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 307 | CODE_SIGN_STYLE = Automatic; 308 | INFOPLIST_FILE = RxAutoBinding/Info.plist; 309 | LD_RUNPATH_SEARCH_PATHS = ( 310 | "$(inherited)", 311 | "@executable_path/Frameworks", 312 | ); 313 | PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.RxAutoBinding; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | SWIFT_VERSION = 5.0; 316 | TARGETED_DEVICE_FAMILY = "1,2"; 317 | }; 318 | name = Debug; 319 | }; 320 | 0C4B2AE325D964D300DDA57C /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 325 | CODE_SIGN_STYLE = Automatic; 326 | INFOPLIST_FILE = RxAutoBinding/Info.plist; 327 | LD_RUNPATH_SEARCH_PATHS = ( 328 | "$(inherited)", 329 | "@executable_path/Frameworks", 330 | ); 331 | PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.RxAutoBinding; 332 | PRODUCT_NAME = "$(TARGET_NAME)"; 333 | SWIFT_VERSION = 5.0; 334 | TARGETED_DEVICE_FAMILY = "1,2"; 335 | }; 336 | name = Release; 337 | }; 338 | /* End XCBuildConfiguration section */ 339 | 340 | /* Begin XCConfigurationList section */ 341 | 0C4B2AC825D964D000DDA57C /* Build configuration list for PBXProject "RxAutoBinding" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 0C4B2ADF25D964D300DDA57C /* Debug */, 345 | 0C4B2AE025D964D300DDA57C /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | 0C4B2AE125D964D300DDA57C /* Build configuration list for PBXNativeTarget "RxAutoBinding" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 0C4B2AE225D964D300DDA57C /* Debug */, 354 | 0C4B2AE325D964D300DDA57C /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | /* End XCConfigurationList section */ 360 | 361 | /* Begin XCRemoteSwiftPackageReference section */ 362 | 0C4B2AE525D9653C00DDA57C /* XCRemoteSwiftPackageReference "RxSwift" */ = { 363 | isa = XCRemoteSwiftPackageReference; 364 | repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; 365 | requirement = { 366 | kind = upToNextMajorVersion; 367 | minimumVersion = 6.1.0; 368 | }; 369 | }; 370 | /* End XCRemoteSwiftPackageReference section */ 371 | 372 | /* Begin XCSwiftPackageProductDependency section */ 373 | 0C4B2AE625D9653C00DDA57C /* RxSwift */ = { 374 | isa = XCSwiftPackageProductDependency; 375 | package = 0C4B2AE525D9653C00DDA57C /* XCRemoteSwiftPackageReference "RxSwift" */; 376 | productName = RxSwift; 377 | }; 378 | 0C4B2AE825D9653C00DDA57C /* RxRelay */ = { 379 | isa = XCSwiftPackageProductDependency; 380 | package = 0C4B2AE525D9653C00DDA57C /* XCRemoteSwiftPackageReference "RxSwift" */; 381 | productName = RxRelay; 382 | }; 383 | 0C4B2AEA25D9653C00DDA57C /* RxCocoa */ = { 384 | isa = XCSwiftPackageProductDependency; 385 | package = 0C4B2AE525D9653C00DDA57C /* XCRemoteSwiftPackageReference "RxSwift" */; 386 | productName = RxCocoa; 387 | }; 388 | /* End XCSwiftPackageProductDependency section */ 389 | }; 390 | rootObject = 0C4B2AC525D964D000DDA57C /* Project object */; 391 | } 392 | --------------------------------------------------------------------------------