├── .swiftlint.yml ├── .spi.yml ├── media └── MultipeerHelper-Header.png ├── MultipeerHelper+Example ├── MultipeerHelper+Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── ContentView.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── RealityViewController.swift │ └── RealityViewController+Gestures.swift └── MultipeerHelper+Example.xcodeproj │ └── project.pbxproj ├── Package.swift ├── .github └── workflows │ ├── swift-build.yml │ └── build-docc.yml ├── install_swiftlint.sh ├── LICENSE ├── Sources └── MultipeerHelper │ ├── HasSynchronization+Extensions.swift │ ├── MultipeerHelperDelegate.swift │ └── MultipeerHelper.swift ├── .gitignore └── README.md /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | line_length: 2 | ignores_comments: true 3 | cyclomatic_complexity: 4 | ignores_case_statements: true 5 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: [MultipeerHelper] 6 | -------------------------------------------------------------------------------- /media/MultipeerHelper-Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/MultipeerHelper/HEAD/media/MultipeerHelper-Header.png -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MultipeerHelper", 8 | platforms: [.iOS(.v11), .macOS(.v10_10), .tvOS(.v9)], 9 | products: [ 10 | .library(name: "MultipeerHelper", targets: ["MultipeerHelper"]) 11 | ], 12 | targets: [ 13 | .target(name: "MultipeerHelper") 14 | ], 15 | swiftLanguageVersions: [.v5] 16 | ) 17 | -------------------------------------------------------------------------------- /.github/workflows/swift-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "!*" 9 | pull_request: 10 | branches: 11 | - "*" 12 | 13 | jobs: 14 | build: 15 | runs-on: macOS-latest 16 | steps: 17 | - name: Checkout 🛎 18 | uses: actions/checkout@v3 19 | - name: Swift Lint 🧹 20 | run: swiftlint --strict 21 | - name: Test Build 🔨 22 | run: | 23 | xcodebuild clean build -scheme $SCHEME -destination 'generic/platform=iOS' 24 | xcodebuild clean build -scheme $SCHEME -destination 'platform=macOS' 25 | # xcodebuild clean build -scheme $SCHEME -destination 'platform=tvOS Simulator,name=Any tvOS Device' 26 | env: 27 | SCHEME: MultipeerHelper 28 | -------------------------------------------------------------------------------- /install_swiftlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Installs the SwiftLint package. 4 | # Tries to get the precompiled .pkg file from Github, but if that 5 | # fails just recompiles from source. 6 | 7 | set -e 8 | 9 | SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg" 10 | SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.37.0/SwiftLint.pkg" 11 | 12 | wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL 13 | 14 | if [ -f $SWIFTLINT_PKG_PATH ]; then 15 | echo "SwiftLint package exists! Installing it..." 16 | sudo installer -pkg $SWIFTLINT_PKG_PATH -target / 17 | else 18 | echo "SwiftLint package doesn't exist. Compiling from source..." && 19 | git clone https://github.com/realm/SwiftLint.git /tmp/SwiftLint && 20 | cd /tmp/SwiftLint && 21 | git submodule update --init --recursive && 22 | sudo make install 23 | fi 24 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MultipeerHelper+Example 4 | // 5 | // Created by Max Cobb on 11/23/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | var window: UIWindow? 15 | 16 | func application( 17 | _: UIApplication, 18 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? 19 | ) -> Bool { 20 | // Create the SwiftUI view that provides the window contents. 21 | let contentView = ContentView() 22 | 23 | // Use a UIHostingController as window root view controller. 24 | let window = UIWindow(frame: UIScreen.main.bounds) 25 | window.rootViewController = UIHostingController(rootView: contentView) 26 | self.window = window 27 | window.makeKeyAndVisible() 28 | return true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MultipeerHelper+Example 4 | // 5 | // Created by Max Cobb on 11/23/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | import FocusEntity 11 | import RealityKit 12 | import SmartHitTest 13 | import SwiftUI 14 | 15 | struct ContentView: View { 16 | var body: some View { 17 | ARViewContainer().edgesIgnoringSafeArea(.all) 18 | } 19 | } 20 | 21 | struct ARViewContainer: UIViewControllerRepresentable { 22 | func makeUIViewController( 23 | context _: UIViewControllerRepresentableContext< 24 | ARViewContainer 25 | > 26 | ) -> RealityViewController { 27 | RealityViewController() 28 | } 29 | 30 | func updateUIViewController( 31 | _: RealityViewController, 32 | context _: UIViewControllerRepresentableContext< 33 | ARViewContainer 34 | > 35 | ) {} 36 | } 37 | 38 | #if DEBUG 39 | struct ContentView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | ContentView() 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Max Fraser Cobb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/MultipeerHelper/HasSynchronization+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HasSynchronization+Extensions.swift 3 | // MultipeerHelper+Example 4 | // 5 | // Created by Max Cobb on 12/8/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if !os(tvOS) 10 | import RealityKit 11 | 12 | /// Error enum for some common Multipeer Errors 13 | public enum MHelperErrors: Error { 14 | /// Request timed out 15 | case timedOut 16 | /// Request failed 17 | case failure 18 | } 19 | 20 | @available(iOS 13.0, macOS 10.15, *) 21 | public extension HasSynchronization { 22 | /// Execute the escaping completion if you are the entity owner, once you receive ownership 23 | /// or call result failure if ownership cannot be granted to the caller. 24 | /// - Parameter completion: completion of type Result, success once ownership granted, failure if not granted 25 | func runWithOwnership( 26 | completion: @escaping (Result) -> Void 27 | ) { 28 | if self.isOwner { 29 | // If caller is already the owner 30 | completion(.success(self)) 31 | } else { 32 | self.requestOwnership { (result) in 33 | if result == .granted { 34 | completion(.success(self)) 35 | } else { 36 | completion( 37 | .failure(result == .timedOut ? 38 | MHelperErrors.timedOut : 39 | MHelperErrors.failure 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /.github/workflows/build-docc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy DocC 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | # Single deploy job since we're just deploying 21 | deploy: 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | runs-on: macos-12 26 | steps: 27 | - name: Checkout 🛎️ 28 | uses: actions/checkout@v3 29 | - name: Build DocC 30 | run: | 31 | xcodebuild docbuild -scheme MultipeerHelper -derivedDataPath /tmp/docbuild -destination 'generic/platform=iOS'; 32 | $(xcrun --find docc) process-archive \ 33 | transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/MultipeerHelper.doccarchive \ 34 | --hosting-base-path MultipeerHelper \ 35 | --output-path docs; 36 | echo "" > docs/index.html 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v1 39 | with: 40 | # Upload docs directory 41 | path: 'docs' 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/.DS_Store 3 | **.log 4 | 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | MultipeerHelper.xcodeproj 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | .build/ 48 | xcshareddata/ 49 | *.xcworkspace/ 50 | Package.resolved 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | Pods/ 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots 77 | fastlane/test_output 78 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | 25 | NSLocalNetworkUsageDescription 26 | This application synchronises with other iOS devices on the same network. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | arkit 33 | 34 | UIStatusBarHidden 35 | 36 | NSBonjourServices 37 | 38 | _helper-test._tcp 39 | _helper-test._udp 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/RealityViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealityViewController.swift 3 | // MultipeerHelper+Example 4 | // 5 | // Created by Max Cobb on 11/23/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | import FocusEntity 11 | import MultipeerConnectivity 12 | import RealityKit 13 | 14 | class RealityViewController: UIViewController, ARSessionDelegate { 15 | let arView = ARView(frame: .zero) 16 | let focusSquare = FESquare() 17 | var multipeerHelp: MultipeerHelper! 18 | required init() { 19 | super.init(nibName: nil, bundle: nil) 20 | setupARView() 21 | setupMultipeer() 22 | setupGestures() 23 | 24 | // Do not synchronize this entity 25 | focusSquare.synchronization = nil 26 | focusSquare.viewDelegate = arView 27 | focusSquare.setAutoUpdate(to: true) 28 | } 29 | 30 | func setupARView() { 31 | arView.frame = view.bounds 32 | arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 33 | 34 | arView.session.delegate = self 35 | 36 | let arConfiguration = ARWorldTrackingConfiguration() 37 | arConfiguration.planeDetection = .horizontal 38 | arConfiguration.isCollaborationEnabled = true 39 | arView.session.run(arConfiguration, options: []) 40 | view.addSubview(arView) 41 | } 42 | 43 | required init?(coder _: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | } 47 | 48 | extension RealityViewController: MultipeerHelperDelegate { 49 | 50 | func shouldSendJoinRequest( 51 | _ peer: MCPeerID, 52 | with discoveryInfo: [String: String]? 53 | ) -> Bool { 54 | if RealityViewController.checkPeerToken(with: discoveryInfo) { 55 | return true 56 | } 57 | print("incompatible peer!") 58 | return false 59 | } 60 | 61 | func setupMultipeer() { 62 | multipeerHelp = MultipeerHelper( 63 | serviceName: "helper-test", 64 | sessionType: .both, 65 | delegate: self 66 | ) 67 | 68 | // MARK: - Setting RealityKit Synchronization 69 | 70 | guard let syncService = multipeerHelp.syncService else { 71 | fatalError("could not create multipeerHelp.syncService") 72 | } 73 | arView.scene.synchronizationService = syncService 74 | } 75 | 76 | func receivedData(_ data: Data, _ peer: MCPeerID) { 77 | print(String(data: data, encoding: .unicode) ?? "Data is not a unicode string") 78 | } 79 | 80 | func peerJoined(_ peer: MCPeerID) { 81 | print("new peer has joined: \(peer.displayName)") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example/RealityViewController+Gestures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealityViewController+Gestures.swift 3 | // MultipeerHelper+Example 4 | // 5 | // Created by Max Cobb on 11/23/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | import RealityKit 11 | import UIKit 12 | 13 | extension RealityViewController: UIGestureRecognizerDelegate { 14 | 15 | func setupGestures() { 16 | let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) 17 | self.arView.addGestureRecognizer(tap) 18 | } 19 | 20 | /// This function does sends a message "hello!" to all peers. 21 | /// If you tap on an existing entity, it will run a scale up and down animation 22 | /// If you tap on the floor without hitting any entities it will create a new Anchor 23 | @objc func handleTap(_ sender: UITapGestureRecognizer? = nil) { 24 | let displayName = self.multipeerHelp.myPeerID.displayName 25 | if let myData = "hello! from \(displayName)" 26 | .data(using: .unicode) { 27 | multipeerHelp.sendToAllPeers(myData, reliably: true) 28 | } 29 | 30 | guard let touchInView = sender?.location(in: self.arView) else { 31 | return 32 | } 33 | if let hitEntity = self.arView.entity(at: touchInView) { 34 | // animate the Entity 35 | hitEntity.runWithOwnership { (result) in 36 | switch result { 37 | case .success: 38 | let origTransform = Transform(scale: .one, rotation: .init(), translation: .zero) 39 | let largerTransform = Transform(scale: .init(repeating: 1.5), rotation: .init(), translation: .zero) 40 | hitEntity.move(to: largerTransform, relativeTo: hitEntity.parent, duration: 0.2) 41 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { 42 | hitEntity.move(to: origTransform, relativeTo: hitEntity.parent, duration: 0.2) 43 | } 44 | case .failure: 45 | print("could not get access to entity") 46 | } 47 | } 48 | } else if let result = arView.raycast( 49 | from: touchInView, 50 | allowing: .existingPlaneGeometry, alignment: .horizontal 51 | ).first { 52 | self.addNewAnchor(transform: result.worldTransform) 53 | } 54 | } 55 | 56 | /// Add a new anchor to the session 57 | /// - Parameter transform: position in world space where the new anchor should be 58 | func addNewAnchor(transform: simd_float4x4) { 59 | let arAnchor = ARAnchor(name: "Cube Anchor", transform: transform) 60 | let newAnchor = AnchorEntity(anchor: arAnchor) 61 | 62 | let cubeModel = ModelEntity( 63 | mesh: .generateBox(size: 0.1), 64 | materials: [SimpleMaterial(color: .red, isMetallic: false)] 65 | ) 66 | cubeModel.generateCollisionShapes(recursive: false) 67 | 68 | newAnchor.addChild(cubeModel) 69 | 70 | newAnchor.synchronization?.ownershipTransferMode = .autoAccept 71 | 72 | newAnchor.anchoring = AnchoringComponent(arAnchor) 73 | arView.scene.addAnchor(newAnchor) 74 | arView.session.add(anchor: arAnchor) 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | ![MultipeerHelper Header](https://github.com/maxxfrazer/MultipeerHelper/blob/main/media/MultipeerHelper-Header.png?raw=true) 12 | 13 | # MultipeerHelper 14 | 15 | MultipeerConnectivity can be a big pill for developers to swallow. This package aims to simplify the creation of a multi-peer experience, while still delivering the full power of Apple's API. 16 | 17 | ## Installation 18 | 19 | This is a Swift Package, and can be installed via Xcode with the URL of this repository: 20 | 21 | `git@github.com:maxxfrazer/MultipeerHelper.git` 22 | 23 | [For more information on how to add a Swift Package using Xcode, see Apple's official documentation.](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) 24 | 25 | 26 | ## Usage 27 | 28 | To use this package, all you have to do is import `MultipeerHelper` and initialise the object: 29 | 30 | ```swift 31 | self.multipeerHelp = MultipeerHelper( 32 | serviceName: "helper-test" 33 | ) 34 | ``` 35 | 36 | Because MultipeerConnectivity looks over your local network to find other devices to connect with, there are a few new things to include since iOS 14. 37 | 38 | The first thing, is to include the key `NSLocalNetworkUsageDescription` in your app's Info.plist, along with a short text explaining why you need to use the local network. For example "This application needs access to the local network to find opponents.". 39 | 40 | As well as the above, you also need to add another key, `NSBonjourServices`. Bonjour services is an array of Bonjour service types. 41 | For example, if your serviceName is "helper-test", you will need to add `_helper-test._tcp` and `_helper-test._udp`. 42 | 43 | The two above keys are included in [the Example Project](MultipeerHelper+Example). 44 | 45 | See full documentation here:
46 | https://maxxfrazer.github.io/MultipeerHelper/documentation/multipeerhelper/ 47 | 48 | ### RealityKit 49 | 50 | To extend this to RealityKit's synchronization service, simply add the following: 51 | 52 | ```swift 53 | self.arView.scene.synchronizationService = self.multipeerHelp.syncService 54 | ``` 55 | 56 | And also make sure that your ARConfiguration's isCollaborationEnabled property is set to true. 57 | 58 | To make sure RealityKit's synchronizationService runs properly, you must ensure that the RealityKit version installed on any two devices are compatible. 59 | 60 | By default, any OS using MultipeerHelper that can install RealityKit (iOS, iPadOS and macOS) will have a key added to the discoveryInfo. 61 | To use this easily, you can add the `shouldSendJoinRequest` method to your `MultipeerHelperDelegate`, and make use of the `checkPeerToken` which is accessible to any class which inherits the `MultipeerHelperDelegate`. Here's an example: 62 | 63 | ```swift 64 | extension RealityViewController: MultipeerHelperDelegate { 65 | func shouldSendJoinRequest( 66 | _ peer: MCPeerID, 67 | with discoveryInfo: [String: String]? 68 | ) -> Bool { 69 | self.checkPeerToken(with: discoveryInfo) 70 | } 71 | } 72 | ``` 73 | 74 | This method is used in [the Example Project](MultipeerHelper+Example). 75 | 76 | ### Initializer Parameters 77 | 78 | #### serviceName 79 | This is the type of service to advertise or search for. Due to how MultipeerConnectivity uses it, it should have the following restrictions: 80 | - Must be 1–15 characters long 81 | - Can contain only ASCII lowercase letters, numbers, and hyphens 82 | - Must contain at least one ASCII letter 83 | - Must not begin or end with a hyphen 84 | - Must not contain hyphens adjacent to other hyphens. 85 | 86 | #### sessionType (default: `.both`) 87 | This lets the service know if it should be acting as a service `host` (advertiser), `peer` (browser), or in a scenario where it doesn't matter, `both`. The default for this parameter is `both`, which is the scenario where all devices want to just connect to each other with no questions asked. 88 | 89 | #### peerName (default: `UIDevice.current.name`) 90 | String name of your device on the network. 91 | 92 | #### encryptionPreference (default: `.required`) 93 | encryptionPreference is how data sent over the network are encrypted. 94 | 95 | #### delegate (default: `nil`) 96 | This delegate object will inherit the `MultipeerHelperDelegate` protocol, which can be used for all the handling of transferring data round the network and seeing when others join and leave. 97 | -------------------------------------------------------------------------------- /Sources/MultipeerHelper/MultipeerHelperDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipeerHelperDelegate.swift 3 | // 4 | // 5 | // Created by Max Cobb on 11/22/19. 6 | // 7 | 8 | import MultipeerConnectivity 9 | 10 | /// Delegate for some useful multipeer connectivity methods 11 | @objc public protocol MultipeerHelperDelegate: AnyObject { 12 | /// Data that has been recieved from another peer 13 | /// - Parameters: 14 | /// - peerHelper: The ``MultipeerHelper`` session that manages the multipeer connectivity 15 | /// - data: The data which has been recieved 16 | /// - peer: The peer that sent the data 17 | @objc optional func receivedData(peerHelper: MultipeerHelper, _ data: Data, _ peer: MCPeerID) 18 | 19 | /// Callback for when a peer joins the network 20 | /// - Parameters: 21 | /// - peerHelper: The ``MultipeerHelper`` session that manages the multipeer connectivity 22 | /// - peer: the `MCPeerID` of the newly joined peer 23 | @objc optional func peerJoined(peerHelper: MultipeerHelper, _ peer: MCPeerID) 24 | 25 | /// Callback for when a peer leaves the network 26 | /// - Parameters: 27 | /// - peerHelper: The ``MultipeerHelper`` session that manages the multipeer connectivity 28 | /// - peer: the `MCPeerID` of the peer that left 29 | @objc optional func peerLeft(peerHelper: MultipeerHelper, _ peer: MCPeerID) 30 | 31 | /// Callback for when a new peer has been found. will default to accept all peers 32 | /// - Parameters: 33 | /// - peerHelper: The ``MultipeerHelper`` session that manages the multipeer connectivity 34 | /// - peer: the `MCPeerID` of the peer who wants to join the network 35 | /// - discoveryInfo: The info dictionary advertised by the discovered peer. For more information on the contents of this dictionary, see the documentation for 36 | /// [init(peer:discoveryInfo:serviceType:)](apple-reference-documentation://ls%2Fdocumentation%2Fmultipeerconnectivity%2Fmcnearbyserviceadvertiser%2F1407102-init) in [MCNearbyServiceAdvertiser](apple-reference-documentation://ls%2Fdocumentation%2Fmultipeerconnectivity%2Fmcnearbyserviceadvertiser). 37 | /// - Returns: Bool if the peer request to join the network or not 38 | @objc optional func shouldSendJoinRequest( 39 | peerHelper: MultipeerHelper, _ peer: MCPeerID, with discoveryInfo: [String: String]? 40 | ) -> Bool 41 | 42 | /// Handle when a peer has requested to join the network 43 | /// - Parameters: 44 | /// - peerHelper: The ``MultipeerHelper`` session that manages the multipeer connectivity 45 | /// - peerID: Peer requesting to join 46 | /// - context: Any data the requesting peer may have sent with their request 47 | /// - Returns: Bool if the peer's join request should be accepted 48 | @objc optional func shouldAcceptJoinRequest(peerHelper: MultipeerHelper, peerID: MCPeerID, context: Data?) -> Bool 49 | 50 | /// This will be set as the base for the discoveryInfo, which is sent out by the advertiser (host). 51 | /// The key "MultipeerHelper.compTokenKey" is in use by MultipeerHelper, for checking the 52 | /// compatibility of RealityKit versions. 53 | /// - Returns: Discovery Info 54 | @objc optional func setDiscoveryInfo() -> [String: String] 55 | 56 | /// Peer can no longer be found on the network, and thus cannot receive data 57 | /// - Parameters: 58 | /// - peerHelper: The ``MultipeerHelper`` session that manages the nearby peer whose state changed 59 | /// - peer: If a peer has left the network in a non typical way 60 | @objc optional func peerLost( 61 | peerHelper: MultipeerHelper, _ peer: MCPeerID 62 | ) 63 | 64 | /// Received a byte stream from remote peer. 65 | /// - Parameters: 66 | /// - peerHelper: The ``MultipeerHelper`` session through which the byte stream was opened 67 | /// - stream: An NSInputStream object that represents the local endpoint for the byte stream. 68 | /// - streamName: The name of the stream, as provided by the originator. 69 | /// - peerID: The peer ID of the originator of the stream. 70 | @objc optional func receivedStream( 71 | peerHelper: MultipeerHelper, _ stream: InputStream, _ streamName: String, _ peer: MCPeerID 72 | ) 73 | /// Start receiving a resource from remote peer. 74 | /// - Parameters: 75 | /// - peerHelper: The ``MultipeerHelper`` session that started receiving the resource 76 | /// - resourceName: name of the resource, as provided by the sender. 77 | /// - peerID: sender’s peer ID. 78 | /// - progress: NSProgress object that can be used to cancel the transfer or queried to determine how far the transfer has progressed. 79 | @objc optional func receivingResource( 80 | peerHelper: MultipeerHelper, _ resourceName: String, _ peer: MCPeerID, _ progress: Progress 81 | ) 82 | /// Received a resource from remote peer. 83 | /// - Parameters: 84 | /// - peerHelper: The ``MultipeerHelper`` session through which the data were received 85 | /// - resourceName: The name of the resource, as provided by the sender. 86 | /// - peerID: The peer ID of the sender. 87 | /// - localURL: An NSURL object that provides the location of a temporary file containing the received data. 88 | /// - error: An error object indicating what went wrong if the file was not received successfully, or nil. 89 | @objc optional func receivedResource( 90 | peerHelper: MultipeerHelper, _ resourceName: String, _ peerID: MCPeerID, _ localUrl: URL?, _ error: Error? 91 | ) 92 | /// Made first contact with peer and have identity information about the 93 | /// remote peer (certificate may be nil). 94 | /// - Parameters: 95 | /// - peerHelper: The ``MultipeerHelper`` session that manages the nearby peer whose state changed 96 | /// - certificate: A certificate chain, presented as an array of SecCertificateRef certificate objects. The first certificate in this chain is the peer’s certificate, which is derived from the identity that the peer provided when it called the `initWithPeer:securityIdentity:encryptionPreference:` method. The other certificates are the (optional) additional chain certificates provided in that same array. 97 | /// If the nearby peer did not provide a security identity, then this parameter’s value is nil. 98 | /// - peerID: The peer ID of the sender. 99 | @objc optional func receivedCertificate( 100 | peerHelper: MultipeerHelper, certificate: [Any]?, fromPeer peerID: MCPeerID) -> Bool 101 | } 102 | 103 | #if canImport(RealityKit) 104 | import RealityKit 105 | extension MultipeerHelperDelegate { 106 | /// Checks whether the discovered session is using a compatible version of RealityKit 107 | /// For collaborative sessions. 108 | /// - Parameter discoveryInfo: The discoveryInfo from the advertiser 109 | /// picked up by a browser. 110 | /// - Returns: Boolean representing whether or not the two devices 111 | /// have compatible versions of RealityKit. 112 | public static func checkPeerToken(with discoveryInfo: [String: String]?) -> Bool { 113 | guard let compTokenStr = discoveryInfo?[MultipeerHelper.compTokenKey] 114 | else { 115 | return false 116 | } 117 | if #available(iOS 13.4, macOS 10.15.4, *) { 118 | if let tokenData = compTokenStr.data(using: .utf8), 119 | let compToken = try? JSONDecoder().decode( 120 | NetworkCompatibilityToken.self, 121 | from: tokenData 122 | ) { 123 | return compToken.compatibilityWith(.local) == .compatible 124 | } 125 | } 126 | return false 127 | } 128 | } 129 | #endif 130 | -------------------------------------------------------------------------------- /Sources/MultipeerHelper/MultipeerHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipeerHelper.swift 3 | // 4 | // 5 | // Created by Max Cobb on 11/22/19. 6 | // 7 | 8 | import MultipeerConnectivity 9 | import Foundation 10 | #if canImport(RealityKit) 11 | import RealityKit 12 | #endif 13 | 14 | /// A class made to wrap around an [MCSession](doc://MultipeerHelper/documentation/multipeerhelper/multipeerhelper/session) to simplify usage. 15 | public class MultipeerHelper: NSObject { 16 | /// What type of session you want to make. 17 | /// 18 | /// `both` creates a session where all users are equal 19 | /// Otherwise if you want one specific user to be the host, choose `host` and `peer` 20 | /// A `host` will create a service advertiser, and a `peer` will create a service browser. 21 | public enum SessionType: Int { 22 | case host = 1 23 | case peer = 2 24 | case both = 3 25 | } 26 | 27 | /// Key added to discovery info to check RealityKit compatibility token 28 | public static let compTokenKey = "MPH_CompToken" 29 | /// Key added to discovery info to show OS version 30 | public static let osVersionKey = "MPH_OSVersion" 31 | /// Key added to discovery info to show device platform 32 | public static let platformKey = "MPH_Platform" 33 | 34 | /// Detemines whether your service is advertising, browsing, or both. 35 | public let sessionType: SessionType 36 | /// Name of the service, created at initialisation 37 | public let serviceName: String 38 | 39 | #if canImport(RealityKit) 40 | /// Used for RealityKit, set this as your scene's synchronizationService 41 | @available(iOS 13.0, macOS 10.15, *) 42 | public var syncService: MultipeerConnectivityService? { 43 | return try? MultipeerConnectivityService(session: session) 44 | } 45 | #endif 46 | 47 | /// ``MCPeerID`` set at initialisation. Default will be device name. 48 | public internal(set) var myPeerID: MCPeerID 49 | 50 | /// Quick lookup for a peer given their displayName 51 | private var peerIDLookup: [String: MCPeerID] = [:] 52 | 53 | /// The MultipeerConnectivity session being used 54 | public private(set) var session: MCSession! 55 | /// MultipeerConnectivity advertiser 56 | public private(set) var serviceAdvertiser: MCNearbyServiceAdvertiser? 57 | /// MultipeerConnectivity browser 58 | public private(set) var serviceBrowser: MCNearbyServiceBrowser? 59 | 60 | /// Delegate used to get some callback methods including data received 61 | public weak var delegate: MultipeerHelperDelegate? 62 | /// Initializes a Multipeer Helper. 63 | /// Pass your own peerName to avoid exceptions, as there are restrictions set by Apple on what is allowed. 64 | /// - Parameters: 65 | /// - serviceName: name of the service to be added, must be less than 15 lowercase ascii characters 66 | /// - sessionType: Type of session (host, peer, both) 67 | /// - peerName: String name of your device on the network, 68 | /// omitting or passing nil gives `UIDevice.current.name`. 69 | /// This value's maximum allowable length is 63 bytes in UTF-8 encoding. 70 | /// The displayName parameter may not be nil or an empty string. 71 | /// An exception will be thrown otherwise. 72 | /// - encryptionPreference: optional `MCEncryptionPreference`, defaults to `.required` 73 | /// - delegate: optional `MultipeerHelperDelegate` for MultipeerConnectivity callbacks 74 | public init( 75 | serviceName: String, 76 | sessionType: SessionType = .both, 77 | peerName: String? = nil, 78 | encryptionPreference: MCEncryptionPreference = .required, 79 | delegate: MultipeerHelperDelegate? = nil 80 | ) { 81 | self.serviceName = serviceName 82 | self.sessionType = sessionType 83 | self.delegate = delegate 84 | if let peerName = peerName { 85 | self.myPeerID = MCPeerID(displayName: peerName) 86 | } else { 87 | #if os(iOS) || os(tvOS) 88 | self.myPeerID = MCPeerID(displayName: UIDevice.current.name) 89 | #elseif os(macOS) 90 | self.myPeerID = MCPeerID( 91 | displayName: Host.current().name ?? UUID().uuidString 92 | ) 93 | #endif 94 | } 95 | super.init() 96 | peerIDLookup[myPeerID.displayName] = myPeerID 97 | session = MCSession( 98 | peer: myPeerID, 99 | securityIdentity: nil, 100 | encryptionPreference: encryptionPreference 101 | ) 102 | session.delegate = self 103 | self.setupSession() 104 | } 105 | 106 | private func setupSession() { 107 | if (self.sessionType.rawValue & SessionType.host.rawValue) != 0 { 108 | var discoveryInfo = self.delegate?.setDiscoveryInfo?() 109 | ?? [String: String]() 110 | 111 | #if canImport(RealityKit) 112 | if #available(iOS 13.4, macOS 10.15.4, *) { 113 | let networkLoc = NetworkCompatibilityToken.local 114 | let jsonData = try? JSONEncoder().encode(networkLoc) 115 | if let encodedToken = String(data: jsonData!, encoding: .utf8) { 116 | discoveryInfo[MultipeerHelper.compTokenKey] = encodedToken 117 | } 118 | } 119 | #endif 120 | #if os(iOS) || os(tvOS) 121 | discoveryInfo[MultipeerHelper.osVersionKey] = UIDevice.current.systemVersion 122 | #if os(iOS) 123 | discoveryInfo[MultipeerHelper.platformKey] = "iOS" 124 | #else 125 | discoveryInfo[MultipeerHelper.platformKey] = "tvOS" 126 | #endif 127 | #elseif os(macOS) 128 | discoveryInfo[MultipeerHelper.osVersionKey] = ProcessInfo.processInfo.operatingSystemVersionString 129 | discoveryInfo[MultipeerHelper.platformKey] = "macOS" 130 | #endif 131 | serviceAdvertiser = MCNearbyServiceAdvertiser( 132 | peer: myPeerID, 133 | discoveryInfo: discoveryInfo, 134 | serviceType: self.serviceName 135 | ) 136 | serviceAdvertiser?.delegate = self 137 | serviceAdvertiser?.startAdvertisingPeer() 138 | } 139 | 140 | if (self.sessionType.rawValue & SessionType.peer.rawValue) != 0 { 141 | serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: self.serviceName) 142 | serviceBrowser?.delegate = self 143 | serviceBrowser?.startBrowsingForPeers() 144 | } 145 | } 146 | 147 | /// Data to be sent to all of the connected peers 148 | /// - Parameters: 149 | /// - data: Encoded data to be sent 150 | /// - reliably: The transmission mode to use (true for data to be sent reliably). 151 | @discardableResult 152 | public func sendToAllPeers(_ data: Data, reliably: Bool = true) -> Bool { 153 | return sendToPeers(data, reliably: reliably, peers: connectedPeers) 154 | } 155 | 156 | /// Data to be sent to a list of peers 157 | /// - Parameters: 158 | /// - data: encoded data to be sent 159 | /// - reliably: The transmission mode to use (true for data to be sent reliably). 160 | /// - peers: An array of all the peers to rec3ive your data 161 | @discardableResult 162 | public func sendToPeers(_ data: Data, reliably: Bool, peers: [MCPeerID]) -> Bool { 163 | guard !peers.isEmpty else { return false } 164 | do { 165 | try session.send(data, toPeers: peers, with: reliably ? .reliable : .unreliable) 166 | } catch { 167 | print("error sending data to peers \(peers): \(error.localizedDescription)") 168 | return false 169 | } 170 | return true 171 | } 172 | 173 | public var connectedPeers: [MCPeerID] { 174 | session.connectedPeers 175 | } 176 | 177 | /// Data to be send to peer using their displayname 178 | /// - Parameters: 179 | /// - displayname: displayname of the peer you want to be sent 180 | /// - data: encoded data to be sent 181 | /// - reliably: The transmission mode to use (true for data to be sent reliably). 182 | public func sendToPeer(named displayname: String, data: Data, reliably: Bool = true) -> Bool { 183 | guard let recipient = self.findPeer(name: displayname) else { 184 | return false 185 | } 186 | return self.sendToPeers(data, reliably: reliably, peers: [recipient]) 187 | } 188 | 189 | /// Look up a peer given their displayname 190 | /// - Parameter name: The displayname of the peer you are looking for 191 | public func findPeer(name: String) -> MCPeerID? { 192 | if let peer = self.peerIDLookup[name] { 193 | return peer 194 | } 195 | defer { 196 | // In case for some reason the peerIDLookup is out of sync, recalculate it 197 | self.peerIDLookup.removeAll(keepingCapacity: false) 198 | for connectedPeer in self.connectedPeers { 199 | self.peerIDLookup[connectedPeer.displayName] = connectedPeer 200 | } 201 | } 202 | return connectedPeers.first { $0.displayName == name } 203 | } 204 | 205 | /// Method used for disconnecting all services. Once completed, 206 | /// create a new MultipeerHelper if you want to connect to sessions again. 207 | public func disconnectAll() { 208 | self.serviceAdvertiser?.stopAdvertisingPeer() 209 | self.serviceBrowser?.stopBrowsingForPeers() 210 | self.serviceAdvertiser = nil 211 | self.serviceBrowser = nil 212 | self.session?.disconnect() 213 | } 214 | } 215 | 216 | extension MultipeerHelper: MCSessionDelegate { 217 | 218 | public func session( 219 | _: MCSession, 220 | peer peerID: MCPeerID, 221 | didChange state: MCSessionState 222 | ) { 223 | if state == .connected { 224 | peerIDLookup[peerID.displayName] = peerID 225 | delegate?.peerJoined?(peerHelper: self, peerID) 226 | self.serviceBrowser?.stopBrowsingForPeers() 227 | } else if state == .notConnected { 228 | peerIDLookup.removeValue(forKey: peerID.displayName) 229 | delegate?.peerLeft?(peerHelper: self, peerID) 230 | } 231 | } 232 | 233 | public func session(_: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { 234 | delegate?.receivedData?(peerHelper: self, data, peerID) 235 | } 236 | 237 | public func session( 238 | _: MCSession, 239 | didReceive stream: InputStream, 240 | withName streamName: String, 241 | fromPeer peerID: MCPeerID 242 | ) { 243 | delegate?.receivedStream?(peerHelper: self, stream, streamName, peerID) 244 | } 245 | 246 | public func session( 247 | _: MCSession, 248 | didStartReceivingResourceWithName resourceName: String, 249 | fromPeer peerID: MCPeerID, 250 | with progress: Progress 251 | ) { 252 | delegate?.receivingResource?(peerHelper: self, resourceName, peerID, progress) 253 | } 254 | 255 | public func session( 256 | _: MCSession, 257 | didFinishReceivingResourceWithName resourceName: String, 258 | fromPeer peerID: MCPeerID, 259 | at localURL: URL?, 260 | withError error: Error? 261 | ) { 262 | delegate?.receivedResource?(peerHelper: self, resourceName, peerID, localURL, error) 263 | } 264 | 265 | public func session( 266 | _ session: MCSession, 267 | didReceiveCertificate certificate: [Any]?, 268 | fromPeer peerID: MCPeerID, 269 | certificateHandler: @escaping (Bool) -> Void 270 | ) { 271 | if let certificateApproved = self.delegate?.receivedCertificate?( 272 | peerHelper: self, certificate: certificate, fromPeer: peerID 273 | ) { 274 | certificateHandler(certificateApproved) 275 | return 276 | } 277 | certificateHandler(true) 278 | } 279 | } 280 | 281 | extension MultipeerHelper: MCNearbyServiceBrowserDelegate { 282 | /// - Tag: SendPeerInvite 283 | public func browser( 284 | _ browser: MCNearbyServiceBrowser, 285 | foundPeer peerID: MCPeerID, 286 | withDiscoveryInfo info: [String: String]? 287 | ) { 288 | // Ask the handler whether we should invite this peer or not 289 | if delegate?.shouldSendJoinRequest == nil 290 | || (delegate?.shouldSendJoinRequest?(peerHelper: self, peerID, with: info) ?? false 291 | ) { 292 | browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10) 293 | } 294 | } 295 | 296 | public func browser(_: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { 297 | delegate?.peerLost?(peerHelper: self, peerID) 298 | } 299 | } 300 | 301 | extension MultipeerHelper: MCNearbyServiceAdvertiserDelegate { 302 | /// - Tag: AcceptInvite 303 | public func advertiser( 304 | _ advo: MCNearbyServiceAdvertiser, 305 | didReceiveInvitationFromPeer peerID: MCPeerID, 306 | withContext data: Data?, 307 | invitationHandler: @escaping (Bool, MCSession?) -> Void 308 | ) { 309 | // Call the handler to accept the peer's invitation to join. 310 | let shouldAccept = self.delegate?.shouldAcceptJoinRequest?(peerHelper: self, peerID: peerID, context: data) 311 | invitationHandler(shouldAccept != nil ? shouldAccept! : true, self.session) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /MultipeerHelper+Example/MultipeerHelper+Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F32BA081238935120075556C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32BA080238935120075556C /* AppDelegate.swift */; }; 11 | F32BA083238935120075556C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32BA082238935120075556C /* ContentView.swift */; }; 12 | F32BA087238935140075556C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F32BA086238935140075556C /* Assets.xcassets */; }; 13 | F32BA08A238935140075556C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F32BA089238935140075556C /* Preview Assets.xcassets */; }; 14 | F32BA08D238935140075556C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F32BA08B238935140075556C /* LaunchScreen.storyboard */; }; 15 | F32BA0982389B1880075556C /* RealityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32BA0972389B1880075556C /* RealityViewController.swift */; }; 16 | F32BA0A12389E6AB0075556C /* RealityViewController+Gestures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32BA0A02389E6AB0075556C /* RealityViewController+Gestures.swift */; }; 17 | F3D758E9249949FC0019A821 /* FocusEntity in Frameworks */ = {isa = PBXBuildFile; productRef = F3D758E8249949FC0019A821 /* FocusEntity */; }; 18 | F3F3F88A2401658700AC1EBF /* MultipeerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3F8872401658700AC1EBF /* MultipeerHelper.swift */; }; 19 | F3F3F88B2401658700AC1EBF /* HasSynchronization+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3F8882401658700AC1EBF /* HasSynchronization+Extensions.swift */; }; 20 | F3F3F88C2401658700AC1EBF /* MultipeerHelperDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3F8892401658700AC1EBF /* MultipeerHelperDelegate.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | F32BA07D238935120075556C /* MultipeerHelper+Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MultipeerHelper+Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | F32BA080238935120075556C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | F32BA082238935120075556C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 27 | F32BA086238935140075556C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | F32BA089238935140075556C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 29 | F32BA08C238935140075556C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | F32BA08E238935140075556C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | F32BA0972389B1880075556C /* RealityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealityViewController.swift; sourceTree = ""; }; 32 | F32BA0A02389E6AB0075556C /* RealityViewController+Gestures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RealityViewController+Gestures.swift"; sourceTree = ""; }; 33 | F3F3F8872401658700AC1EBF /* MultipeerHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultipeerHelper.swift; path = ../Sources/MultipeerHelper/MultipeerHelper.swift; sourceTree = SOURCE_ROOT; }; 34 | F3F3F8882401658700AC1EBF /* HasSynchronization+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "HasSynchronization+Extensions.swift"; path = "../Sources/MultipeerHelper/HasSynchronization+Extensions.swift"; sourceTree = SOURCE_ROOT; }; 35 | F3F3F8892401658700AC1EBF /* MultipeerHelperDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultipeerHelperDelegate.swift; path = ../Sources/MultipeerHelper/MultipeerHelperDelegate.swift; sourceTree = SOURCE_ROOT; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFrameworksBuildPhase section */ 39 | F32BA07A238935120075556C /* Frameworks */ = { 40 | isa = PBXFrameworksBuildPhase; 41 | buildActionMask = 2147483647; 42 | files = ( 43 | F3D758E9249949FC0019A821 /* FocusEntity in Frameworks */, 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | F32BA074238935120075556C = { 51 | isa = PBXGroup; 52 | children = ( 53 | F32BA07F238935120075556C /* MultipeerHelper+Example */, 54 | F32BA07E238935120075556C /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | F32BA07E238935120075556C /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | F32BA07D238935120075556C /* MultipeerHelper+Example.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | F32BA07F238935120075556C /* MultipeerHelper+Example */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | F32BA080238935120075556C /* AppDelegate.swift */, 70 | F32BA082238935120075556C /* ContentView.swift */, 71 | F32BA0972389B1880075556C /* RealityViewController.swift */, 72 | F32BA0A02389E6AB0075556C /* RealityViewController+Gestures.swift */, 73 | F32BA086238935140075556C /* Assets.xcassets */, 74 | F32BA08B238935140075556C /* LaunchScreen.storyboard */, 75 | F32BA08E238935140075556C /* Info.plist */, 76 | F3F3F8862401657900AC1EBF /* MultipeerHelper */, 77 | F32BA088238935140075556C /* Preview Content */, 78 | ); 79 | path = "MultipeerHelper+Example"; 80 | sourceTree = ""; 81 | }; 82 | F32BA088238935140075556C /* Preview Content */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | F32BA089238935140075556C /* Preview Assets.xcassets */, 86 | ); 87 | path = "Preview Content"; 88 | sourceTree = ""; 89 | }; 90 | F3F3F8862401657900AC1EBF /* MultipeerHelper */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | F3F3F8882401658700AC1EBF /* HasSynchronization+Extensions.swift */, 94 | F3F3F8872401658700AC1EBF /* MultipeerHelper.swift */, 95 | F3F3F8892401658700AC1EBF /* MultipeerHelperDelegate.swift */, 96 | ); 97 | path = MultipeerHelper; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | F32BA07C238935120075556C /* MultipeerHelper+Example */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = F32BA091238935140075556C /* Build configuration list for PBXNativeTarget "MultipeerHelper+Example" */; 106 | buildPhases = ( 107 | F32BA079238935120075556C /* Sources */, 108 | F32BA07A238935120075556C /* Frameworks */, 109 | F32BA07B238935120075556C /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = "MultipeerHelper+Example"; 116 | packageProductDependencies = ( 117 | F3D758E8249949FC0019A821 /* FocusEntity */, 118 | ); 119 | productName = "MultipeerHelper+Example"; 120 | productReference = F32BA07D238935120075556C /* MultipeerHelper+Example.app */; 121 | productType = "com.apple.product-type.application"; 122 | }; 123 | /* End PBXNativeTarget section */ 124 | 125 | /* Begin PBXProject section */ 126 | F32BA075238935120075556C /* Project object */ = { 127 | isa = PBXProject; 128 | attributes = { 129 | LastSwiftUpdateCheck = 1120; 130 | LastUpgradeCheck = 1120; 131 | ORGANIZATIONNAME = "Max Cobb"; 132 | TargetAttributes = { 133 | F32BA07C238935120075556C = { 134 | CreatedOnToolsVersion = 11.2; 135 | }; 136 | }; 137 | }; 138 | buildConfigurationList = F32BA078238935120075556C /* Build configuration list for PBXProject "MultipeerHelper+Example" */; 139 | compatibilityVersion = "Xcode 9.3"; 140 | developmentRegion = en; 141 | hasScannedForEncodings = 0; 142 | knownRegions = ( 143 | en, 144 | Base, 145 | ); 146 | mainGroup = F32BA074238935120075556C; 147 | packageReferences = ( 148 | F3D758E7249949FC0019A821 /* XCRemoteSwiftPackageReference "FocusEntity" */, 149 | ); 150 | productRefGroup = F32BA07E238935120075556C /* Products */; 151 | projectDirPath = ""; 152 | projectRoot = ""; 153 | targets = ( 154 | F32BA07C238935120075556C /* MultipeerHelper+Example */, 155 | ); 156 | }; 157 | /* End PBXProject section */ 158 | 159 | /* Begin PBXResourcesBuildPhase section */ 160 | F32BA07B238935120075556C /* Resources */ = { 161 | isa = PBXResourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | F32BA08D238935140075556C /* LaunchScreen.storyboard in Resources */, 165 | F32BA08A238935140075556C /* Preview Assets.xcassets in Resources */, 166 | F32BA087238935140075556C /* Assets.xcassets in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXSourcesBuildPhase section */ 173 | F32BA079238935120075556C /* Sources */ = { 174 | isa = PBXSourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | F32BA083238935120075556C /* ContentView.swift in Sources */, 178 | F3F3F88B2401658700AC1EBF /* HasSynchronization+Extensions.swift in Sources */, 179 | F3F3F88C2401658700AC1EBF /* MultipeerHelperDelegate.swift in Sources */, 180 | F32BA0A12389E6AB0075556C /* RealityViewController+Gestures.swift in Sources */, 181 | F32BA0982389B1880075556C /* RealityViewController.swift in Sources */, 182 | F32BA081238935120075556C /* AppDelegate.swift in Sources */, 183 | F3F3F88A2401658700AC1EBF /* MultipeerHelper.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXSourcesBuildPhase section */ 188 | 189 | /* Begin PBXVariantGroup section */ 190 | F32BA08B238935140075556C /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | F32BA08C238935140075556C /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | F32BA08F238935140075556C /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = dwarf; 235 | ENABLE_STRICT_OBJC_MSGSEND = YES; 236 | ENABLE_TESTABILITY = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu11; 238 | GCC_DYNAMIC_NO_PIC = NO; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_OPTIMIZATION_LEVEL = 0; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "DEBUG=1", 243 | "$(inherited)", 244 | ); 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 252 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 253 | MTL_FAST_MATH = YES; 254 | ONLY_ACTIVE_ARCH = YES; 255 | SDKROOT = iphoneos; 256 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 257 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 258 | }; 259 | name = Debug; 260 | }; 261 | F32BA090238935140075556C /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 268 | CLANG_CXX_LIBRARY = "libc++"; 269 | CLANG_ENABLE_MODULES = YES; 270 | CLANG_ENABLE_OBJC_ARC = YES; 271 | CLANG_ENABLE_OBJC_WEAK = YES; 272 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_COMMA = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 277 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 278 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INFINITE_RECURSION = YES; 282 | CLANG_WARN_INT_CONVERSION = YES; 283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 285 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 286 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 288 | CLANG_WARN_STRICT_PROTOTYPES = YES; 289 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | COPY_PHASE_STRIP = NO; 294 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 295 | ENABLE_NS_ASSERTIONS = NO; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | F32BA092238935140075556C /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_STYLE = Automatic; 320 | DEAD_CODE_STRIPPING = NO; 321 | DEVELOPMENT_ASSET_PATHS = "\"MultipeerHelper+Example/Preview Content\""; 322 | DEVELOPMENT_TEAM = ""; 323 | ENABLE_PREVIEWS = YES; 324 | INFOPLIST_FILE = "MultipeerHelper+Example/Info.plist"; 325 | LD_RUNPATH_SEARCH_PATHS = ( 326 | "$(inherited)", 327 | "@executable_path/Frameworks", 328 | ); 329 | PRODUCT_BUNDLE_IDENTIFIER = "cobb.max.MultipeerHelper-Example"; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | SWIFT_VERSION = 5.0; 332 | TARGETED_DEVICE_FAMILY = "1,2"; 333 | }; 334 | name = Debug; 335 | }; 336 | F32BA093238935140075556C /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | CODE_SIGN_STYLE = Automatic; 341 | DEAD_CODE_STRIPPING = NO; 342 | DEVELOPMENT_ASSET_PATHS = "\"MultipeerHelper+Example/Preview Content\""; 343 | DEVELOPMENT_TEAM = ""; 344 | ENABLE_PREVIEWS = YES; 345 | INFOPLIST_FILE = "MultipeerHelper+Example/Info.plist"; 346 | LD_RUNPATH_SEARCH_PATHS = ( 347 | "$(inherited)", 348 | "@executable_path/Frameworks", 349 | ); 350 | PRODUCT_BUNDLE_IDENTIFIER = "cobb.max.MultipeerHelper-Example"; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | F32BA078238935120075556C /* Build configuration list for PBXProject "MultipeerHelper+Example" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | F32BA08F238935140075556C /* Debug */, 364 | F32BA090238935140075556C /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | F32BA091238935140075556C /* Build configuration list for PBXNativeTarget "MultipeerHelper+Example" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | F32BA092238935140075556C /* Debug */, 373 | F32BA093238935140075556C /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | 380 | /* Begin XCRemoteSwiftPackageReference section */ 381 | F3D758E7249949FC0019A821 /* XCRemoteSwiftPackageReference "FocusEntity" */ = { 382 | isa = XCRemoteSwiftPackageReference; 383 | repositoryURL = "https://github.com/maxxfrazer/FocusEntity.git"; 384 | requirement = { 385 | kind = upToNextMajorVersion; 386 | minimumVersion = 1.1.1; 387 | }; 388 | }; 389 | /* End XCRemoteSwiftPackageReference section */ 390 | 391 | /* Begin XCSwiftPackageProductDependency section */ 392 | F3D758E8249949FC0019A821 /* FocusEntity */ = { 393 | isa = XCSwiftPackageProductDependency; 394 | package = F3D758E7249949FC0019A821 /* XCRemoteSwiftPackageReference "FocusEntity" */; 395 | productName = FocusEntity; 396 | }; 397 | /* End XCSwiftPackageProductDependency section */ 398 | }; 399 | rootObject = F32BA075238935120075556C /* Project object */; 400 | } 401 | --------------------------------------------------------------------------------