├── .gitignore ├── LICENSE.md ├── Logo └── Logo.png ├── Package.swift ├── README.md ├── Reactor.xcodeproj ├── ReactorTests_Info.plist ├── Reactor_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Reactor.xcscheme │ └── xcschememanagement.plist ├── Sources └── Reactor.swift ├── Tests ├── LinuxMain.swift └── ReactorTests │ └── ReactorTests.swift └── bitrise.yml /.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 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | # Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 60 | 61 | fastlane/report.xml 62 | fastlane/screenshots 63 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jason Larsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Logo/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactorSwift/Reactor/c2bbf5bfe722d617219156370b17cb0263641e3b/Logo/Logo.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package(name: "Reactor", 6 | platforms: [.macOS(.v10_12), 7 | .iOS(.v10), 8 | .tvOS(.v10), 9 | .watchOS(.v3)], 10 | products: [.library(name: "Reactor", 11 | targets: ["Reactor"]), 12 | ], 13 | targets: [.target(name: "Reactor", 14 | path: "Sources"), 15 | .testTarget(name: "ReactorTests", 16 | dependencies: ["Reactor"], 17 | path: "Tests")], 18 | swiftLanguageVersions: [.v5]) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](Logo/Logo.png) 2 | 3 | ![bitrise](https://app.bitrise.io/app/e94000174c55ac03.svg?token=qFQSScqJqmNAUfE-1Sv0rA) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 6 | ![macOS](https://img.shields.io/badge/macOS-10.9-lightgrey.svg) 7 | 8 | # Reactor 9 | 10 | Reactor is a framework for making more reactive applications inspired by [Elm](https://github.com/evancz/elm-architecture-tutorial), [Redux](http://redux.js.org/docs/basics/index.html), and recent work on [ReSwift](https://github.com/ReSwift/ReSwift). It's small and simple (just one file), so you can either use Carthage to stay up to date, or just drag and drop into your project and go. Or you can look through it and roll your own. 11 | 12 | Reactor encourages unidirectional data flow from a single source of truth—i.e., there is one global object responsible for managing application data, and all UI is derived and updated from it. This way your [UI is always in sync with your data](https://medium.com/swift-fox/react-native-native-28e37f7de1ae#.tbp9edrnn), and your data is sync with itself since there are not multiple copies of it floating around your app. 13 | 14 | 15 | ## Architecture 16 | 17 | ``` 18 | ┌──────────────────┐ 19 | │ │ 20 | │ │ 21 | │ Command │ 22 | ┌───│ (Async) │ 23 | │ │ │ 24 | │ │ │ 25 | │ └──────────────────┘ 26 | │ 27 | ┌──────────────────┐ │ ┌──────────────────┐ 28 | │ │ │ │ │ 29 | │ │ ┌───────────┐ │ │ │ 30 | │ │◀────────┤ Event ├────◀──┴───┤ │ 31 | │ │ └───────────┘ │ │ 32 | │ │ │ │ 33 | │ Core │ │ Subscriber │ 34 | │ │ │ │ 35 | │ │ │ │ 36 | │ ┌───────┐ │ ┌───────────┐ │ │ 37 | │ │ State │ ├─────────┤ State ├───────┬──▶│ │ 38 | │ └───────┘ │ └───────────┘ │ │ │ 39 | │ │ │ │ │ 40 | └──────────────────┘ │ └──────────────────┘ 41 | │ 42 | │ ┌──────────────────┐ 43 | │ │ │ 44 | │ │ │ 45 | └──▶│ Middleware │ 46 | │ │ 47 | │ │ 48 | └──────────────────┘ 49 | ``` 50 | 51 | There are six objects in the Reactor architecture: 52 | 53 | 1. The `State` object - A struct with properties representing application data. 54 | 1. The `Event` - Can trigger a state update. 55 | 1. The `Core` - Holds the application state and responsible for firing events. 56 | 1. The `Subscriber` - Often a view controller, listens for state updates. 57 | 1. The `Command` - A task that can asynchronously fire events. Useful for networking, working with databases, or any other asynchronous task. 58 | 1. `Middleware` - Receives every event and corresponding state. Useful for analytics, error handling, and other side effects. 59 | 60 | ## State 61 | 62 | State is anything that conforms to `State`. Here is an example: 63 | 64 | ```swift 65 | struct Player: State { 66 | var name: String 67 | var level: Int 68 | 69 | mutating func react(to event: Event) { 70 | switch event { 71 | case let _ as LevelUp: 72 | level += 1 73 | default: 74 | break 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | Here we have a simple `Player` model, which is state in our application. Obviously most application states are more complicated than this, but this is where composition comes into play: we can create state by composing states. 81 | 82 | ```swift 83 | struct RPGState: State { 84 | var player: Player 85 | var monsters: Monsters 86 | 87 | mutating func react(to event: Event) { 88 | player.react(to: event) 89 | monsters.react(to: event) 90 | } 91 | } 92 | ``` 93 | 94 | Parent states can react to events however they wish, although this will in most cases involve delegating to substates default behavior. 95 | 96 | Side note: does the sight of `mutating` make you feel impure? Have no fear, [`mutating` semantics on value types](http://chris.eidhof.nl/post/structs-and-mutation-in-swift/) here are actualy very safe in Swift, and it gives us an imperative look and feel, with the safety of functional programming. 97 | 98 | ## Events 99 | 100 | We've seen that an `Event` can change state. What does an `Event` look like? In it's most basic form, an event might look like this: 101 | 102 | ```swift 103 | struct LevelUp: Event {} 104 | ``` 105 | 106 | In other situations, you might want to pass some data along with the event. For example, in an application with more than one player we need to know which player is leveling up. 107 | 108 | ```swift 109 | struct LevelUp: Event { 110 | var playerID: Int 111 | } 112 | ``` 113 | 114 | For many events, generics work very nicely. 115 | 116 | ```swift 117 | struct Update: Event { 118 | var newValue: T 119 | } 120 | ``` 121 | 122 | ## The Core 123 | 124 | So, how does the state get events? Since the `Core` is responsible for all `State` changes, you can send events to the core which will in turn update the state by calling `react(to event: Event)` on the root state. You can create a shared global `Core` used by your entire application (my suggestion), or tediously pass the reference from object to object if you're a masochist. 125 | 126 | In order to initialize your core, simply call the `Core`'s constructor and pass in your initial state and any middleware (discussed later in this readme). Personally, I like to make my core a shared instance and namespace it inside an enum. 127 | 128 | ```swift 129 | enum App { 130 | static let sharedCore = Core(state: RPGState(), middlewares: [ 131 | ReachabilityMiddleware(), 132 | ErrorLogger(), 133 | AnalyticsMiddleware(), 134 | ]) 135 | } 136 | ``` 137 | 138 | Here is an example of a simple view controller with a label displaying our intrepid character's level, and a "Level Up" button. 139 | 140 | ```swift 141 | class PlayerViewController: UIViewController { 142 | var core = App.sharedCore 143 | @IBOutlet weak var levelLabel: UILabel! 144 | 145 | override func viewDidAppear(_ animated: Bool) { 146 | super.viewDidAppear(animated) 147 | core.add(subscriber: self) 148 | } 149 | 150 | override func viewDidDisappear(_ animated: Bool) { 151 | super.viewDidDisappear(animated) 152 | core.remove(subscriber: self) 153 | } 154 | 155 | @IBAction func didPressLevelUp() { 156 | core.fire(event: LevelUp()) 157 | } 158 | } 159 | 160 | extension ViewController: Reactor.Subscriber { 161 | func update(with state: RPGState) { 162 | levelLabel?.text = String(state.level) 163 | } 164 | } 165 | ``` 166 | 167 | By subscribing and subscribing in `viewDidAppear`/`viewDidDisappear` respectively, we ensure that whenever this view controller is visible it is up to date with the latest application state. Upon initial subscription, the core will send the latest state to the subscriber's `update` function. Button presses forward events back to the core, which will then update the state and result in subsequent calls to `update`. (note: the `Core` always dispatches back to the main thread when it updates subscribers, so it is safe to perform UI updates in `update`.) 168 | 169 | ## Commands 170 | 171 | Sometimes you want to fire an `Event` at a later point, for example after a network request, database query, or other asynchronous operation. In these cases, `Command` helps you interact with the `Core` in a safe and consistent way. 172 | 173 | ```swift 174 | struct CreatePlayer: Command { 175 | var session = URLSession.shared 176 | var player: Player 177 | 178 | func execute(state: RPGState, core: Core) { 179 | let task = session.dataTask(with: player.createRequest()) { data, response, error in 180 | // handle response appropriately 181 | // then fire an update back to the Core 182 | core.fire(event: AddPlayer(player: player)) 183 | } 184 | task.resume() 185 | } 186 | } 187 | 188 | // to fire a command 189 | core.fire(command: CreatePlayer(player: myNewPlayer)) 190 | ``` 191 | 192 | Commands get a copy of the current state, and a reference to the Core which allows them to fire Events as necessary. 193 | 194 | ## Middleware 195 | 196 | Sometimes you want to do something with an event besides just update application state. This is where `Middleware` comes into play. When you create a `Core`, along with the initial state, you may pass in an array of middleware. Each middleware gets called every time an event is fired. Middleware cannot mutate the state, but it does get a copy of the state along with the event. Middleware makes it easy to add things like logging, analytics, and error handling to an application. It is great for monitoring event and state and triggering side effects (for example: looking for HTTP 401 errors and then presenting a login screen). 197 | 198 | ```swift 199 | struct LoggingMiddleware: Middleware { 200 | func process(event: Event, state: State) { 201 | switch event { 202 | case _ as LevelUp: 203 | print("Leveled Up!") 204 | default: 205 | break 206 | } 207 | } 208 | } 209 | ``` 210 | 211 | ## Installation 212 | 213 | Follow the installation guides to integrate `Reactor`in your App. 214 | 215 | ### Swift Package Manager 216 | 217 | To integrate `Reactor` in your App using [Swift Package Manager](https://swift.org/package-manager/), specify it in your `Package.swift` file: 218 | 219 | ``` 220 | import PackageDescription 221 | 222 | let package = Package( 223 | [...] 224 | dependencies: [ 225 | .Package(url: "https://github.com/ReactorSwift/Reactor.git", majorVersion: XYZ) 226 | ] 227 | ) 228 | ``` 229 | 230 | ### Carthage 231 | 232 | To integrate `Reactor` in your App using [Carthage](https://github.com/Carthage/Carthage), specify it in your `Cartfile`: 233 | 234 | ``` 235 | github "ReactorSwift/Reactor" ~> X.Y.Z 236 | ``` 237 | 238 | Run `carthage update` to build the framework and drag the built Reactor.framework into your Xcode project. 239 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/ReactorTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/Reactor_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 | 1 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | OBJ_22 /* Reactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Reactor.swift */; }; 11 | OBJ_29 /* ReactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* ReactorTests.swift */; }; 12 | OBJ_31 /* Reactor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Reactor.framework */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXContainerItemProxy section */ 16 | BE92C9201DD8CD880061FC92 /* PBXContainerItemProxy */ = { 17 | isa = PBXContainerItemProxy; 18 | containerPortal = OBJ_1 /* Project object */; 19 | proxyType = 1; 20 | remoteGlobalIDString = OBJ_17; 21 | remoteInfo = Reactor; 22 | }; 23 | /* End PBXContainerItemProxy section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | OBJ_12 /* ReactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactorTests.swift; sourceTree = ""; }; 27 | OBJ_13 /* Logo */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Logo; sourceTree = SOURCE_ROOT; }; 28 | OBJ_15 /* Reactor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Reactor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | OBJ_16 /* ReactorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = ReactorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 31 | OBJ_9 /* Reactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reactor.swift; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | OBJ_23 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 0; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | OBJ_30 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 0; 45 | files = ( 46 | OBJ_31 /* Reactor.framework in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | OBJ_10 /* Tests */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | OBJ_11 /* ReactorTests */, 57 | ); 58 | path = Tests; 59 | sourceTree = ""; 60 | }; 61 | OBJ_11 /* ReactorTests */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | OBJ_12 /* ReactorTests.swift */, 65 | ); 66 | name = ReactorTests; 67 | path = Tests/ReactorTests; 68 | sourceTree = SOURCE_ROOT; 69 | }; 70 | OBJ_14 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | OBJ_15 /* Reactor.framework */, 74 | OBJ_16 /* ReactorTests.xctest */, 75 | ); 76 | name = Products; 77 | sourceTree = BUILT_PRODUCTS_DIR; 78 | }; 79 | OBJ_5 = { 80 | isa = PBXGroup; 81 | children = ( 82 | OBJ_6 /* Package.swift */, 83 | OBJ_7 /* Sources */, 84 | OBJ_10 /* Tests */, 85 | OBJ_13 /* Logo */, 86 | OBJ_14 /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | OBJ_7 /* Sources */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | OBJ_8 /* Reactor */, 94 | ); 95 | path = Sources; 96 | sourceTree = ""; 97 | }; 98 | OBJ_8 /* Reactor */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | OBJ_9 /* Reactor.swift */, 102 | ); 103 | name = Reactor; 104 | path = Sources; 105 | sourceTree = SOURCE_ROOT; 106 | }; 107 | /* End PBXGroup section */ 108 | 109 | /* Begin PBXNativeTarget section */ 110 | OBJ_17 /* Reactor */ = { 111 | isa = PBXNativeTarget; 112 | buildConfigurationList = OBJ_18 /* Build configuration list for PBXNativeTarget "Reactor" */; 113 | buildPhases = ( 114 | OBJ_21 /* Sources */, 115 | OBJ_23 /* Frameworks */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = Reactor; 122 | productName = Reactor; 123 | productReference = OBJ_15 /* Reactor.framework */; 124 | productType = "com.apple.product-type.framework"; 125 | }; 126 | OBJ_24 /* ReactorTests */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = OBJ_25 /* Build configuration list for PBXNativeTarget "ReactorTests" */; 129 | buildPhases = ( 130 | OBJ_28 /* Sources */, 131 | OBJ_30 /* Frameworks */, 132 | ); 133 | buildRules = ( 134 | ); 135 | dependencies = ( 136 | OBJ_32 /* PBXTargetDependency */, 137 | ); 138 | name = ReactorTests; 139 | productName = ReactorTests; 140 | productReference = OBJ_16 /* ReactorTests.xctest */; 141 | productType = "com.apple.product-type.bundle.unit-test"; 142 | }; 143 | /* End PBXNativeTarget section */ 144 | 145 | /* Begin PBXProject section */ 146 | OBJ_1 /* Project object */ = { 147 | isa = PBXProject; 148 | attributes = { 149 | LastUpgradeCheck = 1010; 150 | TargetAttributes = { 151 | OBJ_17 = { 152 | LastSwiftMigration = 1010; 153 | }; 154 | OBJ_24 = { 155 | LastSwiftMigration = 1010; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Reactor" */; 160 | compatibilityVersion = "Xcode 3.2"; 161 | developmentRegion = English; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | English, 165 | en, 166 | ); 167 | mainGroup = OBJ_5; 168 | productRefGroup = OBJ_14 /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | OBJ_17 /* Reactor */, 173 | OBJ_24 /* ReactorTests */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXSourcesBuildPhase section */ 179 | OBJ_21 /* Sources */ = { 180 | isa = PBXSourcesBuildPhase; 181 | buildActionMask = 0; 182 | files = ( 183 | OBJ_22 /* Reactor.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | OBJ_28 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 0; 190 | files = ( 191 | OBJ_29 /* ReactorTests.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin PBXTargetDependency section */ 198 | OBJ_32 /* PBXTargetDependency */ = { 199 | isa = PBXTargetDependency; 200 | target = OBJ_17 /* Reactor */; 201 | targetProxy = BE92C9201DD8CD880061FC92 /* PBXContainerItemProxy */; 202 | }; 203 | /* End PBXTargetDependency section */ 204 | 205 | /* Begin XCBuildConfiguration section */ 206 | OBJ_19 /* Debug */ = { 207 | isa = XCBuildConfiguration; 208 | buildSettings = { 209 | APPLICATION_EXTENSION_API_ONLY = YES; 210 | CLANG_ENABLE_OBJC_WEAK = YES; 211 | ENABLE_TESTABILITY = YES; 212 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 213 | HEADER_SEARCH_PATHS = ""; 214 | INFOPLIST_FILE = Reactor.xcodeproj/Reactor_Info.plist; 215 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 216 | OTHER_LDFLAGS = "$(inherited)"; 217 | OTHER_SWIFT_FLAGS = "$(inherited)"; 218 | PRODUCT_BUNDLE_IDENTIFIER = Reactor; 219 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 220 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 221 | RESOURCES_TARGETED_DEVICE_FAMILY = "1,2,3,4"; 222 | SKIP_INSTALL = YES; 223 | SUPPORTED_PLATFORMS = "macosx iphoneos appletvos watchos appletvsimulator iphonesimulator watchsimulator"; 224 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 225 | SWIFT_VERSION = 5.0; 226 | TARGET_NAME = Reactor; 227 | }; 228 | name = Debug; 229 | }; 230 | OBJ_20 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | APPLICATION_EXTENSION_API_ONLY = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | ENABLE_TESTABILITY = YES; 236 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 237 | HEADER_SEARCH_PATHS = ""; 238 | INFOPLIST_FILE = Reactor.xcodeproj/Reactor_Info.plist; 239 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 240 | OTHER_LDFLAGS = "$(inherited)"; 241 | OTHER_SWIFT_FLAGS = "$(inherited)"; 242 | PRODUCT_BUNDLE_IDENTIFIER = Reactor; 243 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 244 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 245 | RESOURCES_TARGETED_DEVICE_FAMILY = "1,2,3,4"; 246 | SKIP_INSTALL = YES; 247 | SUPPORTED_PLATFORMS = "macosx iphoneos appletvos watchos appletvsimulator iphonesimulator watchsimulator"; 248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 249 | SWIFT_VERSION = 5.0; 250 | TARGET_NAME = Reactor; 251 | }; 252 | name = Release; 253 | }; 254 | OBJ_26 /* Debug */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 258 | CLANG_ENABLE_OBJC_WEAK = YES; 259 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 260 | HEADER_SEARCH_PATHS = ""; 261 | INFOPLIST_FILE = Reactor.xcodeproj/ReactorTests_Info.plist; 262 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 263 | OTHER_LDFLAGS = "$(inherited)"; 264 | OTHER_SWIFT_FLAGS = "$(inherited)"; 265 | SUPPORTED_PLATFORMS = macosx; 266 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 267 | SWIFT_VERSION = 4.2; 268 | TARGET_NAME = ReactorTests; 269 | }; 270 | name = Debug; 271 | }; 272 | OBJ_27 /* Release */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 276 | CLANG_ENABLE_OBJC_WEAK = YES; 277 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 278 | HEADER_SEARCH_PATHS = ""; 279 | INFOPLIST_FILE = Reactor.xcodeproj/ReactorTests_Info.plist; 280 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 281 | OTHER_LDFLAGS = "$(inherited)"; 282 | OTHER_SWIFT_FLAGS = "$(inherited)"; 283 | SUPPORTED_PLATFORMS = macosx; 284 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 285 | SWIFT_VERSION = 4.2; 286 | TARGET_NAME = ReactorTests; 287 | }; 288 | name = Release; 289 | }; 290 | OBJ_3 /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_COMMA = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 306 | CLANG_WARN_STRICT_PROTOTYPES = YES; 307 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 308 | CLANG_WARN_UNREACHABLE_CODE = YES; 309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 310 | COMBINE_HIDPI_IMAGES = YES; 311 | COPY_PHASE_STRIP = NO; 312 | DEBUG_INFORMATION_FORMAT = dwarf; 313 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 314 | ENABLE_NS_ASSERTIONS = YES; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | ENABLE_TESTABILITY = YES; 317 | GCC_NO_COMMON_BLOCKS = YES; 318 | GCC_OPTIMIZATION_LEVEL = 0; 319 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 320 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 321 | GCC_WARN_UNDECLARED_SELECTOR = YES; 322 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 323 | GCC_WARN_UNUSED_FUNCTION = YES; 324 | GCC_WARN_UNUSED_VARIABLE = YES; 325 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 326 | MACOSX_DEPLOYMENT_TARGET = 10.9; 327 | ONLY_ACTIVE_ARCH = YES; 328 | OTHER_SWIFT_FLAGS = "-DXcode"; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 331 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 332 | TVOS_DEPLOYMENT_TARGET = 9.0; 333 | USE_HEADERMAP = NO; 334 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 335 | }; 336 | name = Debug; 337 | }; 338 | OBJ_4 /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_EMPTY_BODY = YES; 347 | CLANG_WARN_ENUM_CONVERSION = YES; 348 | CLANG_WARN_INFINITE_RECURSION = YES; 349 | CLANG_WARN_INT_CONVERSION = YES; 350 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 352 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 353 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 354 | CLANG_WARN_STRICT_PROTOTYPES = YES; 355 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 356 | CLANG_WARN_UNREACHABLE_CODE = YES; 357 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 358 | COMBINE_HIDPI_IMAGES = YES; 359 | COPY_PHASE_STRIP = YES; 360 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 361 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 362 | ENABLE_STRICT_OBJC_MSGSEND = YES; 363 | GCC_NO_COMMON_BLOCKS = YES; 364 | GCC_OPTIMIZATION_LEVEL = s; 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 372 | MACOSX_DEPLOYMENT_TARGET = 10.9; 373 | OTHER_SWIFT_FLAGS = "-DXcode"; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 377 | TVOS_DEPLOYMENT_TARGET = 9.0; 378 | USE_HEADERMAP = NO; 379 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 380 | }; 381 | name = Release; 382 | }; 383 | /* End XCBuildConfiguration section */ 384 | 385 | /* Begin XCConfigurationList section */ 386 | OBJ_18 /* Build configuration list for PBXNativeTarget "Reactor" */ = { 387 | isa = XCConfigurationList; 388 | buildConfigurations = ( 389 | OBJ_19 /* Debug */, 390 | OBJ_20 /* Release */, 391 | ); 392 | defaultConfigurationIsVisible = 0; 393 | defaultConfigurationName = Debug; 394 | }; 395 | OBJ_2 /* Build configuration list for PBXProject "Reactor" */ = { 396 | isa = XCConfigurationList; 397 | buildConfigurations = ( 398 | OBJ_3 /* Debug */, 399 | OBJ_4 /* Release */, 400 | ); 401 | defaultConfigurationIsVisible = 0; 402 | defaultConfigurationName = Debug; 403 | }; 404 | OBJ_25 /* Build configuration list for PBXNativeTarget "ReactorTests" */ = { 405 | isa = XCConfigurationList; 406 | buildConfigurations = ( 407 | OBJ_26 /* Debug */, 408 | OBJ_27 /* Release */, 409 | ); 410 | defaultConfigurationIsVisible = 0; 411 | defaultConfigurationName = Debug; 412 | }; 413 | /* End XCConfigurationList section */ 414 | }; 415 | rootObject = OBJ_1 /* Project object */; 416 | } 417 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/xcshareddata/xcschemes/Reactor.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Reactor.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | Reactor.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sources/Reactor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | // MARK: - State 5 | 6 | public protocol State { 7 | mutating func react(to event: Event) 8 | } 9 | 10 | 11 | // MARK: - Events 12 | 13 | public protocol Event {} 14 | 15 | 16 | // MARK: - Commands 17 | 18 | public protocol Command { 19 | associatedtype StateType: State 20 | func execute(state: StateType, core: Core) 21 | } 22 | 23 | 24 | // MARK: - Middlewares 25 | 26 | public protocol AnyMiddleware { 27 | func _process(event: Event, state: Any) 28 | } 29 | 30 | public protocol Middleware: AnyMiddleware { 31 | associatedtype StateType 32 | func process(event: Event, state: StateType) 33 | } 34 | 35 | extension Middleware { 36 | public func _process(event: Event, state: Any) { 37 | if let state = state as? StateType { 38 | process(event: event, state: state) 39 | } 40 | } 41 | } 42 | 43 | public struct Middlewares { 44 | private(set) var middleware: AnyMiddleware 45 | } 46 | 47 | 48 | // MARK: - Subscribers 49 | 50 | public protocol AnySubscriber: class { 51 | func _update(with state: Any) 52 | } 53 | 54 | public protocol Subscriber: AnySubscriber { 55 | associatedtype StateType 56 | func update(with state: StateType) 57 | } 58 | 59 | extension Subscriber { 60 | public func _update(with state: Any) { 61 | if let state = state as? StateType { 62 | update(with: state) 63 | } 64 | } 65 | } 66 | 67 | public struct Subscription { 68 | private(set) weak var subscriber: AnySubscriber? = nil 69 | let selector: ((StateType) -> Any)? 70 | let notifyQueue: DispatchQueue 71 | 72 | fileprivate func notify(with state: StateType) { 73 | notifyQueue.async { 74 | if let selector = self.selector { 75 | self.subscriber?._update(with: selector(state)) 76 | } else { 77 | self.subscriber?._update(with: state) 78 | } 79 | } 80 | } 81 | } 82 | 83 | 84 | 85 | // MARK: - Core 86 | 87 | public class Core { 88 | 89 | private let jobQueue: DispatchQueue 90 | private var subscriptions = [Subscription]() 91 | 92 | private let middlewares: [Middlewares] 93 | 94 | public private (set) var state: StateType { 95 | didSet { 96 | for subscription in self.subscriptions { 97 | subscription.notify(with: state) 98 | } 99 | } 100 | } 101 | 102 | public init(state: StateType, middlewares: [AnyMiddleware] = []) { 103 | self.state = state 104 | self.middlewares = middlewares.map(Middlewares.init) 105 | let qos: DispatchQoS 106 | if #available(macOS 10.10, *) { 107 | qos = .userInitiated 108 | } else { 109 | qos = .unspecified 110 | } 111 | self.jobQueue = DispatchQueue(label: "reactor.core.queue", qos: qos, attributes: []) 112 | } 113 | 114 | 115 | // MARK: - Subscriptions 116 | 117 | public func add(subscriber: AnySubscriber, notifyOnQueue queue: DispatchQueue? = DispatchQueue.main, selector: ((StateType) -> Any)? = nil) { 118 | jobQueue.async { 119 | guard !self.subscriptions.contains(where: {$0.subscriber === subscriber}) else { return } 120 | self.subscriptions = self.subscriptions.filter { $0.subscriber != nil } 121 | let subscription = Subscription(subscriber: subscriber, selector: selector, notifyQueue: queue ?? self.jobQueue) 122 | self.subscriptions.append(subscription) 123 | subscription.notify(with: self.state) 124 | } 125 | } 126 | 127 | public func remove(subscriber: AnySubscriber) { 128 | // sync to limit `nil` subscribers by ensuring they're removed before they `deinit`. 129 | jobQueue.sync { 130 | subscriptions = subscriptions.filter { $0.subscriber !== subscriber && $0.subscriber != nil } 131 | } 132 | } 133 | 134 | // MARK: - Events 135 | 136 | public func fire(event: Event) { 137 | jobQueue.async { 138 | self.state.react(to: event) 139 | self.middlewares.forEach { $0.middleware._process(event: event, state: self.state) } 140 | } 141 | } 142 | 143 | public func fire(command: C) where C.StateType == StateType { 144 | jobQueue.async { 145 | command.execute(state: self.state, core: self) 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactorSwift/Reactor/c2bbf5bfe722d617219156370b17cb0263641e3b/Tests/LinuxMain.swift -------------------------------------------------------------------------------- /Tests/ReactorTests/ReactorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Reactor 3 | 4 | // describe Reactor 5 | 6 | // when it has many clients 7 | class WhenReactorHasManyClients: XCTestCase { 8 | 9 | // it guarantees subscribers receive their own events 10 | func testReactorGuaranteesSubscribersReceiveTheirOwnEvents() { 11 | let core = Core(state: TestState()) 12 | 13 | var subscribers = [TestSubscriber]() 14 | for index in 0..<1000 { 15 | subscribers.append(TestSubscriber(id: index)) 16 | } 17 | let predicate = NSPredicate { _, _ -> Bool in 18 | return subscribers.reduce(true, { $0 && $1.received }) 19 | } 20 | let expected = expectation(for: predicate, evaluatedWith: NSObject()) 21 | 22 | let globalQueue: DispatchQueue 23 | if #available(macOS 10.10, *) { 24 | globalQueue = DispatchQueue.global() 25 | } else { 26 | globalQueue = DispatchQueue.global(priority: .default) 27 | } 28 | globalQueue.async { 29 | DispatchQueue.concurrentPerform(iterations: subscribers.count) { index in 30 | let subscriber = subscribers[index] 31 | let event = TestEvent(id: subscriber.id) 32 | core.add(subscriber: subscriber) 33 | core.fire(event: event) 34 | core.remove(subscriber: subscriber) 35 | } 36 | } 37 | 38 | wait(for: [expected], timeout: 10.0) 39 | XCTAssertEqual(subscribers.count, subscribers.reduce(0, { $0 + ($1.received ? 1 : 0)})) 40 | } 41 | 42 | // it guarantees subscribers receive their own commands 43 | func testReactorGuaranteesSubscribersReceiveTheirOwnCommands() { 44 | let core = Core(state: TestState()) 45 | 46 | var subscribers = [TestSubscriber]() 47 | for index in 0..<1000 { 48 | subscribers.append(TestSubscriber(id: index)) 49 | } 50 | let predicate = NSPredicate { _, _ -> Bool in 51 | return subscribers.reduce(true, { $0 && $1.received }) 52 | } 53 | let expected = expectation(for: predicate, evaluatedWith: NSObject()) 54 | 55 | let globalQueue: DispatchQueue 56 | if #available(macOS 10.10, *) { 57 | globalQueue = DispatchQueue.global() 58 | } else { 59 | globalQueue = DispatchQueue.global(priority: .default) 60 | } 61 | globalQueue.async { 62 | DispatchQueue.concurrentPerform(iterations: subscribers.count) { index in 63 | let subscriber = subscribers[index] 64 | let command = TestCommand(id: subscriber.id) 65 | core.add(subscriber: subscriber) 66 | core.fire(command: command) 67 | // don't remove them so that the async commands can run 68 | } 69 | } 70 | 71 | wait(for: [expected], timeout: 5.0) 72 | XCTAssertEqual(subscribers.count, subscribers.reduce(0, { $0 + ($1.received ? 1 : 0)})) 73 | print(subscribers.filter { !$0.received }.map { String(describing: $0) }.joined(separator: ", ")) 74 | } 75 | 76 | } 77 | 78 | 79 | struct TestState: State { 80 | 81 | var latest = -1 82 | 83 | mutating func react(to event: Event) { 84 | switch event { 85 | case let e as TestEvent: 86 | self.latest = e.id 87 | default: 88 | break 89 | } 90 | } 91 | 92 | } 93 | 94 | struct TestEvent: Event { 95 | let id: Int 96 | } 97 | 98 | struct TestCommand: Command { 99 | 100 | let id: Int 101 | 102 | func execute(state: TestState, core: Core) { 103 | core.fire(event: TestEvent(id: id)) 104 | } 105 | 106 | } 107 | 108 | class TestSubscriber: Subscriber { 109 | 110 | let id: Int 111 | var received = false 112 | 113 | init(id: Int) { 114 | self.id = id 115 | } 116 | 117 | func update(with state: TestState) { 118 | if state.latest == id { 119 | received = true 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '7' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: macos 5 | trigger_map: 6 | - push_branch: "*" 7 | workflow: primary 8 | - pull_request_source_branch: "*" 9 | workflow: primary 10 | workflows: 11 | deploy: 12 | steps: 13 | - activate-ssh-key@4.0.3: 14 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 15 | - git-clone@4.0.14: {} 16 | - cache-pull@2.0.1: {} 17 | - script@1.1.5: 18 | title: Do anything with Script step 19 | - certificate-and-profile-installer@1.10.1: {} 20 | - recreate-user-schemes@1.0.2: 21 | inputs: 22 | - project_path: "$BITRISE_PROJECT_PATH" 23 | - cocoapods-install@1.7.2: {} 24 | - xcode-test-mac@1.2.1: 25 | inputs: 26 | - project_path: "$BITRISE_PROJECT_PATH" 27 | - scheme: "$BITRISE_SCHEME" 28 | - xcode-archive-mac@1.6.2: 29 | inputs: 30 | - project_path: "$BITRISE_PROJECT_PATH" 31 | - scheme: "$BITRISE_SCHEME" 32 | - export_method: "$BITRISE_EXPORT_METHOD" 33 | - deploy-to-bitrise-io@1.3.19: {} 34 | - cache-push@2.0.5: {} 35 | primary: 36 | steps: 37 | - activate-ssh-key@4.0.3: 38 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 39 | - git-clone@4.0.14: {} 40 | - cache-pull@2.0.1: {} 41 | - script@1.1.5: 42 | title: Do anything with Script step 43 | - certificate-and-profile-installer@1.10.1: {} 44 | - recreate-user-schemes@1.0.2: 45 | inputs: 46 | - project_path: "$BITRISE_PROJECT_PATH" 47 | - xcode-test-mac@1.2.1: 48 | inputs: 49 | - project_path: "$BITRISE_PROJECT_PATH" 50 | - scheme: "$BITRISE_SCHEME" 51 | - deploy-to-bitrise-io@1.3.19: {} 52 | - cache-push@2.0.5: {} 53 | app: 54 | envs: 55 | - opts: 56 | is_expand: false 57 | BITRISE_PROJECT_PATH: "./Reactor.xcodeproj" 58 | - opts: 59 | is_expand: false 60 | BITRISE_SCHEME: Reactor 61 | - opts: 62 | is_expand: false 63 | BITRISE_EXPORT_METHOD: none 64 | --------------------------------------------------------------------------------