├── .gitignore ├── Cartfile ├── Cartfile.private ├── LICENSE ├── README.md ├── RxMultipeer Example ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Supporting Files │ └── Info.plist └── ViewController.swift ├── RxMultipeer Info.plist ├── RxMultipeer.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── RxMultipeer-tvOS.xcscheme │ └── RxMultipeer.xcscheme ├── RxMultipeer ├── Adapters │ ├── MockSession.swift │ └── MultipeerConnectivitySession.swift ├── Client.swift ├── MCSessionDelegateWrapper.h ├── MCSessionDelegateWrapper.m ├── ResourceState.swift ├── RxMultipeer-Bridging-Header.h ├── RxMultipeerError.swift ├── Session.swift ├── Supporting Files │ └── Info.plist └── Weak.swift ├── RxMultipeerTests ├── Fixtures │ └── Data.txt ├── IntegrationSpec.swift └── Supporting Files │ └── Info.plist └── Screenshots ├── BuildConfiguration-1.png ├── BuildConfiguration-2.png └── BuildConfiguration-3.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | # Pods - for those of you who use CocoaPods 20 | Pods 21 | Carthage 22 | Cartfile.resolved 23 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" ~> 3.0 2 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" ~> 0.10.0 2 | github "Quick/Nimble" "master" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nathan Kot 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A testable RxSwift wrapper around MultipeerConnectivity 2 | 3 | RxMultipeer is a [RxSwift][RxSwift] wrapper for MultipeerConnectivity. 4 | 5 | Using the adapter pattern, we can test multipeer code with heavy mocking. In effect, we are trying to isolate all the 6 | untestable bits of `MultipeerConnectivity` into one library. 7 | 8 | This library also gives you the flexibility to swap out the underlying mechanics of p2p with some other protocol such as 9 | websockets. At the moment it only comes with support for Apple's MultipeerConnectivity, however you can easily write 10 | your own adapters for different protocols. 11 | 12 | ## Installation 13 | 14 | #### Carthage 15 | 16 | Add this to your `Cartfile` 17 | 18 | ``` 19 | github "RxSwiftCommunity/RxMultipeer" ~> 3.0 20 | ``` 21 | 22 | ## Example code 23 | 24 | _For a working example check out the `RxMultipeer Example` folder._ 25 | 26 | #### Advertise and accept nearby peers 27 | 28 | ```swift 29 | import RxSwift 30 | import RxCocoa 31 | import RxMultipeer 32 | 33 | let disposeBag: DisposeBag 34 | let acceptButton: UIButton 35 | let client: CurrentClient 36 | 37 | client.startAdvertising() 38 | let connectionRequests = client.incomingConnections().shareReplay(1) 39 | 40 | acceptButton.rx_tap 41 | .withLatestFrom(connectionRequests) 42 | .subscribe(onNext: { (peer, context, respond) in respond(true) }) 43 | .addDisposableTo(disposeBag) 44 | 45 | client.incomingCertificateVerifications() 46 | .subscribe(onNext: { (peer, certificateChain, respond) in 47 | // Validate the certificateChain 48 | respond(true) 49 | }) 50 | .addDisposableTo(disposeBag) 51 | ``` 52 | 53 | #### Browse for and connect to peers 54 | 55 | ```swift 56 | import RxSwift 57 | import RxMultipeer 58 | 59 | let disposeBag: DisposeBag 60 | let client: CurrentClient 61 | 62 | client.startBrowsing() 63 | 64 | let nearbyPeers = client.nearbyPeers().shareReplay(1) 65 | 66 | // Attempt to connect to all peers 67 | nearbyPeers 68 | .map { (peers: [Client]) in 69 | peers.map { client.connect(toPeer: $0, context: ["Hello": "there"], timeout: 12) }.zip() 70 | } 71 | .subscribe() 72 | .addDisposableTo(disposeBag) 73 | ``` 74 | 75 | #### Sending and receiving strings 76 | 77 | Sending them: 78 | 79 | ```swift 80 | import RxSwift 81 | import RxCocoa 82 | import RxMultipeer 83 | 84 | let disposeBag: DisposeBag 85 | let client: CurrentClient 86 | let peer: Observable> 87 | let sendButton: UIButton 88 | 89 | sendButton.rx_tap 90 | .withLatestFrom(peer) 91 | .map { client.send(toPeer: peer, string: "Hello!") } 92 | .switchLatest() 93 | .subscribe(onNext: { _ in print("Message sent") }) 94 | .addDisposableTo(disposeBag) 95 | ``` 96 | 97 | And receiving them: 98 | 99 | ```swift 100 | import RxSwift 101 | import RxMultipeer 102 | 103 | let disposeBag: DisposeBag 104 | let client: CurrentClient 105 | 106 | client.receive() 107 | .subscribe(onNext: { (peer: Client, message: String) in 108 | print("got message \(message), from peer \(peer)") 109 | }) 110 | .addDisposableTo(disposeBag) 111 | ``` 112 | 113 | #### Establishing a data stream 114 | 115 | RxSwift makes sending streaming data to a persistent connection with another 116 | peer very intuitive. 117 | 118 | The sender: 119 | 120 | ```swift 121 | import RxSwift 122 | import RxMultipeer 123 | 124 | let disposeBag: DisposeBag 125 | let client: CurrentClient 126 | let peer: Observable> 127 | let queuedMessages: Observable<[UInt8]> 128 | 129 | let pipe = peer.map { client.send(toPeer: peer, streamName: "data.stream") } 130 | pipe.withLatestFrom(queuedMessages) { $0 } 131 | .subscribe(onNext: { (sender, message) in sender(message) }) 132 | .addDisposableTo(disposeBag) 133 | ``` 134 | 135 | The receiver: 136 | 137 | ```swift 138 | import RxSwift 139 | import RxMultipeer 140 | 141 | let disposeBag: DisposeBag 142 | let client: CurrentClient 143 | let peer: Observable> 144 | 145 | let incomingData = client.receive(fromPeer: peer, streamName: "data.stream").shareReplay(1) 146 | incomingData 147 | .subscribe(onNext: { (data) in print(data) }) 148 | .addDisposableTo(disposeBag) 149 | ``` 150 | 151 | ## Usage 152 | 153 | #### Imports 154 | 155 | ```swift 156 | import RxSwift 157 | import RxMultipeer 158 | ``` 159 | 160 | #### Make a new build configuration for testing 161 | 162 | Your project comes with `Debug` and `Release` build configurations by default, we need to make a new one called 163 | `Testing`. [Please check here for step-by-step instructions][buildconfig]. 164 | 165 | #### Setting up the client 166 | 167 | ```swift 168 | // See the link above, 169 | // You'll need to define a new build configuration and give it the `TESTING` flag 170 | let name = UIDevice.currentDevice().name 171 | #if TESTING 172 | typealias I = MockIden 173 | let client = CurrentClient(session: MockSession(name: name)) 174 | #else 175 | typealias I = MCPeerID 176 | let client = CurrentClient(session: MultipeerConnectivitySession( 177 | displayName: name, 178 | serviceType: "multipeerex", 179 | idenCacheKey: "com.rxmultipeer.example.mcpeerid", 180 | encryptionPreference: .None)) 181 | #endif 182 | ``` 183 | 184 | ## Supported transfer resource types 185 | 186 | #### String 187 | 188 | ```swift 189 | func send(toPeer: Client, string: String, mode: MCSessionSendDataMode = .Reliable) -> Observable<()> 190 | func receive() -> Observable<(Client, String)> 191 | ``` 192 | 193 | #### Data 194 | 195 | ```swift 196 | func send(toPeer: Client, data: Data, mode: MCSessionSendDataMode = .Reliable) -> Observable<()> 197 | func receive() -> Observable<(Client, Data)> 198 | ``` 199 | 200 | #### JSON 201 | 202 | ```swift 203 | func send(toPeer: Client, json: [String: Any], mode: MCSessionSendDataMode = .Reliable) -> Observable<()> 204 | func receive() -> Observable<(Client, [String: Any])> 205 | ``` 206 | 207 | #### NSURL 208 | 209 | ```swift 210 | func send(toPeer: Client, name: String, url: NSURL, mode: MCSessionSendDataMode = .Reliable) -> Observable 211 | func receive() -> Observable<(Client, String, ResourceState)> 212 | ``` 213 | 214 | #### NSStream 215 | 216 | ```swift 217 | func send(toPeer: Client, streamName: String, runLoop: NSRunLoop = NSRunLoop.mainRunLoop()) -> Observable<([UInt8]) -> Void> 218 | func receive(fromPeer: Client, streamName: String, runLoop: NSRunLoop = NSRunLoop.mainRunLoop(), maxLength: Int = 512) -> Observable<[UInt8]> 219 | ``` 220 | 221 | ## Testing 222 | 223 | When testing, use preprocesser macros to ensure that your code uses a `MockSession` instance instead of 224 | `MultipeerConnectivitySession` one. In order to achieve this you need to use preprocessor flags and swap out anywhere 225 | that references `Client` (because `T` will be different depending on whether you are testing or not.) First you will 226 | need to [set up a new build configuration][buildconfig], and then you can use preprocessor macros like so: 227 | 228 | ```swift 229 | let name = UIDevice.currentDevice().name 230 | #if TESTING 231 | typealias I = MockIden 232 | let client = CurrentClient(session: MockSession(name: name)) 233 | #else 234 | typealias I = MCPeerID 235 | let client = CurrentClient(session: MultipeerConnectivitySession( 236 | displayName: name, 237 | serviceType: "multipeerex", 238 | idenCacheKey: "com.rxmultipeer.example.mcpeerid", 239 | encryptionPreference: .None)) 240 | #endif 241 | ``` 242 | 243 | Don't worry, you should only really need preprocessor macros in one centralized place, the type of your client can be 244 | inferred by the compiler thereafter. 245 | 246 | Mocking other nearby peers in the test environment then becomes as simple as creating other `CurrentClient(session: 247 | MockSession(name: "other"))`. For example, if your app is running in a testing environment the following code will mock 248 | a nearby client: 249 | 250 | ```swift 251 | let disposeBag: DisposeBag 252 | let otherclient = CurrentClient(session: MockSession(name: "mockedother")) 253 | 254 | // Accept all connections 255 | otherclient.startAdvertising() 256 | 257 | otherclient.incomingConnections() 258 | .subscribeNext { (client, context, respond) in respond(true) } 259 | .addDisposableTo(disposeBag) 260 | 261 | // Starting from version 3.0.0 the following handler needs to be implemented 262 | // for this library to work: 263 | otherclient.incomingCertificateVerifications() 264 | .subscribeNext { (client, certificateChain, respond) in respond(true) } 265 | .addDisposableTo(disposeBag) 266 | 267 | // Respond to all messages with 'Roger' 268 | otherclient.receive() 269 | .map { (client: Client, string: String) in otherclient.send(toPeer: client, "Roger") } 270 | .concat() 271 | .subscribeNext { _ in print("Response sent") } 272 | .addDisposableTo(disposeBag) 273 | ``` 274 | 275 | ## Breaking changes 276 | 277 | ### Version 3.0.0 278 | 279 | Starting from version `3.0.0`, `incomingCertificateVerifications() -> Observable<(MCPeerID, [Any]?, (Bool) -> Void)>` 280 | was introduced. This needs to be implemented in order for mock and real 281 | connections to work. Behaviour prior to this update can be reproduced by simply 282 | accepting all certificates: 283 | 284 | ``` 285 | let client: CurrentClient Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /RxMultipeer Example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /RxMultipeer Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 | 36 | 42 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 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 | -------------------------------------------------------------------------------- /RxMultipeer Example/Images.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 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /RxMultipeer Example/Supporting Files/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 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /RxMultipeer Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RxMultipeer Example 4 | // 5 | // Created by Nathan Kot on 5/08/15. 6 | // Copyright (c) 2015 Nathan Kot. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxMultipeer 11 | import MultipeerConnectivity 12 | import RxSwift 13 | import RxCocoa 14 | 15 | class ViewController: UIViewController { 16 | 17 | typealias I = MCPeerID 18 | 19 | @IBOutlet weak var advertiseButton: UIButton! 20 | @IBOutlet weak var browseButton: UIButton! 21 | @IBOutlet weak var yoButton: UIButton! 22 | @IBOutlet weak var outputButton: UIButton! 23 | @IBOutlet weak var disconnectButton: UIButton! 24 | 25 | var disposeBag = DisposeBag() 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | let name = UIDevice.current.name 31 | print("\(name): Loading") 32 | 33 | let client = CurrentClient( 34 | session: MultipeerConnectivitySession( 35 | displayName: name, 36 | serviceType: "multipeerex", 37 | encryptionPreference: .none)) 38 | 39 | let other = client.connectedPeer().shareReplay(1) 40 | 41 | advertiseButton.rx.tap 42 | .subscribe(onNext: { 43 | print("\(name): begin advertising") 44 | client.stopBrowsing() 45 | client.startAdvertising() 46 | }) 47 | .addDisposableTo(disposeBag) 48 | 49 | browseButton.rx.tap 50 | .subscribe(onNext: { 51 | print("\(name): begin browsing") 52 | client.stopAdvertising() 53 | client.startBrowsing() 54 | }) 55 | .addDisposableTo(disposeBag) 56 | 57 | disconnectButton.rx.tap 58 | .subscribe(onNext: { 59 | print("\(name): disconnecting") 60 | client.disconnect() 61 | }) 62 | .addDisposableTo(disposeBag) 63 | 64 | yoButton.rx.tap 65 | .withLatestFrom(client.connections()) 66 | .map { (cs: [Client]) -> Observable> in 67 | Observable.concat(cs.map { Observable.just($0) }) } 68 | .merge() 69 | .map { (c: Client) -> Observable<()> in 70 | print("\(name): sending yo to \(c.iden)") 71 | return client.send(toPeer: c, string: "yo") 72 | } 73 | .merge() 74 | .subscribe(onNext: { _ in }) 75 | .addDisposableTo(disposeBag) 76 | 77 | Observable.combineLatest(client.connections(), 78 | client.nearbyPeers()) { (connections, nearby) in 79 | return nearby.filter { (p, _) in 80 | connections.map { $0.iden }.index(of: p.iden) == nil 81 | } 82 | } 83 | .subscribe(onNext: { 84 | print("\(name): there are \($0.count) devices nearby") 85 | for p in $0 { 86 | print("\(name): connecting to \(p.0.iden)") 87 | client.connect(toPeer: p.0) 88 | } 89 | }) 90 | .addDisposableTo(disposeBag) 91 | 92 | // Just accept everything 93 | client.incomingConnections() 94 | .subscribe(onNext: { (_, _, respond) in respond(true) }) 95 | .addDisposableTo(disposeBag) 96 | 97 | client.incomingCertificateVerifications() 98 | .subscribe(onNext: { (_, _, respond) in respond(true) }) 99 | .addDisposableTo(disposeBag) 100 | 101 | // Logging 102 | other 103 | .subscribe(onNext: { print("\(name): \($0.iden) successfully connected") }) 104 | .addDisposableTo(disposeBag) 105 | 106 | client.disconnectedPeer() 107 | .subscribe(onNext: { print("\(name): \($0.iden) disconnected") }) 108 | .addDisposableTo(disposeBag) 109 | 110 | client.receive() 111 | .subscribe(onNext: { (c, m: String) in print("\(name): received message '\(m)'") }) 112 | .addDisposableTo(disposeBag) 113 | 114 | let stream = other 115 | .map { client.send(toPeer: $0, streamName: "hellothere") } 116 | .debug() 117 | .switchLatest() 118 | .shareReplay(1) 119 | 120 | outputButton.rx.tap 121 | .do(onNext: { _ in print("Attempting to send stream output") }) 122 | .withLatestFrom(stream) 123 | .subscribe(onNext: { fetcher in fetcher([0x00, 0x89]) }) 124 | .addDisposableTo(disposeBag) 125 | 126 | other.map { client.receive(fromPeer: $0, streamName: "hellothere") } 127 | .switchLatest() 128 | .debug() 129 | .subscribe(onNext: { (d: [UInt8]) in 130 | print("Received stream data: \(d)") 131 | }) 132 | .addDisposableTo(disposeBag) 133 | } 134 | 135 | override func didReceiveMemoryWarning() { 136 | super.didReceiveMemoryWarning() 137 | // Dispose of any resources that can be recreated. 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /RxMultipeer 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 | -------------------------------------------------------------------------------- /RxMultipeer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13CBFDD61E7973E700C06615 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C860B1B6F308600CF42CF /* Client.swift */; }; 11 | 13CBFDD71E7973E700C06615 /* MultipeerConnectivitySession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D4C1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift */; }; 12 | 13CBFDD81E7973E700C06615 /* ResourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D481B71A9EB00EF5DE6 /* ResourceState.swift */; }; 13 | 13CBFDD91E7973E700C06615 /* RxMultipeerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80EC2F31BBF372B006BF124 /* RxMultipeerError.swift */; }; 14 | 13CBFDDA1E7973E700C06615 /* MockSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C86151B7060E000CF42CF /* MockSession.swift */; }; 15 | 13CBFDDB1E7973E700C06615 /* MCSessionDelegateWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D823DDB11E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m */; }; 16 | 13CBFDDC1E7973E700C06615 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C86111B6F4F3700CF42CF /* Session.swift */; }; 17 | 13CBFDDD1E7973E700C06615 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8327BD31BD08FE9009BC6FB /* Weak.swift */; }; 18 | 13CBFDDF1E7973E700C06615 /* MultipeerConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D88C86071B6F2D9000CF42CF /* MultipeerConnectivity.framework */; }; 19 | 13CBFDE31E7973E700C06615 /* MCSessionDelegateWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D823DDAF1E41E8B8001C3ED7 /* MCSessionDelegateWrapper.h */; }; 20 | 13CBFDF31E797DB200C06615 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13CBFDEC1E797C8800C06615 /* RxCocoa.framework */; }; 21 | 13CBFDF41E797DB200C06615 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13CBFDED1E797C8800C06615 /* RxSwift.framework */; }; 22 | D80EC2F41BBF372B006BF124 /* RxMultipeerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80EC2F31BBF372B006BF124 /* RxMultipeerError.swift */; }; 23 | D813DA421BB1BA1000BEB501 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA401BB1BA1000BEB501 /* RxCocoa.framework */; }; 24 | D813DA431BB1BA1000BEB501 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA411BB1BA1000BEB501 /* RxSwift.framework */; }; 25 | D813DA491BB1BADE00BEB501 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA471BB1BADE00BEB501 /* Nimble.framework */; }; 26 | D813DA4A1BB1BADE00BEB501 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA481BB1BADE00BEB501 /* Quick.framework */; }; 27 | D8236D491B71A9EB00EF5DE6 /* ResourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D481B71A9EB00EF5DE6 /* ResourceState.swift */; }; 28 | D8236D4D1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D4C1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift */; }; 29 | D8236D571B71EA7800EF5DE6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D561B71EA7800EF5DE6 /* AppDelegate.swift */; }; 30 | D8236D591B71EA7800EF5DE6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8236D581B71EA7800EF5DE6 /* ViewController.swift */; }; 31 | D8236D5C1B71EA7800EF5DE6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D8236D5A1B71EA7800EF5DE6 /* Main.storyboard */; }; 32 | D8236D5E1B71EA7800EF5DE6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D8236D5D1B71EA7800EF5DE6 /* Images.xcassets */; }; 33 | D8236D611B71EA7800EF5DE6 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = D8236D5F1B71EA7800EF5DE6 /* LaunchScreen.xib */; }; 34 | D823DDB01E41E8BC001C3ED7 /* MCSessionDelegateWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D823DDAF1E41E8B8001C3ED7 /* MCSessionDelegateWrapper.h */; }; 35 | D823DDB21E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D823DDB11E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m */; }; 36 | D82568571C69498C005ADB0F /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA401BB1BA1000BEB501 /* RxCocoa.framework */; }; 37 | D82568581C69498C005ADB0F /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D813DA411BB1BA1000BEB501 /* RxSwift.framework */; }; 38 | D82E19E01C87F2040021194C /* RxMultipeer.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = D88C85EB1B6F2B1700CF42CF /* RxMultipeer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 39 | D8327BD41BD08FE9009BC6FB /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8327BD31BD08FE9009BC6FB /* Weak.swift */; }; 40 | D86675A21C6B3F1D005A9A72 /* RxBlocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D86675A01C6B3E10005A9A72 /* RxBlocking.framework */; }; 41 | D86675A51C6B40FD005A9A72 /* Data.txt in Resources */ = {isa = PBXBuildFile; fileRef = D86675A41C6B40FD005A9A72 /* Data.txt */; }; 42 | D881836B1FF3EB5C00DCA3D2 /* RxMultipeer-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = D823DDAC1E41E61A001C3ED7 /* RxMultipeer-Bridging-Header.h */; }; 43 | D881836C1FF3EB5C00DCA3D2 /* RxMultipeer-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = D823DDAC1E41E61A001C3ED7 /* RxMultipeer-Bridging-Header.h */; }; 44 | D88C85F71B6F2B1700CF42CF /* RxMultipeer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D88C85EB1B6F2B1700CF42CF /* RxMultipeer.framework */; }; 45 | D88C86081B6F2D9000CF42CF /* MultipeerConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D88C86071B6F2D9000CF42CF /* MultipeerConnectivity.framework */; }; 46 | D88C860A1B6F307700CF42CF /* IntegrationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C86091B6F307700CF42CF /* IntegrationSpec.swift */; }; 47 | D88C860C1B6F308600CF42CF /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C860B1B6F308600CF42CF /* Client.swift */; }; 48 | D88C86131B6F4F3700CF42CF /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C86111B6F4F3700CF42CF /* Session.swift */; }; 49 | D88C86161B7060E000CF42CF /* MockSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88C86151B7060E000CF42CF /* MockSession.swift */; }; 50 | /* End PBXBuildFile section */ 51 | 52 | /* Begin PBXContainerItemProxy section */ 53 | D825685B1C69506C005ADB0F /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = D88C85E21B6F2B1700CF42CF /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = D88C85EA1B6F2B1700CF42CF; 58 | remoteInfo = RxMultipeer; 59 | }; 60 | D88C85F81B6F2B1700CF42CF /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = D88C85E21B6F2B1700CF42CF /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = D88C85EA1B6F2B1700CF42CF; 65 | remoteInfo = RxMultipeer; 66 | }; 67 | /* End PBXContainerItemProxy section */ 68 | 69 | /* Begin PBXCopyFilesBuildPhase section */ 70 | D82E19DF1C87F1F90021194C /* CopyFiles */ = { 71 | isa = PBXCopyFilesBuildPhase; 72 | buildActionMask = 12; 73 | dstPath = ""; 74 | dstSubfolderSpec = 10; 75 | files = ( 76 | D82E19E01C87F2040021194C /* RxMultipeer.framework in CopyFiles */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXCopyFilesBuildPhase section */ 81 | 82 | /* Begin PBXFileReference section */ 83 | 13CBFDE91E7973E700C06615 /* RxMultipeer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxMultipeer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | 13CBFDEC1E797C8800C06615 /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/tvOS/RxCocoa.framework; sourceTree = ""; }; 85 | 13CBFDED1E797C8800C06615 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/tvOS/RxSwift.framework; sourceTree = ""; }; 86 | D80EC2F31BBF372B006BF124 /* RxMultipeerError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxMultipeerError.swift; sourceTree = ""; }; 87 | D813DA3C1BB1BA0900BEB501 /* RxSwift.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RxSwift.framework.dSYM; path = Carthage/Build/iOS/RxSwift.framework.dSYM; sourceTree = ""; }; 88 | D813DA3D1BB1BA0900BEB501 /* RxCocoa.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RxCocoa.framework.dSYM; path = Carthage/Build/iOS/RxCocoa.framework.dSYM; sourceTree = ""; }; 89 | D813DA401BB1BA1000BEB501 /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 90 | D813DA411BB1BA1000BEB501 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/iOS/RxSwift.framework; sourceTree = ""; }; 91 | D813DA471BB1BADE00BEB501 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 92 | D813DA481BB1BADE00BEB501 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; 93 | D8236D481B71A9EB00EF5DE6 /* ResourceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceState.swift; sourceTree = ""; }; 94 | D8236D4C1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipeerConnectivitySession.swift; sourceTree = ""; }; 95 | D8236D521B71EA7800EF5DE6 /* RxMultipeer Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RxMultipeer Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 96 | D8236D551B71EA7800EF5DE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 97 | D8236D561B71EA7800EF5DE6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 98 | D8236D581B71EA7800EF5DE6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 99 | D8236D5B1B71EA7800EF5DE6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 100 | D8236D5D1B71EA7800EF5DE6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 101 | D8236D601B71EA7800EF5DE6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 102 | D823DDAC1E41E61A001C3ED7 /* RxMultipeer-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RxMultipeer-Bridging-Header.h"; sourceTree = ""; }; 103 | D823DDAF1E41E8B8001C3ED7 /* MCSessionDelegateWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCSessionDelegateWrapper.h; sourceTree = ""; }; 104 | D823DDB11E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCSessionDelegateWrapper.m; sourceTree = ""; }; 105 | D8327BD31BD08FE9009BC6FB /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; 106 | D86675A01C6B3E10005A9A72 /* RxBlocking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxBlocking.framework; path = Carthage/Build/iOS/RxBlocking.framework; sourceTree = ""; }; 107 | D86675A41C6B40FD005A9A72 /* Data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Data.txt; sourceTree = ""; }; 108 | D88C85EB1B6F2B1700CF42CF /* RxMultipeer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxMultipeer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | D88C85EF1B6F2B1700CF42CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 110 | D88C85F61B6F2B1700CF42CF /* RxMultipeerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RxMultipeerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 111 | D88C85FC1B6F2B1700CF42CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 112 | D88C86071B6F2D9000CF42CF /* MultipeerConnectivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MultipeerConnectivity.framework; path = System/Library/Frameworks/MultipeerConnectivity.framework; sourceTree = SDKROOT; }; 113 | D88C86091B6F307700CF42CF /* IntegrationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationSpec.swift; sourceTree = ""; }; 114 | D88C860B1B6F308600CF42CF /* Client.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 115 | D88C86111B6F4F3700CF42CF /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 116 | D88C86151B7060E000CF42CF /* MockSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSession.swift; sourceTree = ""; }; 117 | /* End PBXFileReference section */ 118 | 119 | /* Begin PBXFrameworksBuildPhase section */ 120 | 13CBFDDE1E7973E700C06615 /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 13CBFDDF1E7973E700C06615 /* MultipeerConnectivity.framework in Frameworks */, 125 | 13CBFDF31E797DB200C06615 /* RxCocoa.framework in Frameworks */, 126 | 13CBFDF41E797DB200C06615 /* RxSwift.framework in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | D8236D4F1B71EA7800EF5DE6 /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | D82568571C69498C005ADB0F /* RxCocoa.framework in Frameworks */, 135 | D82568581C69498C005ADB0F /* RxSwift.framework in Frameworks */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | D88C85E71B6F2B1700CF42CF /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | D88C86081B6F2D9000CF42CF /* MultipeerConnectivity.framework in Frameworks */, 144 | D813DA421BB1BA1000BEB501 /* RxCocoa.framework in Frameworks */, 145 | D813DA431BB1BA1000BEB501 /* RxSwift.framework in Frameworks */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | D88C85F31B6F2B1700CF42CF /* Frameworks */ = { 150 | isa = PBXFrameworksBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | D88C85F71B6F2B1700CF42CF /* RxMultipeer.framework in Frameworks */, 154 | D813DA491BB1BADE00BEB501 /* Nimble.framework in Frameworks */, 155 | D813DA4A1BB1BADE00BEB501 /* Quick.framework in Frameworks */, 156 | D86675A21C6B3F1D005A9A72 /* RxBlocking.framework in Frameworks */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXFrameworksBuildPhase section */ 161 | 162 | /* Begin PBXGroup section */ 163 | 6A362B832317E525259BEBB4 /* Frameworks */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | 13CBFDEC1E797C8800C06615 /* RxCocoa.framework */, 167 | 13CBFDED1E797C8800C06615 /* RxSwift.framework */, 168 | D86675A01C6B3E10005A9A72 /* RxBlocking.framework */, 169 | D813DA471BB1BADE00BEB501 /* Nimble.framework */, 170 | D813DA481BB1BADE00BEB501 /* Quick.framework */, 171 | D813DA401BB1BA1000BEB501 /* RxCocoa.framework */, 172 | D813DA411BB1BA1000BEB501 /* RxSwift.framework */, 173 | D813DA3C1BB1BA0900BEB501 /* RxSwift.framework.dSYM */, 174 | D813DA3D1BB1BA0900BEB501 /* RxCocoa.framework.dSYM */, 175 | D88C86071B6F2D9000CF42CF /* MultipeerConnectivity.framework */, 176 | ); 177 | name = Frameworks; 178 | sourceTree = ""; 179 | }; 180 | D8236D531B71EA7800EF5DE6 /* RxMultipeer Example */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | D8236D541B71EA7800EF5DE6 /* Supporting Files */, 184 | D8236D561B71EA7800EF5DE6 /* AppDelegate.swift */, 185 | D8236D5D1B71EA7800EF5DE6 /* Images.xcassets */, 186 | D8236D5F1B71EA7800EF5DE6 /* LaunchScreen.xib */, 187 | D8236D5A1B71EA7800EF5DE6 /* Main.storyboard */, 188 | D8236D581B71EA7800EF5DE6 /* ViewController.swift */, 189 | ); 190 | path = "RxMultipeer Example"; 191 | sourceTree = ""; 192 | }; 193 | D8236D541B71EA7800EF5DE6 /* Supporting Files */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | D8236D551B71EA7800EF5DE6 /* Info.plist */, 197 | ); 198 | path = "Supporting Files"; 199 | sourceTree = ""; 200 | }; 201 | D86675A31C6B40FD005A9A72 /* Fixtures */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | D86675A41C6B40FD005A9A72 /* Data.txt */, 205 | ); 206 | path = Fixtures; 207 | sourceTree = ""; 208 | }; 209 | D88C85E11B6F2B1700CF42CF = { 210 | isa = PBXGroup; 211 | children = ( 212 | 6A362B832317E525259BEBB4 /* Frameworks */, 213 | D88C85ED1B6F2B1700CF42CF /* RxMultipeer */, 214 | D8236D531B71EA7800EF5DE6 /* RxMultipeer Example */, 215 | D88C85FA1B6F2B1700CF42CF /* RxMultipeerTests */, 216 | D88C85EC1B6F2B1700CF42CF /* Products */, 217 | ); 218 | sourceTree = ""; 219 | }; 220 | D88C85EC1B6F2B1700CF42CF /* Products */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | D88C85EB1B6F2B1700CF42CF /* RxMultipeer.framework */, 224 | D88C85F61B6F2B1700CF42CF /* RxMultipeerTests.xctest */, 225 | D8236D521B71EA7800EF5DE6 /* RxMultipeer Example.app */, 226 | 13CBFDE91E7973E700C06615 /* RxMultipeer.framework */, 227 | ); 228 | name = Products; 229 | sourceTree = ""; 230 | }; 231 | D88C85ED1B6F2B1700CF42CF /* RxMultipeer */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | D88C86141B7060E000CF42CF /* Adapters */, 235 | D88C85EE1B6F2B1700CF42CF /* Supporting Files */, 236 | D88C860B1B6F308600CF42CF /* Client.swift */, 237 | D80EC2F31BBF372B006BF124 /* RxMultipeerError.swift */, 238 | D8236D481B71A9EB00EF5DE6 /* ResourceState.swift */, 239 | D88C86111B6F4F3700CF42CF /* Session.swift */, 240 | D8327BD31BD08FE9009BC6FB /* Weak.swift */, 241 | D823DDAC1E41E61A001C3ED7 /* RxMultipeer-Bridging-Header.h */, 242 | D823DDAF1E41E8B8001C3ED7 /* MCSessionDelegateWrapper.h */, 243 | D823DDB11E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m */, 244 | ); 245 | path = RxMultipeer; 246 | sourceTree = ""; 247 | }; 248 | D88C85EE1B6F2B1700CF42CF /* Supporting Files */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | D88C85EF1B6F2B1700CF42CF /* Info.plist */, 252 | ); 253 | path = "Supporting Files"; 254 | sourceTree = ""; 255 | }; 256 | D88C85FA1B6F2B1700CF42CF /* RxMultipeerTests */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | D86675A31C6B40FD005A9A72 /* Fixtures */, 260 | D88C85FB1B6F2B1700CF42CF /* Supporting Files */, 261 | D88C86091B6F307700CF42CF /* IntegrationSpec.swift */, 262 | ); 263 | path = RxMultipeerTests; 264 | sourceTree = ""; 265 | }; 266 | D88C85FB1B6F2B1700CF42CF /* Supporting Files */ = { 267 | isa = PBXGroup; 268 | children = ( 269 | D88C85FC1B6F2B1700CF42CF /* Info.plist */, 270 | ); 271 | path = "Supporting Files"; 272 | sourceTree = ""; 273 | }; 274 | D88C86141B7060E000CF42CF /* Adapters */ = { 275 | isa = PBXGroup; 276 | children = ( 277 | D88C86151B7060E000CF42CF /* MockSession.swift */, 278 | D8236D4C1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift */, 279 | ); 280 | path = Adapters; 281 | sourceTree = ""; 282 | }; 283 | /* End PBXGroup section */ 284 | 285 | /* Begin PBXHeadersBuildPhase section */ 286 | 13CBFDE21E7973E700C06615 /* Headers */ = { 287 | isa = PBXHeadersBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | 13CBFDE31E7973E700C06615 /* MCSessionDelegateWrapper.h in Headers */, 291 | D881836B1FF3EB5C00DCA3D2 /* RxMultipeer-Bridging-Header.h in Headers */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | D88C85E81B6F2B1700CF42CF /* Headers */ = { 296 | isa = PBXHeadersBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | D823DDB01E41E8BC001C3ED7 /* MCSessionDelegateWrapper.h in Headers */, 300 | D881836C1FF3EB5C00DCA3D2 /* RxMultipeer-Bridging-Header.h in Headers */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | /* End PBXHeadersBuildPhase section */ 305 | 306 | /* Begin PBXNativeTarget section */ 307 | 13CBFDD41E7973E700C06615 /* RxMultipeer-tvOS */ = { 308 | isa = PBXNativeTarget; 309 | buildConfigurationList = 13CBFDE61E7973E700C06615 /* Build configuration list for PBXNativeTarget "RxMultipeer-tvOS" */; 310 | buildPhases = ( 311 | 13CBFDD51E7973E700C06615 /* Sources */, 312 | 13CBFDDE1E7973E700C06615 /* Frameworks */, 313 | 13CBFDE21E7973E700C06615 /* Headers */, 314 | 13CBFDE51E7973E700C06615 /* Resources */, 315 | ); 316 | buildRules = ( 317 | ); 318 | dependencies = ( 319 | ); 320 | name = "RxMultipeer-tvOS"; 321 | productName = RxMultipeer; 322 | productReference = 13CBFDE91E7973E700C06615 /* RxMultipeer.framework */; 323 | productType = "com.apple.product-type.framework"; 324 | }; 325 | D8236D511B71EA7800EF5DE6 /* RxMultipeer Example */ = { 326 | isa = PBXNativeTarget; 327 | buildConfigurationList = D8236D721B71EA7800EF5DE6 /* Build configuration list for PBXNativeTarget "RxMultipeer Example" */; 328 | buildPhases = ( 329 | D8236D4E1B71EA7800EF5DE6 /* Sources */, 330 | D8236D4F1B71EA7800EF5DE6 /* Frameworks */, 331 | D8236D501B71EA7800EF5DE6 /* Resources */, 332 | D825685A1C6949DA005ADB0F /* Copy Frameworks */, 333 | D82E19DF1C87F1F90021194C /* CopyFiles */, 334 | ); 335 | buildRules = ( 336 | ); 337 | dependencies = ( 338 | D825685C1C69506C005ADB0F /* PBXTargetDependency */, 339 | ); 340 | name = "RxMultipeer Example"; 341 | productName = "RxMultipeer Example"; 342 | productReference = D8236D521B71EA7800EF5DE6 /* RxMultipeer Example.app */; 343 | productType = "com.apple.product-type.application"; 344 | }; 345 | D88C85EA1B6F2B1700CF42CF /* RxMultipeer */ = { 346 | isa = PBXNativeTarget; 347 | buildConfigurationList = D88C86011B6F2B1700CF42CF /* Build configuration list for PBXNativeTarget "RxMultipeer" */; 348 | buildPhases = ( 349 | D88C85E61B6F2B1700CF42CF /* Sources */, 350 | D88C85E71B6F2B1700CF42CF /* Frameworks */, 351 | D88C85E81B6F2B1700CF42CF /* Headers */, 352 | D88C85E91B6F2B1700CF42CF /* Resources */, 353 | ); 354 | buildRules = ( 355 | ); 356 | dependencies = ( 357 | ); 358 | name = RxMultipeer; 359 | productName = RxMultipeer; 360 | productReference = D88C85EB1B6F2B1700CF42CF /* RxMultipeer.framework */; 361 | productType = "com.apple.product-type.framework"; 362 | }; 363 | D88C85F51B6F2B1700CF42CF /* RxMultipeerTests */ = { 364 | isa = PBXNativeTarget; 365 | buildConfigurationList = D88C86041B6F2B1700CF42CF /* Build configuration list for PBXNativeTarget "RxMultipeerTests" */; 366 | buildPhases = ( 367 | D88C85F21B6F2B1700CF42CF /* Sources */, 368 | D88C85F31B6F2B1700CF42CF /* Frameworks */, 369 | D88C85F41B6F2B1700CF42CF /* Resources */, 370 | D813DA4B1BB1BBC300BEB501 /* Run Script */, 371 | ); 372 | buildRules = ( 373 | ); 374 | dependencies = ( 375 | D88C85F91B6F2B1700CF42CF /* PBXTargetDependency */, 376 | ); 377 | name = RxMultipeerTests; 378 | productName = RxMultipeerTests; 379 | productReference = D88C85F61B6F2B1700CF42CF /* RxMultipeerTests.xctest */; 380 | productType = "com.apple.product-type.bundle.unit-test"; 381 | }; 382 | /* End PBXNativeTarget section */ 383 | 384 | /* Begin PBXProject section */ 385 | D88C85E21B6F2B1700CF42CF /* Project object */ = { 386 | isa = PBXProject; 387 | attributes = { 388 | LastSwiftMigration = 0700; 389 | LastSwiftUpdateCheck = 0700; 390 | LastUpgradeCheck = 0900; 391 | ORGANIZATIONNAME = "Nathan Kot"; 392 | TargetAttributes = { 393 | D8236D511B71EA7800EF5DE6 = { 394 | CreatedOnToolsVersion = 6.4; 395 | DevelopmentTeam = U7299KCYL8; 396 | LastSwiftMigration = 0800; 397 | }; 398 | D88C85EA1B6F2B1700CF42CF = { 399 | CreatedOnToolsVersion = 6.4; 400 | LastSwiftMigration = 0820; 401 | }; 402 | D88C85F51B6F2B1700CF42CF = { 403 | CreatedOnToolsVersion = 6.4; 404 | LastSwiftMigration = 0800; 405 | }; 406 | }; 407 | }; 408 | buildConfigurationList = D88C85E51B6F2B1700CF42CF /* Build configuration list for PBXProject "RxMultipeer" */; 409 | compatibilityVersion = "Xcode 3.2"; 410 | developmentRegion = English; 411 | hasScannedForEncodings = 0; 412 | knownRegions = ( 413 | en, 414 | Base, 415 | ); 416 | mainGroup = D88C85E11B6F2B1700CF42CF; 417 | productRefGroup = D88C85EC1B6F2B1700CF42CF /* Products */; 418 | projectDirPath = ""; 419 | projectRoot = ""; 420 | targets = ( 421 | D88C85EA1B6F2B1700CF42CF /* RxMultipeer */, 422 | 13CBFDD41E7973E700C06615 /* RxMultipeer-tvOS */, 423 | D88C85F51B6F2B1700CF42CF /* RxMultipeerTests */, 424 | D8236D511B71EA7800EF5DE6 /* RxMultipeer Example */, 425 | ); 426 | }; 427 | /* End PBXProject section */ 428 | 429 | /* Begin PBXResourcesBuildPhase section */ 430 | 13CBFDE51E7973E700C06615 /* Resources */ = { 431 | isa = PBXResourcesBuildPhase; 432 | buildActionMask = 2147483647; 433 | files = ( 434 | ); 435 | runOnlyForDeploymentPostprocessing = 0; 436 | }; 437 | D8236D501B71EA7800EF5DE6 /* Resources */ = { 438 | isa = PBXResourcesBuildPhase; 439 | buildActionMask = 2147483647; 440 | files = ( 441 | D8236D5C1B71EA7800EF5DE6 /* Main.storyboard in Resources */, 442 | D8236D611B71EA7800EF5DE6 /* LaunchScreen.xib in Resources */, 443 | D8236D5E1B71EA7800EF5DE6 /* Images.xcassets in Resources */, 444 | ); 445 | runOnlyForDeploymentPostprocessing = 0; 446 | }; 447 | D88C85E91B6F2B1700CF42CF /* Resources */ = { 448 | isa = PBXResourcesBuildPhase; 449 | buildActionMask = 2147483647; 450 | files = ( 451 | ); 452 | runOnlyForDeploymentPostprocessing = 0; 453 | }; 454 | D88C85F41B6F2B1700CF42CF /* Resources */ = { 455 | isa = PBXResourcesBuildPhase; 456 | buildActionMask = 2147483647; 457 | files = ( 458 | D86675A51C6B40FD005A9A72 /* Data.txt in Resources */, 459 | ); 460 | runOnlyForDeploymentPostprocessing = 0; 461 | }; 462 | /* End PBXResourcesBuildPhase section */ 463 | 464 | /* Begin PBXShellScriptBuildPhase section */ 465 | D813DA4B1BB1BBC300BEB501 /* Run Script */ = { 466 | isa = PBXShellScriptBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | ); 470 | inputPaths = ( 471 | "$(SRCROOT)/Carthage/Build/iOS/Quick.framework", 472 | "$(SRCROOT)/Carthage/Build/iOS/Nimble.framework", 473 | "$(SRCROOT)/Carthage/Build/iOS/RxBlocking.framework", 474 | "$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework", 475 | "$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework", 476 | ); 477 | name = "Run Script"; 478 | outputPaths = ( 479 | ); 480 | runOnlyForDeploymentPostprocessing = 0; 481 | shellPath = /bin/sh; 482 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 483 | }; 484 | D825685A1C6949DA005ADB0F /* Copy Frameworks */ = { 485 | isa = PBXShellScriptBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | ); 489 | inputPaths = ( 490 | "$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework", 491 | "$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework", 492 | ); 493 | name = "Copy Frameworks"; 494 | outputPaths = ( 495 | ); 496 | runOnlyForDeploymentPostprocessing = 0; 497 | shellPath = /bin/sh; 498 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 499 | }; 500 | /* End PBXShellScriptBuildPhase section */ 501 | 502 | /* Begin PBXSourcesBuildPhase section */ 503 | 13CBFDD51E7973E700C06615 /* Sources */ = { 504 | isa = PBXSourcesBuildPhase; 505 | buildActionMask = 2147483647; 506 | files = ( 507 | 13CBFDD61E7973E700C06615 /* Client.swift in Sources */, 508 | 13CBFDD71E7973E700C06615 /* MultipeerConnectivitySession.swift in Sources */, 509 | 13CBFDD81E7973E700C06615 /* ResourceState.swift in Sources */, 510 | 13CBFDD91E7973E700C06615 /* RxMultipeerError.swift in Sources */, 511 | 13CBFDDA1E7973E700C06615 /* MockSession.swift in Sources */, 512 | 13CBFDDB1E7973E700C06615 /* MCSessionDelegateWrapper.m in Sources */, 513 | 13CBFDDC1E7973E700C06615 /* Session.swift in Sources */, 514 | 13CBFDDD1E7973E700C06615 /* Weak.swift in Sources */, 515 | ); 516 | runOnlyForDeploymentPostprocessing = 0; 517 | }; 518 | D8236D4E1B71EA7800EF5DE6 /* Sources */ = { 519 | isa = PBXSourcesBuildPhase; 520 | buildActionMask = 2147483647; 521 | files = ( 522 | D8236D591B71EA7800EF5DE6 /* ViewController.swift in Sources */, 523 | D8236D571B71EA7800EF5DE6 /* AppDelegate.swift in Sources */, 524 | ); 525 | runOnlyForDeploymentPostprocessing = 0; 526 | }; 527 | D88C85E61B6F2B1700CF42CF /* Sources */ = { 528 | isa = PBXSourcesBuildPhase; 529 | buildActionMask = 2147483647; 530 | files = ( 531 | D88C860C1B6F308600CF42CF /* Client.swift in Sources */, 532 | D8236D4D1B71DE4400EF5DE6 /* MultipeerConnectivitySession.swift in Sources */, 533 | D8236D491B71A9EB00EF5DE6 /* ResourceState.swift in Sources */, 534 | D80EC2F41BBF372B006BF124 /* RxMultipeerError.swift in Sources */, 535 | D88C86161B7060E000CF42CF /* MockSession.swift in Sources */, 536 | D823DDB21E41E8CD001C3ED7 /* MCSessionDelegateWrapper.m in Sources */, 537 | D88C86131B6F4F3700CF42CF /* Session.swift in Sources */, 538 | D8327BD41BD08FE9009BC6FB /* Weak.swift in Sources */, 539 | ); 540 | runOnlyForDeploymentPostprocessing = 0; 541 | }; 542 | D88C85F21B6F2B1700CF42CF /* Sources */ = { 543 | isa = PBXSourcesBuildPhase; 544 | buildActionMask = 2147483647; 545 | files = ( 546 | D88C860A1B6F307700CF42CF /* IntegrationSpec.swift in Sources */, 547 | ); 548 | runOnlyForDeploymentPostprocessing = 0; 549 | }; 550 | /* End PBXSourcesBuildPhase section */ 551 | 552 | /* Begin PBXTargetDependency section */ 553 | D825685C1C69506C005ADB0F /* PBXTargetDependency */ = { 554 | isa = PBXTargetDependency; 555 | target = D88C85EA1B6F2B1700CF42CF /* RxMultipeer */; 556 | targetProxy = D825685B1C69506C005ADB0F /* PBXContainerItemProxy */; 557 | }; 558 | D88C85F91B6F2B1700CF42CF /* PBXTargetDependency */ = { 559 | isa = PBXTargetDependency; 560 | target = D88C85EA1B6F2B1700CF42CF /* RxMultipeer */; 561 | targetProxy = D88C85F81B6F2B1700CF42CF /* PBXContainerItemProxy */; 562 | }; 563 | /* End PBXTargetDependency section */ 564 | 565 | /* Begin PBXVariantGroup section */ 566 | D8236D5A1B71EA7800EF5DE6 /* Main.storyboard */ = { 567 | isa = PBXVariantGroup; 568 | children = ( 569 | D8236D5B1B71EA7800EF5DE6 /* Base */, 570 | ); 571 | name = Main.storyboard; 572 | path = .; 573 | sourceTree = ""; 574 | }; 575 | D8236D5F1B71EA7800EF5DE6 /* LaunchScreen.xib */ = { 576 | isa = PBXVariantGroup; 577 | children = ( 578 | D8236D601B71EA7800EF5DE6 /* Base */, 579 | ); 580 | name = LaunchScreen.xib; 581 | path = .; 582 | sourceTree = ""; 583 | }; 584 | /* End PBXVariantGroup section */ 585 | 586 | /* Begin XCBuildConfiguration section */ 587 | 13CBFDE71E7973E700C06615 /* Debug */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | CLANG_ENABLE_MODULES = YES; 591 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 592 | DEFINES_MODULE = YES; 593 | DYLIB_COMPATIBILITY_VERSION = 1; 594 | DYLIB_CURRENT_VERSION = 1; 595 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 596 | FRAMEWORK_SEARCH_PATHS = ( 597 | "$(inherited)", 598 | "$(PROJECT_DIR)/Carthage/Build/tvOS", 599 | ); 600 | INFOPLIST_FILE = "$(SRCROOT)/RxMultipeer/Supporting Files/Info.plist"; 601 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 602 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 603 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 604 | PRODUCT_NAME = RxMultipeer; 605 | SDKROOT = appletvos; 606 | SKIP_INSTALL = YES; 607 | SWIFT_OBJC_BRIDGING_HEADER = "RxMultipeer/RxMultipeer-Bridging-Header.h"; 608 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 609 | SWIFT_VERSION = 3.0; 610 | TARGETED_DEVICE_FAMILY = 3; 611 | }; 612 | name = Debug; 613 | }; 614 | 13CBFDE81E7973E700C06615 /* Release */ = { 615 | isa = XCBuildConfiguration; 616 | buildSettings = { 617 | CLANG_ENABLE_MODULES = YES; 618 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 619 | DEFINES_MODULE = YES; 620 | DYLIB_COMPATIBILITY_VERSION = 1; 621 | DYLIB_CURRENT_VERSION = 1; 622 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 623 | FRAMEWORK_SEARCH_PATHS = ( 624 | "$(inherited)", 625 | "$(PROJECT_DIR)/Carthage/Build/tvOS", 626 | ); 627 | INFOPLIST_FILE = "$(SRCROOT)/RxMultipeer/Supporting Files/Info.plist"; 628 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 629 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 630 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 631 | PRODUCT_NAME = RxMultipeer; 632 | SDKROOT = appletvos; 633 | SKIP_INSTALL = YES; 634 | SWIFT_OBJC_BRIDGING_HEADER = "RxMultipeer/RxMultipeer-Bridging-Header.h"; 635 | SWIFT_VERSION = 3.0; 636 | TARGETED_DEVICE_FAMILY = 3; 637 | }; 638 | name = Release; 639 | }; 640 | D8236D6E1B71EA7800EF5DE6 /* Debug */ = { 641 | isa = XCBuildConfiguration; 642 | buildSettings = { 643 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 644 | DEVELOPMENT_TEAM = U7299KCYL8; 645 | FRAMEWORK_SEARCH_PATHS = ( 646 | "$(inherited)", 647 | "$(PROJECT_DIR)/Carthage/Build/iOS", 648 | ); 649 | GCC_PREPROCESSOR_DEFINITIONS = ( 650 | "DEBUG=1", 651 | "$(inherited)", 652 | ); 653 | INFOPLIST_FILE = "RxMultipeer Example/Supporting Files/Info.plist"; 654 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 655 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 656 | PRODUCT_NAME = "$(TARGET_NAME)"; 657 | SWIFT_VERSION = 3.0; 658 | }; 659 | name = Debug; 660 | }; 661 | D8236D6F1B71EA7800EF5DE6 /* Release */ = { 662 | isa = XCBuildConfiguration; 663 | buildSettings = { 664 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 665 | DEVELOPMENT_TEAM = U7299KCYL8; 666 | FRAMEWORK_SEARCH_PATHS = ( 667 | "$(inherited)", 668 | "$(PROJECT_DIR)/Carthage/Build/iOS", 669 | ); 670 | INFOPLIST_FILE = "RxMultipeer Example/Supporting Files/Info.plist"; 671 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 672 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 673 | PRODUCT_NAME = "$(TARGET_NAME)"; 674 | SWIFT_VERSION = 3.0; 675 | }; 676 | name = Release; 677 | }; 678 | D88C85FF1B6F2B1700CF42CF /* Debug */ = { 679 | isa = XCBuildConfiguration; 680 | buildSettings = { 681 | ALWAYS_SEARCH_USER_PATHS = NO; 682 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 683 | CLANG_CXX_LIBRARY = "libc++"; 684 | CLANG_ENABLE_MODULES = YES; 685 | CLANG_ENABLE_OBJC_ARC = YES; 686 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 687 | CLANG_WARN_BOOL_CONVERSION = YES; 688 | CLANG_WARN_COMMA = YES; 689 | CLANG_WARN_CONSTANT_CONVERSION = YES; 690 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 691 | CLANG_WARN_EMPTY_BODY = YES; 692 | CLANG_WARN_ENUM_CONVERSION = YES; 693 | CLANG_WARN_INFINITE_RECURSION = YES; 694 | CLANG_WARN_INT_CONVERSION = YES; 695 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 696 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 697 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 698 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 699 | CLANG_WARN_STRICT_PROTOTYPES = YES; 700 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 701 | CLANG_WARN_UNREACHABLE_CODE = YES; 702 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 703 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 704 | COPY_PHASE_STRIP = NO; 705 | CURRENT_PROJECT_VERSION = 1; 706 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 707 | ENABLE_STRICT_OBJC_MSGSEND = YES; 708 | ENABLE_TESTABILITY = YES; 709 | GCC_C_LANGUAGE_STANDARD = gnu99; 710 | GCC_DYNAMIC_NO_PIC = NO; 711 | GCC_NO_COMMON_BLOCKS = YES; 712 | GCC_OPTIMIZATION_LEVEL = 0; 713 | GCC_PREPROCESSOR_DEFINITIONS = ( 714 | "DEBUG=1", 715 | "$(inherited)", 716 | ); 717 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 718 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 719 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 720 | GCC_WARN_UNDECLARED_SELECTOR = YES; 721 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 722 | GCC_WARN_UNUSED_FUNCTION = YES; 723 | GCC_WARN_UNUSED_VARIABLE = YES; 724 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 725 | MTL_ENABLE_DEBUG_INFO = YES; 726 | ONLY_ACTIVE_ARCH = YES; 727 | SDKROOT = iphoneos; 728 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 729 | TARGETED_DEVICE_FAMILY = "1,2"; 730 | VERSIONING_SYSTEM = "apple-generic"; 731 | VERSION_INFO_PREFIX = ""; 732 | }; 733 | name = Debug; 734 | }; 735 | D88C86001B6F2B1700CF42CF /* Release */ = { 736 | isa = XCBuildConfiguration; 737 | buildSettings = { 738 | ALWAYS_SEARCH_USER_PATHS = NO; 739 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 740 | CLANG_CXX_LIBRARY = "libc++"; 741 | CLANG_ENABLE_MODULES = YES; 742 | CLANG_ENABLE_OBJC_ARC = YES; 743 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 744 | CLANG_WARN_BOOL_CONVERSION = YES; 745 | CLANG_WARN_COMMA = YES; 746 | CLANG_WARN_CONSTANT_CONVERSION = YES; 747 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 748 | CLANG_WARN_EMPTY_BODY = YES; 749 | CLANG_WARN_ENUM_CONVERSION = YES; 750 | CLANG_WARN_INFINITE_RECURSION = YES; 751 | CLANG_WARN_INT_CONVERSION = YES; 752 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 753 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 754 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 755 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 756 | CLANG_WARN_STRICT_PROTOTYPES = YES; 757 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 758 | CLANG_WARN_UNREACHABLE_CODE = YES; 759 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 760 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 761 | COPY_PHASE_STRIP = NO; 762 | CURRENT_PROJECT_VERSION = 1; 763 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 764 | ENABLE_NS_ASSERTIONS = NO; 765 | ENABLE_STRICT_OBJC_MSGSEND = YES; 766 | GCC_C_LANGUAGE_STANDARD = gnu99; 767 | GCC_NO_COMMON_BLOCKS = YES; 768 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 769 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 770 | GCC_WARN_UNDECLARED_SELECTOR = YES; 771 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 772 | GCC_WARN_UNUSED_FUNCTION = YES; 773 | GCC_WARN_UNUSED_VARIABLE = YES; 774 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 775 | MTL_ENABLE_DEBUG_INFO = NO; 776 | SDKROOT = iphoneos; 777 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 778 | TARGETED_DEVICE_FAMILY = "1,2"; 779 | VALIDATE_PRODUCT = YES; 780 | VERSIONING_SYSTEM = "apple-generic"; 781 | VERSION_INFO_PREFIX = ""; 782 | }; 783 | name = Release; 784 | }; 785 | D88C86021B6F2B1700CF42CF /* Debug */ = { 786 | isa = XCBuildConfiguration; 787 | buildSettings = { 788 | CLANG_ENABLE_MODULES = YES; 789 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 790 | DEFINES_MODULE = YES; 791 | DYLIB_COMPATIBILITY_VERSION = 1; 792 | DYLIB_CURRENT_VERSION = 1; 793 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 794 | FRAMEWORK_SEARCH_PATHS = ( 795 | "$(inherited)", 796 | "$(PROJECT_DIR)/Carthage/Build/iOS", 797 | ); 798 | INFOPLIST_FILE = "RxMultipeer/Supporting Files/Info.plist"; 799 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 800 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 801 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 802 | PRODUCT_NAME = "$(TARGET_NAME)"; 803 | SKIP_INSTALL = YES; 804 | SWIFT_OBJC_BRIDGING_HEADER = "RxMultipeer/RxMultipeer-Bridging-Header.h"; 805 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 806 | SWIFT_VERSION = 3.0; 807 | }; 808 | name = Debug; 809 | }; 810 | D88C86031B6F2B1700CF42CF /* Release */ = { 811 | isa = XCBuildConfiguration; 812 | buildSettings = { 813 | CLANG_ENABLE_MODULES = YES; 814 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 815 | DEFINES_MODULE = YES; 816 | DYLIB_COMPATIBILITY_VERSION = 1; 817 | DYLIB_CURRENT_VERSION = 1; 818 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 819 | FRAMEWORK_SEARCH_PATHS = ( 820 | "$(inherited)", 821 | "$(PROJECT_DIR)/Carthage/Build/iOS", 822 | ); 823 | INFOPLIST_FILE = "RxMultipeer/Supporting Files/Info.plist"; 824 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 825 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 826 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 827 | PRODUCT_NAME = "$(TARGET_NAME)"; 828 | SKIP_INSTALL = YES; 829 | SWIFT_OBJC_BRIDGING_HEADER = "RxMultipeer/RxMultipeer-Bridging-Header.h"; 830 | SWIFT_VERSION = 3.0; 831 | }; 832 | name = Release; 833 | }; 834 | D88C86051B6F2B1700CF42CF /* Debug */ = { 835 | isa = XCBuildConfiguration; 836 | buildSettings = { 837 | FRAMEWORK_SEARCH_PATHS = ( 838 | "$(inherited)", 839 | "$(PROJECT_DIR)/Carthage/Build/iOS", 840 | ); 841 | GCC_PREPROCESSOR_DEFINITIONS = ( 842 | "DEBUG=1", 843 | "$(inherited)", 844 | ); 845 | INFOPLIST_FILE = "RxMultipeerTests/Supporting Files/Info.plist"; 846 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 847 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 848 | PRODUCT_NAME = "$(TARGET_NAME)"; 849 | SWIFT_VERSION = 3.0; 850 | }; 851 | name = Debug; 852 | }; 853 | D88C86061B6F2B1700CF42CF /* Release */ = { 854 | isa = XCBuildConfiguration; 855 | buildSettings = { 856 | FRAMEWORK_SEARCH_PATHS = ( 857 | "$(inherited)", 858 | "$(PROJECT_DIR)/Carthage/Build/iOS", 859 | ); 860 | INFOPLIST_FILE = "RxMultipeerTests/Supporting Files/Info.plist"; 861 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 862 | PRODUCT_BUNDLE_IDENTIFIER = "com.nathankot.$(PRODUCT_NAME:rfc1034identifier)"; 863 | PRODUCT_NAME = "$(TARGET_NAME)"; 864 | SWIFT_VERSION = 3.0; 865 | }; 866 | name = Release; 867 | }; 868 | /* End XCBuildConfiguration section */ 869 | 870 | /* Begin XCConfigurationList section */ 871 | 13CBFDE61E7973E700C06615 /* Build configuration list for PBXNativeTarget "RxMultipeer-tvOS" */ = { 872 | isa = XCConfigurationList; 873 | buildConfigurations = ( 874 | 13CBFDE71E7973E700C06615 /* Debug */, 875 | 13CBFDE81E7973E700C06615 /* Release */, 876 | ); 877 | defaultConfigurationIsVisible = 0; 878 | defaultConfigurationName = Release; 879 | }; 880 | D8236D721B71EA7800EF5DE6 /* Build configuration list for PBXNativeTarget "RxMultipeer Example" */ = { 881 | isa = XCConfigurationList; 882 | buildConfigurations = ( 883 | D8236D6E1B71EA7800EF5DE6 /* Debug */, 884 | D8236D6F1B71EA7800EF5DE6 /* Release */, 885 | ); 886 | defaultConfigurationIsVisible = 0; 887 | defaultConfigurationName = Release; 888 | }; 889 | D88C85E51B6F2B1700CF42CF /* Build configuration list for PBXProject "RxMultipeer" */ = { 890 | isa = XCConfigurationList; 891 | buildConfigurations = ( 892 | D88C85FF1B6F2B1700CF42CF /* Debug */, 893 | D88C86001B6F2B1700CF42CF /* Release */, 894 | ); 895 | defaultConfigurationIsVisible = 0; 896 | defaultConfigurationName = Release; 897 | }; 898 | D88C86011B6F2B1700CF42CF /* Build configuration list for PBXNativeTarget "RxMultipeer" */ = { 899 | isa = XCConfigurationList; 900 | buildConfigurations = ( 901 | D88C86021B6F2B1700CF42CF /* Debug */, 902 | D88C86031B6F2B1700CF42CF /* Release */, 903 | ); 904 | defaultConfigurationIsVisible = 0; 905 | defaultConfigurationName = Release; 906 | }; 907 | D88C86041B6F2B1700CF42CF /* Build configuration list for PBXNativeTarget "RxMultipeerTests" */ = { 908 | isa = XCConfigurationList; 909 | buildConfigurations = ( 910 | D88C86051B6F2B1700CF42CF /* Debug */, 911 | D88C86061B6F2B1700CF42CF /* Release */, 912 | ); 913 | defaultConfigurationIsVisible = 0; 914 | defaultConfigurationName = Release; 915 | }; 916 | /* End XCConfigurationList section */ 917 | }; 918 | rootObject = D88C85E21B6F2B1700CF42CF /* Project object */; 919 | } 920 | -------------------------------------------------------------------------------- /RxMultipeer.xcodeproj/xcshareddata/xcschemes/RxMultipeer-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /RxMultipeer.xcodeproj/xcshareddata/xcschemes/RxMultipeer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /RxMultipeer/Adapters/MockSession.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import MultipeerConnectivity 4 | 5 | open class MockIden : Hashable { 6 | 7 | open let uid = ProcessInfo.processInfo.globallyUniqueString 8 | open let string: String 9 | open var displayName: String { return string } 10 | 11 | open var hashValue: Int { 12 | return uid.hashValue 13 | } 14 | 15 | public init(_ string: String) { 16 | self.string = string 17 | } 18 | 19 | convenience public init(displayName: String) { 20 | self.init(displayName) 21 | } 22 | 23 | } 24 | 25 | public func ==(left: MockIden, right: MockIden) -> Bool { 26 | return left.uid == right.uid 27 | } 28 | 29 | open class MockSession : Session { 30 | 31 | public typealias I = MockIden 32 | 33 | // Store all available sessions in a global 34 | static open var sessions: [MockSession] = [] { 35 | didSet { digest() } 36 | } 37 | 38 | static open let advertisingSessions: Variable<[MockSession]> = Variable([]) 39 | 40 | static open func digest() { 41 | advertisingSessions.value = sessions.filter { $0.isAdvertising } 42 | } 43 | 44 | static open func findForClient(_ client: I) -> MockSession? { 45 | return sessions.filter({ o in return o.iden == client }).first 46 | } 47 | 48 | static open func reset() { 49 | self.sessions = [] 50 | } 51 | 52 | // Structure and initialization 53 | ////////////////////////////////////////////////////////////////////////// 54 | 55 | let _iden: I 56 | open var iden: I { return _iden } 57 | 58 | let _meta: [String: String]? 59 | open var meta: [String: String]? { return _meta } 60 | 61 | open var emulateCertificationHandler = true 62 | 63 | public init( 64 | name: String, 65 | meta: [String: String]? = nil, 66 | emulateCertificationHandler emulateCert: Bool = true) { 67 | 68 | self._iden = I(name) 69 | self._meta = meta 70 | self.emulateCertificationHandler = emulateCert 71 | MockSession.sessions.append(self) 72 | } 73 | 74 | // Connection concerns 75 | ////////////////////////////////////////////////////////////////////////// 76 | 77 | let rx_connections = Variable<[Weak]>([]) 78 | let rx_connectRequests = PublishSubject<(I, [String: Any]?, (Bool) -> ())>() 79 | let rx_certificateVerificationRequests = PublishSubject<(I, [Any]?, (Bool) -> ())>() 80 | 81 | open var isAdvertising = false { 82 | didSet { MockSession.digest() } 83 | } 84 | 85 | open var isBrowsing = false { 86 | didSet { MockSession.digest() } 87 | } 88 | 89 | open func connections() -> Observable<[I]> { 90 | return rx_connections.asObservable() 91 | .map { $0.filter { $0.value != nil }.map { $0.value!.iden } } 92 | } 93 | 94 | open func nearbyPeers() -> Observable<[(I, [String: String]?)]> { 95 | return MockSession.advertisingSessions 96 | .asObservable() 97 | .filter { _ in self.isBrowsing } 98 | .map { $0.map { ($0.iden, $0.meta) } } 99 | } 100 | 101 | open func incomingConnections() -> Observable<(I, [String: Any]?, (Bool) -> ())> { 102 | return rx_connectRequests.filter { _ in self.isAdvertising } 103 | } 104 | 105 | open func incomingCertificateVerifications() -> Observable<(I, [Any]?, (Bool) -> Void)> { 106 | return rx_certificateVerificationRequests.filter { _ in self.isAdvertising || self.isBrowsing } 107 | } 108 | 109 | open func startBrowsing() { 110 | self.isBrowsing = true 111 | } 112 | 113 | open func stopBrowsing() { 114 | self.isBrowsing = false 115 | } 116 | 117 | open func startAdvertising() { 118 | self.isAdvertising = true 119 | } 120 | 121 | open func stopAdvertising() { 122 | self.isAdvertising = false 123 | } 124 | 125 | open func connect(_ peer: I, context: [String: Any]? = nil, timeout: TimeInterval = 12) { 126 | let otherm = MockSession.sessions.filter({ return $0.iden == peer }).first 127 | if let other = otherm { 128 | // Skip if already connected 129 | if self.rx_connections.value.filter({ $0.value?.iden == other.iden }).count > 0 { 130 | return 131 | } 132 | 133 | if !other.isAdvertising { 134 | return 135 | } 136 | 137 | let makeConnection = { (certificateResponse: Bool) in 138 | if !certificateResponse { 139 | return 140 | } 141 | 142 | other.rx_connectRequests.on( 143 | .next( 144 | (self.iden, 145 | context, 146 | { [unowned self] (response: Bool) in 147 | if !response { return } 148 | self.rx_connections.value = self.rx_connections.value + [Weak(other)] 149 | other.rx_connections.value = other.rx_connections.value + [Weak(self)] 150 | }) as (I, [String: Any]?, (Bool) -> ()))) 151 | } 152 | 153 | emulateCertificationHandler ? 154 | other.rx_certificateVerificationRequests.on(.next(self.iden, nil, makeConnection)) : 155 | makeConnection(true); 156 | } 157 | } 158 | 159 | open func disconnect() { 160 | self.rx_connections.value = [] 161 | MockSession.sessions = MockSession.sessions.filter { $0.iden != self.iden } 162 | for session in MockSession.sessions { 163 | session.rx_connections.value = session.rx_connections.value.filter { 164 | $0.value?.iden != self.iden 165 | } 166 | } 167 | } 168 | 169 | open func connectionErrors() -> Observable { 170 | return PublishSubject() 171 | } 172 | 173 | // Data reception concerns 174 | ////////////////////////////////////////////////////////////////////////// 175 | 176 | let rx_receivedData = PublishSubject<(I, Data)>() 177 | let rx_receivedResources = PublishSubject<(I, String, ResourceState)>() 178 | let rx_receivedStreamData = PublishSubject<(I, String, [UInt8])>() 179 | 180 | open func receive() -> Observable<(I, Data)> { 181 | return rx_receivedData 182 | } 183 | 184 | open func receive() -> Observable<(I, String, ResourceState)> { 185 | return rx_receivedResources 186 | } 187 | 188 | open func receive(fromPeer other: I, 189 | streamName: String, 190 | runLoop _: RunLoop = RunLoop.main, 191 | maxLength: Int = 512) 192 | -> Observable<[UInt8]> { 193 | return rx_receivedStreamData 194 | .filter { (c, n, d) in c == other && n == streamName } 195 | .map { (o: (I, String, [UInt8])) in o.2 } 196 | // need to convert to max `maxLength` sizes 197 | .map({ data -> Observable<[UInt8]> in 198 | Observable.from(stride(from: 0, to: data.count, by: maxLength) 199 | .map({ Array(data[$0..<$0.advanced(by: min(maxLength, data.count - $0))]) })) 200 | }) 201 | .concat() 202 | } 203 | 204 | // Data delivery concerns 205 | ////////////////////////////////////////////////////////////////////////// 206 | 207 | func isConnected(_ other: MockSession) -> Bool { 208 | return rx_connections.value.filter({ $0.value?.iden == other.iden }).first != nil 209 | } 210 | 211 | open func send(toPeer other: I, 212 | data: Data, 213 | mode: MCSessionSendDataMode) 214 | -> Observable<()> { 215 | return Observable.create { [unowned self] observer in 216 | if let otherSession = MockSession.findForClient(other) { 217 | // Can't send if not connected 218 | if !self.isConnected(otherSession) { 219 | observer.on(.error(RxMultipeerError.connectionError)) 220 | } else { 221 | otherSession.rx_receivedData.on(.next((self.iden, data))) 222 | observer.on(.next(())) 223 | observer.on(.completed) 224 | } 225 | } else { 226 | observer.on(.error(RxMultipeerError.connectionError)) 227 | } 228 | 229 | return Disposables.create {} 230 | } 231 | } 232 | 233 | open func send 234 | (toPeer other: I, 235 | name: String, 236 | resource url: URL, 237 | mode: MCSessionSendDataMode) 238 | -> Observable<(Progress)> { 239 | return Observable.create { [unowned self] observer in 240 | if let otherSession = MockSession.findForClient(other) { 241 | // Can't send if not connected 242 | if !self.isConnected(otherSession) { 243 | observer.on(.error(RxMultipeerError.connectionError)) 244 | } else { 245 | let c = self.iden 246 | otherSession.rx_receivedResources.on(.next(c, name, .progress(Progress(totalUnitCount: 1)))) 247 | otherSession.rx_receivedResources.on(.next(c, name, .finished(url))) 248 | let completed = Progress(totalUnitCount: 1) 249 | completed.completedUnitCount = 1 250 | observer.on(.next(completed)) 251 | observer.on(.completed) 252 | } 253 | } else { 254 | observer.on(.error(RxMultipeerError.connectionError)) 255 | } 256 | 257 | return Disposables.create {} 258 | } 259 | } 260 | 261 | open func send 262 | (toPeer other: I, 263 | streamName name: String, 264 | runLoop: RunLoop = RunLoop.main) 265 | -> Observable<([UInt8]) -> Void> { 266 | 267 | return Observable.create { [unowned self] observer in 268 | 269 | var handler: ([UInt8]) -> Void = { _ in } 270 | 271 | if let otherSession = MockSession.findForClient(other) { 272 | handler = { d in 273 | otherSession.rx_receivedStreamData.on(.next(self.iden, name, d)) 274 | observer.on(.next(handler)) 275 | } 276 | 277 | if self.isConnected(otherSession) { 278 | observer.on(.next(handler)) 279 | } else { 280 | observer.on(.error(RxMultipeerError.connectionError)) 281 | } 282 | } else { 283 | observer.on(.error(RxMultipeerError.connectionError)) 284 | } 285 | 286 | return Disposables.create { 287 | handler = { _ in } 288 | } 289 | 290 | } 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /RxMultipeer/Adapters/MultipeerConnectivitySession.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | import MultipeerConnectivity 5 | 6 | /// A RxMultipeer adapter for Apple's MultipeerConnectivity framework. 7 | open class MultipeerConnectivitySession : NSObject, Session { 8 | 9 | public typealias I = MCPeerID 10 | 11 | open let session: MCSession 12 | open let serviceType: String 13 | 14 | open var iden: MCPeerID { return session.myPeerID } 15 | open var meta: [String: String]? { return self.advertiser.discoveryInfo } 16 | 17 | fileprivate let disposeBag = DisposeBag() 18 | 19 | fileprivate let advertiser: MCNearbyServiceAdvertiser 20 | fileprivate let browser: MCNearbyServiceBrowser 21 | 22 | fileprivate let _incomingConnections: PublishSubject<(MCPeerID, [String: Any]?, (Bool, MCSession?) -> Void)> = PublishSubject() 23 | fileprivate let _incomingCertificateVerifications: PublishSubject<(MCPeerID, [Any]?, (Bool) -> Void)> = PublishSubject() 24 | fileprivate let _connections = Variable<[MCPeerID]>([]) 25 | fileprivate let _nearbyPeers: Variable<[(MCPeerID, [String: String]?)]> = Variable([]) 26 | fileprivate let _connectionErrors: PublishSubject = PublishSubject() 27 | fileprivate let _receivedData: PublishSubject<(MCPeerID, Data)> = PublishSubject() 28 | fileprivate let _receivedResource: PublishSubject<(MCPeerID, String, ResourceState)> = PublishSubject() 29 | fileprivate let _receivedStreams = PublishSubject<(MCPeerID, String, InputStream)>() 30 | 31 | fileprivate var sessionDelegate: MCSessionDelegate? 32 | 33 | /// - Parameters: 34 | /// - displayName: Name to display to nearby peers 35 | /// - serviceType: The scope of the multipeer connectivity service, e.g `com.rxmultipeer.example` 36 | /// - meta: Additional data that nearby peers can read when browsing 37 | /// - idenCacheKey: The key to use to store the generated session identity. 38 | /// By default a new `MCPeerID` is generated for each new session, however 39 | /// it may be desirable to recycle existing idens with the same 40 | /// `displayName` in order to prevent weird MultipeerConnectivity bugs. 41 | /// [Read this SO answer for more information](http://goo.gl/mXlQj0) 42 | /// - encryptionPreference: The session's encryption requirement 43 | public init( 44 | displayName: String, 45 | serviceType: String, 46 | meta: [String: String]? = nil, 47 | idenCacheKey: String? = nil, 48 | securityIdentity: [Any]? = nil, 49 | encryptionPreference: MCEncryptionPreference = .none) { 50 | 51 | let peerId = MultipeerConnectivitySession.getRecycledPeerID(forKey: idenCacheKey, 52 | displayName: displayName) 53 | 54 | self.serviceType = serviceType 55 | self.session = MCSession(peer: peerId, 56 | securityIdentity: securityIdentity, 57 | encryptionPreference: encryptionPreference) 58 | 59 | self.advertiser = MCNearbyServiceAdvertiser( 60 | peer: self.session.myPeerID, 61 | discoveryInfo: meta, 62 | serviceType: self.serviceType) 63 | 64 | self.browser = MCNearbyServiceBrowser( 65 | peer: self.session.myPeerID, 66 | serviceType: self.serviceType) 67 | 68 | super.init() 69 | 70 | self.sessionDelegate = MCSessionDelegateWrapper(delegate: self) 71 | self.session.delegate = sessionDelegate 72 | self.advertiser.delegate = self 73 | self.browser.delegate = self 74 | } 75 | 76 | /// Given an iden cache key, retrieve either the existing serialized `MCPeerID` 77 | /// or generate a new one. 78 | /// 79 | /// It will return an existing `MCPeerID` when there is both a cache hit and the display names 80 | /// are identical. Otherwise, it will create a new one. 81 | open static func getRecycledPeerID(forKey key: String?, displayName: String) -> MCPeerID { 82 | let defaults = UserDefaults.standard 83 | if let k = key, 84 | let d = defaults.data(forKey: k), 85 | let p = NSKeyedUnarchiver.unarchiveObject(with: d) as? MCPeerID, 86 | p.displayName == displayName { 87 | return p 88 | } 89 | 90 | let iden = MCPeerID(displayName: displayName) 91 | 92 | if let k = key { 93 | defaults.set(NSKeyedArchiver.archivedData(withRootObject: iden), forKey: k) 94 | } 95 | 96 | return iden 97 | } 98 | 99 | /// - Seealso: `MCNearbyServiceAdvertiser.startAdvertisingPeer()` 100 | open func startAdvertising() { 101 | advertiser.startAdvertisingPeer() 102 | } 103 | 104 | /// - Seealso: `MCNearbyServiceAdvertiser.stopAdvertisingPeer()` 105 | open func stopAdvertising() { 106 | advertiser.stopAdvertisingPeer() 107 | } 108 | 109 | open func connections() -> Observable<[MCPeerID]> { 110 | return _connections.asObservable() 111 | } 112 | 113 | open func nearbyPeers() -> Observable<[(MCPeerID, [String: String]?)]> { 114 | return _nearbyPeers.asObservable() 115 | } 116 | 117 | /// - Seealso: `MCNearbyServiceBrowser.startBrowsingForPeers()` 118 | open func startBrowsing() { 119 | browser.startBrowsingForPeers() 120 | } 121 | 122 | /// - Seealso: `MCNearbyServiceBrowser.stopBrowsingForPeers()` 123 | open func stopBrowsing() { 124 | browser.stopBrowsingForPeers() 125 | // Because we are aggregating found and lost peers in order 126 | // to get nearby peers, we should start with a clean slate when 127 | // browsing is kicked off again. 128 | _nearbyPeers.value = [] 129 | } 130 | 131 | open func connect(_ peer: MCPeerID, context: [String: Any]?, timeout: TimeInterval) { 132 | let data: Data? 133 | if let c = context { 134 | data = try? JSONSerialization.data( 135 | withJSONObject: c, options: JSONSerialization.WritingOptions()) 136 | } else { 137 | data = nil 138 | } 139 | 140 | browser.invitePeer( 141 | peer, 142 | to: self.session, 143 | withContext: data, 144 | timeout: timeout) 145 | } 146 | 147 | open func incomingConnections() -> Observable<(MCPeerID, [String: Any]?, (Bool) -> ())> { 148 | return _incomingConnections 149 | .map { [unowned self] (client, context, handler) in 150 | return (client, context, { (accept: Bool) in handler(accept, self.session) }) 151 | } 152 | } 153 | 154 | open func incomingCertificateVerifications() -> Observable<(I, [Any]?, (Bool) -> Void)> { 155 | return _incomingCertificateVerifications 156 | } 157 | 158 | open func disconnect() { 159 | self.session.disconnect() 160 | } 161 | 162 | open func connectionErrors() -> Observable { 163 | return _connectionErrors 164 | } 165 | 166 | open func receive() -> Observable<(MCPeerID, Data)> { 167 | return _receivedData 168 | } 169 | 170 | open func receive() -> Observable<(MCPeerID, String, ResourceState)> { 171 | return _receivedResource 172 | .map { (p, n, s) -> Observable<(MCPeerID, String, ResourceState)> in 173 | switch s { 174 | case .progress(let progress): 175 | return progress 176 | .rx.observe(Double.self, "fractionCompleted", retainSelf: false) 177 | .map { _ in (p, n, s) } 178 | default: 179 | return Observable.just((p, n, s)) 180 | } 181 | } 182 | .merge() 183 | } 184 | 185 | open func receive( 186 | fromPeer other: MCPeerID, 187 | streamName: String, 188 | runLoop: RunLoop = RunLoop.main, 189 | maxLength: Int = 512) 190 | -> Observable<[UInt8]> { 191 | 192 | return _receivedStreams 193 | .filter { (c, n, _) in c == other && n == streamName } 194 | .map { $2 } 195 | .map { (stream) in 196 | Observable.create { observer in 197 | var delegate: NSStreamDelegateProxy? 198 | delegate = NSStreamDelegateProxy { (stream, event) in 199 | if event.contains(Stream.Event.hasBytesAvailable) { 200 | guard let s = stream as? InputStream else { return } 201 | var buffer = [UInt8](repeating: 0, count: maxLength) 202 | let readBytes = s.read(&buffer, maxLength: maxLength) 203 | if readBytes > 0 { 204 | observer.on(.next(Array(buffer[0.. Observable<()> { 235 | return Observable.create { observer in 236 | do { 237 | try self.session.send(data, toPeers: [other], with: mode) 238 | observer.on(.next(())) 239 | observer.on(.completed) 240 | } catch let error { 241 | observer.on(.error(error)) 242 | } 243 | 244 | // There's no way to cancel this operation, 245 | // so do nothing on dispose. 246 | return Disposables.create {} 247 | } 248 | } 249 | 250 | open func send(toPeer other: MCPeerID, 251 | name: String, 252 | resource url: URL, 253 | mode: MCSessionSendDataMode) -> Observable { 254 | return Observable.create { observer in 255 | let progress = self.session.sendResource(at: url, withName: name, toPeer: other) { (err) in 256 | if let e = err { observer.on(.error(e)) } 257 | else { 258 | observer.on(.completed) 259 | } 260 | } 261 | 262 | let progressDisposable = progress?.rx.observe(Double.self, "fractionCompleted", retainSelf: false) 263 | .subscribe(onNext: { (_: Double?) in observer.on(.next(progress!)) }) 264 | 265 | return CompositeDisposable( 266 | progressDisposable ?? Disposables.create { }, 267 | Disposables.create { 268 | if let cancellable = progress?.isCancellable { 269 | if cancellable == true { 270 | progress?.cancel() 271 | } 272 | } 273 | }) 274 | } 275 | } 276 | 277 | open func send( 278 | toPeer other: MCPeerID, 279 | streamName: String, 280 | runLoop: RunLoop = RunLoop.main) 281 | -> Observable<([UInt8]) -> Void> { 282 | 283 | return Observable.create { observer in 284 | var stream: OutputStream? 285 | var delegate: NSStreamDelegateProxy? 286 | 287 | do { 288 | stream = try self.session.startStream(withName: streamName, toPeer: other) 289 | delegate = NSStreamDelegateProxy { (s, event) in 290 | guard let stream = s as? OutputStream else { return } 291 | 292 | if event.contains(Stream.Event.hasSpaceAvailable) { 293 | observer.on(.next({ d in 294 | d.withUnsafeBufferPointer { 295 | if let baseAddress = $0.baseAddress { 296 | stream.write(baseAddress, maxLength: d.count) 297 | } 298 | } 299 | })) 300 | } 301 | 302 | if event.contains(Stream.Event.errorOccurred) { 303 | observer.on(.error(stream.streamError ?? RxMultipeerError.unknownError)) 304 | } 305 | 306 | if event.contains(Stream.Event.endEncountered) { 307 | observer.on(.completed) 308 | } 309 | } 310 | 311 | stream?.delegate = delegate 312 | stream?.schedule(in: runLoop, forMode: RunLoopMode.defaultRunLoopMode) 313 | stream?.open() 314 | } catch let e { 315 | observer.on(.error(e)) 316 | } 317 | 318 | return Disposables.create { 319 | stream?.delegate = nil 320 | stream?.close() 321 | delegate = nil 322 | } 323 | } 324 | } 325 | 326 | } 327 | 328 | private class NSStreamDelegateProxy : NSObject, StreamDelegate { 329 | 330 | let handler: (Stream, Stream.Event) -> () 331 | 332 | init(handler: @escaping (Stream, Stream.Event) -> ()) { 333 | self.handler = handler 334 | } 335 | 336 | @objc func stream(_ stream: Stream, handle event: Stream.Event) { 337 | handler(stream, event) 338 | } 339 | 340 | } 341 | 342 | extension MultipeerConnectivitySession : MCNearbyServiceAdvertiserDelegate { 343 | 344 | public func advertiser(_ advertiser: MCNearbyServiceAdvertiser, 345 | didReceiveInvitationFromPeer peerID: MCPeerID, 346 | withContext context: Data?, 347 | invitationHandler: (@escaping (Bool, MCSession?) -> Void)) { 348 | 349 | var json: Any? = nil 350 | if let c = context { 351 | json = try? JSONSerialization.jsonObject(with: c) 352 | } 353 | 354 | let jsonCast = json as? [String: Any] 355 | _incomingConnections.on(.next(peerID, jsonCast, invitationHandler)) 356 | } 357 | 358 | public func advertiser(_ advertiser: MCNearbyServiceAdvertiser, 359 | didNotStartAdvertisingPeer err: Error) { 360 | _connectionErrors.on(.next(err)) 361 | } 362 | 363 | } 364 | 365 | extension MultipeerConnectivitySession : MCNearbyServiceBrowserDelegate { 366 | 367 | public func browser(_ browser: MCNearbyServiceBrowser, 368 | foundPeer peerId: MCPeerID, 369 | withDiscoveryInfo info: [String: String]?) { 370 | // Get a unique list of peers 371 | var result: [(MCPeerID, [String: String]?)] = [] 372 | for o in (self._nearbyPeers.value + [(peerId, info)]) { 373 | if (result.map { $0.0 }).index(of: o.0) == nil { 374 | result = result + [o] 375 | } 376 | } 377 | 378 | self._nearbyPeers.value = result 379 | } 380 | 381 | public func browser(_ browser: MCNearbyServiceBrowser, 382 | lostPeer peerId: MCPeerID) { 383 | self._nearbyPeers.value = self._nearbyPeers.value.filter { (id, _) in 384 | id != peerId 385 | } 386 | } 387 | 388 | public func browser(_ browser: MCNearbyServiceBrowser, 389 | didNotStartBrowsingForPeers err: Error) { 390 | _connectionErrors.on(.next(err)) 391 | } 392 | 393 | } 394 | 395 | extension MultipeerConnectivitySession : MCSessionDelegateWrapperDelegate { 396 | 397 | public func session(_ session: MCSession, 398 | peer peerID: MCPeerID, 399 | didChange state: MCSessionState) { 400 | // If the peer is connecting, then we know that connected peers has not 401 | // changed, so we can avoid re-emitting the observable by not setting the value 402 | if state != .connecting { 403 | _connections.value = session.connectedPeers 404 | } 405 | } 406 | 407 | public func session(_ session: MCSession, 408 | didReceive data: Data, 409 | fromPeer peerID: MCPeerID) { 410 | _receivedData.on(.next(peerID, data)) 411 | } 412 | 413 | public func session(_ session: MCSession, 414 | didStartReceivingResourceWithName name: String, 415 | fromPeer peerID: MCPeerID, 416 | with progress: Progress) { 417 | _receivedResource.on(.next(peerID, name, .progress(progress))) 418 | } 419 | 420 | public func session(_ session: MCSession, 421 | didFinishReceivingResourceWithName name: String, 422 | fromPeer peerID: MCPeerID, 423 | at url: URL?, 424 | withError err: Error?) { 425 | 426 | if let e = err { 427 | return _receivedResource.on(.next(peerID, name, ResourceState.errored(e))) 428 | } 429 | 430 | guard let u = url else { 431 | return _receivedResource.on(.next(peerID, name, ResourceState.errored(RxMultipeerError.unknownError))) 432 | } 433 | 434 | _receivedResource.on(.next(peerID, name, .finished(u))) 435 | } 436 | 437 | public func session(_ session: MCSession, 438 | didReceive stream: InputStream, 439 | withName streamName: String, 440 | fromPeer peerID: MCPeerID) { 441 | _receivedStreams.on(.next(peerID, streamName, stream)) 442 | } 443 | 444 | public func session(_: MCSession, 445 | didReceiveCertificate certificateChain: [Any]?, 446 | fromPeer peerID: MCPeerID, 447 | certificateHandler: @escaping (Bool) -> Void) { 448 | _incomingCertificateVerifications.on( 449 | .next(peerID, 450 | certificateChain, 451 | certificateHandler)) 452 | } 453 | 454 | } 455 | -------------------------------------------------------------------------------- /RxMultipeer/Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MultipeerConnectivity 3 | import RxSwift 4 | 5 | /// A client represents a peer in an arbitrary network. 6 | /// It is a wrapper around `IdenType` which is defined by the 7 | /// underlying session adapter in use. 8 | /// 9 | /// It's only function is to be able to correctly identify the 10 | /// client it represents within the given adapter's network. 11 | open class Client where I: Hashable { 12 | 13 | public typealias IdenType = I 14 | 15 | /// The identifier specified by the given `Session`. 16 | /// Anything `Hashable` is a valid candidate. 17 | open let iden: IdenType 18 | 19 | public init(iden: IdenType) { 20 | self.iden = iden 21 | } 22 | } 23 | 24 | /// The client that represents the host device. Like the `Client`, it identifies itself. 25 | /// On top of that, it provides an interface for discovering and interacting with other clients. 26 | open class CurrentClient : Client where S.I == I { 27 | 28 | /// The `Session` coupled with this client, and space in which this client 29 | /// will operate. Every `CurrentClient` has a one-to-one relationship with 30 | /// a `Sesssion`. 31 | open let session: S 32 | 33 | var disposeBag = DisposeBag() 34 | 35 | public init(session: S) { 36 | self.session = session 37 | super.init(iden: session.iden) 38 | } 39 | 40 | /// - Returns: An `Observable` of clients currently connected in the session 41 | open func connections() -> Observable<[Client]> { 42 | return session.connections() 43 | .map { $0.map { Client(iden: $0) } } 44 | } 45 | 46 | /// - Returns: An `Observable` of newly connected clients, as they become connected 47 | open func connectedPeer() -> Observable> { 48 | return session.connections() 49 | .scan(([], [])) { (previousSet: ([I], [I]), current: [I]) in (previousSet.1, current) } 50 | .map { (previous, current) in Array(Set(current).subtracting(previous)) } 51 | .map { Observable.from($0.map(Client.init)) } 52 | .concat() 53 | } 54 | 55 | /// - Returns: An `Observable` of newly disconnected clients 56 | open func disconnectedPeer() -> Observable> { 57 | return session.connections() 58 | .scan(([], [])) { (previousSet: ([I], [I]), current: [I]) in (previousSet.1, current) } 59 | .map { (previous, current) in Array(Set(previous).subtracting(current)) } 60 | .map { Observable.from($0.map(Client.init)) } 61 | .concat() 62 | } 63 | 64 | /// - Returns: An `Observable` of incoming connections, as a tuple of: 65 | /// - Sender's `Client` 66 | /// - Context dictionary, passed in to `connect(peer:context:timeout)` 67 | /// - The response handler, calling it with `true` will attempt to establish the connection 68 | open func incomingConnections() -> Observable<(Client, [String: Any]?, (Bool) -> ())> { 69 | return session.incomingConnections() 70 | .map { (iden, context, respond) in 71 | (Client(iden: iden), context, respond) 72 | } 73 | } 74 | 75 | /// - Returns: An `Observable` of incoming certificate verifications, as a tuple of: 76 | /// - Sender's `Client` 77 | /// - Maybe a certificate chain 78 | /// - The response handler, calling it with `true` will attempt to establish the connection 79 | open func incomingCertificateVerifications() -> Observable<(Client, [Any]?, (Bool) -> ())> { 80 | return session.incomingCertificateVerifications() 81 | .map { (iden, certificateChain, respond) in 82 | (Client(iden: iden), certificateChain, respond) 83 | } 84 | } 85 | 86 | /// - Returns: An `Observable` of clients that are nearby, as a tuple of: 87 | /// - The nearby peer's `Client` 88 | /// - The nearby peer's `metaData` 89 | open func nearbyPeers() -> Observable<[(Client, [String: String]?)]> { 90 | return session.nearbyPeers().map { $0.map { (Client(iden: $0), $1) } } 91 | } 92 | 93 | /// Start advertising using the underlying `session` 94 | open func startAdvertising() { 95 | session.startAdvertising() 96 | } 97 | 98 | /// Stop advertising using the underlying `session` 99 | open func stopAdvertising() { 100 | session.stopAdvertising() 101 | } 102 | 103 | /// Start browsing using the underlying `session` 104 | open func startBrowsing() { 105 | session.startBrowsing() 106 | } 107 | 108 | /// Stop browsing using the underlying `session` 109 | open func stopBrowsing() { 110 | session.stopBrowsing() 111 | } 112 | 113 | /// Invite the given peer to connect. 114 | /// - Parameters: 115 | /// - peer: The recipient peer 116 | /// - context: The context 117 | /// - timeout: The amount of time to wait for a response before giving up 118 | open func connect(toPeer peer: Client, context: [String: Any]? = nil, timeout: TimeInterval = 12) { 119 | return session.connect(peer.iden, context: context, timeout: timeout) 120 | } 121 | 122 | /// Disconnect using the underlying `session`. 123 | /// This behavior of this depends on the implementation of the `Session` adapter. 124 | open func disconnect() { 125 | return session.disconnect() 126 | } 127 | 128 | /// - Returns: An `Observable` of errors that occur during client connection time. 129 | open func connectionErrors() -> Observable { 130 | return session.connectionErrors() 131 | } 132 | 133 | // Sending Data 134 | 135 | /// Send `Data` to the given peer. 136 | /// 137 | /// - Returns: An `Observable` that calls `Event.Completed` once the transfer is complete. 138 | /// The semantics of _completed_ depends on the `mode` parameter. 139 | open func send 140 | (toPeer other: Client, 141 | data: Data, 142 | mode: MCSessionSendDataMode = .reliable) -> Observable<()> { 143 | return session.send(toPeer: other.iden, data: data, mode: mode) 144 | } 145 | 146 | /// Send `String` to the given peer. 147 | /// 148 | /// - Returns: An `Observable` that calls `Event.Completed` once the transfer is complete. 149 | /// The semantics of _completed_ depends on the `mode` parameter. 150 | open func send( 151 | toPeer other: Client, 152 | string: String, 153 | mode: MCSessionSendDataMode = .reliable) -> Observable<()> { 154 | return send(toPeer: other, 155 | data: string.data(using: String.Encoding.utf8)!, 156 | mode: mode) 157 | } 158 | 159 | /// Send json in the form of `[String: Any]` to the given peer. 160 | /// 161 | /// - Parameter json: This is serialized with `NSJSONSerialization`. An `Event.Error` is emitted from the 162 | /// `Observable` if a serialization error occurs. 163 | /// - Returns: An `Observable` that calls `Event.Completed` once the transfer is complete. 164 | /// The semantics of _completed_ depends on the `mode` parameter. 165 | open func send( 166 | toPeer other: Client, 167 | json: [String: Any], 168 | mode: MCSessionSendDataMode = .reliable) -> Observable<()> { 169 | do { 170 | let data = try JSONSerialization.data( 171 | withJSONObject: json, options: JSONSerialization.WritingOptions()) 172 | return send(toPeer: other, data: data, mode: mode) 173 | } catch let error as NSError { 174 | return Observable.error(error) 175 | } 176 | } 177 | 178 | /// Send a file-system resource to the given peer. 179 | /// 180 | /// - Parameter url: The URL to the underlying file that needs to be sent. 181 | /// - Returns: An `Observable` that represents the `NSProgress` of the file transfer. 182 | /// It emits `Event.Completed` once the transfer is complete. 183 | /// The semantics of _completed_ depends on the `mode` parameter. 184 | open func send( 185 | toPeer other: Client, 186 | name: String, 187 | url: URL, 188 | mode: MCSessionSendDataMode = .reliable) -> Observable { 189 | return session.send(toPeer: other.iden, name: name, resource: url, mode: mode) 190 | } 191 | 192 | /// Open a pipe to the given peer, allowing you send them bits. 193 | /// 194 | /// - Parameters: 195 | /// - streamName: The name of the stream that is passed to the recipient 196 | /// - runLoop: The runloop that is respondsible for fetching more source data when necessary 197 | /// - Returns: An `Observable` that emits requests for more data, in the form of a callback. 198 | open func send( 199 | toPeer other: Client, 200 | streamName: String, 201 | runLoop: RunLoop = RunLoop.main) -> Observable<([UInt8]) -> Void> { 202 | return session.send(toPeer: other.iden, 203 | streamName: streamName, 204 | runLoop: runLoop) 205 | } 206 | 207 | // Receiving data 208 | 209 | /// Receive `Data` streams from the `session`. 210 | /// 211 | /// - Returns: An `Observable` of: 212 | /// - Sender 213 | /// - Received data 214 | open func receive() -> Observable<(Client, Data)> { 215 | return session.receive().map { (Client(iden: $0), $1) } 216 | } 217 | 218 | /// Receive json streams from the `session`. 219 | /// 220 | /// - Returns: An `Observable` of: 221 | /// - Sender 222 | /// - Received json 223 | open func receive() -> Observable<(Client, [String: Any])> { 224 | return (receive() as Observable<(Client, Data)>) 225 | .map { (client: Client, data: Data) -> Observable<(Client, [String: Any])> in 226 | do { 227 | let json = try JSONSerialization.jsonObject( 228 | with: data, options: JSONSerialization.ReadingOptions()) 229 | if let j = json as? [String: Any] { 230 | return Observable.just((client, j)) 231 | } 232 | return Observable.never() 233 | } catch let error { 234 | return Observable.error(error) 235 | } 236 | } 237 | .merge() 238 | } 239 | 240 | /// Receive `String` streams from the `session`. 241 | /// 242 | /// - Returns: An `Observable` of: 243 | /// - Sender 244 | /// - Message 245 | open func receive() -> Observable<(Client, String)> { 246 | return session.receive() 247 | .map { (Client(iden: $0), NSString(data: $1, encoding: String.Encoding.utf8.rawValue)) } 248 | .filter { $1 != nil } 249 | .map { ($0, String($1!)) } 250 | } 251 | 252 | /// Receive a file from the `session`. `ResourceState` encapsulates the progress of 253 | /// the transfer. 254 | /// 255 | /// - Seealso: `receive() -> Observable<(Client, String, NSURL)>` 256 | /// - Returns: An `Observable` of: 257 | /// - Sender 258 | /// - File name 259 | /// - The `ResourceState` of the resource 260 | open func receive() -> Observable<(Client, String, ResourceState)> { 261 | return session.receive().map { (Client(iden: $0), $1, $2) } 262 | } 263 | 264 | /// Receive a file from the `session`. Ignore the progress, a single `Event.Next` 265 | /// will be emitted for when the transfer is complete. 266 | /// 267 | /// - Seealso: `receive() -> Observable<(Client, String, ResourceState)>` 268 | /// - Returns: An `Observable` of: 269 | /// - Sender 270 | /// - File name 271 | /// - The `NSURL` of the file's temporary location 272 | open func receive() -> Observable<(Client, String, URL)> { 273 | return session.receive() 274 | .filter { $2.fromFinished() != nil } 275 | .map { (Client(iden: $0), $1, $2.fromFinished()!) } 276 | } 277 | 278 | /// Receive a specific bitstream from a specific sender. 279 | /// 280 | /// - Parameters: 281 | /// - streamName: The stream name to accept data from 282 | /// - runLoop: The run loop on which to queue newly received data 283 | /// - maxLength: The maximum buffer size before flush 284 | /// 285 | /// - Returns: An `Observable` of bytes as they are received. 286 | /// - Remark: Even though most of the time data is received in the exact 287 | /// same buffer sizes/segments as they were sent, this is not guaranteed. 288 | open func receive( 289 | fromPeer other: Client, 290 | streamName: String, 291 | runLoop: RunLoop = RunLoop.main, 292 | maxLength: Int = 512) -> Observable<[UInt8]> { 293 | 294 | return session.receive(fromPeer: other.iden, 295 | streamName: streamName, 296 | runLoop: runLoop, 297 | maxLength: maxLength) 298 | } 299 | 300 | deinit { 301 | self.disconnect() 302 | } 303 | 304 | } 305 | -------------------------------------------------------------------------------- /RxMultipeer/MCSessionDelegateWrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSessionDelegateWrapper.h 3 | // RxMultipeer 4 | // 5 | // Created by Nathan Kot on 1/02/17. 6 | // Copyright © 2017 Nathan Kot. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | @import MultipeerConnectivity; 11 | 12 | // MCSessionDelegate has a bug introduced when annotating the 13 | // session:didFinishRecivingResourceWithName:fromPeer:atURL:withError method, 14 | // atURL: is meant to be optional however it wasn't marked as so. Thus if 15 | // a Swift class implementing this Delegate was ever called with an error for 16 | // the above method, it would crash. 17 | // 18 | // This protocol is the 'fixed' version, and can be trampolined onto by using 19 | // the MCSessionDelegateWrapper. 20 | @protocol MCSessionDelegateWrapperDelegate 21 | 22 | // Remote peer changed state. 23 | - (void) session:(MCSession * _Nonnull)session peer:(MCPeerID * _Nonnull)peerID didChangeState:(MCSessionState)state; 24 | // Received data from remote peer. 25 | - (void) session:(MCSession * _Nonnull)session didReceiveData:(NSData * _Nonnull)data fromPeer:(MCPeerID * _Nonnull)peerID; 26 | // Received a byte stream from remote peer. 27 | - (void) session:(MCSession * _Nonnull)session didReceiveStream:(NSInputStream * _Nonnull)stream withName:(NSString * _Nonnull)streamName fromPeer:(MCPeerID * _Nonnull)peerID; 28 | // Start receiving a resource from remote peer. 29 | - (void) session:(MCSession * _Nonnull)session didStartReceivingResourceWithName:(NSString * _Nonnull)resourceName fromPeer:(MCPeerID * _Nonnull)peerID withProgress:(NSProgress * _Nonnull)progress; 30 | // Finished receiving a resource from remote peer and saved the content 31 | // in a temporary location - the app is responsible for moving the file 32 | // to a permanent location within its sandbox. 33 | - (void) session:(MCSession * _Nonnull)session didFinishReceivingResourceWithName:(NSString * _Nonnull)resourceName fromPeer:(MCPeerID * _Nonnull)peerID atURL:(nullable NSURL *)localURL withError:(nullable NSError *)error; 34 | // Made first contact with peer and have identity information about the 35 | // remote peer (certificate may be nil). 36 | - (void) session:(MCSession * _Nonnull)session didReceiveCertificate:(nullable NSArray *)certificate fromPeer:(MCPeerID * _Nonnull)peerID certificateHandler:(nonnull void (^)(BOOL accept))certificateHandler; 37 | 38 | @end 39 | 40 | @interface MCSessionDelegateWrapper : NSObject 41 | 42 | @property (weak, nullable) id delegate; 43 | - (instancetype _Nonnull) initWithDelegate: (id _Nonnull) delegate; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /RxMultipeer/MCSessionDelegateWrapper.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSessionDelegateWrapper.m 3 | // RxMultipeer 4 | // 5 | // Created by Nathan Kot on 1/02/17. 6 | // Copyright © 2017 Nathan Kot. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | #import "MCSessionDelegateWrapper.h" 12 | 13 | @implementation MCSessionDelegateWrapper 14 | 15 | // @synthesize delegate = _delegate; 16 | 17 | - (id _Nonnull) initWithDelegate: (id _Nonnull) delegate { 18 | self = [super init]; 19 | 20 | if (self) { 21 | _delegate = delegate; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | #pragma mark - MCSessionDelegate 28 | 29 | - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { 30 | if ([self.delegate respondsToSelector:@selector(session:didReceiveData:fromPeer:)]) { 31 | [self.delegate session:session didReceiveData:data fromPeer:peerID]; 32 | } 33 | } 34 | 35 | - (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress { 36 | if ([self.delegate respondsToSelector:@selector(session:didStartReceivingResourceWithName:fromPeer:withProgress:)]) { 37 | [self.delegate session:session didStartReceivingResourceWithName:resourceName fromPeer:peerID withProgress:progress]; 38 | } 39 | } 40 | 41 | - (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state { 42 | if ([self.delegate respondsToSelector:@selector(session:peer:didChangeState:)]) { 43 | [self.delegate session:session peer:peerID didChangeState:state]; 44 | } 45 | } 46 | 47 | - (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error { 48 | if ([self.delegate respondsToSelector:@selector(session:didFinishReceivingResourceWithName:fromPeer:atURL:withError:)]) { 49 | [self.delegate session:session didFinishReceivingResourceWithName:resourceName fromPeer:peerID atURL:localURL withError:error]; 50 | } 51 | } 52 | 53 | - (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID { 54 | if ([self.delegate respondsToSelector:@selector(session:didReceiveStream:withName:fromPeer:)]) { 55 | [self.delegate session:session didReceiveStream:stream withName:streamName fromPeer:peerID]; 56 | } 57 | } 58 | 59 | - (void)session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler { 60 | if ([self.delegate respondsToSelector:@selector(session:didReceiveCertificate:fromPeer:certificateHandler:)]) { 61 | [self.delegate session:session didReceiveCertificate:certificate fromPeer:peerID certificateHandler:certificateHandler]; 62 | } 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /RxMultipeer/ResourceState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ResourceState { 4 | case progress(Progress) 5 | case finished(URL) 6 | case errored(Error) 7 | 8 | public func fromFinished() -> URL? { 9 | switch self { 10 | case .finished(let u): return u 11 | default: return nil 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RxMultipeer/RxMultipeer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // RxMultipeer-Bridging-Header.h 3 | // RxMultipeer 4 | // 5 | // Created by Nathan Kot on 1/02/17. 6 | // Copyright © 2017 Nathan Kot. All rights reserved. 7 | // 8 | 9 | #ifndef RxMultipeer_Bridging_Header_h 10 | #define RxMultipeer_Bridging_Header_h 11 | 12 | #import "MCSessionDelegateWrapper.h" 13 | 14 | 15 | #endif /* RxMultipeer_Bridging_Header_h */ 16 | -------------------------------------------------------------------------------- /RxMultipeer/RxMultipeerError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum RxMultipeerError : Error { 4 | case connectionError 5 | case unknownError 6 | 7 | public var description: String { 8 | switch self { 9 | case .connectionError: return "Could not establish connection with peer" 10 | case .unknownError: return "An unknown error occurred" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RxMultipeer/Session.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import MultipeerConnectivity 4 | 5 | // The protocol that adapters must adhere to. 6 | // We want a concise common interface for p2p related operations. 7 | public protocol Session { 8 | 9 | #if swift(>=2.2) 10 | associatedtype I 11 | #else 12 | typealias I 13 | #endif 14 | 15 | var iden: I { get } 16 | var meta: [String: String]? { get } 17 | 18 | // Connection concerns 19 | ////////////////////////////////////////////////////////////////////////// 20 | 21 | func incomingConnections() -> Observable<(I, [String: Any]?, (Bool) -> ())> 22 | func incomingCertificateVerifications() -> Observable<(I, [Any]?, (Bool) -> Void)> 23 | func connections() -> Observable<[I]> 24 | func nearbyPeers() -> Observable<[(I, [String: String]?)]> 25 | func startAdvertising() 26 | func stopAdvertising() 27 | func startBrowsing() 28 | func stopBrowsing() 29 | func connect(_ peer: I, context: [String: Any]?, timeout: TimeInterval) 30 | func disconnect() 31 | func connectionErrors() -> Observable 32 | 33 | // Data reception concerns 34 | ////////////////////////////////////////////////////////////////////////// 35 | 36 | func receive() -> Observable<(I, Data)> 37 | func receive() -> Observable<(I, String, ResourceState)> 38 | func receive(fromPeer: I, streamName: String, runLoop: RunLoop, maxLength: Int) -> Observable<[UInt8]> 39 | 40 | // Data delivery concerns 41 | ////////////////////////////////////////////////////////////////////////// 42 | 43 | func send(toPeer: I, data: Data, mode: MCSessionSendDataMode) -> Observable<()> 44 | func send(toPeer: I, name: String, resource: URL, mode: MCSessionSendDataMode) -> Observable 45 | func send(toPeer: I, streamName: String, runLoop: RunLoop) -> Observable<([UInt8]) -> Void> 46 | 47 | } 48 | -------------------------------------------------------------------------------- /RxMultipeer/Supporting Files/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 | -------------------------------------------------------------------------------- /RxMultipeer/Weak.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Helper class to let us create a list of weak objects 4 | class Weak { 5 | weak var value : T? 6 | init (_ value: T) { 7 | self.value = value 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RxMultipeerTests/Fixtures/Data.txt: -------------------------------------------------------------------------------- 1 | hello there this is random data -------------------------------------------------------------------------------- /RxMultipeerTests/IntegrationSpec.swift: -------------------------------------------------------------------------------- 1 | import RxMultipeer 2 | import Quick 3 | import Nimble 4 | import RxSwift 5 | 6 | public class IntegrationSpec : QuickSpec { 7 | 8 | override public func spec() { 9 | 10 | var disposeBag = DisposeBag() 11 | 12 | describe("two clients") { 13 | 14 | var clientone: CurrentClient! 15 | var clienttwo: CurrentClient! 16 | 17 | beforeEach { 18 | MockSession.reset() 19 | disposeBag = DisposeBag() 20 | clientone = CurrentClient(session: MockSession(name: "one")) 21 | clienttwo = CurrentClient(session: MockSession(name: "two")) 22 | } 23 | 24 | describe("client two advertises") { 25 | 26 | beforeEach { 27 | // Advertise and always accept connections 28 | clienttwo.startAdvertising() 29 | 30 | clienttwo.incomingCertificateVerifications() 31 | .subscribe(onNext: { $2(true) }) 32 | .addDisposableTo(disposeBag) 33 | 34 | clienttwo.incomingConnections() 35 | .subscribe(onNext: { $2(true) }) 36 | .addDisposableTo(disposeBag) 37 | } 38 | 39 | it("allows a client to find another even if browsing is begun before advertising") { 40 | waitUntil { done in 41 | clienttwo.stopAdvertising() 42 | clienttwo.disconnect() 43 | let clientthree = CurrentClient(session: MockSession(name: "two")) 44 | clientone.startBrowsing() 45 | defer { clientthree.startAdvertising() } 46 | clientone.nearbyPeers() 47 | .filter { $0.count > 0 } 48 | .take(1).subscribe(onCompleted: done) 49 | .addDisposableTo(disposeBag) 50 | } 51 | } 52 | 53 | it("allows client one to browse for client two") { 54 | waitUntil { done in 55 | clientone.startBrowsing() 56 | clientone.nearbyPeers() 57 | .filter { $0.count > 0 } 58 | .subscribe(onNext: { clients in 59 | expect(clients[0].0.iden).to(equal(clienttwo.iden)) 60 | done() 61 | }) 62 | .addDisposableTo(disposeBag) 63 | } 64 | } 65 | 66 | it("allows clients to connect with eachother") { 67 | waitUntil { done in 68 | clientone.connectedPeer() 69 | .take(1) 70 | .subscribe(onCompleted: { _ in done() }) 71 | .addDisposableTo(disposeBag) 72 | 73 | clientone.connect(toPeer: clienttwo) 74 | } 75 | } 76 | 77 | it("alters connections when clients connect") { 78 | waitUntil { done in 79 | Observable.combineLatest( 80 | clientone.connections(), 81 | clienttwo.connections()) { $0.count + $1.count } 82 | .subscribe(onNext: { if $0 == 2 { done() } }) 83 | .addDisposableTo(disposeBag) 84 | 85 | clientone.connect(toPeer: clienttwo) 86 | } 87 | } 88 | 89 | it("notifies connections") { 90 | waitUntil { done in 91 | Observable.zip(clientone.connectedPeer(), clienttwo.connectedPeer()) { $0 } 92 | .take(1) 93 | .subscribe(onNext: { (two, one) in 94 | expect(two.iden).to(equal(clienttwo.iden)) 95 | expect(one.iden).to(equal(clientone.iden)) 96 | done() 97 | }) 98 | .addDisposableTo(disposeBag) 99 | 100 | clientone.connect(toPeer: clienttwo) 101 | } 102 | } 103 | 104 | it("notifies disconnections") { 105 | waitUntil { done in 106 | clientone.connect(toPeer: clienttwo) 107 | Observable.zip(clientone.disconnectedPeer(), clienttwo.disconnectedPeer()) { $0 } 108 | .take(1) 109 | .subscribe(onNext: { (two, one) in 110 | expect(two.iden).to(equal(clienttwo.iden)) 111 | expect(one.iden).to(equal(clientone.iden)) 112 | done() 113 | }) 114 | .addDisposableTo(disposeBag) 115 | 116 | clientone.disconnect() 117 | } 118 | } 119 | 120 | describe("clients are connected") { 121 | 122 | beforeEach { 123 | waitUntil { done in 124 | clientone.connectedPeer() 125 | .take(1) 126 | .subscribe(onNext: { _ in done() }) 127 | .addDisposableTo(disposeBag) 128 | 129 | clientone.connect(toPeer: clienttwo) 130 | } 131 | } 132 | 133 | it("allows client two to disconnect") { 134 | waitUntil { done in 135 | clientone.connections() 136 | .skip(1).take(1) 137 | .subscribe(onNext: { (connections) in 138 | expect(connections.count).to(equal(0)) 139 | done() 140 | }) 141 | .addDisposableTo(disposeBag) 142 | 143 | clienttwo.disconnect() 144 | } 145 | } 146 | 147 | it("fires a next event when sending data") { 148 | waitUntil { done in 149 | clientone.send(toPeer: clienttwo, string: "hello") 150 | .subscribe(onNext: { _ in done() }) 151 | .addDisposableTo(disposeBag) 152 | } 153 | } 154 | 155 | it("lets clients send strings to eachother") { 156 | waitUntil { done in 157 | clienttwo.receive() 158 | .subscribe(onNext: { (client: Client, string: String) in 159 | expect(client.iden).to(equal(clientone.iden)) 160 | expect(string).to(equal("hello")) 161 | }) 162 | .addDisposableTo(disposeBag) 163 | 164 | clientone.send(toPeer: clienttwo, string: "hello") 165 | .subscribe(onCompleted: { done() }) 166 | .addDisposableTo(disposeBag) 167 | } 168 | } 169 | 170 | it("lets clients send resource urls to each other") { 171 | waitUntil { done in 172 | clienttwo.receive() 173 | .subscribe(onNext: { (client: Client, name: String, url: URL) in 174 | expect(client.iden).to(equal(clientone.iden)) 175 | expect(name).to(equal("txt file")) 176 | let contents = String(data: try! Data(contentsOf: url), encoding: String.Encoding.utf8) 177 | expect(contents).to(equal("hello there this is random data")) 178 | }) 179 | .addDisposableTo(disposeBag) 180 | 181 | let url = Bundle(for: type(of: self)).url(forResource: "Data", withExtension: "txt") 182 | clientone.send(toPeer: clienttwo, name: "txt file", url: url!) 183 | .subscribe(onCompleted: { done() }) 184 | .addDisposableTo(disposeBag) 185 | } 186 | } 187 | 188 | it("lets clients send JSON data to each other via foundation objects") { 189 | waitUntil { done in 190 | clienttwo.receive() 191 | .subscribe(onNext: { (client: Client, json: [String: Any]) in 192 | expect(client.iden).to(equal(clientone.iden)) 193 | expect(json["one"] as? String).to(equal("two")) 194 | expect(json["three"] as? Int).to(equal(4)) 195 | expect(json["five"] as? Bool).to(beTrue()) 196 | }) 197 | .addDisposableTo(disposeBag) 198 | 199 | clientone.send( 200 | toPeer: clienttwo, 201 | json: ["one": "two", "three": 4, "five": true]) 202 | .subscribe(onCompleted: { done() }) 203 | .addDisposableTo(disposeBag) 204 | } 205 | } 206 | 207 | it("lets clients send streams of bytes to each other") { 208 | waitUntil { done in 209 | clienttwo.receive(fromPeer: clientone, streamName: "hello") 210 | .take(1) 211 | .subscribe(onNext: { data in 212 | expect(data).to(equal([0b00110011, 0b11111111])) 213 | done() 214 | }) 215 | .addDisposableTo(disposeBag) 216 | 217 | let data: Observable<[UInt8]> = Observable.of( 218 | [0b00110011, 0b11111111], 219 | [0b00000000, 0b00000001]) 220 | 221 | Observable.zip(clientone.send(toPeer: clienttwo, streamName: "hello"), data) { $0 } 222 | .subscribe(onNext: { (fetcher, data) in fetcher(data) }) 223 | .addDisposableTo(disposeBag) 224 | } 225 | } 226 | 227 | it("limits stream reads to the `maxLength` passed in") { 228 | waitUntil { done in 229 | clienttwo.receive( 230 | fromPeer: clientone, 231 | streamName: "hello") 232 | .take(2) 233 | .reduce([], accumulator: { $0 + [$1] }) 234 | .subscribe(onNext: { data in 235 | expect(data[0].count).to(equal(512)) 236 | expect(data[1].count).to(equal(4)) 237 | done() 238 | }) 239 | .addDisposableTo(disposeBag) 240 | 241 | let data = Observable.just([UInt8](repeating: 0x4D, count: 516)) 242 | 243 | Observable.zip(clientone.send(toPeer: clienttwo, streamName: "hello"), data) { $0 } 244 | .subscribe(onNext: { (fetcher, data) in fetcher(data) }) 245 | .addDisposableTo(disposeBag) 246 | } 247 | 248 | } 249 | 250 | } 251 | 252 | } 253 | 254 | } 255 | 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /RxMultipeerTests/Supporting Files/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 | -------------------------------------------------------------------------------- /Screenshots/BuildConfiguration-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxSwiftCommunity/RxMultipeer/7d295f44632eab05e3cef89e439a237ac793fdab/Screenshots/BuildConfiguration-1.png -------------------------------------------------------------------------------- /Screenshots/BuildConfiguration-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxSwiftCommunity/RxMultipeer/7d295f44632eab05e3cef89e439a237ac793fdab/Screenshots/BuildConfiguration-2.png -------------------------------------------------------------------------------- /Screenshots/BuildConfiguration-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxSwiftCommunity/RxMultipeer/7d295f44632eab05e3cef89e439a237ac793fdab/Screenshots/BuildConfiguration-3.png --------------------------------------------------------------------------------