├── CloudCube ├── .DS_Store ├── CloudCube │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── edIcon.imageset │ │ │ ├── edIcon.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Icon.png │ │ │ ├── Icon-40.png │ │ │ ├── Icon-72.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon@2x.png │ │ │ ├── Icon-40@2x.png │ │ │ ├── Icon-40@3x.png │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-72@2x.png │ │ │ ├── Icon-76@2x.png │ │ │ ├── Icon-Small.png │ │ │ ├── Icon-83.5@2x.png │ │ │ ├── Icon-Small-50.png │ │ │ ├── Icon-Small@2x.png │ │ │ ├── Icon-Small@3x.png │ │ │ ├── Icon-Small-50@2x.png │ │ │ ├── NotificationIcon@2x.png │ │ │ ├── NotificationIcon@3x.png │ │ │ ├── NotificationIcon~ipad.png │ │ │ ├── NotificationIcon~ipad@2x.png │ │ │ └── Contents.json │ │ ├── restart.imageset │ │ │ ├── restart@2x.png │ │ │ ├── restart@3x.png │ │ │ └── Contents.json │ │ └── edIconSmall.imageset │ │ │ ├── edIconSmall.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Info.plist │ ├── Apple Focus Square │ │ ├── Utilities │ │ │ └── Utilities.swift │ │ ├── FocusSquare+Extensions.swift │ │ ├── FocusSquareSegment.swift │ │ └── FocusSquare.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Custom Views │ │ └── BlackMirrorzViews.swift │ ├── Custom Nodes │ │ └── Box.swift │ ├── Multipeer │ │ └── ARCloudShare.swift │ ├── AR Settings │ │ └── ARSettings.swift │ └── AR ViewControllers │ │ ├── ViewController+Interactions.swift │ │ ├── ARDelegation+Updates.swift │ │ └── ViewController.swift └── CloudCube.xcodeproj │ ├── xcuserdata │ └── saurus.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── saurus.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── LICENSE └── README.md /CloudCube/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/.DS_Store -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/edIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/edIcon.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@3x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/xcuserdata/saurus.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/edIconSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/edIconSmall.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcuserdata/saurus.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcuserdata/saurus.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "edIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "edIconSmall.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/xcuserdata/saurus.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CloudCube.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/restart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "restart@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "restart@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /CloudCube/CloudCube/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { } 23 | 24 | func applicationDidEnterBackground(_ application: UIApplication) { } 25 | 26 | func applicationWillEnterForeground(_ application: UIApplication) { } 27 | 28 | func applicationDidBecomeActive(_ application: UIApplication) { } 29 | 30 | func applicationWillTerminate(_ application: UIApplication) { } 31 | 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Josh Robbins 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ARKitWorldMaps 3 | 4 | This project is a basic example of using `MultipeerConnectivity` within `ARKit2` to share `ARWorldMaps` and `ARAnchors` across multiple devices. 5 | 6 | All the code is fully commented so the apps functionality should be clear to everyone. 7 | 8 | **Branches:** 9 | 10 | The Master Branch was originally compiled in XCode10 Beta using Swift 4. 11 | 12 | An updated Branch called 'Swift4.2' contains the project built in XCode 10.5 Beta and uses Swift 4.2. 13 | 14 | **Requirements:** 15 | 16 | The project is setup for iPhone, and in Landscape Orientation. 17 | 18 | **Core Functionality:** 19 | 20 | The application automatically creates an `MCSession` which can be found in `ARCloudShare.swift` and shares `ARWorldMaps` and `ARAnchors` in real time. 21 | 22 | Using `NSKeyedArchiver` and `NSKeyedUnarchiver` the users can rotate (using a `UIPanGestureRecognizer`), scale (using `UIPinchGestureRecognizer`), and change the colours of each of the faces of a single `SCNBox` using a `UITapGestureRecognizer`. 23 | 24 | For the purpose of this application, I have only allowed placement of one Cube but this can easily be adapted to meet your needs. 25 | 26 | The cube can only be placed on a detected ARPlaneAnchor, which again can easily be customised as per your needs. 27 | 28 | The core idea behind this app, was a basic comparison between the functionality of `Google Cloud Anchors`, and `ARKit`, with `ARKit` winning easily. 29 | 30 | To change the colour of one of the faces of the cube, you need to double tap on the face and then select one of the colours from the bottom menu. 31 | 32 | 33 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | For BlackMirrorz AR 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Apple Focus Square/Utilities/Utilities.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Utility functions and type extensions used throughout the projects. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | // MARK: - float4x4 extensions 12 | 13 | extension float4x4 { 14 | /** 15 | Treats matrix as a (right-hand column-major convention) transform matrix 16 | and factors out the translation component of the transform. 17 | */ 18 | var translation: float3 { 19 | get { 20 | let translation = columns.3 21 | return float3(translation.x, translation.y, translation.z) 22 | } 23 | set(newValue) { 24 | columns.3 = float4(newValue.x, newValue.y, newValue.z, columns.3.w) 25 | } 26 | } 27 | 28 | /** 29 | Factors out the orientation component of the transform. 30 | */ 31 | var orientation: simd_quatf { 32 | return simd_quaternion(self) 33 | } 34 | 35 | /** 36 | Creates a transform matrix with a uniform scale factor in all directions. 37 | */ 38 | init(uniformScale scale: Float) { 39 | self = matrix_identity_float4x4 40 | columns.0.x = scale 41 | columns.1.y = scale 42 | columns.2.z = scale 43 | } 44 | } 45 | 46 | // MARK: - CGPoint extensions 47 | 48 | extension CGPoint { 49 | /// Extracts the screen space point from a vector returned by SCNView.projectPoint(_:). 50 | init(_ vector: SCNVector3) { 51 | self.init(x: CGFloat(vector.x), y: CGFloat(vector.y)) 52 | } 53 | 54 | /// Returns the length of a point when considered as a vector. (Used with gesture recognizers.) 55 | var length: CGFloat { 56 | return sqrt(x * x + y * y) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/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 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Custom Views/BlackMirrorzViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackMirrorzViews.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ARKit 11 | 12 | //--------------------------------- 13 | //MARK: - BlackMirrorz Round Button 14 | //--------------------------------- 15 | 16 | @IBDesignable public class BlackMirrorzRoundButton: UIButton { 17 | 18 | @IBInspectable var borderColor: UIColor = .white{ 19 | didSet { 20 | layer.borderColor = borderColor.cgColor 21 | } 22 | } 23 | 24 | @IBInspectable var borderWidth: CGFloat = 2.0 { 25 | didSet { 26 | layer.borderWidth = borderWidth 27 | } 28 | } 29 | 30 | @IBInspectable var cornerRadius: CGFloat = 0 { 31 | didSet { 32 | layer.cornerRadius = 0.5 * bounds.size.width 33 | 34 | } 35 | } 36 | 37 | override public func layoutSubviews() { 38 | super.layoutSubviews() 39 | self.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 40 | clipsToBounds = true 41 | } 42 | } 43 | 44 | //------------------------------------ 45 | //MARK: - BlackMirrorz Round ImageView 46 | //------------------------------------ 47 | 48 | @IBDesignable public class BlackMirrorzRoundImageView: UIImageView { 49 | 50 | @IBInspectable var borderColor: UIColor = .white{ 51 | didSet { 52 | layer.borderColor = borderColor.cgColor 53 | } 54 | } 55 | 56 | @IBInspectable var borderWidth: CGFloat = 2.0 { 57 | didSet { 58 | layer.borderWidth = borderWidth 59 | } 60 | } 61 | 62 | @IBInspectable var cornerRadius: CGFloat = 0 { 63 | didSet { 64 | layer.cornerRadius = 0.5 * bounds.size.width 65 | 66 | } 67 | } 68 | 69 | override public func layoutSubviews() { 70 | super.layoutSubviews() 71 | clipsToBounds = true 72 | } 73 | } 74 | 75 | //------------------------------- 76 | //MARK: - BlackMirrorz Round View 77 | //------------------------------- 78 | 79 | @IBDesignable public class BlackMirrorzRoundView: UIView { 80 | 81 | public override init(frame: CGRect) { 82 | super.init(frame: frame) 83 | layoutView() 84 | } 85 | 86 | public required init?(coder aDecoder: NSCoder) { 87 | super.init(coder: aDecoder) 88 | layoutView() 89 | } 90 | 91 | func layoutView(){ 92 | 93 | self.layer.borderColor = UIColor.white.cgColor 94 | self.layer.borderWidth = 2 95 | self.layer.cornerRadius = 5 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Apple Focus Square/FocusSquare+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusSquare+Extensions.swift 3 | // TCloud 4 | // 5 | // Created by Josh Robbins on 04/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SceneKit 11 | import ARKit 12 | 13 | extension ARSCNView{ 14 | 15 | func smartHitTest(_ point: CGPoint, 16 | infinitePlane: Bool = false, 17 | objectPosition: float3? = nil, 18 | allowedAlignments: [ARPlaneAnchor.Alignment] = [.horizontal, .vertical]) -> ARHitTestResult? { 19 | 20 | // Perform the hit test. 21 | let results = hitTest(point, types: [.existingPlaneUsingGeometry, .estimatedVerticalPlane, .estimatedHorizontalPlane]) 22 | 23 | // 1. Check for a result on an existing plane using geometry. 24 | if let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }), 25 | let planeAnchor = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) { 26 | return existingPlaneUsingGeometryResult 27 | } 28 | 29 | if infinitePlane { 30 | 31 | // 2. Check for a result on an existing plane, assuming its dimensions are infinite. 32 | // Loop through all hits against infinite existing planes and either return the 33 | // nearest one (vertical planes) or return the nearest one which is within 5 cm 34 | // of the object's position. 35 | let infinitePlaneResults = hitTest(point, types: .existingPlane) 36 | 37 | for infinitePlaneResult in infinitePlaneResults { 38 | if let planeAnchor = infinitePlaneResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) { 39 | if planeAnchor.alignment == .vertical { 40 | // Return the first vertical plane hit test result. 41 | return infinitePlaneResult 42 | } else { 43 | // For horizontal planes we only want to return a hit test result 44 | // if it is close to the current object's position. 45 | if let objectY = objectPosition?.y { 46 | let planeY = infinitePlaneResult.worldTransform.translation.y 47 | if objectY > planeY - 0.05 && objectY < planeY + 0.05 { 48 | return infinitePlaneResult 49 | } 50 | } else { 51 | return infinitePlaneResult 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | // 3. As a final fallback, check for a result on estimated planes. 59 | let vResult = results.first(where: { $0.type == .estimatedVerticalPlane }) 60 | let hResult = results.first(where: { $0.type == .estimatedHorizontalPlane }) 61 | switch (allowedAlignments.contains(.horizontal), allowedAlignments.contains(.vertical)) { 62 | case (true, false): 63 | return hResult 64 | case (false, true): 65 | // Allow fallback to horizontal because we assume that objects meant for vertical placement 66 | // (like a picture) can always be placed on a horizontal surface, too. 67 | return vResult ?? hResult 68 | case (true, true): 69 | if hResult != nil && vResult != nil { 70 | return hResult!.distance < vResult!.distance ? hResult! : vResult! 71 | } else { 72 | return hResult ?? vResult 73 | } 74 | default: 75 | return nil 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "NotificationIcon@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "NotificationIcon@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "NotificationIcon~ipad.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "NotificationIcon~ipad@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-Small.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-Small@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-40.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-72.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-76.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "idiom" : "ios-marketing", 149 | "size" : "1024x1024", 150 | "scale" : "1x" 151 | } 152 | ], 153 | "info" : { 154 | "version" : 1, 155 | "author" : "xcode" 156 | } 157 | } -------------------------------------------------------------------------------- /CloudCube/CloudCube/Custom Nodes/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoxNode.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ARKit 11 | 12 | 13 | /// The Geometry Index Of An SCNBox Geometry 14 | enum BoxFaces: Int{ 15 | 16 | case Front, Right, Back, Left, Top, Botton 17 | } 18 | 19 | class Box: SCNNode { 20 | 21 | private var faceArray = [SCNMaterial]() 22 | 23 | /// Creates An SCNBox With A Single Colour Or Image For It's Material 24 | /// 25 | /// - Parameters: 26 | /// - width: Optional CGFloat (Defaults To 20cm) 27 | /// - height: Optional CGFloat (Defaults To 20cm) 28 | /// - length: Optional CGFloat (Defaults To 20cm) 29 | /// - chamferRadius: Optional CGFloat (Defaults To 0cm) 30 | /// - colour: Optional UIColor 31 | /// - image: Optional UIColor 32 | init(width: CGFloat = 0.2, height: CGFloat = 0.2, length: CGFloat = 0.2, chamferRadius: CGFloat = 0, colour: UIColor?, image: UIImage?) { 33 | 34 | super.init() 35 | 36 | //1. Create The Box Geometry With Our Width, Height, Length & Chamfer Radius Parameters 37 | self.geometry = SCNBox(width: width, height: height, length: length, chamferRadius: chamferRadius) 38 | 39 | //2. Create A New Material 40 | let material = SCNMaterial() 41 | 42 | //3. If A Colour Has Not Be Set The Then Material Will Be A UIImage 43 | if colour == nil{ 44 | material.diffuse.contents = image 45 | }else{ 46 | //The Material Will Be A UIColor 47 | material.diffuse.contents = colour 48 | } 49 | 50 | //4. Set The Material Of The Box 51 | self.geometry?.firstMaterial = material 52 | 53 | } 54 | 55 | /// Creates An SCNBox With Either A Colour Or UIImage For Each Of It's Faces 56 | /// (Either An Array [Colour] Or [UIImage] Must Be Input) 57 | /// - Parameters: 58 | /// - width: Optional CGFloat (Defaults To 20cm) 59 | /// - height: Optional CGFloat (Defaults To 20cm) 60 | /// - length: Optional CGFloat (Defaults To 20cm) 61 | /// - chamferRadius: Optional CGFloat (Defaults To 0cm) 62 | /// - colours: Optional [UIColor] - [Front, Right, Back, Left, Top, Bottom] 63 | /// - images: Optional [UIImage] - [Front, Right, Back, Left, Top, Bottom] 64 | init(width: CGFloat = 0.2, height: CGFloat = 0.2, length: CGFloat = 0.2, chamferRadius: CGFloat = 0, colours: [UIColor]?, images: [UIImage]?) { 65 | 66 | super.init() 67 | 68 | //1. Create The Box Geometry With Our Width, Height, Length & Chamfer Radius Parameters 69 | self.geometry = SCNBox(width: width, height: height, length: length, chamferRadius: chamferRadius) 70 | 71 | //2. Create A Temporary Array To Store Either Our UIColors Or UIImages 72 | var sideArray = [Any]() 73 | 74 | //3. If Our Color Array Is Nil Then Our Side Array Will Be Equal To Our Images Array 75 | if colours == nil{ 76 | guard let imageArray = images else { return } 77 | sideArray = imageArray 78 | }else{ 79 | //Our Side Array Will Be Equal To Our Colours Array 80 | guard let coloursArray = colours else { return } 81 | sideArray = coloursArray 82 | } 83 | 84 | //4. Loop Through The Number Of Faces & Create A New Material For Each 85 | for index in 0 ..< 6{ 86 | let face = SCNMaterial() 87 | face.diffuse.contents = sideArray[index] 88 | //Add The Material To Our Face Array 89 | faceArray.append(face) 90 | } 91 | 92 | //5. Set The Boxes Materials From Our Face Array 93 | self.geometry?.materials = faceArray 94 | 95 | } 96 | 97 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Multipeer/ARCloudShare.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ARCloudShare.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MultipeerConnectivity 11 | 12 | //-------------------------- 13 | // MARK: - MCSessionDelegate 14 | //-------------------------- 15 | 16 | extension ARCloudShare: MCSessionDelegate { 17 | 18 | func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { } 19 | 20 | func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { 21 | receivedDataHandler(data, peerID) 22 | } 23 | 24 | func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { 25 | fatalError("This Service Does Not Send Or Receive Streams") 26 | } 27 | 28 | func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { 29 | fatalError("This Service Does Not Send Or Receive Resources") 30 | } 31 | 32 | func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { 33 | fatalError("This Service Does Not Send Or Receive Resources") 34 | } 35 | 36 | } 37 | 38 | //--------------------------------------- 39 | // MARK: - MCNearbyServiceBrowserDelegate 40 | //--------------------------------------- 41 | 42 | extension ARCloudShare: MCNearbyServiceBrowserDelegate { 43 | 44 | public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) { 45 | 46 | //Invite A New User To The Session 47 | browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10) 48 | } 49 | 50 | public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { } 51 | 52 | } 53 | 54 | //------------------------------------------ 55 | // MARK: - MCNearbyServiceAdvertiserDelegate 56 | //------------------------------------------ 57 | 58 | extension ARCloudShare: MCNearbyServiceAdvertiserDelegate { 59 | 60 | //---------------------------------------------------------- 61 | // MARK: - Allows The User To Accept The Invitation To Share 62 | //---------------------------------------------------------- 63 | 64 | func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { 65 | 66 | //Allow The User To Accept The Invitation & Join The Twunkl Session 67 | invitationHandler(true, self.session) 68 | } 69 | 70 | } 71 | 72 | class ARCloudShare: NSObject{ 73 | 74 | static let serviceType = "arcloud-share" 75 | 76 | let myPeerID = MCPeerID(displayName: UIDevice.current.name) 77 | var session: MCSession! 78 | var serviceAdvertiser: MCNearbyServiceAdvertiser! 79 | var serviceBrowser: MCNearbyServiceBrowser! 80 | let receivedDataHandler: (Data, MCPeerID) -> Void 81 | 82 | //----------------------- 83 | // MARK: - Initialization 84 | //----------------------- 85 | 86 | init(receivedDataHandler: @escaping (Data, MCPeerID) -> Void ) { 87 | 88 | self.receivedDataHandler = receivedDataHandler 89 | 90 | super.init() 91 | 92 | session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required) 93 | session.delegate = self 94 | 95 | serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: ARCloudShare.serviceType) 96 | serviceAdvertiser.delegate = self 97 | serviceAdvertiser.startAdvertisingPeer() 98 | 99 | serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: ARCloudShare.serviceType) 100 | serviceBrowser.delegate = self 101 | serviceBrowser.startBrowsingForPeers() 102 | } 103 | 104 | //--------------------- 105 | // MARK: - Data Sending 106 | //--------------------- 107 | 108 | func sendDataToUsers(_ data: Data) { 109 | do { 110 | try session.send(data, toPeers: session.connectedPeers, with: .reliable) 111 | } catch { 112 | print("Error Sending Data To Users: \(error.localizedDescription)") 113 | } 114 | } 115 | 116 | //---------------------- 117 | // MARK: - Peer Tracking 118 | //---------------------- 119 | 120 | var connectedPeers: [MCPeerID] { return session.connectedPeers } 121 | } 122 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Apple Focus Square/FocusSquareSegment.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Corner segments for the focus square UI. 6 | */ 7 | 8 | import SceneKit 9 | 10 | extension FocusSquare { 11 | 12 | /* 13 | The focus square consists of eight segments as follows, which can be individually animated. 14 | 15 | s1 s2 16 | _ _ 17 | s3 | | s4 18 | 19 | s5 | | s6 20 | - - 21 | s7 s8 22 | */ 23 | enum Corner { 24 | case topLeft // s1, s3 25 | case topRight // s2, s4 26 | case bottomRight // s6, s8 27 | case bottomLeft // s5, s7 28 | } 29 | 30 | enum Alignment { 31 | case horizontal // s1, s2, s7, s8 32 | case vertical // s3, s4, s5, s6 33 | } 34 | 35 | enum Direction { 36 | case up, down, left, right 37 | 38 | var reversed: Direction { 39 | switch self { 40 | case .up: return .down 41 | case .down: return .up 42 | case .left: return .right 43 | case .right: return .left 44 | } 45 | } 46 | } 47 | 48 | class Segment: SCNNode { 49 | 50 | // MARK: - Configuration & Initialization 51 | 52 | /// Thickness of the focus square lines in m. 53 | static let thickness: CGFloat = 0.018 54 | 55 | /// Length of the focus square lines in m. 56 | static let length: CGFloat = 0.5 // segment length 57 | 58 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 59 | static let openLength: CGFloat = 0.2 60 | 61 | let corner: Corner 62 | let alignment: Alignment 63 | let plane: SCNPlane 64 | 65 | init(name: String, corner: Corner, alignment: Alignment) { 66 | self.corner = corner 67 | self.alignment = alignment 68 | 69 | switch alignment { 70 | case .vertical: 71 | plane = SCNPlane(width: Segment.thickness, height: Segment.length) 72 | case .horizontal: 73 | plane = SCNPlane(width: Segment.length, height: Segment.thickness) 74 | } 75 | super.init() 76 | self.name = name 77 | 78 | let material = plane.firstMaterial! 79 | material.diffuse.contents = FocusSquare.primaryColor 80 | material.isDoubleSided = true 81 | material.ambient.contents = UIColor.black 82 | material.lightingModel = .constant 83 | material.emission.contents = FocusSquare.primaryColor 84 | geometry = plane 85 | } 86 | 87 | required init?(coder aDecoder: NSCoder) { 88 | fatalError("\(#function) has not been implemented") 89 | } 90 | 91 | // MARK: - Animating Open/Closed 92 | 93 | var openDirection: Direction { 94 | switch (corner, alignment) { 95 | case (.topLeft, .horizontal): return .left 96 | case (.topLeft, .vertical): return .up 97 | case (.topRight, .horizontal): return .right 98 | case (.topRight, .vertical): return .up 99 | case (.bottomLeft, .horizontal): return .left 100 | case (.bottomLeft, .vertical): return .down 101 | case (.bottomRight, .horizontal): return .right 102 | case (.bottomRight, .vertical): return .down 103 | } 104 | } 105 | 106 | func open() { 107 | if alignment == .horizontal { 108 | plane.width = Segment.openLength 109 | } else { 110 | plane.height = Segment.openLength 111 | } 112 | 113 | let offset = Segment.length / 2 - Segment.openLength / 2 114 | updatePosition(withOffset: Float(offset), for: openDirection) 115 | } 116 | 117 | func close() { 118 | let oldLength: CGFloat 119 | if alignment == .horizontal { 120 | oldLength = plane.width 121 | plane.width = Segment.length 122 | } else { 123 | oldLength = plane.height 124 | plane.height = Segment.length 125 | } 126 | 127 | let offset = Segment.length / 2 - oldLength / 2 128 | updatePosition(withOffset: Float(offset), for: openDirection.reversed) 129 | } 130 | 131 | private func updatePosition(withOffset offset: Float, for direction: Direction) { 132 | switch direction { 133 | case .left: position.x -= offset 134 | case .right: position.x += offset 135 | case .up: position.y -= offset 136 | case .down: position.y += offset 137 | } 138 | } 139 | 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/AR Settings/ARSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ARSettings.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | import UIKit 12 | import ARKit 13 | 14 | //---------------------------------------------------- 15 | //MARK: VIewController Extensions For Setting Up ARKit 16 | //---------------------------------------------------- 17 | 18 | extension UIViewController{ 19 | 20 | /// The Type Of Plane Detection Needed During The ARSession 21 | /// 22 | /// - Horizontal: Horizontal Plane Detection 23 | /// - Vertical: Vertical Plane Detection 24 | /// - Both: Horizontal & Vertical Plane Detection 25 | /// - None: No Plane Detection 26 | enum ARPlaneDetection { 27 | 28 | case Horizontal, Vertical, Both, None 29 | } 30 | 31 | /// The Type Of Debug Options Needed During The ARSession 32 | /// 33 | /// - FeaturePoints: Show Feature Points 34 | /// - WorldOrigin: Show The World Origin 35 | /// - Both: Show Both Feature Points & The World Origin 36 | /// - **None**: Show None (Development Build) 37 | enum ARDebugOptions{ 38 | 39 | case FeaturePoints, WorldOrigin, Both, None 40 | } 41 | 42 | /// The Type Of ARConfiguration Needed 43 | /// 44 | /// - ResetTracking: Resets World Tracking 45 | /// - RemoveAnchors: Removes All Existing Session Anchors 46 | /// - **ResetAndRemove**: Resets World Tracking & Removes All Existing Anchors 47 | /// - None: No Congifuration 48 | enum ARConfigurationOptions{ 49 | 50 | case ResetTracking, RemoveAnchors, ResetAndRemove, None 51 | 52 | } 53 | 54 | /// Sets The Type Of Plane Detection Needed During The Session 55 | /// 56 | /// - Parameter planeDetection: ARPlaneDetection 57 | /// - Returns: ARWorldTrackingConfiguration.PlaneDetection 58 | func planeDetection(_ planeDetection: ARPlaneDetection) -> ARWorldTrackingConfiguration.PlaneDetection{ 59 | 60 | switch planeDetection { 61 | 62 | case .Horizontal: 63 | return [.horizontal] 64 | case .Vertical: 65 | return [.vertical] 66 | case .Both: 67 | return [.horizontal, .vertical] 68 | case .None: 69 | return [] 70 | 71 | } 72 | } 73 | 74 | /// Sets The ARSession Debug Options 75 | /// 76 | /// - Parameter options: ARDebugOptions 77 | /// - Returns: SCNDebugOptions 78 | func debug(_ options: ARDebugOptions) -> SCNDebugOptions{ 79 | 80 | switch options { 81 | 82 | case .FeaturePoints: 83 | return [ARSCNDebugOptions.showFeaturePoints] 84 | case .WorldOrigin: 85 | return [ARSCNDebugOptions.showWorldOrigin] 86 | case .Both: 87 | return [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin] 88 | case .None: 89 | return [] 90 | } 91 | } 92 | 93 | /// Sets The ARSession Run Options 94 | /// 95 | /// - Parameter configuration: ARConfigurationOptions 96 | /// - Returns: ARSession.RunOptions 97 | func runOptions(_ configuration: ARConfigurationOptions) -> ARSession.RunOptions{ 98 | 99 | switch configuration { 100 | 101 | case .ResetTracking: 102 | return [.resetTracking] 103 | case .RemoveAnchors: 104 | return [.removeExistingAnchors] 105 | case .ResetAndRemove: 106 | return [.resetTracking, .removeExistingAnchors ] 107 | case .None: 108 | return [] 109 | } 110 | } 111 | } 112 | 113 | //------------------------------------------------ 114 | //MARK: ARSession Extension To Log Tracking States 115 | //------------------------------------------------ 116 | 117 | extension ARCamera.TrackingState: CustomStringConvertible{ 118 | 119 | public var description: String { 120 | 121 | switch self { 122 | 123 | case .notAvailable: return "Tracking Unavailable" 124 | case .limited(.excessiveMotion): return "Please Slow Your Movement" 125 | case .limited(.insufficientFeatures): return "Try To Point At A Flat Surface" 126 | case .limited(.initializing): return "Initializing" 127 | case .limited(.relocalizing): return "Relocalizing" 128 | case .normal: return "" 129 | } 130 | } 131 | } 132 | 133 | //------------------------------- 134 | //MARK ARFrame WorldMappingStatus 135 | //------------------------------- 136 | 137 | extension ARFrame.WorldMappingStatus: CustomStringConvertible{ 138 | 139 | public var description: String { 140 | switch self { 141 | case .notAvailable: 142 | return "World Mapping Not Available" 143 | case .limited: 144 | return "World Mapping Is Limited" 145 | case .extending: 146 | return "World Mapping Is Extending" 147 | case .mapped: 148 | return "World Is Succesfully Mapped" 149 | } 150 | } 151 | } 152 | 153 | //-------------------------------------------- 154 | //MARK: ARSCNView Extension For Lighting Setup 155 | //-------------------------------------------- 156 | 157 | extension ARSCNView{ 158 | 159 | /// Applies Auto Lighting Of The ARSCNView 160 | func applyLighting() { 161 | self.autoenablesDefaultLighting = true 162 | self.automaticallyUpdatesLighting = true 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/AR ViewControllers/ViewController+Interactions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+Interactions.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 16/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ARKit 11 | 12 | extension ViewController{ 13 | 14 | //----------------------------------- 15 | // MARK: - World Map & Anchor Sharing 16 | //----------------------------------- 17 | 18 | /// Shares An ARWorldMap With Any Connected Users 19 | @IBAction func shareWorldMap(){ 20 | 21 | //1. Attempt To Get The World Map From Our ARSession 22 | augmentedRealitySession.getCurrentWorldMap { worldMap, error in 23 | 24 | guard let mapToShare = worldMap else { print("Error: \(error!.localizedDescription)"); return } 25 | 26 | //2. We Have A Valid ARWorldMap So Send It To Any Peers 27 | guard let data = try? NSKeyedArchiver.archivedData(withRootObject: mapToShare, requiringSecureCoding: true) else { fatalError("Can't Encode Map") } 28 | self.cloudSession.sendDataToUsers(data) 29 | } 30 | } 31 | 32 | //------------------------- 33 | // MARK: - Anchor Placement 34 | //------------------------- 35 | 36 | /// Allows The User To Create An ARAnchor 37 | /// 38 | /// - Parameter gesture: UITapGestureRecognizer 39 | @IBAction func placeAnchor(_ gesture: UITapGestureRecognizer){ 40 | 41 | //1. Get The Current Touch Location 42 | let currentTouchLocation = gesture.location(in: self.augmentedRealityView) 43 | 44 | //2. Perform An ARSCNHitTest For Any Existing Or Perceived Horizontal Planes 45 | guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, types: [.existingPlaneUsingGeometry, .estimatedHorizontalPlane]).first else { return } 46 | 47 | //3. Create Our Anchor & Add It To The Scene 48 | let validAnchor = ARAnchor(name: CLOUD_ANCHOR_ID, transform: hitTest.worldTransform) 49 | self.augmentedRealitySession.add(anchor: validAnchor) 50 | 51 | //4. Share The Angle, Rotation & Scale With Peers 52 | guard let anchorData = try? NSKeyedArchiver.archivedData(withRootObject: validAnchor, requiringSecureCoding: true) else { fatalError("Can't Encode Anchor") } 53 | self.cloudSession.sendDataToUsers(anchorData) 54 | 55 | } 56 | 57 | //--------------------------------- 58 | // MARK: - Model Scaling & Rotation 59 | //--------------------------------- 60 | 61 | /// Rotates The Model On It's YAxis 62 | /// 63 | /// - Parameter gesture: UIPanGestureRecognizer 64 | @objc func rotateModel(_ gesture: UIPanGestureRecognizer) { 65 | 66 | guard let modelNode = modelNode else { return } 67 | 68 | let translation = gesture.translation(in: gesture.view!) 69 | var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0 70 | newAngleY += currentAngleY 71 | 72 | modelNode.eulerAngles.y = newAngleY 73 | 74 | if(gesture.state == .ended) { currentAngleY = newAngleY } 75 | 76 | //2. Send It To Any Connected Users 77 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: newAngleY, requiringSecureCoding: true){ 78 | 79 | cloudSession.sendDataToUsers(data) 80 | } 81 | 82 | } 83 | 84 | /// Scales The Model 85 | /// 86 | /// - Parameter gesture: UIPinchGestureRecognizer 87 | @objc func scaleModel(_ gesture: UIPinchGestureRecognizer) { 88 | 89 | guard let nodeToScale = modelNode else { return } 90 | 91 | if gesture.state == .changed { 92 | 93 | let pinchScaleX: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.x)) 94 | let pinchScaleY: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.y)) 95 | let pinchScaleZ: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.z)) 96 | let scaleVector = SCNVector3(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ)) 97 | nodeToScale.scale = scaleVector 98 | 99 | //2. Send It To Any Connected Users 100 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: scaleVector, requiringSecureCoding: true){ 101 | 102 | cloudSession.sendDataToUsers(data) 103 | } 104 | 105 | gesture.scale = 1 106 | 107 | } 108 | if gesture.state == .ended { } 109 | 110 | } 111 | 112 | //----------------------- 113 | // MARK: - Cube Colouring 114 | //----------------------- 115 | 116 | /// Sets The Current Face Of The Cube 117 | /// 118 | /// - Parameter gesture: UITapGestureRecognizer 119 | @IBAction func selectCubeFace(_ gesture: UITapGestureRecognizer){ 120 | 121 | //1. Get The Current Touch Location 122 | let currentTouchLocation = gesture.location(in: self.augmentedRealityView) 123 | 124 | //2. Perform An SCNHitTest 125 | guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first else { return } 126 | 127 | if let index = BoxFaces(rawValue: hitTest.geometryIndex){ 128 | 129 | print("User Has Hit \(index)") 130 | 131 | //2. Stores The Face Index 132 | faceIndex = hitTest.geometryIndex 133 | 134 | //3. Sets The Face Colour 135 | setFaceColourFromGeometryIndex(hitTest.geometryIndex) 136 | 137 | } 138 | } 139 | 140 | /// Replaces The SCNMaterial For The FaceIndex Of The Cube 141 | /// 142 | /// - Parameter index: Int 143 | func setFaceColourFromGeometryIndex(_ index: Int){ 144 | 145 | if let validModel = modelNode, let validColour = colourToUse{ 146 | 147 | let material = SCNMaterial() 148 | material.diffuse.contents = validColour 149 | validModel.geometry?.replaceMaterial(at: index, with: material) 150 | sendColourData() 151 | colourToUse = nil 152 | faceIndex = nil 153 | } 154 | } 155 | 156 | /// Sets The Face Colour Of The Cube 157 | /// 158 | /// - Parameter sender: UIButton 159 | @objc func setFaceColour(_ sender: UIButton){ 160 | 161 | colourToUse = UIColor(cgColor: sender.layer.borderColor!) 162 | 163 | if let validIndex = faceIndex { setFaceColourFromGeometryIndex(validIndex) } 164 | } 165 | 166 | /// Sends The Colour Data 167 | func sendColourData(){ 168 | 169 | var colourData = [Int: UIColor]() 170 | guard let validFaceIndex = faceIndex, let validColour = colourToUse else { return } 171 | colourData[validFaceIndex] = validColour 172 | 173 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: colourData, requiringSecureCoding: true){ 174 | cloudSession.sendDataToUsers(data) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/AR ViewControllers/ARDelegation+Updates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ARDelegation+Updates.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 16/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ARKit 11 | 12 | //-------------------------- 13 | // MARK: - ARSessionDelegate 14 | //-------------------------- 15 | 16 | extension ViewController: ARSessionDelegate{ 17 | 18 | func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { 19 | 20 | DispatchQueue.main.async { self.updateFocusSquare() } 21 | } 22 | 23 | func session(_ session: ARSession, didUpdate frame: ARFrame) { 24 | 25 | switch frame.worldMappingStatus { 26 | case .notAvailable, .limited: 27 | shareButton.isUserInteractionEnabled = false 28 | case .extending: 29 | shareButton.isUserInteractionEnabled = !cloudSession.connectedPeers.isEmpty 30 | case .mapped: 31 | shareButton.isUserInteractionEnabled = !cloudSession.connectedPeers.isEmpty 32 | } 33 | 34 | if canShowControls{ 35 | mappingStatusLabel.text = frame.worldMappingStatus.description 36 | } 37 | 38 | updateUserSessionInformationFor(frame, trackingState: frame.camera.trackingState) 39 | } 40 | 41 | func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { 42 | 43 | //1. Update The Seesion Information 44 | updateUserSessionInformationFor(session.currentFrame!, trackingState: camera.trackingState) 45 | } 46 | 47 | func sessionWasInterrupted(_ session: ARSession) { statusLabel.text = "Session Was Interrupted" } 48 | 49 | func sessionInterruptionEnded(_ session: ARSession) { statusLabel.text = "Session Resumed" } 50 | 51 | func session(_ session: ARSession, didFailWithError error: Error) { 52 | 53 | statusLabel.text = "AR Session Failed" 54 | resetSession() 55 | } 56 | 57 | func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { return true } 58 | } 59 | 60 | //-------------------------- 61 | // MARK: - ARSCNViewDelegate 62 | //-------------------------- 63 | 64 | extension ViewController: ARSCNViewDelegate{ 65 | 66 | func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { 67 | 68 | //1. If Our ARAnchor Matches Our Anchor Name & We Havent Placed Our Cube Then Display It 69 | if anchor.name == CLOUD_ANCHOR_ID{ 70 | 71 | if !modelExists{ 72 | modelNode = Box(colours: [.red, .green, .blue, .purple, .orange, .cyan], images: nil) 73 | canDisplayFocusSquare = false 74 | node.addChildNode(modelNode!) 75 | modelExists = true 76 | 77 | } 78 | 79 | } 80 | } 81 | } 82 | 83 | //---------------------------- 84 | // MARK: - User Status Updates 85 | //---------------------------- 86 | 87 | extension ViewController{ 88 | 89 | /// Updates The User Regarding The Tracking State Of The ARSession & Whether They Are Connected To Any Peers 90 | /// 91 | /// - Parameters: 92 | /// - frame: ARFrame 93 | /// - trackingState: ARCamera.TrackingState 94 | func updateUserSessionInformationFor(_ frame: ARFrame, trackingState: ARCamera.TrackingState){ 95 | 96 | let displayMessage: String 97 | 98 | //1. Adjust The Information Based On The Tracking State 99 | switch trackingState { 100 | case .normal where frame.anchors.isEmpty && cloudSession.connectedPeers.isEmpty: 101 | 102 | displayMessage = "Please Move Around To Map The Environment Or Wait To Join A Shared Session." 103 | 104 | case .normal where !cloudSession.connectedPeers.isEmpty && mapProvider == nil: 105 | let peerNames = cloudSession.connectedPeers.map({ $0.displayName }).joined(separator: ", ") 106 | displayMessage = "You Are Connected With \(peerNames)." 107 | 108 | case .notAvailable: 109 | displayMessage = "Tracking Unavailable" 110 | 111 | case .limited(.excessiveMotion): 112 | displayMessage = "Please Slow Your Movement" 113 | 114 | case .limited(.insufficientFeatures): 115 | displayMessage = "Try To Point At A Flat Surface" 116 | 117 | case .limited(.initializing) where mapProvider != nil, 118 | .limited(.relocalizing) where mapProvider != nil: 119 | displayMessage = "Receiving ARMap From \(mapProvider!.displayName)." 120 | 121 | case .limited(.relocalizing): 122 | displayMessage = "Resuming ARSession" 123 | 124 | case .limited(.initializing): 125 | displayMessage = "Initializing" 126 | 127 | default: 128 | 129 | displayMessage = "" 130 | 131 | } 132 | 133 | if canShowControls{ 134 | //2. Update The Display Message Or Hide If Neccessary 135 | statusLabel.text = displayMessage 136 | statusLabel.isHidden = displayMessage.isEmpty 137 | } 138 | 139 | } 140 | 141 | } 142 | 143 | //------------------ 144 | //MARK: Focus Square 145 | //------------------ 146 | 147 | extension ViewController{ 148 | 149 | func updateFocusSquare() { 150 | 151 | //1. If Our Model Has Been Placed Hide The Focus Square 152 | if !canDisplayFocusSquare { 153 | focusSquare.hide() 154 | } else { 155 | focusSquare.unhide() 156 | } 157 | 158 | if let camera = self.augmentedRealitySession.currentFrame?.camera, 159 | case .normal = camera.trackingState, 160 | let result = self.augmentedRealityView.smartHitTest(screenCenter) { 161 | updateQueue.async { 162 | 163 | if self.canDisplayFocusSquare{ 164 | self.augmentedRealityView.scene.rootNode.addChildNode(self.focusSquare) 165 | self.focusSquare.state = .detecting(hitTestResult: result, camera: camera) 166 | 167 | 168 | //2. Only Allow Placement Of Our Cube If We Have Detected An ARPlaneAnchor 169 | if !self.focusSquare.isOpen{ 170 | self.planeDetected = true 171 | self.placementGesture.isEnabled = true 172 | }else{ 173 | self.planeDetected = false 174 | self.placementGesture.isEnabled = false 175 | } 176 | } 177 | } 178 | 179 | } else { 180 | 181 | updateQueue.async { 182 | 183 | if self.canDisplayFocusSquare{ 184 | self.focusSquare.state = .initializing 185 | self.augmentedRealityView.pointOfView?.addChildNode(self.focusSquare) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/AR ViewControllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CloudCube 4 | // 5 | // Created by Josh Robbins on 15/06/2018. 6 | // Copyright © 2018 BlackMirrorz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ARKit 11 | import MultipeerConnectivity 12 | 13 | class ViewController: UIViewController { 14 | 15 | @IBOutlet weak var instructionsView: UIView! 16 | var canShowControls = false 17 | 18 | //-------------------- 19 | //MARK: - AR Variables 20 | //-------------------- 21 | 22 | @IBOutlet var augmentedRealityView: ARSCNView! 23 | let augmentedRealitySession = ARSession() 24 | var configuration = ARWorldTrackingConfiguration() 25 | @IBOutlet weak var statusLabel: UILabel! 26 | 27 | //-------------------------- 28 | //MARK: - Apple Focus Square 29 | //-------------------------- 30 | 31 | var focusSquare = FocusSquare() 32 | var canDisplayFocusSquare = true 33 | var screenCenter: CGPoint { 34 | let bounds = self.augmentedRealityView.bounds 35 | return CGPoint(x: bounds.midX, y: bounds.midY) 36 | } 37 | let updateQueue = DispatchQueue(label: "blackMirroz") 38 | 39 | //-------------- 40 | // MARK: - Model 41 | //-------------- 42 | var currentAngleY: Float = 0.0 43 | var modelNode: SCNNode? 44 | var placementGesture: UITapGestureRecognizer! 45 | var planeDetected = false 46 | var modelExists = false 47 | var colourToUse: UIColor? 48 | var faceIndex: Int? 49 | 50 | @IBOutlet var colourButtonHolder: UIVisualEffectView! 51 | @IBOutlet var colourButtons: [UIButton]! 52 | 53 | 54 | //------------------ 55 | // MARK: - Multipeer 56 | //------------------ 57 | 58 | var mapProvider: MCPeerID? 59 | var cloudSession: ARCloudShare! 60 | let CLOUD_ANCHOR_ID = "blackMirrorzAnchor" 61 | @IBOutlet weak var mappingStatusLabel: UILabel! 62 | @IBOutlet weak var shareButton: UIImageView! 63 | @IBOutlet weak var resetButton: UIButton! 64 | 65 | 66 | //------------------------ 67 | // MARK: - View Life Cycle 68 | //------------------------ 69 | 70 | override func viewDidLoad() { 71 | super.viewDidLoad() 72 | 73 | //1. Initialize The Sharing Session 74 | cloudSession = ARCloudShare(receivedDataHandler: receivedData) 75 | 76 | //2. Setup Our Placement Gesture Recognizet 77 | setupGestures() 78 | 79 | //3. Add The Colour Changing Function To Our Colour Buttons 80 | colourButtons.forEach { $0.addTarget(self, action: #selector(setFaceColour(_:)), for: .touchUpInside) } 81 | 82 | //4. Show The Instructions View 83 | showInstructionsView() 84 | } 85 | 86 | override func viewDidAppear(_ animated: Bool) { setupARSession() } 87 | 88 | override func viewDidLayoutSubviews() { 89 | 90 | self.view.addSubview(instructionsView) 91 | instructionsView.center = self.view.center 92 | instructionsView.backgroundColor = .clear 93 | 94 | } 95 | 96 | //-------------------------- 97 | // MARK: - User Instructions 98 | //-------------------------- 99 | 100 | /// Shows The Initial Instructions 101 | func showInstructionsView(){ 102 | 103 | canShowControls = false 104 | canDisplayFocusSquare = false 105 | colourButtonHolder.alpha = 0 106 | resetButton.alpha = 0 107 | shareButton.alpha = 0 108 | 109 | UIView.animate(withDuration: 12, animations: { self.instructionsView.alpha = 0 }) { (instructionsHidden) in 110 | 111 | UIView.animate(withDuration: 1, animations: { 112 | 113 | self.colourButtonHolder.alpha = 1 114 | self.resetButton.alpha = 1 115 | self.shareButton.alpha = 1 116 | 117 | }, completion: { (controlsShown) in 118 | self.canShowControls = true 119 | self.canDisplayFocusSquare = true 120 | }) 121 | } 122 | } 123 | 124 | //---------------------- 125 | // MARK: - Data Handling 126 | //---------------------- 127 | 128 | /// Handles The Data Received From Our ARMultipeer Session 129 | /// 130 | /// - Parameters: 131 | /// - data: Data 132 | /// - peer: MCPeerID 133 | func receivedData(_ data: Data, from peer: MCPeerID) { 134 | 135 | //1. Try To UnArchive Our Data As An ARWorldMap 136 | if let unarchivedMap = try? NSKeyedUnarchiver.unarchivedObject(of: ARWorldMap.classForKeyedUnarchiver(), from: data), 137 | let worldMap = unarchivedMap as? ARWorldMap { 138 | 139 | //2. Now A Map Is Available Restart The Session 140 | let configuration = ARWorldTrackingConfiguration() 141 | configuration.planeDetection = .horizontal 142 | configuration.initialWorldMap = worldMap 143 | self.augmentedRealityView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) 144 | 145 | mapProvider = peer 146 | 147 | } 148 | //2. Try To Unarchive Our Data As An ARAnchor 149 | else if let unarchivedAnchor = try? NSKeyedUnarchiver.unarchivedObject(of: ARAnchor.classForKeyedUnarchiver(), from: data), 150 | let anchor = unarchivedAnchor as? ARAnchor { 151 | 152 | augmentedRealitySession.add(anchor: anchor) 153 | } 154 | 155 | //3. Try To Unarchive Our Data To Adjust The Scale & Or Rotation Of Our Model 156 | else if let unarchivedData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data){ 157 | 158 | if unarchivedData is Float, let unarchivedRotationData = unarchivedData as? Float, let model = modelNode { 159 | 160 | model.eulerAngles.y = unarchivedRotationData 161 | 162 | }else if unarchivedData is SCNVector3, let unarchivedScaleData = unarchivedData as? SCNVector3, let model = modelNode { 163 | 164 | model.scale = unarchivedScaleData 165 | 166 | }else if unarchivedData is [Int: UIColor], let colourDictionary = unarchivedData as? [Int: UIColor], let _ = modelNode{ 167 | 168 | guard let index = colourDictionary.keys.first, let colour = colourDictionary[index] else { return } 169 | colourToUse = colour 170 | setFaceColourFromGeometryIndex(index) 171 | colourToUse = nil 172 | } 173 | else { 174 | print("Unknown Data Recieved From = \(peer)") 175 | 176 | } 177 | } 178 | 179 | } 180 | 181 | //---------------------------- 182 | // MARK: - Gesture Recongizers 183 | //---------------------------- 184 | 185 | /// Adds The Placement Gesture To Our ARSession 186 | func setupGestures(){ 187 | 188 | //1. Setup The Placement Gesture Recognizer & Disable It Until An ARPlaneAnchor Has Been Detected 189 | placementGesture = UITapGestureRecognizer(target: self, action: #selector(placeAnchor(_:))) 190 | self.view.addGestureRecognizer(placementGesture) 191 | placementGesture.isEnabled = false 192 | 193 | //2. Add A Tap Gesture To Our ImageView To Enable Sharing 194 | let tapToShareGesture = UITapGestureRecognizer(target: self, action: #selector(shareWorldMap)) 195 | shareButton.addGestureRecognizer(tapToShareGesture) 196 | 197 | //3. Add A Rotation Gesture So We Can Rotate Our Model 198 | let rotationGesture = UIPanGestureRecognizer(target: self, action: #selector(rotateModel(_:))) 199 | self.view.addGestureRecognizer(rotationGesture) 200 | 201 | //3. Add A Scale Gesture So We Can Scale Our Model 202 | let scaleGesture = UIPinchGestureRecognizer(target: self, action: #selector(scaleModel(_:))) 203 | self.view.addGestureRecognizer(scaleGesture) 204 | 205 | //4. Add A Double Tap Gesture To Colourizer The Face Our Cube 206 | let tapToColourize = UITapGestureRecognizer(target: self, action: #selector(selectCubeFace(_:))) 207 | tapToColourize.numberOfTapsRequired = 2 208 | self.view.addGestureRecognizer(tapToColourize) 209 | } 210 | 211 | //----------------------- 212 | //MARK: - ARSetup & Reset 213 | //----------------------- 214 | 215 | /// Runs The ARSession 216 | func setupARSession() { 217 | 218 | augmentedRealityView.session = augmentedRealitySession 219 | configuration.planeDetection = planeDetection(.Horizontal) 220 | configuration.environmentTexturing = .automatic 221 | 222 | augmentedRealityView.debugOptions = debug(.None) 223 | augmentedRealitySession.run(configuration, options: runOptions(.ResetAndRemove)) 224 | 225 | augmentedRealityView.delegate = self 226 | augmentedRealitySession.delegate = self 227 | 228 | UIApplication.shared.isIdleTimerDisabled = true 229 | } 230 | 231 | /// Resets The ARSession 232 | @IBAction func resetSession(){ 233 | 234 | canDisplayFocusSquare = true 235 | planeDetected = false 236 | placementGesture.isEnabled = false 237 | modelExists = false 238 | modelNode = nil 239 | colourToUse = nil 240 | faceIndex = nil 241 | 242 | let configuration = ARWorldTrackingConfiguration() 243 | configuration.planeDetection = planeDetection(.Horizontal) 244 | augmentedRealitySession.run(configuration, options: runOptions(.ResetAndRemove)) 245 | } 246 | 247 | 248 | } 249 | 250 | -------------------------------------------------------------------------------- /CloudCube/CloudCube.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AF08729320D3EE7300C800E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF08729220D3EE7300C800E3 /* AppDelegate.swift */; }; 11 | AF08729520D3EE7300C800E3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF08729420D3EE7300C800E3 /* ViewController.swift */; }; 12 | AF08729820D3EE7300C800E3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF08729620D3EE7300C800E3 /* Main.storyboard */; }; 13 | AF08729A20D3EE7400C800E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF08729920D3EE7400C800E3 /* Assets.xcassets */; }; 14 | AF08729D20D3EE7400C800E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */; }; 15 | AF0872A520D3EEAD00C800E3 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0872A420D3EEAD00C800E3 /* Box.swift */; }; 16 | AF0872A720D3EEEC00C800E3 /* ARSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0872A620D3EEEC00C800E3 /* ARSettings.swift */; }; 17 | AF41F91020D48AC200AB0012 /* ARCloudShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */; }; 18 | AF41F91720D48B7800AB0012 /* FocusSquareSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */; }; 19 | AF41F91820D48B7800AB0012 /* FocusSquare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */; }; 20 | AF41F91920D48B7800AB0012 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91520D48B7800AB0012 /* Utilities.swift */; }; 21 | AF41F91A20D48B7800AB0012 /* FocusSquare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91620D48B7800AB0012 /* FocusSquare.swift */; }; 22 | AF41F91C20D4909600AB0012 /* ARDelegation+Updates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */; }; 23 | AF41F91E20D4936B00AB0012 /* BlackMirrorzViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */; }; 24 | AF41F92220D4B32800AB0012 /* ViewController+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | AF08728F20D3EE7300C800E3 /* CloudCube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudCube.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | AF08729220D3EE7300C800E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 30 | AF08729420D3EE7300C800E3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 31 | AF08729720D3EE7300C800E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 32 | AF08729920D3EE7400C800E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | AF08729C20D3EE7400C800E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | AF08729E20D3EE7400C800E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | AF0872A420D3EEAD00C800E3 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; 36 | AF0872A620D3EEEC00C800E3 /* ARSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARSettings.swift; sourceTree = ""; }; 37 | AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARCloudShare.swift; sourceTree = ""; }; 38 | AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusSquareSegment.swift; sourceTree = ""; }; 39 | AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FocusSquare+Extensions.swift"; sourceTree = ""; }; 40 | AF41F91520D48B7800AB0012 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 41 | AF41F91620D48B7800AB0012 /* FocusSquare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusSquare.swift; sourceTree = ""; }; 42 | AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ARDelegation+Updates.swift"; sourceTree = ""; }; 43 | AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlackMirrorzViews.swift; sourceTree = ""; }; 44 | AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Interactions.swift"; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | AF08728C20D3EE7300C800E3 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | AF08728620D3EE7300C800E3 = { 59 | isa = PBXGroup; 60 | children = ( 61 | AF08729120D3EE7300C800E3 /* CloudCube */, 62 | AF08729020D3EE7300C800E3 /* Products */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | AF08729020D3EE7300C800E3 /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | AF08728F20D3EE7300C800E3 /* CloudCube.app */, 70 | ); 71 | name = Products; 72 | sourceTree = ""; 73 | }; 74 | AF08729120D3EE7300C800E3 /* CloudCube */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | AF41F91120D48B7800AB0012 /* Apple Focus Square */, 78 | AF41F92620D4C1E400AB0012 /* Multipeer */, 79 | AF41F92320D4C1A200AB0012 /* Custom Views */, 80 | AF41F92520D4C1BB00AB0012 /* Custom Nodes */, 81 | AF41F92720D4C1FE00AB0012 /* AR Settings */, 82 | AF41F92420D4C1AF00AB0012 /* AR ViewControllers */, 83 | AF08729220D3EE7300C800E3 /* AppDelegate.swift */, 84 | AF08729620D3EE7300C800E3 /* Main.storyboard */, 85 | AF08729920D3EE7400C800E3 /* Assets.xcassets */, 86 | AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */, 87 | AF08729E20D3EE7400C800E3 /* Info.plist */, 88 | ); 89 | path = CloudCube; 90 | sourceTree = ""; 91 | }; 92 | AF41F91120D48B7800AB0012 /* Apple Focus Square */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */, 96 | AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */, 97 | AF41F91420D48B7800AB0012 /* Utilities */, 98 | AF41F91620D48B7800AB0012 /* FocusSquare.swift */, 99 | ); 100 | path = "Apple Focus Square"; 101 | sourceTree = ""; 102 | }; 103 | AF41F91420D48B7800AB0012 /* Utilities */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | AF41F91520D48B7800AB0012 /* Utilities.swift */, 107 | ); 108 | path = Utilities; 109 | sourceTree = ""; 110 | }; 111 | AF41F92320D4C1A200AB0012 /* Custom Views */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */, 115 | ); 116 | path = "Custom Views"; 117 | sourceTree = ""; 118 | }; 119 | AF41F92420D4C1AF00AB0012 /* AR ViewControllers */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | AF08729420D3EE7300C800E3 /* ViewController.swift */, 123 | AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */, 124 | AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */, 125 | ); 126 | path = "AR ViewControllers"; 127 | sourceTree = ""; 128 | }; 129 | AF41F92520D4C1BB00AB0012 /* Custom Nodes */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | AF0872A420D3EEAD00C800E3 /* Box.swift */, 133 | ); 134 | path = "Custom Nodes"; 135 | sourceTree = ""; 136 | }; 137 | AF41F92620D4C1E400AB0012 /* Multipeer */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */, 141 | ); 142 | path = Multipeer; 143 | sourceTree = ""; 144 | }; 145 | AF41F92720D4C1FE00AB0012 /* AR Settings */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | AF0872A620D3EEEC00C800E3 /* ARSettings.swift */, 149 | ); 150 | path = "AR Settings"; 151 | sourceTree = ""; 152 | }; 153 | /* End PBXGroup section */ 154 | 155 | /* Begin PBXNativeTarget section */ 156 | AF08728E20D3EE7300C800E3 /* CloudCube */ = { 157 | isa = PBXNativeTarget; 158 | buildConfigurationList = AF0872A120D3EE7400C800E3 /* Build configuration list for PBXNativeTarget "CloudCube" */; 159 | buildPhases = ( 160 | AF08728B20D3EE7300C800E3 /* Sources */, 161 | AF08728C20D3EE7300C800E3 /* Frameworks */, 162 | AF08728D20D3EE7300C800E3 /* Resources */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | ); 168 | name = CloudCube; 169 | productName = CloudCube; 170 | productReference = AF08728F20D3EE7300C800E3 /* CloudCube.app */; 171 | productType = "com.apple.product-type.application"; 172 | }; 173 | /* End PBXNativeTarget section */ 174 | 175 | /* Begin PBXProject section */ 176 | AF08728720D3EE7300C800E3 /* Project object */ = { 177 | isa = PBXProject; 178 | attributes = { 179 | LastSwiftUpdateCheck = 1000; 180 | LastUpgradeCheck = 1000; 181 | ORGANIZATIONNAME = BlackMirrorz; 182 | TargetAttributes = { 183 | AF08728E20D3EE7300C800E3 = { 184 | CreatedOnToolsVersion = 10.0; 185 | }; 186 | }; 187 | }; 188 | buildConfigurationList = AF08728A20D3EE7300C800E3 /* Build configuration list for PBXProject "CloudCube" */; 189 | compatibilityVersion = "Xcode 9.3"; 190 | developmentRegion = en; 191 | hasScannedForEncodings = 0; 192 | knownRegions = ( 193 | en, 194 | Base, 195 | ); 196 | mainGroup = AF08728620D3EE7300C800E3; 197 | productRefGroup = AF08729020D3EE7300C800E3 /* Products */; 198 | projectDirPath = ""; 199 | projectRoot = ""; 200 | targets = ( 201 | AF08728E20D3EE7300C800E3 /* CloudCube */, 202 | ); 203 | }; 204 | /* End PBXProject section */ 205 | 206 | /* Begin PBXResourcesBuildPhase section */ 207 | AF08728D20D3EE7300C800E3 /* Resources */ = { 208 | isa = PBXResourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | AF08729D20D3EE7400C800E3 /* LaunchScreen.storyboard in Resources */, 212 | AF08729A20D3EE7400C800E3 /* Assets.xcassets in Resources */, 213 | AF08729820D3EE7300C800E3 /* Main.storyboard in Resources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXResourcesBuildPhase section */ 218 | 219 | /* Begin PBXSourcesBuildPhase section */ 220 | AF08728B20D3EE7300C800E3 /* Sources */ = { 221 | isa = PBXSourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | AF41F91020D48AC200AB0012 /* ARCloudShare.swift in Sources */, 225 | AF41F91E20D4936B00AB0012 /* BlackMirrorzViews.swift in Sources */, 226 | AF41F91720D48B7800AB0012 /* FocusSquareSegment.swift in Sources */, 227 | AF41F92220D4B32800AB0012 /* ViewController+Interactions.swift in Sources */, 228 | AF0872A520D3EEAD00C800E3 /* Box.swift in Sources */, 229 | AF08729520D3EE7300C800E3 /* ViewController.swift in Sources */, 230 | AF41F91A20D48B7800AB0012 /* FocusSquare.swift in Sources */, 231 | AF08729320D3EE7300C800E3 /* AppDelegate.swift in Sources */, 232 | AF0872A720D3EEEC00C800E3 /* ARSettings.swift in Sources */, 233 | AF41F91920D48B7800AB0012 /* Utilities.swift in Sources */, 234 | AF41F91820D48B7800AB0012 /* FocusSquare+Extensions.swift in Sources */, 235 | AF41F91C20D4909600AB0012 /* ARDelegation+Updates.swift in Sources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXSourcesBuildPhase section */ 240 | 241 | /* Begin PBXVariantGroup section */ 242 | AF08729620D3EE7300C800E3 /* Main.storyboard */ = { 243 | isa = PBXVariantGroup; 244 | children = ( 245 | AF08729720D3EE7300C800E3 /* Base */, 246 | ); 247 | name = Main.storyboard; 248 | sourceTree = ""; 249 | }; 250 | AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */ = { 251 | isa = PBXVariantGroup; 252 | children = ( 253 | AF08729C20D3EE7400C800E3 /* Base */, 254 | ); 255 | name = LaunchScreen.storyboard; 256 | sourceTree = ""; 257 | }; 258 | /* End PBXVariantGroup section */ 259 | 260 | /* Begin XCBuildConfiguration section */ 261 | AF08729F20D3EE7400C800E3 /* Debug */ = { 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 | CODE_SIGN_IDENTITY = "iPhone Developer"; 294 | COPY_PHASE_STRIP = NO; 295 | DEBUG_INFORMATION_FORMAT = dwarf; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | ENABLE_TESTABILITY = YES; 298 | GCC_C_LANGUAGE_STANDARD = gnu11; 299 | GCC_DYNAMIC_NO_PIC = NO; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_OPTIMIZATION_LEVEL = 0; 302 | GCC_PREPROCESSOR_DEFINITIONS = ( 303 | "DEBUG=1", 304 | "$(inherited)", 305 | ); 306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 308 | GCC_WARN_UNDECLARED_SELECTOR = YES; 309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 310 | GCC_WARN_UNUSED_FUNCTION = YES; 311 | GCC_WARN_UNUSED_VARIABLE = YES; 312 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 313 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 314 | ONLY_ACTIVE_ARCH = YES; 315 | SDKROOT = iphoneos; 316 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 317 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 318 | }; 319 | name = Debug; 320 | }; 321 | AF0872A020D3EE7400C800E3 /* Release */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 328 | CLANG_CXX_LIBRARY = "libc++"; 329 | CLANG_ENABLE_MODULES = YES; 330 | CLANG_ENABLE_OBJC_ARC = YES; 331 | CLANG_ENABLE_OBJC_WEAK = YES; 332 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 333 | CLANG_WARN_BOOL_CONVERSION = YES; 334 | CLANG_WARN_COMMA = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 337 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 338 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 339 | CLANG_WARN_EMPTY_BODY = YES; 340 | CLANG_WARN_ENUM_CONVERSION = YES; 341 | CLANG_WARN_INFINITE_RECURSION = YES; 342 | CLANG_WARN_INT_CONVERSION = YES; 343 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 344 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 345 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 347 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 348 | CLANG_WARN_STRICT_PROTOTYPES = YES; 349 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 350 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 351 | CLANG_WARN_UNREACHABLE_CODE = YES; 352 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 353 | CODE_SIGN_IDENTITY = "iPhone Developer"; 354 | COPY_PHASE_STRIP = NO; 355 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 356 | ENABLE_NS_ASSERTIONS = NO; 357 | ENABLE_STRICT_OBJC_MSGSEND = YES; 358 | GCC_C_LANGUAGE_STANDARD = gnu11; 359 | GCC_NO_COMMON_BLOCKS = YES; 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 367 | MTL_ENABLE_DEBUG_INFO = NO; 368 | SDKROOT = iphoneos; 369 | SWIFT_COMPILATION_MODE = wholemodule; 370 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 371 | VALIDATE_PRODUCT = YES; 372 | }; 373 | name = Release; 374 | }; 375 | AF0872A220D3EE7400C800E3 /* Debug */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 379 | CODE_SIGN_STYLE = Automatic; 380 | DEVELOPMENT_TEAM = DHKEQ8PRAX; 381 | INFOPLIST_FILE = CloudCube/Info.plist; 382 | LD_RUNPATH_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "@executable_path/Frameworks", 385 | ); 386 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.BlackMirrorz.CloudCube; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SWIFT_VERSION = 4.2; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | }; 391 | name = Debug; 392 | }; 393 | AF0872A320D3EE7400C800E3 /* Release */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | CODE_SIGN_STYLE = Automatic; 398 | DEVELOPMENT_TEAM = DHKEQ8PRAX; 399 | INFOPLIST_FILE = CloudCube/Info.plist; 400 | LD_RUNPATH_SEARCH_PATHS = ( 401 | "$(inherited)", 402 | "@executable_path/Frameworks", 403 | ); 404 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.BlackMirrorz.CloudCube; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | SWIFT_VERSION = 4.2; 407 | TARGETED_DEVICE_FAMILY = "1,2"; 408 | }; 409 | name = Release; 410 | }; 411 | /* End XCBuildConfiguration section */ 412 | 413 | /* Begin XCConfigurationList section */ 414 | AF08728A20D3EE7300C800E3 /* Build configuration list for PBXProject "CloudCube" */ = { 415 | isa = XCConfigurationList; 416 | buildConfigurations = ( 417 | AF08729F20D3EE7400C800E3 /* Debug */, 418 | AF0872A020D3EE7400C800E3 /* Release */, 419 | ); 420 | defaultConfigurationIsVisible = 0; 421 | defaultConfigurationName = Release; 422 | }; 423 | AF0872A120D3EE7400C800E3 /* Build configuration list for PBXNativeTarget "CloudCube" */ = { 424 | isa = XCConfigurationList; 425 | buildConfigurations = ( 426 | AF0872A220D3EE7400C800E3 /* Debug */, 427 | AF0872A320D3EE7400C800E3 /* Release */, 428 | ); 429 | defaultConfigurationIsVisible = 0; 430 | defaultConfigurationName = Release; 431 | }; 432 | /* End XCConfigurationList section */ 433 | }; 434 | rootObject = AF08728720D3EE7300C800E3 /* Project object */; 435 | } 436 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Apple Focus Square/FocusSquare.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | SceneKit node giving the user hints about the status of ARKit world tracking. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | /** 12 | An `SCNNode` which is used to provide uses with visual cues about the status of ARKit world tracking. 13 | - Tag: FocusSquare 14 | */ 15 | class FocusSquare: SCNNode { 16 | // MARK: - Types 17 | 18 | enum State: Equatable { 19 | case initializing 20 | case detecting(hitTestResult: ARHitTestResult, camera: ARCamera?) 21 | } 22 | 23 | // MARK: - Configuration Properties 24 | 25 | // Original size of the focus square in meters. 26 | static let size: Float = 0.17 27 | 28 | // Thickness of the focus square lines in meters. 29 | static let thickness: Float = 0.018 30 | 31 | // Scale factor for the focus square when it is closed, w.r.t. the original size. 32 | static let scaleForClosedSquare: Float = 0.97 33 | 34 | // Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 35 | static let sideLengthForOpenSegments: CGFloat = 0.2 36 | 37 | // Duration of the open/close animation 38 | static let animationDuration = 0.7 39 | 40 | static let primaryColor = #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1) 41 | 42 | // Color of the focus square fill. 43 | static let fillColor = #colorLiteral(red: 1, green: 0.9254901961, blue: 0.4117647059, alpha: 1) 44 | 45 | // MARK: - Properties 46 | 47 | /// The most recent position of the focus square based on the current state. 48 | var lastPosition: float3? { 49 | switch state { 50 | case .initializing: return nil 51 | case .detecting(let hitTestResult, _): return hitTestResult.worldTransform.translation 52 | } 53 | } 54 | 55 | var state: State = .initializing { 56 | didSet { 57 | guard state != oldValue else { return } 58 | 59 | switch state { 60 | case .initializing: 61 | displayAsBillboard() 62 | 63 | case let .detecting(hitTestResult, camera): 64 | if let planeAnchor = hitTestResult.anchor as? ARPlaneAnchor { 65 | displayAsClosed(for: hitTestResult, planeAnchor: planeAnchor, camera: camera) 66 | currentPlaneAnchor = planeAnchor 67 | } else { 68 | displayAsOpen(for: hitTestResult, camera: camera) 69 | currentPlaneAnchor = nil 70 | } 71 | } 72 | } 73 | } 74 | 75 | /// Indicates whether the segments of the focus square are disconnected. 76 | public var isOpen = false 77 | 78 | /// Indicates if the square is currently being animated. 79 | private var isAnimating = false 80 | 81 | /// Indicates if the square is currently changing its alignment. 82 | private var isChangingAlignment = false 83 | 84 | /// The focus square's current alignment. 85 | private var currentAlignment: ARPlaneAnchor.Alignment? 86 | 87 | /// The current plane anchor if the focus square is on a plane. 88 | private(set) var currentPlaneAnchor: ARPlaneAnchor? 89 | 90 | /// The focus square's most recent positions. 91 | private var recentFocusSquarePositions: [float3] = [] 92 | 93 | /// The focus square's most recent alignments. 94 | private(set) var recentFocusSquareAlignments: [ARPlaneAnchor.Alignment] = [] 95 | 96 | /// Previously visited plane anchors. 97 | private var anchorsOfVisitedPlanes: Set = [] 98 | 99 | /// List of the segments in the focus square. 100 | private var segments: [FocusSquare.Segment] = [] 101 | 102 | /// The primary node that controls the position of other `FocusSquare` nodes. 103 | private let positioningNode = SCNNode() 104 | 105 | // MARK: - Initialization 106 | 107 | override init() { 108 | super.init() 109 | opacity = 0.0 110 | 111 | /* 112 | The focus square consists of eight segments as follows, which can be individually animated. 113 | 114 | s1 s2 115 | _ _ 116 | s3 | | s4 117 | 118 | s5 | | s6 119 | - - 120 | s7 s8 121 | */ 122 | let s1 = Segment(name: "s1", corner: .topLeft, alignment: .horizontal) 123 | let s2 = Segment(name: "s2", corner: .topRight, alignment: .horizontal) 124 | let s3 = Segment(name: "s3", corner: .topLeft, alignment: .vertical) 125 | let s4 = Segment(name: "s4", corner: .topRight, alignment: .vertical) 126 | let s5 = Segment(name: "s5", corner: .bottomLeft, alignment: .vertical) 127 | let s6 = Segment(name: "s6", corner: .bottomRight, alignment: .vertical) 128 | let s7 = Segment(name: "s7", corner: .bottomLeft, alignment: .horizontal) 129 | let s8 = Segment(name: "s8", corner: .bottomRight, alignment: .horizontal) 130 | segments = [s1, s2, s3, s4, s5, s6, s7, s8] 131 | 132 | let sl: Float = 0.5 // segment length 133 | let c: Float = FocusSquare.thickness / 2 // correction to align lines perfectly 134 | s1.simdPosition += float3(-(sl / 2 - c), -(sl - c), 0) 135 | s2.simdPosition += float3(sl / 2 - c, -(sl - c), 0) 136 | s3.simdPosition += float3(-sl, -sl / 2, 0) 137 | s4.simdPosition += float3(sl, -sl / 2, 0) 138 | s5.simdPosition += float3(-sl, sl / 2, 0) 139 | s6.simdPosition += float3(sl, sl / 2, 0) 140 | s7.simdPosition += float3(-(sl / 2 - c), sl - c, 0) 141 | s8.simdPosition += float3(sl / 2 - c, sl - c, 0) 142 | 143 | positioningNode.eulerAngles.x = .pi / 2 // Horizontal 144 | positioningNode.simdScale = float3(FocusSquare.size * FocusSquare.scaleForClosedSquare) 145 | for segment in segments { 146 | positioningNode.addChildNode(segment) 147 | } 148 | positioningNode.addChildNode(fillPlane) 149 | 150 | // Always render focus square on top of other content. 151 | displayNodeHierarchyOnTop(true) 152 | 153 | addChildNode(positioningNode) 154 | 155 | // Start the focus square as a billboard. 156 | displayAsBillboard() 157 | } 158 | 159 | required init?(coder aDecoder: NSCoder) { 160 | fatalError("\(#function) has not been implemented") 161 | } 162 | 163 | // MARK: - Appearance 164 | 165 | /// Hides the focus square. 166 | func hide() { 167 | guard action(forKey: "hide") == nil else { return } 168 | 169 | displayNodeHierarchyOnTop(false) 170 | runAction(.fadeOut(duration: 0.5), forKey: "hide") 171 | } 172 | 173 | /// Unhides the focus square. 174 | func unhide() { 175 | guard action(forKey: "unhide") == nil else { return } 176 | 177 | displayNodeHierarchyOnTop(true) 178 | runAction(.fadeIn(duration: 0.5), forKey: "unhide") 179 | } 180 | 181 | /// Displays the focus square parallel to the camera plane. 182 | private func displayAsBillboard() { 183 | simdTransform = matrix_identity_float4x4 184 | eulerAngles.x = .pi / 2 185 | simdPosition = float3(0, 0, -0.8) 186 | unhide() 187 | performOpenAnimation() 188 | } 189 | 190 | /// Called when a surface has been detected. 191 | private func displayAsOpen(for hitTestResult: ARHitTestResult, camera: ARCamera?) { 192 | performOpenAnimation() 193 | let position = hitTestResult.worldTransform.translation 194 | recentFocusSquarePositions.append(position) 195 | updateTransform(for: position, hitTestResult: hitTestResult, camera: camera) 196 | } 197 | 198 | /// Called when a plane has been detected. 199 | private func displayAsClosed(for hitTestResult: ARHitTestResult, planeAnchor: ARPlaneAnchor, camera: ARCamera?) { 200 | performCloseAnimation(flash: !anchorsOfVisitedPlanes.contains(planeAnchor)) 201 | anchorsOfVisitedPlanes.insert(planeAnchor) 202 | let position = hitTestResult.worldTransform.translation 203 | recentFocusSquarePositions.append(position) 204 | updateTransform(for: position, hitTestResult: hitTestResult, camera: camera) 205 | } 206 | 207 | // MARK: Helper Methods 208 | 209 | /// Update the transform of the focus square to be aligned with the camera. 210 | private func updateTransform(for position: float3, hitTestResult: ARHitTestResult, camera: ARCamera?) { 211 | // Average using several most recent positions. 212 | recentFocusSquarePositions = Array(recentFocusSquarePositions.suffix(10)) 213 | 214 | // Move to average of recent positions to avoid jitter. 215 | let average = recentFocusSquarePositions.reduce(float3(0), { $0 + $1 }) / Float(recentFocusSquarePositions.count) 216 | self.simdPosition = average 217 | self.simdScale = float3(scaleBasedOnDistance(camera: camera)) 218 | 219 | // Correct y rotation of camera square. 220 | guard let camera = camera else { return } 221 | let tilt = abs(camera.eulerAngles.x) 222 | let threshold1: Float = .pi / 2 * 0.65 223 | let threshold2: Float = .pi / 2 * 0.75 224 | let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x) 225 | var angle: Float = 0 226 | 227 | switch tilt { 228 | case 0.. 15 || 279 | alignment == .vertical && verticalHistory > 10 || 280 | hitTestResult.anchor is ARPlaneAnchor { 281 | if alignment != currentAlignment { 282 | shouldAnimateAlignmentChange = true 283 | currentAlignment = alignment 284 | recentFocusSquareAlignments.removeAll() 285 | } 286 | } else { 287 | // Alignment is different than most of the history - ignore it 288 | alignment = currentAlignment 289 | return 290 | } 291 | 292 | if alignment == .vertical { 293 | tempNode.simdOrientation = hitTestResult.worldTransform.orientation 294 | shouldAnimateAlignmentChange = true 295 | } 296 | 297 | // Change the focus square's alignment 298 | if shouldAnimateAlignmentChange { 299 | performAlignmentAnimation(to: tempNode.simdOrientation) 300 | } else { 301 | simdOrientation = tempNode.simdOrientation 302 | } 303 | } 304 | 305 | private func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float { 306 | // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal 307 | var normalized = angle 308 | while abs(normalized - ref) > .pi / 4 { 309 | if angle > ref { 310 | normalized -= .pi / 2 311 | } else { 312 | normalized += .pi / 2 313 | } 314 | } 315 | return normalized 316 | } 317 | 318 | /** 319 | Reduce visual size change with distance by scaling up when close and down when far away. 320 | 321 | These adjustments result in a scale of 1.0x for a distance of 0.7 m or less 322 | (estimated distance when looking at a table), and a scale of 1.2x 323 | for a distance 1.5 m distance (estimated distance when looking at the floor). 324 | */ 325 | private func scaleBasedOnDistance(camera: ARCamera?) -> Float { 326 | guard let camera = camera else { return 1.0 } 327 | 328 | let distanceFromCamera = simd_length(simdWorldPosition - camera.transform.translation) 329 | if distanceFromCamera < 0.7 { 330 | return distanceFromCamera / 0.7 331 | } else { 332 | return 0.25 * distanceFromCamera + 0.825 333 | } 334 | } 335 | 336 | // MARK: Animations 337 | 338 | private func performOpenAnimation() { 339 | guard !isOpen, !isAnimating else { return } 340 | isOpen = true 341 | isAnimating = true 342 | 343 | // Open animation 344 | SCNTransaction.begin() 345 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 346 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4 347 | positioningNode.opacity = 1.0 348 | for segment in segments { 349 | segment.open() 350 | } 351 | SCNTransaction.completionBlock = { 352 | self.positioningNode.runAction(pulseAction(), forKey: "pulse") 353 | // This is a safe operation because `SCNTransaction`'s completion block is called back on the main thread. 354 | self.isAnimating = false 355 | } 356 | SCNTransaction.commit() 357 | 358 | // Add a scale/bounce animation. 359 | SCNTransaction.begin() 360 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 361 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4 362 | positioningNode.simdScale = float3(FocusSquare.size) 363 | SCNTransaction.commit() 364 | } 365 | 366 | private func performCloseAnimation(flash: Bool = false) { 367 | guard isOpen, !isAnimating else { return } 368 | isOpen = false 369 | isAnimating = true 370 | 371 | positioningNode.removeAction(forKey: "pulse") 372 | positioningNode.opacity = 1.0 373 | 374 | // Close animation 375 | SCNTransaction.begin() 376 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 377 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 2 378 | positioningNode.opacity = 0.99 379 | SCNTransaction.completionBlock = { 380 | SCNTransaction.begin() 381 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 382 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4 383 | for segment in self.segments { 384 | segment.close() 385 | } 386 | SCNTransaction.completionBlock = { self.isAnimating = false } 387 | SCNTransaction.commit() 388 | } 389 | SCNTransaction.commit() 390 | 391 | // Scale/bounce animation 392 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.x"), forKey: "transform.scale.x") 393 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.y"), forKey: "transform.scale.y") 394 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.z"), forKey: "transform.scale.z") 395 | 396 | if flash { 397 | let waitAction = SCNAction.wait(duration: FocusSquare.animationDuration * 0.75) 398 | let fadeInAction = SCNAction.fadeOpacity(to: 0.25, duration: FocusSquare.animationDuration * 0.125) 399 | let fadeOutAction = SCNAction.fadeOpacity(to: 0.0, duration: FocusSquare.animationDuration * 0.125) 400 | fillPlane.runAction(SCNAction.sequence([waitAction, fadeInAction, fadeOutAction])) 401 | 402 | let flashSquareAction = flashAnimation(duration: FocusSquare.animationDuration * 0.25) 403 | for segment in segments { 404 | segment.runAction(.sequence([waitAction, flashSquareAction])) 405 | } 406 | } 407 | } 408 | 409 | private func performAlignmentAnimation(to newOrientation: simd_quatf) { 410 | isChangingAlignment = true 411 | SCNTransaction.begin() 412 | SCNTransaction.completionBlock = { 413 | self.isChangingAlignment = false 414 | } 415 | SCNTransaction.animationDuration = 0.5 416 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 417 | simdOrientation = newOrientation 418 | SCNTransaction.commit() 419 | } 420 | 421 | // MARK: Convenience Methods 422 | 423 | private func scaleAnimation(for keyPath: String) -> CAKeyframeAnimation { 424 | let scaleAnimation = CAKeyframeAnimation(keyPath: keyPath) 425 | 426 | let easeOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 427 | let easeInOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 428 | let linear = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 429 | 430 | let size = FocusSquare.size 431 | let ts = FocusSquare.size * FocusSquare.scaleForClosedSquare 432 | let values = [size, size * 1.15, size * 1.15, ts * 0.97, ts] 433 | let keyTimes: [NSNumber] = [0.00, 0.25, 0.50, 0.75, 1.00] 434 | let timingFunctions = [easeOut, linear, easeOut, easeInOut] 435 | 436 | scaleAnimation.values = values 437 | scaleAnimation.keyTimes = keyTimes 438 | scaleAnimation.timingFunctions = timingFunctions 439 | scaleAnimation.duration = FocusSquare.animationDuration 440 | 441 | return scaleAnimation 442 | } 443 | 444 | /// Sets the rendering order of the `positioningNode` to show on top or under other scene content. 445 | func displayNodeHierarchyOnTop(_ isOnTop: Bool) { 446 | // Recursivley traverses the node's children to update the rendering order depending on the `isOnTop` parameter. 447 | func updateRenderOrder(for node: SCNNode) { 448 | node.renderingOrder = isOnTop ? 2 : 0 449 | 450 | for material in node.geometry?.materials ?? [] { 451 | material.readsFromDepthBuffer = !isOnTop 452 | } 453 | 454 | for child in node.childNodes { 455 | updateRenderOrder(for: child) 456 | } 457 | } 458 | 459 | updateRenderOrder(for: positioningNode) 460 | } 461 | 462 | private lazy var fillPlane: SCNNode = { 463 | let correctionFactor = FocusSquare.thickness / 2 // correction to align lines perfectly 464 | let length = CGFloat(1.0 - FocusSquare.thickness * 2 + correctionFactor) 465 | 466 | let plane = SCNPlane(width: length, height: length) 467 | let node = SCNNode(geometry: plane) 468 | node.name = "fillPlane" 469 | node.opacity = 0.0 470 | 471 | let material = plane.firstMaterial! 472 | material.diffuse.contents = FocusSquare.fillColor 473 | material.isDoubleSided = true 474 | material.ambient.contents = UIColor.black 475 | material.lightingModel = .constant 476 | material.emission.contents = FocusSquare.fillColor 477 | 478 | return node 479 | }() 480 | } 481 | 482 | // MARK: - Animations and Actions 483 | 484 | private func pulseAction() -> SCNAction { 485 | let pulseOutAction = SCNAction.fadeOpacity(to: 0.4, duration: 0.5) 486 | let pulseInAction = SCNAction.fadeOpacity(to: 1.0, duration: 0.5) 487 | pulseOutAction.timingMode = .easeInEaseOut 488 | pulseInAction.timingMode = .easeInEaseOut 489 | 490 | return SCNAction.repeatForever(SCNAction.sequence([pulseOutAction, pulseInAction])) 491 | } 492 | 493 | private func flashAnimation(duration: TimeInterval) -> SCNAction { 494 | let action = SCNAction.customAction(duration: duration) { (node, elapsedTime) -> Void in 495 | // animate color from HSB 48/100/100 to 48/30/100 and back 496 | let elapsedTimePercentage = elapsedTime / CGFloat(duration) 497 | let saturation = 2.8 * (elapsedTimePercentage - 0.5) * (elapsedTimePercentage - 0.5) + 0.3 498 | if let material = node.geometry?.firstMaterial { 499 | material.diffuse.contents = UIColor(hue: 0.1333, saturation: saturation, brightness: 1.0, alpha: 1.0) 500 | } 501 | } 502 | return action 503 | } 504 | 505 | -------------------------------------------------------------------------------- /CloudCube/CloudCube/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 51 | 70 | 88 | 106 | 124 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 162 | 184 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | When A Horizontal Plane Has Been Detected, The Foucs Square Will Close, And You Can Place Your Cube By Tapping On The Screen. 254 | 255 | To Share Your World Map & Cube With Your Friends, Press This Button: 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | You Can Rotate The Cube Using A PanGesture, & Scale It By Using A Pinch Gesture. 269 | 270 | To Change The Colour Of The Cube, Double Tap On Any Of It's Faces & Then Select A Colour From The Buttons At The Bottom! 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | --------------------------------------------------------------------------------