├── .swift-version ├── Cartfile ├── Configurations ├── Debug.xcconfig ├── Release.xcconfig └── Base.xcconfig ├── Assets └── login-diagram.png ├── Demo ├── Assets.xcassets │ ├── Contents.json │ ├── automaton.imageset │ │ ├── automaton.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── State.swift ├── Input.swift ├── Helpers.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Automaton.storyboard └── ViewController.swift ├── Cartfile.private ├── Cartfile.resolved ├── RxAutomaton.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── RxAutomaton.xcscheme └── project.pbxproj ├── RxAutomaton.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Tests ├── LinuxMain.swift └── RxAutomatonTests │ ├── Info.plist │ ├── Fixtures │ ├── ToRACHelper.swift │ └── Fixtures.swift │ ├── StateFuncMappingSpec.swift │ ├── AnyMappingSpec.swift │ ├── EffectMappingLatestSpec.swift │ ├── MappingSpec.swift │ ├── TerminatingSpec.swift │ └── EffectMappingSpec.swift ├── Sources ├── RxSwift+Pipe.swift ├── RxAutomaton.h ├── RxSwift+Then.swift ├── Info.plist ├── Reply.swift ├── FlattenStrategy.swift ├── Mapping+Helper.swift └── Automaton.swift ├── .gitmodules ├── RxAutomaton.podspec ├── .swiftlint.yml ├── Package.resolved ├── LICENSE ├── Package.swift ├── .gitignore ├── .travis.yml └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" ~> 5.0 2 | -------------------------------------------------------------------------------- /Configurations/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Base.xcconfig" 2 | 3 | SWIFT_OPTIMIZATION_LEVEL = -Onone; -------------------------------------------------------------------------------- /Assets/login-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inamiy/RxAutomaton/HEAD/Assets/login-diagram.png -------------------------------------------------------------------------------- /Configurations/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Base.xcconfig" 2 | 3 | SWIFT_OPTIMIZATION_LEVEL = -Owholemodule; -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | github "mrackwitz/xcconfigs" 4 | github "shu223/Pulsator" ~> 0.4 5 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/automaton.imageset/automaton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inamiy/RxAutomaton/HEAD/Demo/Assets.xcassets/automaton.imageset/automaton.png -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.2" 2 | github "Quick/Quick" "v2.1.0" 3 | github "ReactiveX/RxSwift" "5.0.1" 4 | github "mrackwitz/xcconfigs" "3.0" 5 | github "shu223/Pulsator" "0.4.2" 6 | -------------------------------------------------------------------------------- /Configurations/Base.xcconfig: -------------------------------------------------------------------------------- 1 | MACOSX_DEPLOYMENT_TARGET = 10.10 2 | IPHONEOS_DEPLOYMENT_TARGET = 8.0 3 | WATCHOS_DEPLOYMENT_TARGET = 3.0 4 | TVOS_DEPLOYMENT_TARGET = 9.0 5 | 6 | SWIFT_VERSION = 4.0 7 | -------------------------------------------------------------------------------- /RxAutomaton.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RxAutomaton.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Quick 3 | 4 | @testable import RxAutomatonTests 5 | 6 | Quick.QCKMain([ 7 | MappingSpec.self, 8 | EffectMappingSpec.self, 9 | AnyMappingSpec.self, 10 | StateFuncMappingSpec.self, 11 | EffectMappingLatestSpec.self, 12 | TerminatingSpec.self 13 | ]) 14 | -------------------------------------------------------------------------------- /Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RxAutomatonDemo 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Demo/State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // State.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | enum State: String, CustomStringConvertible 10 | { 11 | case loggedOut 12 | case loggingIn 13 | case loggedIn 14 | case loggingOut 15 | 16 | var description: String { return self.rawValue } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/automaton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "automaton.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Sources/RxSwift+Pipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxSwift+Pipe.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | extension ObservableType { 12 | 13 | /// From ReactiveCocoa. 14 | public static func pipe() -> (Observable, AnyObserver) { 15 | let p = PublishSubject() 16 | return (p.asObservable(), AnyObserver(eventHandler: p.asObserver().on)) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Demo/Input.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Input.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// - Note: 12 | /// `LoginOK` and `LogoutOK` should only be used internally 13 | /// (but Swift can't make them as `private case`) 14 | enum Input: String, CustomStringConvertible 15 | { 16 | case login 17 | case loginOK 18 | case logout 19 | case forceLogout 20 | case logoutOK 21 | 22 | var description: String { return self.rawValue } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/RxAutomaton.h: -------------------------------------------------------------------------------- 1 | // 2 | // RxAutomaton.h 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for RxAutomaton. 12 | FOUNDATION_EXPORT double RxAutomatonVersionNumber; 13 | 14 | //! Project version string for RxAutomaton. 15 | FOUNDATION_EXPORT const unsigned char RxAutomatonVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/Nimble"] 2 | path = Carthage/Checkouts/Nimble 3 | url = https://github.com/Quick/Nimble.git 4 | [submodule "Carthage/Checkouts/Pulsator"] 5 | path = Carthage/Checkouts/Pulsator 6 | url = https://github.com/shu223/Pulsator.git 7 | [submodule "Carthage/Checkouts/Quick"] 8 | path = Carthage/Checkouts/Quick 9 | url = https://github.com/Quick/Quick.git 10 | [submodule "Carthage/Checkouts/RxSwift"] 11 | path = Carthage/Checkouts/RxSwift 12 | url = https://github.com/ReactiveX/RxSwift.git 13 | [submodule "Carthage/Checkouts/xcconfigs"] 14 | path = Carthage/Checkouts/xcconfigs 15 | url = https://github.com/mrackwitz/xcconfigs.git 16 | -------------------------------------------------------------------------------- /RxAutomaton.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "RxAutomaton" 3 | s.version = "0.4.0" 4 | s.summary = "RxSwift + State Machine, inspired by Redux and Elm." 5 | s.homepage = "https://github.com/inamiy/RxAutomaton" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Yasuhiro Inami" => "inamiy@gmail.com" } 8 | 9 | s.ios.deployment_target = "8.0" 10 | s.osx.deployment_target = "10.10" 11 | s.watchos.deployment_target = "3.0" 12 | s.tvos.deployment_target = "9.0" 13 | 14 | s.source = { :git => "https://github.com/inamiy/RxAutomaton.git", :tag => "#{s.version}" } 15 | s.source_files = "Sources/**/*.swift" 16 | 17 | s.dependency "RxSwift", "~> 5.0" 18 | end 19 | -------------------------------------------------------------------------------- /Sources/RxSwift+Then.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxSwift+Then.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | extension ObservableType { 12 | 13 | /// From ReactiveCocoa (naive implementation). 14 | public func then(_ second: O) -> Observable 15 | where O.E == E2 16 | { 17 | return self 18 | .filter { _ in false } 19 | .flatMap { _ in Observable.empty() } 20 | .concat(second) 21 | } 22 | 23 | public func then(value: E2) -> Observable { 24 | return self.then(Observable.just(value)) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /RxAutomaton.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/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 | -------------------------------------------------------------------------------- /Demo/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | import RxSwift 11 | import RxCocoa 12 | 13 | // MARK: CoreAnimation 14 | 15 | extension CALayer 16 | { 17 | public var rx_position: AnyObserver { 18 | return Binder(self) { layer, value in 19 | layer.position = value 20 | }.asObserver() 21 | } 22 | 23 | public var rx_hidden: AnyObserver { 24 | return Binder(self) { layer, value in 25 | layer.isHidden = value 26 | }.asObserver() 27 | } 28 | 29 | public var rx_backgroundColor: AnyObserver { 30 | return Binder(self) { layer, value in 31 | layer.backgroundColor = value 32 | }.asObserver() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: 3 | # To fix `trailing_whitespace` error, 4 | # go to Xcode Preferences -> Text Editing -> turn on both "Automatically trim trailing whitespace" and "Including whitespace-only lines". 5 | # 6 | 7 | disabled_rules: 8 | - line_length 9 | - function_body_length 10 | - type_body_length 11 | - file_length 12 | - cyclomatic_complexity 13 | 14 | - force_cast 15 | 16 | - opening_brace # prefer Allman-Style 17 | - closing_brace # allow `}\n)` 18 | - statement_position # allow `if {}\nelse {}` 19 | - type_name # allow "_" prefix name 20 | - variable_name # allow "_" prefix name 21 | - todo 22 | - valid_docs 23 | 24 | opt_in_rules: 25 | - empty_count # local variable name `count` is frequently used 26 | 27 | included: 28 | - Sources 29 | - Tests 30 | 31 | excluded: 32 | - Carthage 33 | - Packages 34 | 35 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle) 36 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Nimble", 6 | "repositoryURL": "https://github.com/Quick/Nimble.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc", 10 | "version": "8.0.2" 11 | } 12 | }, 13 | { 14 | "package": "Quick", 15 | "repositoryURL": "https://github.com/Quick/Quick.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "94df9b449508344667e5afc7e80f8bcbff1e4c37", 19 | "version": "2.1.0" 20 | } 21 | }, 22 | { 23 | "package": "RxSwift", 24 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395", 28 | "version": "5.0.1" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Reply.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reply.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | public enum Reply 10 | { 11 | /// Transition success, i.e. `(input, fromState, toState)`. 12 | case success(Input, State, State) 13 | 14 | /// Transition failure, i.e. `(input, fromState)`. 15 | case failure(Input, State) 16 | 17 | public var input: Input 18 | { 19 | switch self { 20 | case let .success(input, _, _): return input 21 | case let .failure(input, _): return input 22 | } 23 | } 24 | 25 | public var fromState: State 26 | { 27 | switch self { 28 | case let .success(_, fromState, _): return fromState 29 | case let .failure(_, fromState): return fromState 30 | } 31 | } 32 | 33 | public var toState: State? 34 | { 35 | switch self { 36 | case let .success(_, _, toState): return toState 37 | case .failure: return nil 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yasuhiro Inami 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import Foundation 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "RxAutomaton", 8 | products: [ 9 | .library( 10 | name: "RxAutomaton", 11 | targets: ["RxAutomaton"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.0.0"), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "RxAutomaton", 19 | dependencies: ["RxSwift", "RxCocoa"], 20 | path: "Sources"), 21 | ] 22 | ) 23 | 24 | // `$ RXAUTOMATON_SPM_TEST=1 swift test` 25 | if ProcessInfo.processInfo.environment.keys.contains("RXAUTOMATON_SPM_TEST") { 26 | package.targets.append( 27 | .testTarget( 28 | name: "RxAutomatonTests", 29 | dependencies: ["RxAutomaton", "RxTest", "Quick", "Nimble"]) 30 | ) 31 | 32 | package.dependencies.append( 33 | contentsOf: [ 34 | .package(url: "https://github.com/Quick/Quick.git", from: "2.1.0"), 35 | .package(url: "https://github.com/Quick/Nimble.git", from: "8.0.0"), 36 | ] 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /Sources/FlattenStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlattenStrategy.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | /// From ReactiveCocoa. 10 | public enum FlattenStrategy: Equatable { 11 | /// The producers should be merged, so that any value received on any of the 12 | /// input producers will be forwarded immediately to the output producer. 13 | /// 14 | /// The resulting producer will complete only when all inputs have completed. 15 | case merge 16 | 17 | /// The producers should be concatenated, so that their values are sent in the 18 | /// order of the producers themselves. 19 | /// 20 | /// The resulting producer will complete only when all inputs have completed. 21 | // case concat // TODO: implement `flatMapConcat` 22 | 23 | /// Only the events from the latest input producer should be considered for 24 | /// the output. Any producers received before that point will be disposed of. 25 | /// 26 | /// The resulting producer will complete only when the producer-of-producers and 27 | /// the latest producer has completed. 28 | case latest 29 | } 30 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | Packages/ 36 | .build/ 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | # Pods/ 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 59 | 60 | fastlane/report.xml 61 | fastlane/screenshots 62 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/Fixtures/ToRACHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToRACHelper.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | // R-A-C! R-A-C! 13 | 14 | extension ObservableType { 15 | func delay(_ time: TimeInterval, onScheduler scheduler: SchedulerType) -> Observable { 16 | return self.flatMap { element in 17 | return Observable.interval(time, scheduler: scheduler) 18 | .map { _ in element } 19 | .take(1) 20 | } 21 | } 22 | } 23 | 24 | extension ObservableType { 25 | @discardableResult 26 | func observeValues(_ next: @escaping (E) -> Void) -> Disposable { 27 | return self.subscribe(onNext: next) 28 | } 29 | 30 | @discardableResult 31 | func observe(_ observer: @escaping (Event) -> Void) -> Disposable { 32 | return self.subscribe(AnyObserver(eventHandler: observer)) 33 | } 34 | } 35 | 36 | extension ObserverType { 37 | func send(next value: E) { 38 | self.onNext(value) 39 | } 40 | 41 | func sendCompleted() { 42 | self.onCompleted() 43 | } 44 | } 45 | 46 | extension Event { 47 | var isTerminating: Bool { 48 | return self.isStopEvent 49 | } 50 | } 51 | 52 | typealias TestScheduler = HistoricalScheduler 53 | 54 | extension HistoricalScheduler { 55 | func advanceByInterval(_ interval: TimeInterval) { 56 | self.advanceTo(Date(timeInterval: interval, since: self.clock)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Automaton 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Demo/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 | 27 | 28 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/Fixtures/Fixtures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fixtures.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxAutomaton 11 | 12 | // MARK: AuthState/Input 13 | 14 | enum AuthState: String, CustomStringConvertible 15 | { 16 | case loggedOut 17 | case loggingIn 18 | case loggedIn 19 | case loggingOut 20 | 21 | var description: String { return self.rawValue } 22 | } 23 | 24 | /// - Note: 25 | /// `LoginOK` and `LogoutOK` should only be used internally 26 | /// (but Swift can't make them as `private case`) 27 | enum AuthInput: String, CustomStringConvertible 28 | { 29 | case login 30 | case loginOK 31 | case logout 32 | case forceLogout 33 | case logoutOK 34 | 35 | var description: String { return self.rawValue } 36 | } 37 | 38 | // MARK: CountState/Input 39 | 40 | typealias CountState = Int 41 | 42 | enum CountInput: String, CustomStringConvertible 43 | { 44 | case increment 45 | case decrement 46 | 47 | var description: String { return self.rawValue } 48 | } 49 | 50 | // MARK: MyState/Input 51 | 52 | enum MyState 53 | { 54 | case state0, state1, state2 55 | } 56 | 57 | enum MyInput 58 | { 59 | case input0, input1, input2 60 | } 61 | 62 | // MARK: Extensions 63 | 64 | extension Event 65 | { 66 | public var isCompleting: Bool 67 | { 68 | switch self { 69 | case .next, .error: 70 | return false 71 | 72 | case .completed: 73 | return true 74 | } 75 | } 76 | 77 | // Comment-Out: RxSwift doesn't have `.Interrupted`. 78 | // public var isInterrupting: Bool 79 | // { 80 | // switch self { 81 | // case .Next, .Failed, .Completed: 82 | // return false 83 | // 84 | // case .Interrupted: 85 | // return true 86 | // } 87 | // } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/StateFuncMappingSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateFuncMappingSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-11-26. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxTest 11 | import RxAutomaton 12 | import Quick 13 | import Nimble 14 | 15 | /// Tests for state-change function mapping. 16 | class StateFuncMappingSpec: QuickSpec 17 | { 18 | override func spec() 19 | { 20 | describe("State-change function mapping") { 21 | 22 | typealias Automaton = RxAutomaton.Automaton 23 | typealias EffectMapping = Automaton.EffectMapping 24 | 25 | let (signal, observer) = Observable.pipe() 26 | var automaton: Automaton? 27 | 28 | beforeEach { 29 | var mappings: [Automaton.EffectMapping] = [ 30 | .increment | { $0 + 1 } | .empty() 31 | // Comment-Out: Type inference is super slow in Swift 4.2... (use `+=` instead) 32 | // .decrement | { $0 - 1 } | .empty() 33 | ] 34 | mappings += [ .decrement | { $0 - 1 } | .empty() ] 35 | 36 | // strategy = `.merge` 37 | automaton = Automaton(state: 0, input: signal, mapping: reduce(mappings), strategy: .merge) 38 | } 39 | 40 | it("`.increment` and `.decrement` succeed") { 41 | expect(automaton?.state.value) == 0 42 | observer.send(next: .increment) 43 | expect(automaton?.state.value) == 1 44 | observer.send(next: .increment) 45 | expect(automaton?.state.value) == 2 46 | observer.send(next: .decrement) 47 | expect(automaton?.state.value) == 1 48 | observer.send(next: .decrement) 49 | expect(automaton?.state.value) == 0 50 | } 51 | 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - LC_CTYPE=en_US.UTF-8 4 | - XCPROJ="-workspace RxAutomaton.xcworkspace -scheme RxAutomaton" 5 | 6 | osx_image: xcode10.2 7 | 8 | matrix: 9 | include: 10 | - os: osx 11 | language: objective-c 12 | script: 13 | - set -o pipefail 14 | - xcodebuild build-for-testing test-without-building -destination 'platform=OS X' ENABLE_TESTABILITY=YES $XCPROJ | xcpretty 15 | env: 16 | - JOB=xcodebuild-macOS 17 | 18 | - os: osx 19 | language: objective-c 20 | script: 21 | - set -o pipefail 22 | - xcodebuild build-for-testing test-without-building -destination 'platform=iOS Simulator,name=iPhone XS' ENABLE_TESTABILITY=YES $XCPROJ | xcpretty 23 | env: 24 | - JOB=xcodebuild-iOS 25 | 26 | - os: osx 27 | language: objective-c 28 | script: 29 | - set -o pipefail 30 | - xcodebuild build-for-testing test-without-building -destination 'platform=tvOS Simulator,name=Apple TV 4K' ENABLE_TESTABILITY=YES $XCPROJ | xcpretty 31 | env: 32 | - JOB=xcodebuild-tvOS 33 | 34 | - os: osx 35 | language: objective-c 36 | script: 37 | - set -o pipefail 38 | - xcodebuild build -destination 'platform=watchOS Simulator,name=Apple Watch Series 4 - 44mm' $XCPROJ | xcpretty 39 | env: 40 | - JOB=xcodebuild-watchOS 41 | 42 | # Comment-Out: Unknown CI failure (unreproducible in local) 43 | # https://travis-ci.org/inamiy/ReactiveAutomaton/jobs/492530708 44 | # - os: osx 45 | # script: 46 | # - pod repo update --silent 47 | # - pod lib lint --allow-warnings 48 | # env: JOB=pod-lint 49 | 50 | - os: osx 51 | language: generic 52 | script: 53 | - swift build 54 | - RXAUTOMATON_SPM_TEST=1 swift test 55 | env: 56 | - JOB=swiftpm-mac 57 | 58 | - os: linux 59 | language: generic 60 | sudo: required 61 | dist: trusty 62 | before_install: 63 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)" 64 | script: 65 | - swift build 66 | - RXAUTOMATON_SPM_TEST=1 swift test 67 | env: JOB=swiftpm-linux 68 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/AnyMappingSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyMappingSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxAutomaton 11 | import Quick 12 | import Nimble 13 | 14 | /// Tests for `anyState`/`anyInput` (predicate functions). 15 | class AnyMappingSpec: QuickSpec 16 | { 17 | override func spec() 18 | { 19 | typealias Automaton = RxAutomaton.Automaton 20 | 21 | let (signal, observer) = Observable.pipe() 22 | var automaton: Automaton? 23 | var lastReply: Reply? 24 | 25 | describe("`anyState`/`anyInput` mapping") { 26 | 27 | beforeEach { 28 | let mappings: [Automaton.Mapping] = [ 29 | .input0 | any => .state1, 30 | any | .state1 => .state2 31 | ] 32 | 33 | automaton = Automaton(state: .state0, input: signal, mapping: reduce(mappings)) 34 | 35 | automaton?.replies.observeValues { reply in 36 | lastReply = reply 37 | } 38 | 39 | lastReply = nil 40 | } 41 | 42 | it("`anyState`/`anyInput` succeeds") { 43 | expect(automaton?.state.value) == .state0 44 | expect(lastReply).to(beNil()) 45 | 46 | // try any input (fails) 47 | observer.send(next: .input2) 48 | 49 | expect(lastReply?.input) == .input2 50 | expect(lastReply?.fromState) == .state0 51 | expect(lastReply?.toState).to(beNil()) 52 | expect(automaton?.state.value) == .state0 53 | 54 | // try `.login` from any state 55 | observer.send(next: .input0) 56 | 57 | expect(lastReply?.input) == .input0 58 | expect(lastReply?.fromState) == .state0 59 | expect(lastReply?.toState) == .state1 60 | expect(automaton?.state.value) == .state1 61 | 62 | // try any input 63 | observer.send(next: .input2) 64 | 65 | expect(lastReply?.input) == .input2 66 | expect(lastReply?.fromState) == .state1 67 | expect(lastReply?.toState) == .state2 68 | expect(automaton?.state.value) == .state2 69 | } 70 | 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/EffectMappingLatestSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrategyLatestSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxAutomaton 11 | import Quick 12 | import Nimble 13 | 14 | /// EffectMapping tests with `strategy = .latest`. 15 | class EffectMappingLatestSpec: QuickSpec 16 | { 17 | override func spec() 18 | { 19 | typealias Automaton = RxAutomaton.Automaton 20 | typealias EffectMapping = Automaton.EffectMapping 21 | 22 | let (signal, observer) = Observable.pipe() 23 | var automaton: Automaton? 24 | var lastReply: Reply? 25 | 26 | describe("strategy = `.latest`") { 27 | 28 | var testScheduler: TestScheduler! 29 | 30 | beforeEach { 31 | testScheduler = TestScheduler() 32 | 33 | /// Sends `.loginOK` after delay, simulating async work during `.loggingIn`. 34 | let loginOKProducer = 35 | Observable.just(AuthInput.loginOK) 36 | .delay(1, onScheduler: testScheduler) 37 | 38 | /// Sends `.logoutOK` after delay, simulating async work during `.loggingOut`. 39 | let logoutOKProducer = 40 | Observable.just(AuthInput.logoutOK) 41 | .delay(1, onScheduler: testScheduler) 42 | 43 | let mappings: [Automaton.EffectMapping] = [ 44 | .login | .loggedOut => .loggingIn | loginOKProducer, 45 | .loginOK | .loggingIn => .loggedIn | .empty(), 46 | .logout | .loggedIn => .loggingOut | logoutOKProducer, 47 | .logoutOK | .loggingOut => .loggedOut | .empty(), 48 | ] 49 | 50 | // strategy = `.latest` 51 | automaton = Automaton(state: .loggedOut, input: signal, mapping: reduce(mappings), strategy: .latest) 52 | 53 | automaton?.replies.observeValues { reply in 54 | lastReply = reply 55 | } 56 | 57 | lastReply = nil 58 | } 59 | 60 | it("`strategy = .latest` should not interrupt inner effects when transition fails") { 61 | expect(automaton?.state.value) == .loggedOut 62 | expect(lastReply).to(beNil()) 63 | 64 | observer.send(next: .login) 65 | 66 | expect(lastReply?.input) == .login 67 | expect(lastReply?.fromState) == .loggedOut 68 | expect(lastReply?.toState) == .loggingIn 69 | expect(automaton?.state.value) == .loggingIn 70 | 71 | testScheduler.advanceByInterval(0.1) 72 | 73 | // fails (`loginOKProducer` will not be interrupted) 74 | observer.send(next: .login) 75 | 76 | expect(lastReply?.input) == .login 77 | expect(lastReply?.fromState) == .loggingIn 78 | expect(lastReply?.toState).to(beNil()) 79 | expect(automaton?.state.value) == .loggingIn 80 | 81 | // `loginOKProducer` will automatically send `.loginOK` 82 | testScheduler.advanceByInterval(1) 83 | 84 | expect(lastReply?.input) == .loginOK 85 | expect(lastReply?.fromState) == .loggingIn 86 | expect(lastReply?.toState) == .loggedIn 87 | expect(automaton?.state.value) == .loggedIn 88 | } 89 | 90 | } 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTE: This repository has been discontinued in favor of [Actomaton](https://github.com/inamiy/Actomaton). 2 | 3 | # RxAutomaton 4 | 5 | [RxSwift](https://github.com/ReactiveX/RxSwift) port of [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (State Machine). 6 | 7 | > 8 | > ### Terminology 9 | > 10 | > Whenever the word "signal" or "(signal) producer" appears (derived from [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)), they mean "hot-observable" and "cold-observable". 11 | 12 | ## Example 13 | 14 | (Demo app is bundled in the project) 15 | 16 | ![](Assets/login-diagram.png) 17 | 18 | To make a state transition diagram like above _with additional effects_, follow these steps: 19 | 20 | ```swift 21 | // 1. Define `State`s and `Input`s. 22 | enum State { 23 | case loggedOut, loggingIn, loggedIn, loggingOut 24 | } 25 | 26 | enum Input { 27 | case login, loginOK, logout, logoutOK 28 | case forceLogout 29 | } 30 | 31 | // Additional effects (`Observable`s) while state-transitioning. 32 | // (NOTE: Use `Observable.empty()` for no effect) 33 | let loginOKProducer = /* show UI, setup DB, request APIs, ..., and send `Input.loginOK` */ 34 | let logoutOKProducer = /* show UI, clear cache, cancel APIs, ..., and send `Input.logoutOK` */ 35 | let forcelogoutOKProducer = /* do something more special, ..., and send `Input.logoutOK` */ 36 | 37 | let canForceLogout: State -> Bool = [.loggingIn, .loggedIn].contains 38 | 39 | // 2. Setup state-transition mappings. 40 | let mappings: [Automaton.EffectMapping] = [ 41 | 42 | /* Input | fromState => toState | Effect */ 43 | /* ----------------------------------------------------------------*/ 44 | .login | .loggedOut => .loggingIn | loginOKProducer, 45 | .loginOK | .loggingIn => .loggedIn | .empty(), 46 | .logout | .loggedIn => .loggingOut | logoutOKProducer, 47 | .logoutOK | .loggingOut => .loggedOut | .empty(), 48 | .forceLogout | canForceLogout => .loggingOut | forceLogoutOKProducer 49 | ] 50 | 51 | // 3. Prepare input pipe for sending `Input` to `Automaton`. 52 | let (inputSignal, inputObserver) = Observable.pipe() 53 | 54 | // 4. Setup `Automaton`. 55 | let automaton = Automaton( 56 | state: .loggedOut, 57 | input: inputSignal, 58 | mapping: reduce(mappings), // combine mappings using `reduce` helper 59 | strategy: .latest // NOTE: `.latest` cancels previous running effect 60 | ) 61 | 62 | // Observe state-transition replies (`.success` or `.failure`). 63 | automaton.replies.subscribe(next: { reply in 64 | print("received reply = \(reply)") 65 | }) 66 | 67 | // Observe current state changes. 68 | automaton.state.asObservable().subscribe(next: { state in 69 | print("current state = \(state)") 70 | }) 71 | ``` 72 | 73 | And let's test! 74 | 75 | ```swift 76 | let send = inputObserver.onNext 77 | 78 | expect(automaton.state.value) == .loggedIn // already logged in 79 | send(Input.logout) 80 | expect(automaton.state.value) == .loggingOut // logging out... 81 | // `logoutOKProducer` will automatically send `Input.logoutOK` later 82 | // and transit to `State.loggedOut`. 83 | 84 | expect(automaton.state.value) == .loggedOut // already logged out 85 | send(Input.login) 86 | expect(automaton.state.value) == .loggingIn // logging in... 87 | // `loginOKProducer` will automatically send `Input.loginOK` later 88 | // and transit to `State.loggedIn`. 89 | 90 | // 👨🏽 < But wait, there's more! 91 | // Let's send `Input.forceLogout` immediately after `State.loggingIn`. 92 | 93 | send(Input.forceLogout) // 💥💣💥 94 | expect(automaton.state.value) == .loggingOut // logging out... 95 | // `forcelogoutOKProducer` will automatically send `Input.logoutOK` later 96 | // and transit to `State.loggedOut`. 97 | ``` 98 | 99 | ## License 100 | 101 | [MIT](LICENSE) 102 | -------------------------------------------------------------------------------- /Sources/Mapping+Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mapping+Helper.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | /// "From-" and "to-" states represented as `.state1 => .state2` or `anyState => .state3`. 12 | public struct Transition 13 | { 14 | public let fromState: (State) -> Bool 15 | public let toState: State 16 | } 17 | 18 | // MARK: - Custom Operators 19 | 20 | // MARK: `=>` (Transition constructor) 21 | 22 | precedencegroup TransitionPrecedence { 23 | associativity: left 24 | higherThan: AdditionPrecedence 25 | } 26 | infix operator => : TransitionPrecedence // higher than `|` 27 | 28 | public func => (left: @escaping (State) -> Bool, right: State) -> Transition 29 | { 30 | return Transition(fromState: left, toState: right) 31 | } 32 | 33 | public func => (left: State, right: State) -> Transition 34 | { 35 | return { $0 == left } => right 36 | } 37 | 38 | // MARK: `|` (Automaton.Mapping constructor) 39 | 40 | //infix operator | { associativity left precedence 140 } // Comment-Out: already built-in 41 | 42 | public func | (inputFunc: @escaping (Input) -> Bool, transition: Transition) -> Automaton.Mapping 43 | { 44 | return { fromState, input in 45 | if inputFunc(input) && transition.fromState(fromState) { 46 | return transition.toState 47 | } 48 | else { 49 | return nil 50 | } 51 | } 52 | } 53 | 54 | public func | (input: Input, transition: Transition) -> Automaton.Mapping 55 | { 56 | return { $0 == input } | transition 57 | } 58 | 59 | public func | (inputFunc: @escaping (Input) -> Bool, transition: @escaping (State) -> State) -> Automaton.Mapping 60 | { 61 | return { fromState, input in 62 | if inputFunc(input) { 63 | return transition(fromState) 64 | } 65 | else { 66 | return nil 67 | } 68 | } 69 | } 70 | 71 | public func | (input: Input, transition: @escaping (State) -> State) -> Automaton.Mapping 72 | { 73 | return { $0 == input } | transition 74 | } 75 | 76 | // MARK: `|` (Automaton.EffectMapping constructor) 77 | 78 | public func | (mapping: @escaping Automaton.Mapping, nextInputProducer: Observable) -> Automaton.EffectMapping 79 | { 80 | return { fromState, input in 81 | if let toState = mapping(fromState, input) { 82 | return (toState, nextInputProducer) 83 | } 84 | else { 85 | return nil 86 | } 87 | } 88 | } 89 | 90 | // MARK: Functions 91 | 92 | /// Helper for "any state" or "any input" mappings, e.g. 93 | /// - `let mapping = .input0 | any => .state1` 94 | /// - `let mapping = any | .state1 => .state2` 95 | public func any(_: T) -> Bool 96 | { 97 | return true 98 | } 99 | 100 | /// Folds multiple `Automaton.Mapping`s into one (preceding mapping has higher priority). 101 | public func reduce(_ mappings: Mappings) -> Automaton.Mapping 102 | where Mappings.Iterator.Element == Automaton.Mapping 103 | { 104 | return { fromState, input in 105 | for mapping in mappings { 106 | if let toState = mapping(fromState, input) { 107 | return toState 108 | } 109 | } 110 | return nil 111 | } 112 | } 113 | 114 | /// Folds multiple `Automaton.EffectMapping`s into one (preceding mapping has higher priority). 115 | public func reduce(_ mappings: Mappings) -> Automaton.EffectMapping 116 | where Mappings.Iterator.Element == Automaton.EffectMapping 117 | { 118 | return { fromState, input in 119 | for mapping in mappings { 120 | if let tuple = mapping(fromState, input) { 121 | return tuple 122 | } 123 | } 124 | return nil 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /RxAutomaton.xcodeproj/xcshareddata/xcschemes/RxAutomaton.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RxAutomatonDemo 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxAutomaton 13 | import Pulsator 14 | 15 | class AutomatonViewController: UIViewController 16 | { 17 | @IBOutlet weak var diagramView: UIImageView? 18 | @IBOutlet weak var label: UILabel? 19 | 20 | @IBOutlet weak var loginButton: UIButton? 21 | @IBOutlet weak var logoutButton: UIButton? 22 | @IBOutlet weak var forceLogoutButton: UIButton? 23 | 24 | private var pulsator: Pulsator? 25 | 26 | private var _automaton: Automaton? 27 | 28 | private let _disposeBag = DisposeBag() 29 | 30 | override func viewDidLoad() 31 | { 32 | super.viewDidLoad() 33 | 34 | let (textSignal, textObserver) = Observable.pipe() 35 | 36 | /// Count-up effect. 37 | func countUpProducer(status: String, count: Int = 4, interval: TimeInterval = 1, nextInput: Input) -> Observable 38 | { 39 | return Observable.interval(interval, scheduler: MainScheduler.instance) 40 | .take(count) 41 | .scan(0) { x, _ in x + 1 } 42 | .startWith(0) 43 | .map { 44 | switch $0 { 45 | case 0: return "\(status)..." 46 | case count: return "\(status) Done!" 47 | default: return "\(status)... (\($0))" 48 | } 49 | } 50 | .do(onNext: textObserver.onNext) 51 | .then(value: nextInput) 52 | } 53 | 54 | let loginOKProducer = countUpProducer(status: "Login", nextInput: .loginOK) 55 | let logoutOKProducer = countUpProducer(status: "Logout", nextInput: .logoutOK) 56 | let forceLogoutOKProducer = countUpProducer(status: "ForceLogout", nextInput: .logoutOK) 57 | 58 | // NOTE: predicate style i.e. `T -> Bool` is also available. 59 | let canForceLogout: (State) -> Bool = [.loggingIn, .loggedIn].contains 60 | 61 | /// Transition mapping. 62 | let mappings: [Automaton.EffectMapping] = [ 63 | 64 | /* Input | fromState => toState | Effect */ 65 | /* ----------------------------------------------------------*/ 66 | .login | .loggedOut => .loggingIn | loginOKProducer, 67 | .loginOK | .loggingIn => .loggedIn | .empty(), 68 | .logout | .loggedIn => .loggingOut | logoutOKProducer, 69 | .logoutOK | .loggingOut => .loggedOut | .empty(), 70 | 71 | .forceLogout | canForceLogout => .loggingOut | forceLogoutOKProducer 72 | ] 73 | 74 | let (inputSignal, inputObserver) = Observable.pipe() 75 | 76 | let automaton = Automaton(state: .loggedOut, input: inputSignal, mapping: reduce(mappings), strategy: .latest) 77 | self._automaton = automaton 78 | 79 | automaton.replies 80 | .subscribe(onNext: { reply in 81 | print("received reply = \(reply)") 82 | }) 83 | .disposed(by: _disposeBag) 84 | 85 | automaton.state.asObservable() 86 | .subscribe(onNext: { state in 87 | print("current state = \(state)") 88 | }) 89 | .disposed(by: _disposeBag) 90 | 91 | // Setup buttons. 92 | do { 93 | self.loginButton?.rx.tap 94 | .subscribe(onNext: { _ in inputObserver.onNext(.login) }) 95 | .disposed(by: _disposeBag) 96 | 97 | self.logoutButton?.rx.tap 98 | .subscribe(onNext: { _ in inputObserver.onNext(.logout) }) 99 | .disposed(by: _disposeBag) 100 | 101 | self.forceLogoutButton?.rx.tap 102 | .subscribe(onNext: { _ in inputObserver.onNext(.forceLogout) }) 103 | .disposed(by: _disposeBag) 104 | } 105 | 106 | // Setup label. 107 | do { 108 | textSignal 109 | .bind(to: self.label!.rx.text) 110 | .disposed(by: _disposeBag) 111 | } 112 | 113 | // Setup Pulsator. 114 | do { 115 | let pulsator = _createPulsator() 116 | self.pulsator = pulsator 117 | 118 | self.diagramView?.layer.addSublayer(pulsator) 119 | 120 | automaton.state.asDriver() 121 | .map(_pulsatorColor) 122 | .map { $0.cgColor } 123 | .drive(pulsator.rx_backgroundColor) 124 | .disposed(by: _disposeBag) 125 | 126 | automaton.state.asDriver() 127 | .map(_pulsatorPosition) 128 | .drive(pulsator.rx_position) 129 | .disposed(by: _disposeBag) 130 | 131 | // Overwrite the pulsator color to red if `.forceLogout` succeeded. 132 | automaton.replies 133 | .filter { $0.toState != nil && $0.input == .forceLogout } 134 | .map { _ in UIColor.red.cgColor } 135 | .bind(to: pulsator.rx_backgroundColor) 136 | .disposed(by: _disposeBag) 137 | } 138 | 139 | } 140 | 141 | } 142 | 143 | // MARK: Pulsator 144 | 145 | private func _createPulsator() -> Pulsator 146 | { 147 | let pulsator = Pulsator() 148 | pulsator.numPulse = 5 149 | pulsator.radius = 100 150 | pulsator.animationDuration = 7 151 | pulsator.backgroundColor = UIColor(red: 0, green: 0.455, blue: 0.756, alpha: 1).cgColor 152 | 153 | pulsator.start() 154 | 155 | return pulsator 156 | } 157 | 158 | private func _pulsatorPosition(state: State) -> CGPoint 159 | { 160 | switch state { 161 | case .loggedOut: return CGPoint(x: 40, y: 100) 162 | case .loggingIn: return CGPoint(x: 190, y: 20) 163 | case .loggedIn: return CGPoint(x: 330, y: 100) 164 | case .loggingOut: return CGPoint(x: 190, y: 180) 165 | } 166 | } 167 | 168 | private func _pulsatorColor(state: State) -> UIColor 169 | { 170 | switch state { 171 | case .loggedOut: 172 | return UIColor(red: 0, green: 0.455, blue: 0.756, alpha: 1) // blue 173 | case .loggingIn, .loggingOut: 174 | return UIColor(red: 0.97, green: 0.82, blue: 0.30, alpha: 1) // yellow 175 | case .loggedIn: 176 | return UIColor(red: 0.50, green: 0.85, blue: 0.46, alpha: 1) // green 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/Automaton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Automaton.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | // 12 | // Terminology: 13 | // Whenever the word "signal" or "(signal) producer" appears (derived from ReactiveCocoa), 14 | // they mean "hot-observable" and "cold-observable". 15 | // See also https://github.com/inamiy/ReactiveAutomaton (RAC version). 16 | // 17 | 18 | /// Deterministic finite state machine that receives "input" 19 | /// and with "current state" transform to "next state" & "output (additional effect)". 20 | public final class Automaton 21 | { 22 | /// Basic state-transition function type. 23 | public typealias Mapping = (State, Input) -> State? 24 | 25 | /// Transducer (input & output) mapping with 26 | /// `Observable` (additional effect) as output, 27 | /// which may emit next input values for continuous state-transitions. 28 | public typealias EffectMapping = (State, Input) -> (State, Observable)? 29 | 30 | /// `Reply` signal that notifies either `.success` or `.failure` of state-transition on every input. 31 | public let replies: Observable> 32 | 33 | /// Current state. 34 | /// - Todo: Use RxProperty https://github.com/inamiy/RxProperty 35 | public let state: Variable 36 | 37 | private let _replyObserver: AnyObserver> 38 | 39 | private var _disposeBag = DisposeBag() 40 | 41 | /// 42 | /// Initializer using `Mapping`. 43 | /// 44 | /// - Parameters: 45 | /// - state: Initial state. 46 | /// - input: `Observable` that automaton receives. 47 | /// - mapping: Simple `Mapping` that designates next state only (no additional effect). 48 | /// 49 | public convenience init(state initialState: State, input inputSignal: Observable, mapping: @escaping Mapping) 50 | { 51 | self.init(state: initialState, input: inputSignal, mapping: _compose(_toEffectMapping, mapping)) 52 | } 53 | 54 | /// 55 | /// Initializer using `EffectMapping`. 56 | /// 57 | /// - Parameters: 58 | /// - state: Initial state. 59 | /// - input: `Observable` that automaton receives. 60 | /// - mapping: `EffectMapping` that designates next state and also generates additional effect. 61 | /// - strategy: `FlattenStrategy` that flattens additional effect generated by `EffectMapping`. 62 | /// 63 | public init(state initialState: State, input inputSignal: Observable, mapping: @escaping EffectMapping, strategy: FlattenStrategy = .merge) 64 | { 65 | let stateProperty = Variable(initialState) 66 | self.state = stateProperty // TODO: AnyProperty(stateProperty) 67 | 68 | let p = PublishSubject>() 69 | (self.replies, self._replyObserver) = (p.asObservable(), AnyObserver(eventHandler: p.asObserver().on)) 70 | 71 | /// Recursive input-producer that sends inputs from `inputSignal` 72 | /// and also additional effects generated by `EffectMapping`. 73 | func recurInputProducer(_ inputProducer: Observable, strategy: FlattenStrategy) -> Observable> 74 | { 75 | return Observable.create { observer in 76 | let mappingSignal = inputProducer 77 | .withLatestFrom(stateProperty.asObservable()) { ($0, $1) } 78 | .map { input, fromState in 79 | return (input, fromState, mapping(fromState, input)) 80 | } 81 | .share(replay: 1, scope: .forever) 82 | 83 | let successSignal = mappingSignal 84 | .filterMap { input, fromState, mapped in 85 | return mapped.map { (input, fromState, $0) } 86 | } 87 | .flatMap(strategy) { input, fromState, mapped -> Observable> in 88 | let (toState, effect) = mapped 89 | return recurInputProducer(effect, strategy: strategy) 90 | .startWith(.success(input, fromState, toState)) 91 | } 92 | 93 | let failureSignal = mappingSignal 94 | .filterMap { input, fromState, mapped -> Reply? in 95 | return mapped == nil ? .failure(input, fromState) : nil 96 | } 97 | 98 | let mergedProducer = Observable.merge(failureSignal, successSignal) 99 | 100 | return mergedProducer.subscribe(observer) 101 | } 102 | } 103 | 104 | let replySignal = recurInputProducer(inputSignal, strategy: strategy) 105 | .share(replay: 1, scope: .forever) 106 | 107 | replySignal 108 | .filterMap { $0.toState } 109 | .bindTo(stateProperty) 110 | .disposed(by: _disposeBag) 111 | 112 | replySignal 113 | .subscribe(self._replyObserver) 114 | .disposed(by: _disposeBag) 115 | } 116 | 117 | deinit 118 | { 119 | self._replyObserver.onCompleted() 120 | } 121 | } 122 | 123 | // MARK: Private 124 | 125 | private func _compose(_ g: @escaping ((C) -> D), _ f: @escaping ((A, B) -> C)) -> ((A, B) -> D) 126 | { 127 | return { x, y in g(f(x, y)) } 128 | } 129 | 130 | private func _toEffectMapping(toState: State?) -> (State, Observable)? 131 | { 132 | if let toState = toState { 133 | return (toState, .empty()) 134 | } 135 | else { 136 | return nil 137 | } 138 | } 139 | 140 | extension Observable { 141 | /// Naive implementation. 142 | fileprivate func filterMap(transform: @escaping (Element) -> U?) -> Observable { 143 | return self.map(transform).filter { $0 != nil }.map { $0! } 144 | } 145 | 146 | fileprivate func flatMap(_ strategy: FlattenStrategy, transform: @escaping (Element) -> Observable) -> Observable { 147 | switch strategy { 148 | case .merge: return self.flatMap(transform) 149 | case .latest: return self.flatMapLatest(transform) 150 | } 151 | } 152 | } 153 | 154 | // No idea why this is not in RxSwift but RxCocoa... 155 | extension ObservableType { 156 | fileprivate func bindTo(_ variable: Variable) -> Disposable { 157 | return subscribe { e in 158 | switch e { 159 | case let .next(element): 160 | variable.value = element 161 | case let .error(error): 162 | let error = "Binding error to variable: \(error)" 163 | #if DEBUG 164 | fatalError(error) 165 | #else 166 | print(error) 167 | #endif 168 | case .completed: 169 | break 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Demo/Base.lproj/Automaton.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 42 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/MappingSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappingSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxTest 11 | import RxAutomaton 12 | import Quick 13 | import Nimble 14 | 15 | /// Tests for `(State, Input) -> State?` mapping. 16 | class MappingSpec: QuickSpec 17 | { 18 | override func spec() 19 | { 20 | typealias Automaton = RxAutomaton.Automaton 21 | typealias Mapping = Automaton.Mapping 22 | 23 | let (signal, observer) = Observable.pipe() 24 | var automaton: Automaton? 25 | var lastReply: Reply? 26 | 27 | describe("Syntax-sugar Mapping") { 28 | 29 | beforeEach { 30 | // NOTE: predicate style i.e. `T -> Bool` is also available. 31 | let canForceLogout: (AuthState) -> Bool = [AuthState.loggingIn, .loggedIn].contains 32 | 33 | let mappings: [Mapping] = [ 34 | .login | .loggedOut => .loggingIn, 35 | .loginOK | .loggingIn => .loggedIn, 36 | .logout | .loggedIn => .loggingOut, 37 | .logoutOK | .loggingOut => .loggedOut, 38 | 39 | .forceLogout | canForceLogout => .loggingOut 40 | ] 41 | 42 | // NOTE: Use `concat` to combine all mappings. 43 | automaton = Automaton(state: .loggedOut, input: signal, mapping: reduce(mappings)) 44 | 45 | automaton?.replies.observeValues { reply in 46 | lastReply = reply 47 | } 48 | 49 | lastReply = nil 50 | } 51 | 52 | it("`LoggedOut => LoggingIn => LoggedIn => LoggingOut => LoggedOut` succeed") { 53 | expect(automaton?.state.value) == .loggedOut 54 | expect(lastReply).to(beNil()) 55 | 56 | observer.send(next: .login) 57 | 58 | expect(lastReply?.input) == .login 59 | expect(lastReply?.fromState) == .loggedOut 60 | expect(lastReply?.toState) == .loggingIn 61 | expect(automaton?.state.value) == .loggingIn 62 | 63 | observer.send(next: .loginOK) 64 | 65 | expect(lastReply?.input) == .loginOK 66 | expect(lastReply?.fromState) == .loggingIn 67 | expect(lastReply?.toState) == .loggedIn 68 | expect(automaton?.state.value) == .loggedIn 69 | 70 | observer.send(next: .logout) 71 | 72 | expect(lastReply?.input) == .logout 73 | expect(lastReply?.fromState) == .loggedIn 74 | expect(lastReply?.toState) == .loggingOut 75 | expect(automaton?.state.value) == .loggingOut 76 | 77 | observer.send(next: .logoutOK) 78 | 79 | expect(lastReply?.input) == .logoutOK 80 | expect(lastReply?.fromState) == .loggingOut 81 | expect(lastReply?.toState) == .loggedOut 82 | expect(automaton?.state.value) == .loggedOut 83 | } 84 | 85 | it("`LoggedOut => LoggingIn ==(ForceLogout)==> LoggingOut => LoggedOut` succeed") { 86 | expect(automaton?.state.value) == .loggedOut 87 | expect(lastReply).to(beNil()) 88 | 89 | observer.send(next: .login) 90 | 91 | expect(lastReply?.input) == .login 92 | expect(lastReply?.fromState) == .loggedOut 93 | expect(lastReply?.toState) == .loggingIn 94 | expect(automaton?.state.value) == .loggingIn 95 | 96 | observer.send(next: .forceLogout) 97 | 98 | expect(lastReply?.input) == .forceLogout 99 | expect(lastReply?.fromState) == .loggingIn 100 | expect(lastReply?.toState) == .loggingOut 101 | expect(automaton?.state.value) == .loggingOut 102 | 103 | // fails 104 | observer.send(next: .loginOK) 105 | 106 | expect(lastReply?.input) == .loginOK 107 | expect(lastReply?.fromState) == .loggingOut 108 | expect(lastReply?.toState).to(beNil()) 109 | expect(automaton?.state.value) == .loggingOut 110 | 111 | // fails 112 | observer.send(next: .logout) 113 | 114 | expect(lastReply?.input) == .logout 115 | expect(lastReply?.fromState) == .loggingOut 116 | expect(lastReply?.toState).to(beNil()) 117 | expect(automaton?.state.value) == .loggingOut 118 | 119 | observer.send(next: .logoutOK) 120 | 121 | expect(lastReply?.input) == .logoutOK 122 | expect(lastReply?.fromState) == .loggingOut 123 | expect(lastReply?.toState) == .loggedOut 124 | expect(automaton?.state.value) == .loggedOut 125 | } 126 | 127 | } 128 | 129 | describe("Func-based Mapping") { 130 | 131 | beforeEach { 132 | let mapping: Mapping = { fromState, input in 133 | switch (fromState, input) { 134 | case (.loggedOut, .login): 135 | return .loggingIn 136 | case (.loggingIn, .loginOK): 137 | return .loggedIn 138 | case (.loggedIn, .logout): 139 | return .loggingOut 140 | case (.loggingOut, .logoutOK): 141 | return .loggedOut 142 | 143 | // ForceLogout 144 | case (.loggingIn, .forceLogout), (.loggedIn, .forceLogout): 145 | return .loggingOut 146 | 147 | default: 148 | return nil 149 | } 150 | } 151 | 152 | automaton = Automaton(state: .loggedOut, input: signal, mapping: mapping) 153 | automaton?.replies.observeValues { reply in 154 | lastReply = reply 155 | } 156 | 157 | lastReply = nil 158 | } 159 | 160 | it("`LoggedOut => LoggingIn => LoggedIn => LoggingOut => LoggedOut` succeed") { 161 | expect(automaton?.state.value) == .loggedOut 162 | expect(lastReply).to(beNil()) 163 | 164 | observer.send(next: .login) 165 | 166 | expect(lastReply?.input) == .login 167 | expect(lastReply?.fromState) == .loggedOut 168 | expect(lastReply?.toState) == .loggingIn 169 | expect(automaton?.state.value) == .loggingIn 170 | 171 | observer.send(next: .loginOK) 172 | 173 | expect(lastReply?.input) == .loginOK 174 | expect(lastReply?.fromState) == .loggingIn 175 | expect(lastReply?.toState) == .loggedIn 176 | expect(automaton?.state.value) == .loggedIn 177 | 178 | observer.send(next: .logout) 179 | 180 | expect(lastReply?.input) == .logout 181 | expect(lastReply?.fromState) == .loggedIn 182 | expect(lastReply?.toState) == .loggingOut 183 | expect(automaton?.state.value) == .loggingOut 184 | 185 | observer.send(next: .logoutOK) 186 | 187 | expect(lastReply?.input) == .logoutOK 188 | expect(lastReply?.fromState) == .loggingOut 189 | expect(lastReply?.toState) == .loggedOut 190 | expect(automaton?.state.value) == .loggedOut 191 | } 192 | 193 | it("`LoggedOut => LoggingIn ==(ForceLogout)==> LoggingOut => LoggedOut` succeed") { 194 | expect(automaton?.state.value) == .loggedOut 195 | expect(lastReply).to(beNil()) 196 | 197 | observer.send(next: .login) 198 | 199 | expect(lastReply?.input) == .login 200 | expect(lastReply?.fromState) == .loggedOut 201 | expect(lastReply?.toState) == .loggingIn 202 | expect(automaton?.state.value) == .loggingIn 203 | 204 | observer.send(next: .forceLogout) 205 | 206 | expect(lastReply?.input) == .forceLogout 207 | expect(lastReply?.fromState) == .loggingIn 208 | expect(lastReply?.toState) == .loggingOut 209 | expect(automaton?.state.value) == .loggingOut 210 | 211 | // fails 212 | observer.send(next: .loginOK) 213 | 214 | expect(lastReply?.input) == .loginOK 215 | expect(lastReply?.fromState) == .loggingOut 216 | expect(lastReply?.toState).to(beNil()) 217 | expect(automaton?.state.value) == .loggingOut 218 | 219 | // fails 220 | observer.send(next: .logout) 221 | 222 | expect(lastReply?.input) == .logout 223 | expect(lastReply?.fromState) == .loggingOut 224 | expect(lastReply?.toState).to(beNil()) 225 | expect(automaton?.state.value) == .loggingOut 226 | 227 | observer.send(next: .logoutOK) 228 | 229 | expect(lastReply?.input) == .logoutOK 230 | expect(lastReply?.fromState) == .loggingOut 231 | expect(lastReply?.toState) == .loggedOut 232 | expect(automaton?.state.value) == .loggedOut 233 | } 234 | 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/TerminatingSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TerminatingSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxAutomaton 11 | import Quick 12 | import Nimble 13 | 14 | class TerminatingSpec: QuickSpec 15 | { 16 | override func spec() 17 | { 18 | typealias Automaton = RxAutomaton.Automaton 19 | typealias Mapping = Automaton.Mapping 20 | 21 | var automaton: Automaton? 22 | var lastReply: Reply? 23 | var lastRepliesEvent: Event>? 24 | 25 | /// Flag for internal effect `sendInput1And2AfterDelay` disposed. 26 | // var effectDisposed: Bool? 27 | 28 | var signal: Observable! 29 | var observer: AnyObserver! 30 | var testScheduler: TestScheduler! 31 | 32 | describe("Deinit") { 33 | 34 | beforeEach { 35 | testScheduler = TestScheduler() 36 | let (signal_, observer_) = Observable.pipe() 37 | signal = signal_ 38 | observer = observer_ 39 | 40 | 41 | let sendInput1And2AfterDelay: Observable = Observable.concat([ 42 | Observable.just(.input1).delay(1, onScheduler: testScheduler), 43 | Observable.just(.input2).delay(1, onScheduler: testScheduler), 44 | ]) 45 | 46 | let mappings: [Automaton.EffectMapping] = [ 47 | .input0 | .state0 => .state1 | sendInput1And2AfterDelay, 48 | .input1 | .state1 => .state2 | .empty(), 49 | .input2 | .state2 => .state0 | .empty() 50 | ] 51 | 52 | // strategy = `.merge` 53 | automaton = Automaton(state: .state0, input: signal, mapping: reduce(mappings), strategy: .merge) 54 | 55 | automaton?.replies.observe { event in 56 | lastRepliesEvent = event 57 | 58 | if let reply = event.element { 59 | lastReply = reply 60 | } 61 | } 62 | 63 | lastReply = nil 64 | lastRepliesEvent = nil 65 | // effectDisposed = false 66 | } 67 | 68 | describe("Automaton deinit") { 69 | 70 | it("automaton deinits before sending input") { 71 | expect(automaton?.state.value) == .state0 72 | expect(lastReply).to(beNil()) 73 | expect(lastRepliesEvent).to(beNil()) 74 | 75 | weak var weakAutomaton = automaton 76 | automaton = nil 77 | 78 | expect(weakAutomaton).to(beNil()) 79 | expect(lastReply).to(beNil()) 80 | expect(lastRepliesEvent?.isCompleting) == true 81 | } 82 | 83 | it("automaton deinits while sending input") { 84 | expect(automaton?.state.value) == .state0 85 | expect(lastReply).to(beNil()) 86 | expect(lastRepliesEvent).to(beNil()) 87 | // expect(effectDisposed) == false 88 | 89 | observer.send(next: .input0) 90 | 91 | expect(automaton?.state.value) == .state1 92 | expect(lastReply?.input) == .input0 93 | expect(lastRepliesEvent?.isTerminating) == false 94 | // expect(effectDisposed) == false 95 | 96 | // `sendInput1And2AfterDelay` will automatically send `.input1` at this point 97 | testScheduler.advanceByInterval(1) 98 | 99 | expect(automaton?.state.value) == .state2 100 | expect(lastReply?.input) == .input1 101 | expect(lastRepliesEvent?.isTerminating) == false 102 | // expect(effectDisposed) == false 103 | 104 | weak var weakAutomaton = automaton 105 | automaton = nil 106 | 107 | expect(weakAutomaton).to(beNil()) 108 | expect(lastReply?.input) == .input1 109 | expect(lastRepliesEvent?.isCompleting) == true // isCompleting 110 | // expect(effectDisposed) == true 111 | 112 | // If `sendInput1And2AfterDelay` is still alive, it will send `.input2` at this point, 113 | // but it's already interrupted because `automaton` is deinited. 114 | testScheduler.advanceByInterval(1) 115 | 116 | // Last input should NOT change. 117 | expect(lastReply?.input) == .input1 118 | } 119 | 120 | } 121 | 122 | // This basically behaves similar to `automaton.deinit`, 123 | // except `replies` will emit `.Interrupted` instead of `.Completed`. 124 | // describe("inputSignal sendInterrupted") { 125 | // 126 | // it("inputSignal sendInterrupted before sending input") { 127 | // expect(automaton?.state.value) == .state0 128 | // expect(lastReply).to(beNil()) 129 | // expect(lastRepliesEvent).to(beNil()) 130 | // 131 | // observer.sendInterrupted() 132 | // 133 | // expect(automaton?.state.value) == .state0 134 | // expect(lastReply).to(beNil()) 135 | //// expect(lastRepliesEvent?.isInterrupting) == true 136 | // } 137 | // 138 | // it("inputSignal sendInterrupted while sending input") { 139 | // expect(automaton?.state.value) == .state0 140 | // expect(automaton).toNot(beNil()) 141 | // expect(lastReply).to(beNil()) 142 | // expect(lastRepliesEvent).to(beNil()) 143 | // expect(effectDisposed) == false 144 | // 145 | // observer.send(next: .input0) 146 | // 147 | // expect(automaton?.state.value) == .state1 148 | // expect(lastReply?.input) == .input0 149 | // expect(lastRepliesEvent?.isTerminating) == false 150 | // expect(effectDisposed) == false 151 | // 152 | // // `sendInput1And2AfterDelay` will automatically send `.input1` at this point 153 | // testScheduler.advanceByInterval(1) 154 | // 155 | // expect(automaton?.state.value) == .state2 156 | // expect(lastReply?.input) == .input1 157 | // expect(lastRepliesEvent?.isTerminating) == false 158 | // expect(effectDisposed) == false 159 | // 160 | // observer.sendInterrupted() 161 | // 162 | // expect(automaton?.state.value) == .state2 163 | // expect(lastReply?.input) == .input1 164 | //// expect(lastRepliesEvent?.isInterrupting) == true // interrupting, not isCompleting 165 | // expect(effectDisposed) == true 166 | // 167 | // // If `sendInput1And2AfterDelay` is still alive, it will send `.input2` at this point, 168 | // // but it's already interrupted because of `sendInterrupted`. 169 | // testScheduler.advanceByInterval(1) 170 | // 171 | // // Last state & input should NOT change. 172 | // expect(automaton?.state.value) == .state2 173 | // expect(lastReply?.input) == .input1 174 | // } 175 | // 176 | // } 177 | 178 | // Unlike `automaton.deinit` or `inputSignal` sending `.Interrupted`, 179 | // inputSignal` sending `.Completed` does NOT cancel internal effect, 180 | // i.e. `sendInput1And2AfterDelay`. 181 | describe("inputSignal sendCompleted") { 182 | 183 | it("inputSignal sendCompleted before sending input") { 184 | expect(automaton?.state.value) == .state0 185 | expect(lastReply).to(beNil()) 186 | expect(lastRepliesEvent).to(beNil()) 187 | 188 | observer.sendCompleted() 189 | 190 | expect(automaton?.state.value) == .state0 191 | expect(lastReply).to(beNil()) 192 | expect(lastRepliesEvent?.isCompleting) == true 193 | } 194 | 195 | it("inputSignal sendCompleted while sending input") { 196 | expect(automaton?.state.value) == .state0 197 | expect(lastReply).to(beNil()) 198 | expect(lastRepliesEvent).to(beNil()) 199 | // expect(effectDisposed) == false 200 | 201 | observer.send(next: .input0) 202 | 203 | expect(automaton?.state.value) == .state1 204 | expect(lastReply?.input) == .input0 205 | expect(lastRepliesEvent?.isTerminating) == false 206 | // expect(effectDisposed) == false 207 | 208 | // `sendInput1And2AfterDelay` will automatically send `.input1` at this point. 209 | testScheduler.advanceByInterval(1) 210 | 211 | expect(automaton?.state.value) == .state2 212 | expect(lastReply?.input) == .input1 213 | expect(lastRepliesEvent?.isTerminating) == false 214 | // expect(effectDisposed) == false 215 | 216 | observer.sendCompleted() 217 | 218 | // Not completed yet because `sendInput1And2AfterDelay` is still in progress. 219 | expect(automaton?.state.value) == .state2 220 | expect(lastReply?.input) == .input1 221 | expect(lastRepliesEvent?.isTerminating) == false 222 | // expect(effectDisposed) == false 223 | 224 | // `sendInput1And2AfterDelay` will automatically send `.input2` at this point. 225 | testScheduler.advanceByInterval(2) 226 | 227 | // Last state & input should change. 228 | expect(automaton?.state.value) == .state0 229 | expect(lastReply?.input) == .input2 230 | expect(lastRepliesEvent?.isCompleting) == true 231 | // expect(effectDisposed) == true 232 | } 233 | 234 | } 235 | 236 | } 237 | 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Tests/RxAutomatonTests/EffectMappingSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EffectMappingSpec.swift 3 | // RxAutomaton 4 | // 5 | // Created by Yasuhiro Inami on 2016-08-15. 6 | // Copyright © 2016 Yasuhiro Inami. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxTest 11 | import RxAutomaton 12 | import Quick 13 | import Nimble 14 | 15 | /// Tests for `(State, Input) -> (State, Output)?` mapping 16 | /// where `Output = Observable`. 17 | class EffectMappingSpec: QuickSpec 18 | { 19 | override func spec() 20 | { 21 | typealias Automaton = RxAutomaton.Automaton 22 | typealias EffectMapping = Automaton.EffectMapping 23 | 24 | let (signal, observer) = Observable.pipe() 25 | var automaton: Automaton? 26 | var lastReply: Reply? 27 | var testScheduler: TestScheduler! 28 | 29 | describe("Syntax-sugar EffectMapping") { 30 | 31 | beforeEach { 32 | testScheduler = TestScheduler() 33 | 34 | /// Sends `.loginOK` after delay, simulating async work during `.loggingIn`. 35 | let loginOKProducer = 36 | Observable.just(AuthInput.loginOK) 37 | .delay(1, onScheduler: testScheduler) 38 | 39 | /// Sends `.logoutOK` after delay, simulating async work during `.loggingOut`. 40 | let logoutOKProducer = 41 | Observable.just(AuthInput.logoutOK) 42 | .delay(1, onScheduler: testScheduler) 43 | 44 | let mappings: [Automaton.EffectMapping] = [ 45 | .login | .loggedOut => .loggingIn | loginOKProducer, 46 | .loginOK | .loggingIn => .loggedIn | .empty(), 47 | .logout | .loggedIn => .loggingOut | logoutOKProducer, 48 | .logoutOK | .loggingOut => .loggedOut | .empty(), 49 | ] 50 | 51 | // strategy = `.merge` 52 | automaton = Automaton(state: .loggedOut, input: signal, mapping: reduce(mappings), strategy: .merge) 53 | 54 | automaton?.replies.observeValues { reply in 55 | lastReply = reply 56 | } 57 | 58 | lastReply = nil 59 | } 60 | 61 | it("`LoggedOut => LoggingIn => LoggedIn => LoggingOut => LoggedOut` succeed") { 62 | expect(automaton?.state.value) == .loggedOut 63 | expect(lastReply).to(beNil()) 64 | 65 | observer.send(next: .login) 66 | 67 | expect(lastReply?.input) == .login 68 | expect(lastReply?.fromState) == .loggedOut 69 | expect(lastReply?.toState) == .loggingIn 70 | expect(automaton?.state.value) == .loggingIn 71 | 72 | // `loginOKProducer` will automatically send `.loginOK` 73 | testScheduler.advanceByInterval(1) 74 | 75 | expect(lastReply?.input) == .loginOK 76 | expect(lastReply?.fromState) == .loggingIn 77 | expect(lastReply?.toState) == .loggedIn 78 | expect(automaton?.state.value) == .loggedIn 79 | 80 | observer.send(next: .logout) 81 | 82 | expect(lastReply?.input) == .logout 83 | expect(lastReply?.fromState) == .loggedIn 84 | expect(lastReply?.toState) == .loggingOut 85 | expect(automaton?.state.value) == .loggingOut 86 | 87 | // `logoutOKProducer` will automatically send `.logoutOK` 88 | testScheduler.advanceByInterval(1) 89 | 90 | expect(lastReply?.input) == .logoutOK 91 | expect(lastReply?.fromState) == .loggingOut 92 | expect(lastReply?.toState) == .loggedOut 93 | expect(automaton?.state.value) == .loggedOut 94 | } 95 | 96 | } 97 | 98 | describe("Edge Invocation") { 99 | var subscriptionsCount = 0 100 | 101 | beforeEach { 102 | subscriptionsCount = 0 103 | 104 | // To reproduce the bug we need a plain cold observable 105 | let loginOKProducer = Observable.just(AuthInput.loginOK) 106 | .do(onSubscribe: { subscriptionsCount += 1 }) 107 | 108 | let mappings: [Automaton.EffectMapping] = [ 109 | .login | .loggedOut => .loggingIn | loginOKProducer 110 | ] 111 | 112 | automaton = Automaton(state: .loggedOut, input: signal, mapping: reduce(mappings), strategy: .merge) 113 | } 114 | 115 | describe("loggedOut => .loggingIn") { 116 | it("subscribes to testableLoginOKProducer only once") { 117 | observer.onNext(.login) 118 | expect(subscriptionsCount) == 1 119 | } 120 | } 121 | } 122 | 123 | describe("Func-based EffectMapping") { 124 | 125 | beforeEach { 126 | testScheduler = TestScheduler() 127 | 128 | /// Sends `.loginOK` after delay, simulating async work during `.loggingIn`. 129 | let loginOKProducer = 130 | Observable.just(AuthInput.loginOK) 131 | .delay(1, onScheduler: testScheduler) 132 | 133 | /// Sends `.logoutOK` after delay, simulating async work during `.loggingOut`. 134 | let logoutOKProducer = 135 | Observable.just(AuthInput.logoutOK) 136 | .delay(1, onScheduler: testScheduler) 137 | 138 | let mapping: EffectMapping = { fromState, input in 139 | switch (fromState, input) { 140 | case (.loggedOut, .login): 141 | return (.loggingIn, loginOKProducer) 142 | case (.loggingIn, .loginOK): 143 | return (.loggedIn, .empty()) 144 | case (.loggedIn, .logout): 145 | return (.loggingOut, logoutOKProducer) 146 | case (.loggingOut, .logoutOK): 147 | return (.loggedOut, .empty()) 148 | default: 149 | return nil 150 | } 151 | } 152 | 153 | // strategy = `.merge` 154 | automaton = Automaton(state: .loggedOut, input: signal, mapping: mapping, strategy: .merge) 155 | 156 | automaton?.replies.observeValues { reply in 157 | lastReply = reply 158 | } 159 | 160 | lastReply = nil 161 | } 162 | 163 | it("`LoggedOut => LoggingIn => LoggedIn => LoggingOut => LoggedOut` succeed") { 164 | expect(automaton?.state.value) == .loggedOut 165 | expect(lastReply).to(beNil()) 166 | 167 | observer.send(next: .login) 168 | 169 | expect(lastReply?.input) == .login 170 | expect(lastReply?.fromState) == .loggedOut 171 | expect(lastReply?.toState) == .loggingIn 172 | expect(automaton?.state.value) == .loggingIn 173 | 174 | // `loginOKProducer` will automatically send `.loginOK` 175 | testScheduler.advanceByInterval(1) 176 | 177 | expect(lastReply?.input) == .loginOK 178 | expect(lastReply?.fromState) == .loggingIn 179 | expect(lastReply?.toState) == .loggedIn 180 | expect(automaton?.state.value) == .loggedIn 181 | 182 | observer.send(next: .logout) 183 | 184 | expect(lastReply?.input) == .logout 185 | expect(lastReply?.fromState) == .loggedIn 186 | expect(lastReply?.toState) == .loggingOut 187 | expect(automaton?.state.value) == .loggingOut 188 | 189 | // `logoutOKProducer` will automatically send `.logoutOK` 190 | testScheduler.advanceByInterval(1) 191 | 192 | expect(lastReply?.input) == .logoutOK 193 | expect(lastReply?.fromState) == .loggingOut 194 | expect(lastReply?.toState) == .loggedOut 195 | expect(automaton?.state.value) == .loggedOut 196 | } 197 | 198 | } 199 | 200 | /// https://github.com/inamiy/RxAutomaton/issues/3 201 | describe("Additional effect should be called only once per input") { 202 | 203 | var effectCallCount = 0 204 | 205 | beforeEach { 206 | testScheduler = TestScheduler() 207 | effectCallCount = 0 208 | 209 | /// Sends `.loginOK` after delay, simulating async work during `.loggingIn`. 210 | let loginOKProducer = 211 | Observable.create { observer in 212 | effectCallCount += 1 213 | return testScheduler.scheduleRelative((), dueTime: .milliseconds(100), action: { () -> Disposable in 214 | observer.send(next: .loginOK) 215 | observer.sendCompleted() 216 | return Disposables.create() 217 | }) 218 | } 219 | 220 | let mappings: [Automaton.EffectMapping] = [ 221 | .login | .loggedOut => .loggingIn | loginOKProducer, 222 | .loginOK | .loggingIn => .loggedIn | .empty(), 223 | ] 224 | 225 | // strategy = `.merge` 226 | automaton = Automaton(state: .loggedOut, input: signal, mapping: reduce(mappings), strategy: .merge) 227 | 228 | _ = automaton?.replies.observeValues { reply in 229 | lastReply = reply 230 | } 231 | 232 | lastReply = nil 233 | } 234 | 235 | it("`LoggedOut => LoggingIn => LoggedIn => LoggingOut => LoggedOut` succeed") { 236 | expect(automaton?.state.value) == .loggedOut 237 | expect(lastReply).to(beNil()) 238 | expect(effectCallCount) == 0 239 | 240 | observer.send(next: .login) 241 | 242 | expect(lastReply?.input) == .login 243 | expect(lastReply?.fromState) == .loggedOut 244 | expect(lastReply?.toState) == .loggingIn 245 | expect(automaton?.state.value) == .loggingIn 246 | expect(effectCallCount) == 1 247 | 248 | // `loginOKProducer` will automatically send `.loginOK` 249 | testScheduler.advanceByInterval(1) 250 | 251 | expect(lastReply?.input) == .loginOK 252 | expect(lastReply?.fromState) == .loggingIn 253 | expect(lastReply?.toState) == .loggedIn 254 | expect(automaton?.state.value) == .loggedIn 255 | expect(effectCallCount) == 1 256 | } 257 | 258 | } 259 | 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /RxAutomaton.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1FA0AC461DE8AC2B007F01E0 /* StateFuncMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA0AC451DE8AC2B007F01E0 /* StateFuncMappingSpec.swift */; }; 11 | 1FCAB4CD1DC7845200EA6EBF /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCAB4CC1DC7845200EA6EBF /* RxCocoa.framework */; }; 12 | 1FCAB4CF1DC7845700EA6EBF /* Pulsator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCAB4CE1DC7845700EA6EBF /* Pulsator.framework */; }; 13 | 1FCAB4D01DC784E400EA6EBF /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4822CC391D61961300783A77 /* RxSwift.framework */; }; 14 | 1FCAB4E31DC7947400EA6EBF /* RxTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCAB4E21DC7947400EA6EBF /* RxTest.framework */; }; 15 | 1FCAB4E61DC794A900EA6EBF /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCAB4E41DC794A900EA6EBF /* Nimble.framework */; }; 16 | 1FCAB4E71DC794A900EA6EBF /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCAB4E51DC794A900EA6EBF /* Quick.framework */; }; 17 | 4822CC351D6194FD00783A77 /* MappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC301D6194FD00783A77 /* MappingSpec.swift */; }; 18 | 4822CC3A1D61961300783A77 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4822CC391D61961300783A77 /* RxSwift.framework */; }; 19 | 4822CC401D61969C00783A77 /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC3F1D61969C00783A77 /* Fixtures.swift */; }; 20 | 4822CC421D6197A800783A77 /* ToRACHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC411D6197A800783A77 /* ToRACHelper.swift */; }; 21 | 487BDE631D619D3200C86902 /* AnyMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC2F1D6194FD00783A77 /* AnyMappingSpec.swift */; }; 22 | 487BDE641D619D4500C86902 /* TerminatingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC331D6194FD00783A77 /* TerminatingSpec.swift */; }; 23 | 487BDE6B1D61AFF300C86902 /* EffectMappingLatestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC321D6194FD00783A77 /* EffectMappingLatestSpec.swift */; }; 24 | 487BDE6C1D61B03700C86902 /* EffectMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822CC311D6194FD00783A77 /* EffectMappingSpec.swift */; }; 25 | 488738E01D61689000BF70F4 /* RxAutomaton.h in Headers */ = {isa = PBXBuildFile; fileRef = 488738DF1D61689000BF70F4 /* RxAutomaton.h */; settings = {ATTRIBUTES = (Public, ); }; }; 26 | 488738E71D61689100BF70F4 /* RxAutomaton.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 488738DC1D61689000BF70F4 /* RxAutomaton.framework */; }; 27 | 488738F71D6168A600BF70F4 /* Automaton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488738F61D6168A600BF70F4 /* Automaton.swift */; }; 28 | 48873F461D616AFD00BF70F4 /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48873F451D616AFD00BF70F4 /* Reply.swift */; }; 29 | 48873F481D616B4900BF70F4 /* Mapping+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48873F471D616B4900BF70F4 /* Mapping+Helper.swift */; }; 30 | 48873F4A1D617EF600BF70F4 /* FlattenStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48873F491D617EF600BF70F4 /* FlattenStrategy.swift */; }; 31 | 48AC076D1D61C962000293FD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC076C1D61C962000293FD /* AppDelegate.swift */; }; 32 | 48AC076F1D61C962000293FD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC076E1D61C962000293FD /* ViewController.swift */; }; 33 | 48AC07741D61C962000293FD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 48AC07731D61C962000293FD /* Assets.xcassets */; }; 34 | 48AC07771D61C962000293FD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 48AC07751D61C962000293FD /* LaunchScreen.storyboard */; }; 35 | 48AC07B41D61CA5B000293FD /* RxAutomaton.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 488738DC1D61689000BF70F4 /* RxAutomaton.framework */; }; 36 | 48AC07B51D61CA5B000293FD /* RxAutomaton.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 488738DC1D61689000BF70F4 /* RxAutomaton.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 37 | 48AC07C21D61CAF0000293FD /* Automaton.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 48AC07C01D61CAF0000293FD /* Automaton.storyboard */; }; 38 | 48AC07C31D61CC02000293FD /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC07BE1D61CAA7000293FD /* State.swift */; }; 39 | 48AC07C41D61CC04000293FD /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC07BC1D61CAA0000293FD /* Input.swift */; }; 40 | 48AC07CB1D61CD6A000293FD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC07CA1D61CD6A000293FD /* Helpers.swift */; }; 41 | 48AC07CF1D61DA94000293FD /* RxSwift+Then.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC07CE1D61DA94000293FD /* RxSwift+Then.swift */; }; 42 | 48AC07D21D61DADA000293FD /* RxSwift+Pipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AC07D11D61DADA000293FD /* RxSwift+Pipe.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXContainerItemProxy section */ 46 | 488738E81D61689100BF70F4 /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 488738D31D61689000BF70F4 /* Project object */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 488738DB1D61689000BF70F4; 51 | remoteInfo = RxAutomaton; 52 | }; 53 | 48AC07B61D61CA5B000293FD /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 488738D31D61689000BF70F4 /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 488738DB1D61689000BF70F4; 58 | remoteInfo = RxAutomaton; 59 | }; 60 | /* End PBXContainerItemProxy section */ 61 | 62 | /* Begin PBXCopyFilesBuildPhase section */ 63 | 48AC07B31D61CA43000293FD /* Embed Frameworks */ = { 64 | isa = PBXCopyFilesBuildPhase; 65 | buildActionMask = 2147483647; 66 | dstPath = ""; 67 | dstSubfolderSpec = 10; 68 | files = ( 69 | 48AC07B51D61CA5B000293FD /* RxAutomaton.framework in Embed Frameworks */, 70 | ); 71 | name = "Embed Frameworks"; 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXCopyFilesBuildPhase section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 1FA0AC451DE8AC2B007F01E0 /* StateFuncMappingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateFuncMappingSpec.swift; sourceTree = ""; }; 78 | 1FB0B5251E226FA800052073 /* LinuxMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; }; 79 | 1FCAB4CC1DC7845200EA6EBF /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | 1FCAB4CE1DC7845700EA6EBF /* Pulsator.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Pulsator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 1FCAB4E21DC7947400EA6EBF /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 82 | 1FCAB4E41DC794A900EA6EBF /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 1FCAB4E51DC794A900EA6EBF /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | 4822CC2F1D6194FD00783A77 /* AnyMappingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyMappingSpec.swift; sourceTree = ""; }; 85 | 4822CC301D6194FD00783A77 /* MappingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappingSpec.swift; sourceTree = ""; }; 86 | 4822CC311D6194FD00783A77 /* EffectMappingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EffectMappingSpec.swift; sourceTree = ""; }; 87 | 4822CC321D6194FD00783A77 /* EffectMappingLatestSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EffectMappingLatestSpec.swift; sourceTree = ""; }; 88 | 4822CC331D6194FD00783A77 /* TerminatingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TerminatingSpec.swift; sourceTree = ""; }; 89 | 4822CC391D61961300783A77 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | 4822CC3F1D61969C00783A77 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; 91 | 4822CC411D6197A800783A77 /* ToRACHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToRACHelper.swift; sourceTree = ""; }; 92 | 488738DC1D61689000BF70F4 /* RxAutomaton.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxAutomaton.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 93 | 488738DF1D61689000BF70F4 /* RxAutomaton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RxAutomaton.h; sourceTree = ""; }; 94 | 488738E11D61689000BF70F4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 488738E61D61689100BF70F4 /* RxAutomatonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RxAutomatonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 96 | 488738ED1D61689100BF70F4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 97 | 488738F61D6168A600BF70F4 /* Automaton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Automaton.swift; sourceTree = ""; }; 98 | 48873F451D616AFD00BF70F4 /* Reply.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = ""; }; 99 | 48873F471D616B4900BF70F4 /* Mapping+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Mapping+Helper.swift"; sourceTree = ""; }; 100 | 48873F491D617EF600BF70F4 /* FlattenStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlattenStrategy.swift; sourceTree = ""; }; 101 | 488741181D61923300BF70F4 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = ""; }; 102 | 488741191D61923300BF70F4 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = ""; }; 103 | 4887411A1D61923300BF70F4 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = ""; }; 104 | 488743021D61927700BF70F4 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; 105 | 488743031D61927700BF70F4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 106 | 488743041D61927700BF70F4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 107 | 48AC076A1D61C962000293FD /* RxAutomatonDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RxAutomatonDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 108 | 48AC076C1D61C962000293FD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 109 | 48AC076E1D61C962000293FD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 110 | 48AC07731D61C962000293FD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 111 | 48AC07761D61C962000293FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 112 | 48AC07781D61C962000293FD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 113 | 48AC07BC1D61CAA0000293FD /* Input.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Input.swift; sourceTree = ""; }; 114 | 48AC07BE1D61CAA7000293FD /* State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; 115 | 48AC07C11D61CAF0000293FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Automaton.storyboard; sourceTree = ""; }; 116 | 48AC07CA1D61CD6A000293FD /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 117 | 48AC07CE1D61DA94000293FD /* RxSwift+Then.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RxSwift+Then.swift"; sourceTree = ""; }; 118 | 48AC07D11D61DADA000293FD /* RxSwift+Pipe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RxSwift+Pipe.swift"; sourceTree = ""; }; 119 | /* End PBXFileReference section */ 120 | 121 | /* Begin PBXFrameworksBuildPhase section */ 122 | 488738D81D61689000BF70F4 /* Frameworks */ = { 123 | isa = PBXFrameworksBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | 4822CC3A1D61961300783A77 /* RxSwift.framework in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | 488738E31D61689100BF70F4 /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | 488738E71D61689100BF70F4 /* RxAutomaton.framework in Frameworks */, 135 | 1FCAB4E31DC7947400EA6EBF /* RxTest.framework in Frameworks */, 136 | 1FCAB4E71DC794A900EA6EBF /* Quick.framework in Frameworks */, 137 | 1FCAB4E61DC794A900EA6EBF /* Nimble.framework in Frameworks */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | 48AC07671D61C962000293FD /* Frameworks */ = { 142 | isa = PBXFrameworksBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 48AC07B41D61CA5B000293FD /* RxAutomaton.framework in Frameworks */, 146 | 1FCAB4D01DC784E400EA6EBF /* RxSwift.framework in Frameworks */, 147 | 1FCAB4CD1DC7845200EA6EBF /* RxCocoa.framework in Frameworks */, 148 | 1FCAB4CF1DC7845700EA6EBF /* Pulsator.framework in Frameworks */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXFrameworksBuildPhase section */ 153 | 154 | /* Begin PBXGroup section */ 155 | 1FB0B5241E226FA800052073 /* Tests */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 1FB0B5251E226FA800052073 /* LinuxMain.swift */, 159 | 488738EA1D61689100BF70F4 /* RxAutomatonTests */, 160 | ); 161 | path = Tests; 162 | sourceTree = ""; 163 | }; 164 | 4822CC3E1D61969C00783A77 /* Fixtures */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 4822CC3F1D61969C00783A77 /* Fixtures.swift */, 168 | 4822CC411D6197A800783A77 /* ToRACHelper.swift */, 169 | ); 170 | path = Fixtures; 171 | sourceTree = ""; 172 | }; 173 | 488738D21D61689000BF70F4 = { 174 | isa = PBXGroup; 175 | children = ( 176 | 488743011D61927700BF70F4 /* Configurations */, 177 | 488738DE1D61689000BF70F4 /* RxAutomaton */, 178 | 1FB0B5241E226FA800052073 /* Tests */, 179 | 48AC076B1D61C962000293FD /* RxAutomatonDemo */, 180 | 48873F441D616A0800BF70F4 /* Frameworks */, 181 | 488738DD1D61689000BF70F4 /* Products */, 182 | ); 183 | sourceTree = ""; 184 | }; 185 | 488738DD1D61689000BF70F4 /* Products */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 488738DC1D61689000BF70F4 /* RxAutomaton.framework */, 189 | 488738E61D61689100BF70F4 /* RxAutomatonTests.xctest */, 190 | 48AC076A1D61C962000293FD /* RxAutomatonDemo.app */, 191 | ); 192 | name = Products; 193 | sourceTree = ""; 194 | }; 195 | 488738DE1D61689000BF70F4 /* RxAutomaton */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 488738DF1D61689000BF70F4 /* RxAutomaton.h */, 199 | 488738F61D6168A600BF70F4 /* Automaton.swift */, 200 | 48873F451D616AFD00BF70F4 /* Reply.swift */, 201 | 48873F471D616B4900BF70F4 /* Mapping+Helper.swift */, 202 | 48873F491D617EF600BF70F4 /* FlattenStrategy.swift */, 203 | 48AC07D01D61DAAA000293FD /* RxSwift Extensions */, 204 | 488738E11D61689000BF70F4 /* Info.plist */, 205 | ); 206 | name = RxAutomaton; 207 | path = Sources; 208 | sourceTree = ""; 209 | }; 210 | 488738EA1D61689100BF70F4 /* RxAutomatonTests */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 4822CC3E1D61969C00783A77 /* Fixtures */, 214 | 4822CC301D6194FD00783A77 /* MappingSpec.swift */, 215 | 4822CC311D6194FD00783A77 /* EffectMappingSpec.swift */, 216 | 4822CC2F1D6194FD00783A77 /* AnyMappingSpec.swift */, 217 | 1FA0AC451DE8AC2B007F01E0 /* StateFuncMappingSpec.swift */, 218 | 4822CC321D6194FD00783A77 /* EffectMappingLatestSpec.swift */, 219 | 4822CC331D6194FD00783A77 /* TerminatingSpec.swift */, 220 | 488738ED1D61689100BF70F4 /* Info.plist */, 221 | ); 222 | path = RxAutomatonTests; 223 | sourceTree = ""; 224 | }; 225 | 48873F441D616A0800BF70F4 /* Frameworks */ = { 226 | isa = PBXGroup; 227 | children = ( 228 | 4822CC391D61961300783A77 /* RxSwift.framework */, 229 | 1FCAB4CC1DC7845200EA6EBF /* RxCocoa.framework */, 230 | 1FCAB4E21DC7947400EA6EBF /* RxTest.framework */, 231 | 1FCAB4E51DC794A900EA6EBF /* Quick.framework */, 232 | 1FCAB4E41DC794A900EA6EBF /* Nimble.framework */, 233 | 1FCAB4CE1DC7845700EA6EBF /* Pulsator.framework */, 234 | ); 235 | name = Frameworks; 236 | sourceTree = ""; 237 | }; 238 | 488741151D61923300BF70F4 /* mrackwitz/xcconfigs (for target) */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 488741181D61923300BF70F4 /* UniversalFramework_Base.xcconfig */, 242 | 488741191D61923300BF70F4 /* UniversalFramework_Framework.xcconfig */, 243 | 4887411A1D61923300BF70F4 /* UniversalFramework_Test.xcconfig */, 244 | ); 245 | name = "mrackwitz/xcconfigs (for target)"; 246 | path = ../Carthage/Checkouts/xcconfigs; 247 | sourceTree = ""; 248 | }; 249 | 488743011D61927700BF70F4 /* Configurations */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | 488743051D61928600BF70F4 /* project xcconfigs */, 253 | 488741151D61923300BF70F4 /* mrackwitz/xcconfigs (for target) */, 254 | ); 255 | path = Configurations; 256 | sourceTree = ""; 257 | }; 258 | 488743051D61928600BF70F4 /* project xcconfigs */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 488743021D61927700BF70F4 /* Base.xcconfig */, 262 | 488743031D61927700BF70F4 /* Debug.xcconfig */, 263 | 488743041D61927700BF70F4 /* Release.xcconfig */, 264 | ); 265 | name = "project xcconfigs"; 266 | sourceTree = ""; 267 | }; 268 | 48AC076B1D61C962000293FD /* RxAutomatonDemo */ = { 269 | isa = PBXGroup; 270 | children = ( 271 | 48AC07CA1D61CD6A000293FD /* Helpers.swift */, 272 | 48AC076C1D61C962000293FD /* AppDelegate.swift */, 273 | 48AC076E1D61C962000293FD /* ViewController.swift */, 274 | 48AC07BE1D61CAA7000293FD /* State.swift */, 275 | 48AC07BC1D61CAA0000293FD /* Input.swift */, 276 | 48AC07C01D61CAF0000293FD /* Automaton.storyboard */, 277 | 48AC07731D61C962000293FD /* Assets.xcassets */, 278 | 48AC07751D61C962000293FD /* LaunchScreen.storyboard */, 279 | 48AC07781D61C962000293FD /* Info.plist */, 280 | ); 281 | name = RxAutomatonDemo; 282 | path = Demo; 283 | sourceTree = ""; 284 | }; 285 | 48AC07D01D61DAAA000293FD /* RxSwift Extensions */ = { 286 | isa = PBXGroup; 287 | children = ( 288 | 48AC07CE1D61DA94000293FD /* RxSwift+Then.swift */, 289 | 48AC07D11D61DADA000293FD /* RxSwift+Pipe.swift */, 290 | ); 291 | name = "RxSwift Extensions"; 292 | sourceTree = ""; 293 | }; 294 | /* End PBXGroup section */ 295 | 296 | /* Begin PBXHeadersBuildPhase section */ 297 | 488738D91D61689000BF70F4 /* Headers */ = { 298 | isa = PBXHeadersBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 488738E01D61689000BF70F4 /* RxAutomaton.h in Headers */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXHeadersBuildPhase section */ 306 | 307 | /* Begin PBXNativeTarget section */ 308 | 488738DB1D61689000BF70F4 /* RxAutomaton */ = { 309 | isa = PBXNativeTarget; 310 | buildConfigurationList = 488738F01D61689100BF70F4 /* Build configuration list for PBXNativeTarget "RxAutomaton" */; 311 | buildPhases = ( 312 | 488738D71D61689000BF70F4 /* Sources */, 313 | 488738D81D61689000BF70F4 /* Frameworks */, 314 | 488738D91D61689000BF70F4 /* Headers */, 315 | 488738DA1D61689000BF70F4 /* Resources */, 316 | ); 317 | buildRules = ( 318 | ); 319 | dependencies = ( 320 | ); 321 | name = RxAutomaton; 322 | productName = RxAutomaton; 323 | productReference = 488738DC1D61689000BF70F4 /* RxAutomaton.framework */; 324 | productType = "com.apple.product-type.framework"; 325 | }; 326 | 488738E51D61689100BF70F4 /* RxAutomatonTests */ = { 327 | isa = PBXNativeTarget; 328 | buildConfigurationList = 488738F31D61689100BF70F4 /* Build configuration list for PBXNativeTarget "RxAutomatonTests" */; 329 | buildPhases = ( 330 | 488738E21D61689100BF70F4 /* Sources */, 331 | 488738E31D61689100BF70F4 /* Frameworks */, 332 | 488738E41D61689100BF70F4 /* Resources */, 333 | ); 334 | buildRules = ( 335 | ); 336 | dependencies = ( 337 | 488738E91D61689100BF70F4 /* PBXTargetDependency */, 338 | ); 339 | name = RxAutomatonTests; 340 | productName = RxAutomatonTests; 341 | productReference = 488738E61D61689100BF70F4 /* RxAutomatonTests.xctest */; 342 | productType = "com.apple.product-type.bundle.unit-test"; 343 | }; 344 | 48AC07691D61C962000293FD /* RxAutomatonDemo */ = { 345 | isa = PBXNativeTarget; 346 | buildConfigurationList = 48AC079D1D61C962000293FD /* Build configuration list for PBXNativeTarget "RxAutomatonDemo" */; 347 | buildPhases = ( 348 | 48AC07661D61C962000293FD /* Sources */, 349 | 48AC07671D61C962000293FD /* Frameworks */, 350 | 48AC07681D61C962000293FD /* Resources */, 351 | 48AC07B31D61CA43000293FD /* Embed Frameworks */, 352 | ); 353 | buildRules = ( 354 | ); 355 | dependencies = ( 356 | 48AC07B71D61CA5B000293FD /* PBXTargetDependency */, 357 | ); 358 | name = RxAutomatonDemo; 359 | productName = RxAutomatonDemo; 360 | productReference = 48AC076A1D61C962000293FD /* RxAutomatonDemo.app */; 361 | productType = "com.apple.product-type.application"; 362 | }; 363 | /* End PBXNativeTarget section */ 364 | 365 | /* Begin PBXProject section */ 366 | 488738D31D61689000BF70F4 /* Project object */ = { 367 | isa = PBXProject; 368 | attributes = { 369 | LastSwiftUpdateCheck = 0730; 370 | LastUpgradeCheck = 0900; 371 | ORGANIZATIONNAME = "Yasuhiro Inami"; 372 | TargetAttributes = { 373 | 488738DB1D61689000BF70F4 = { 374 | CreatedOnToolsVersion = 7.3.1; 375 | LastSwiftMigration = 1020; 376 | ProvisioningStyle = Manual; 377 | }; 378 | 488738E51D61689100BF70F4 = { 379 | CreatedOnToolsVersion = 7.3.1; 380 | LastSwiftMigration = 0800; 381 | }; 382 | 48AC07691D61C962000293FD = { 383 | CreatedOnToolsVersion = 7.3.1; 384 | LastSwiftMigration = 1020; 385 | ProvisioningStyle = Manual; 386 | }; 387 | }; 388 | }; 389 | buildConfigurationList = 488738D61D61689000BF70F4 /* Build configuration list for PBXProject "RxAutomaton" */; 390 | compatibilityVersion = "Xcode 3.2"; 391 | developmentRegion = en; 392 | hasScannedForEncodings = 0; 393 | knownRegions = ( 394 | en, 395 | Base, 396 | ); 397 | mainGroup = 488738D21D61689000BF70F4; 398 | productRefGroup = 488738DD1D61689000BF70F4 /* Products */; 399 | projectDirPath = ""; 400 | projectRoot = ""; 401 | targets = ( 402 | 488738DB1D61689000BF70F4 /* RxAutomaton */, 403 | 488738E51D61689100BF70F4 /* RxAutomatonTests */, 404 | 48AC07691D61C962000293FD /* RxAutomatonDemo */, 405 | ); 406 | }; 407 | /* End PBXProject section */ 408 | 409 | /* Begin PBXResourcesBuildPhase section */ 410 | 488738DA1D61689000BF70F4 /* Resources */ = { 411 | isa = PBXResourcesBuildPhase; 412 | buildActionMask = 2147483647; 413 | files = ( 414 | ); 415 | runOnlyForDeploymentPostprocessing = 0; 416 | }; 417 | 488738E41D61689100BF70F4 /* Resources */ = { 418 | isa = PBXResourcesBuildPhase; 419 | buildActionMask = 2147483647; 420 | files = ( 421 | ); 422 | runOnlyForDeploymentPostprocessing = 0; 423 | }; 424 | 48AC07681D61C962000293FD /* Resources */ = { 425 | isa = PBXResourcesBuildPhase; 426 | buildActionMask = 2147483647; 427 | files = ( 428 | 48AC07771D61C962000293FD /* LaunchScreen.storyboard in Resources */, 429 | 48AC07741D61C962000293FD /* Assets.xcassets in Resources */, 430 | 48AC07C21D61CAF0000293FD /* Automaton.storyboard in Resources */, 431 | ); 432 | runOnlyForDeploymentPostprocessing = 0; 433 | }; 434 | /* End PBXResourcesBuildPhase section */ 435 | 436 | /* Begin PBXSourcesBuildPhase section */ 437 | 488738D71D61689000BF70F4 /* Sources */ = { 438 | isa = PBXSourcesBuildPhase; 439 | buildActionMask = 2147483647; 440 | files = ( 441 | 48873F461D616AFD00BF70F4 /* Reply.swift in Sources */, 442 | 48873F4A1D617EF600BF70F4 /* FlattenStrategy.swift in Sources */, 443 | 48873F481D616B4900BF70F4 /* Mapping+Helper.swift in Sources */, 444 | 48AC07D21D61DADA000293FD /* RxSwift+Pipe.swift in Sources */, 445 | 48AC07CF1D61DA94000293FD /* RxSwift+Then.swift in Sources */, 446 | 488738F71D6168A600BF70F4 /* Automaton.swift in Sources */, 447 | ); 448 | runOnlyForDeploymentPostprocessing = 0; 449 | }; 450 | 488738E21D61689100BF70F4 /* Sources */ = { 451 | isa = PBXSourcesBuildPhase; 452 | buildActionMask = 2147483647; 453 | files = ( 454 | 4822CC351D6194FD00783A77 /* MappingSpec.swift in Sources */, 455 | 1FA0AC461DE8AC2B007F01E0 /* StateFuncMappingSpec.swift in Sources */, 456 | 4822CC401D61969C00783A77 /* Fixtures.swift in Sources */, 457 | 487BDE631D619D3200C86902 /* AnyMappingSpec.swift in Sources */, 458 | 487BDE6C1D61B03700C86902 /* EffectMappingSpec.swift in Sources */, 459 | 487BDE641D619D4500C86902 /* TerminatingSpec.swift in Sources */, 460 | 487BDE6B1D61AFF300C86902 /* EffectMappingLatestSpec.swift in Sources */, 461 | 4822CC421D6197A800783A77 /* ToRACHelper.swift in Sources */, 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | }; 465 | 48AC07661D61C962000293FD /* Sources */ = { 466 | isa = PBXSourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | 48AC07CB1D61CD6A000293FD /* Helpers.swift in Sources */, 470 | 48AC076F1D61C962000293FD /* ViewController.swift in Sources */, 471 | 48AC076D1D61C962000293FD /* AppDelegate.swift in Sources */, 472 | 48AC07C41D61CC04000293FD /* Input.swift in Sources */, 473 | 48AC07C31D61CC02000293FD /* State.swift in Sources */, 474 | ); 475 | runOnlyForDeploymentPostprocessing = 0; 476 | }; 477 | /* End PBXSourcesBuildPhase section */ 478 | 479 | /* Begin PBXTargetDependency section */ 480 | 488738E91D61689100BF70F4 /* PBXTargetDependency */ = { 481 | isa = PBXTargetDependency; 482 | target = 488738DB1D61689000BF70F4 /* RxAutomaton */; 483 | targetProxy = 488738E81D61689100BF70F4 /* PBXContainerItemProxy */; 484 | }; 485 | 48AC07B71D61CA5B000293FD /* PBXTargetDependency */ = { 486 | isa = PBXTargetDependency; 487 | target = 488738DB1D61689000BF70F4 /* RxAutomaton */; 488 | targetProxy = 48AC07B61D61CA5B000293FD /* PBXContainerItemProxy */; 489 | }; 490 | /* End PBXTargetDependency section */ 491 | 492 | /* Begin PBXVariantGroup section */ 493 | 48AC07751D61C962000293FD /* LaunchScreen.storyboard */ = { 494 | isa = PBXVariantGroup; 495 | children = ( 496 | 48AC07761D61C962000293FD /* Base */, 497 | ); 498 | name = LaunchScreen.storyboard; 499 | sourceTree = ""; 500 | }; 501 | 48AC07C01D61CAF0000293FD /* Automaton.storyboard */ = { 502 | isa = PBXVariantGroup; 503 | children = ( 504 | 48AC07C11D61CAF0000293FD /* Base */, 505 | ); 506 | name = Automaton.storyboard; 507 | sourceTree = ""; 508 | }; 509 | /* End PBXVariantGroup section */ 510 | 511 | /* Begin XCBuildConfiguration section */ 512 | 488738EE1D61689100BF70F4 /* Debug */ = { 513 | isa = XCBuildConfiguration; 514 | baseConfigurationReference = 488743031D61927700BF70F4 /* Debug.xcconfig */; 515 | buildSettings = { 516 | ALWAYS_SEARCH_USER_PATHS = NO; 517 | CLANG_ANALYZER_NONNULL = YES; 518 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 519 | CLANG_CXX_LIBRARY = "libc++"; 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_ENABLE_OBJC_ARC = YES; 522 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 523 | CLANG_WARN_BOOL_CONVERSION = YES; 524 | CLANG_WARN_COMMA = YES; 525 | CLANG_WARN_CONSTANT_CONVERSION = YES; 526 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 527 | CLANG_WARN_EMPTY_BODY = YES; 528 | CLANG_WARN_ENUM_CONVERSION = YES; 529 | CLANG_WARN_INFINITE_RECURSION = YES; 530 | CLANG_WARN_INT_CONVERSION = YES; 531 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 532 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 533 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 534 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 535 | CLANG_WARN_STRICT_PROTOTYPES = YES; 536 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 537 | CLANG_WARN_UNREACHABLE_CODE = YES; 538 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 539 | COPY_PHASE_STRIP = NO; 540 | CURRENT_PROJECT_VERSION = 1; 541 | DEBUG_INFORMATION_FORMAT = dwarf; 542 | ENABLE_STRICT_OBJC_MSGSEND = YES; 543 | ENABLE_TESTABILITY = YES; 544 | GCC_C_LANGUAGE_STANDARD = gnu99; 545 | GCC_DYNAMIC_NO_PIC = NO; 546 | GCC_NO_COMMON_BLOCKS = YES; 547 | GCC_OPTIMIZATION_LEVEL = 0; 548 | GCC_PREPROCESSOR_DEFINITIONS = ( 549 | "DEBUG=1", 550 | "$(inherited)", 551 | ); 552 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 553 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 554 | GCC_WARN_UNDECLARED_SELECTOR = YES; 555 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 556 | GCC_WARN_UNUSED_FUNCTION = YES; 557 | GCC_WARN_UNUSED_VARIABLE = YES; 558 | MTL_ENABLE_DEBUG_INFO = YES; 559 | ONLY_ACTIVE_ARCH = YES; 560 | SDKROOT = iphoneos; 561 | SWIFT_VERSION = 4.0; 562 | TARGETED_DEVICE_FAMILY = "1,2"; 563 | VERSIONING_SYSTEM = "apple-generic"; 564 | VERSION_INFO_PREFIX = ""; 565 | }; 566 | name = Debug; 567 | }; 568 | 488738EF1D61689100BF70F4 /* Release */ = { 569 | isa = XCBuildConfiguration; 570 | baseConfigurationReference = 488743041D61927700BF70F4 /* Release.xcconfig */; 571 | buildSettings = { 572 | ALWAYS_SEARCH_USER_PATHS = NO; 573 | CLANG_ANALYZER_NONNULL = YES; 574 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 575 | CLANG_CXX_LIBRARY = "libc++"; 576 | CLANG_ENABLE_MODULES = YES; 577 | CLANG_ENABLE_OBJC_ARC = YES; 578 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 579 | CLANG_WARN_BOOL_CONVERSION = YES; 580 | CLANG_WARN_COMMA = YES; 581 | CLANG_WARN_CONSTANT_CONVERSION = YES; 582 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 583 | CLANG_WARN_EMPTY_BODY = YES; 584 | CLANG_WARN_ENUM_CONVERSION = YES; 585 | CLANG_WARN_INFINITE_RECURSION = YES; 586 | CLANG_WARN_INT_CONVERSION = YES; 587 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 588 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 589 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 590 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 591 | CLANG_WARN_STRICT_PROTOTYPES = YES; 592 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 593 | CLANG_WARN_UNREACHABLE_CODE = YES; 594 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 595 | COPY_PHASE_STRIP = NO; 596 | CURRENT_PROJECT_VERSION = 1; 597 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 598 | ENABLE_NS_ASSERTIONS = NO; 599 | ENABLE_STRICT_OBJC_MSGSEND = YES; 600 | GCC_C_LANGUAGE_STANDARD = gnu99; 601 | GCC_NO_COMMON_BLOCKS = YES; 602 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 603 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 604 | GCC_WARN_UNDECLARED_SELECTOR = YES; 605 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 606 | GCC_WARN_UNUSED_FUNCTION = YES; 607 | GCC_WARN_UNUSED_VARIABLE = YES; 608 | MTL_ENABLE_DEBUG_INFO = NO; 609 | SDKROOT = iphoneos; 610 | SWIFT_VERSION = 4.0; 611 | TARGETED_DEVICE_FAMILY = "1,2"; 612 | VALIDATE_PRODUCT = YES; 613 | VERSIONING_SYSTEM = "apple-generic"; 614 | VERSION_INFO_PREFIX = ""; 615 | }; 616 | name = Release; 617 | }; 618 | 488738F11D61689100BF70F4 /* Debug */ = { 619 | isa = XCBuildConfiguration; 620 | baseConfigurationReference = 488741191D61923300BF70F4 /* UniversalFramework_Framework.xcconfig */; 621 | buildSettings = { 622 | APPLICATION_EXTENSION_API_ONLY = YES; 623 | CLANG_ENABLE_MODULES = YES; 624 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 625 | DEFINES_MODULE = YES; 626 | DEVELOPMENT_TEAM = ""; 627 | DYLIB_COMPATIBILITY_VERSION = 1; 628 | DYLIB_CURRENT_VERSION = 1; 629 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 630 | INFOPLIST_FILE = Sources/Info.plist; 631 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 632 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomaton; 633 | PRODUCT_NAME = "$(TARGET_NAME)"; 634 | SKIP_INSTALL = YES; 635 | SWIFT_VERSION = 5.0; 636 | }; 637 | name = Debug; 638 | }; 639 | 488738F21D61689100BF70F4 /* Release */ = { 640 | isa = XCBuildConfiguration; 641 | baseConfigurationReference = 488741191D61923300BF70F4 /* UniversalFramework_Framework.xcconfig */; 642 | buildSettings = { 643 | APPLICATION_EXTENSION_API_ONLY = YES; 644 | CLANG_ENABLE_MODULES = YES; 645 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 646 | DEFINES_MODULE = YES; 647 | DEVELOPMENT_TEAM = ""; 648 | DYLIB_COMPATIBILITY_VERSION = 1; 649 | DYLIB_CURRENT_VERSION = 1; 650 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 651 | INFOPLIST_FILE = Sources/Info.plist; 652 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 653 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomaton; 654 | PRODUCT_NAME = "$(TARGET_NAME)"; 655 | SKIP_INSTALL = YES; 656 | SWIFT_VERSION = 5.0; 657 | }; 658 | name = Release; 659 | }; 660 | 488738F41D61689100BF70F4 /* Debug */ = { 661 | isa = XCBuildConfiguration; 662 | baseConfigurationReference = 4887411A1D61923300BF70F4 /* UniversalFramework_Test.xcconfig */; 663 | buildSettings = { 664 | CLANG_ENABLE_MODULES = YES; 665 | INFOPLIST_FILE = Tests/RxAutomatonTests/Info.plist; 666 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomatonTests; 667 | PRODUCT_NAME = "$(TARGET_NAME)"; 668 | }; 669 | name = Debug; 670 | }; 671 | 488738F51D61689100BF70F4 /* Release */ = { 672 | isa = XCBuildConfiguration; 673 | baseConfigurationReference = 4887411A1D61923300BF70F4 /* UniversalFramework_Test.xcconfig */; 674 | buildSettings = { 675 | CLANG_ENABLE_MODULES = YES; 676 | INFOPLIST_FILE = Tests/RxAutomatonTests/Info.plist; 677 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomatonTests; 678 | PRODUCT_NAME = "$(TARGET_NAME)"; 679 | }; 680 | name = Release; 681 | }; 682 | 48AC07791D61C962000293FD /* Debug */ = { 683 | isa = XCBuildConfiguration; 684 | buildSettings = { 685 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 686 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 687 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 688 | DEVELOPMENT_TEAM = ""; 689 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Info.plist"; 690 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 691 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 692 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomatonDemo; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 695 | SWIFT_VERSION = 5.0; 696 | }; 697 | name = Debug; 698 | }; 699 | 48AC077A1D61C962000293FD /* Release */ = { 700 | isa = XCBuildConfiguration; 701 | buildSettings = { 702 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 703 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 704 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 705 | DEVELOPMENT_TEAM = ""; 706 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Info.plist"; 707 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 708 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 709 | PRODUCT_BUNDLE_IDENTIFIER = com.inamiy.RxAutomatonDemo; 710 | PRODUCT_NAME = "$(TARGET_NAME)"; 711 | SWIFT_VERSION = 5.0; 712 | }; 713 | name = Release; 714 | }; 715 | /* End XCBuildConfiguration section */ 716 | 717 | /* Begin XCConfigurationList section */ 718 | 488738D61D61689000BF70F4 /* Build configuration list for PBXProject "RxAutomaton" */ = { 719 | isa = XCConfigurationList; 720 | buildConfigurations = ( 721 | 488738EE1D61689100BF70F4 /* Debug */, 722 | 488738EF1D61689100BF70F4 /* Release */, 723 | ); 724 | defaultConfigurationIsVisible = 0; 725 | defaultConfigurationName = Release; 726 | }; 727 | 488738F01D61689100BF70F4 /* Build configuration list for PBXNativeTarget "RxAutomaton" */ = { 728 | isa = XCConfigurationList; 729 | buildConfigurations = ( 730 | 488738F11D61689100BF70F4 /* Debug */, 731 | 488738F21D61689100BF70F4 /* Release */, 732 | ); 733 | defaultConfigurationIsVisible = 0; 734 | defaultConfigurationName = Release; 735 | }; 736 | 488738F31D61689100BF70F4 /* Build configuration list for PBXNativeTarget "RxAutomatonTests" */ = { 737 | isa = XCConfigurationList; 738 | buildConfigurations = ( 739 | 488738F41D61689100BF70F4 /* Debug */, 740 | 488738F51D61689100BF70F4 /* Release */, 741 | ); 742 | defaultConfigurationIsVisible = 0; 743 | defaultConfigurationName = Release; 744 | }; 745 | 48AC079D1D61C962000293FD /* Build configuration list for PBXNativeTarget "RxAutomatonDemo" */ = { 746 | isa = XCConfigurationList; 747 | buildConfigurations = ( 748 | 48AC07791D61C962000293FD /* Debug */, 749 | 48AC077A1D61C962000293FD /* Release */, 750 | ); 751 | defaultConfigurationIsVisible = 0; 752 | defaultConfigurationName = Release; 753 | }; 754 | /* End XCConfigurationList section */ 755 | }; 756 | rootObject = 488738D31D61689000BF70F4 /* Project object */; 757 | } 758 | --------------------------------------------------------------------------------