├── .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 | 
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 |
--------------------------------------------------------------------------------