├── icon@2x.png ├── ARRecorder ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ ├── icon-1024.png │ │ ├── icon-120.png │ │ ├── icon-121.png │ │ ├── icon-152.png │ │ ├── icon-167.png │ │ ├── icon-180.png │ │ ├── icon-20.png │ │ ├── icon-29.png │ │ ├── icon-40.png │ │ ├── icon-41.png │ │ ├── icon-42.png │ │ ├── icon-58.png │ │ ├── icon-59.png │ │ ├── icon-60.png │ │ ├── icon-76.png │ │ ├── icon-80.png │ │ ├── icon-81.png │ │ ├── icon-87.png │ │ └── Contents.json ├── ARRecorder-Bridging-Header.h ├── AppDelegate.swift ├── ARReplaySensorPublic.h ├── ARReplaySensor.h ├── ReplayStorage.swift ├── ARReplaySensorDelegate.h ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ReplaySelectionViewController.swift ├── ARRecordingTechnique.h ├── ARReplaySensorProtocol.h └── MainViewController.swift ├── .gitignore ├── LICENSE ├── README.md └── ARRecorder.xcodeproj └── project.pbxproj /icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/icon@2x.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ARRecorder/ARRecorder-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "ARRecordingTechnique.h" 2 | #import "ARReplaySensor.h" 3 | #import "ARReplaySensorPublic.h" 4 | -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-120.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-121.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-152.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-167.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-180.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-41.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-42.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-58.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-59.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-60.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-80.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-81.png -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ittybittyapps/ARRecorder/HEAD/ARRecorder/Assets.xcassets/AppIcon.appiconset/icon-87.png -------------------------------------------------------------------------------- /ARRecorder/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Itty Bitty Apps Pty Ltd. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | var window: UIWindow? 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ARRecorder/ARReplaySensorPublic.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARKit 3 | // Copyright © 2016-2019 Apple Inc. All rights reserved. 4 | // 5 | 6 | #import 7 | #import "ARReplaySensorProtocol.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | API_AVAILABLE(ios(13.0)) 12 | @interface ARReplaySensorPublic : NSObject 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /ARRecorder/ARReplaySensor.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARKit 3 | // Copyright © 2016-2019 Apple Inc. All rights reserved. 4 | // 5 | 6 | #import 7 | #import "ARReplaySensorProtocol.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | API_DEPRECATED("Use ARReplaySensorPublic instead.", ios(11.0, 13.0)) 12 | @interface ARReplaySensor : NSObject 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Dirs 2 | build/ 3 | DerivedData/ 4 | 5 | # Xcode 6 | xcuserdata 7 | *.xccheckout 8 | profile 9 | *.moved-aside 10 | *.hmap 11 | *.pbxuser 12 | !default.pbxuser 13 | *.perspective 14 | *.perspectivev3 15 | !default.perspectivev3 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.xcuserdatad 21 | *.xcscmblueprint 22 | *.trace 23 | *project.xcworkspace/ 24 | *~.nib 25 | *.xccheckout 26 | 27 | # OSX 28 | .DS_Store 29 | 30 | # Git 31 | *.orig 32 | *.BACKUP.* 33 | *.REMOTE.* 34 | *.LOCAL.* 35 | *.BASE.* 36 | *.bak 37 | -------------------------------------------------------------------------------- /ARRecorder/ReplayStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Itty Bitty Apps Pty Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | struct ReplayStorage { 8 | 9 | static var location: URL { 10 | return try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 11 | } 12 | 13 | static func findReplayOverlays() throws -> [URL] { 14 | return try FileManager.default 15 | .contentsOfDirectory(at: self.location, includingPropertiesForKeys: nil) 16 | .filter { $0.pathExtension.caseInsensitiveCompare(self.replayFileExtension) == .orderedSame } 17 | .sorted { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending } 18 | } 19 | 20 | static func makeNewReplayURL() -> URL { 21 | let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium) 22 | return self.location.appendingPathComponent(timestamp).appendingPathExtension(self.replayFileExtension) 23 | } 24 | 25 | static func deleteReplay(at url: URL) throws { 26 | try FileManager.default.removeItem(at: url) 27 | } 28 | 29 | private static let replayFileExtension = "mov" 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Itty Bitty Apps 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /ARRecorder/ARReplaySensorDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARKit 3 | // Copyright © 2016-2019 Apple Inc. All rights reserved. 4 | // 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @protocol ARReplaySensorDelegate 11 | @optional 12 | 13 | /** 14 | Notifies the replay sensor delegate that it finishes loading its metadata and will start the replay shortly. 15 | @param framesCount Number of frames contained in the replay. 16 | @note This method is called on an arbitrary thread. 17 | @deprecated This method is not called by ARReplaySensorPublic and is deprecated. Use `replaySensorDidFinishLoadingWithStartTimestamp:endTimestamp:` on iOS 13 and later instead. 18 | */ 19 | - (void)replaySensorDidFinishLoadingFrames:(NSUInteger)framesCount API_DEPRECATED("This method is only called by ARReplaySensor, not ARReplaySensorPublic.", ios(11.0, 13.0)); 20 | 21 | /// Notifies the replay sensor delegate that it finishes loading its metadata and will start the replay shortly. 22 | /// @param startTimestamp Timestamp corresponding to the system uptime at the start point of the sensor data replay. 23 | /// @param endTimestamp Timestamp corresponding to the system uptime at the end point of the sensor data replay. 24 | - (void)replaySensorDidFinishLoadingWithStartTimestamp:(NSTimeInterval)startTimestamp endTimestamp:(NSTimeInterval)endTimestamp API_AVAILABLE(ios(13.0)); 25 | 26 | /** 27 | Notifies the replay sensor delegate that the replay has been completed. 28 | @note This method is called on an arbitrary thread. 29 | */ 30 | - (void)replaySensorDidFinishReplayingData; 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /ARRecorder/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ARRecorder/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 | LSSupportsOpeningDocumentsInPlace 24 | 25 | NSCameraUsageDescription 26 | This application will use the camera for Augmented Reality. 27 | NSPhotoLibraryUsageDescription 28 | This will allow ARKit to save a sensor replay to the Photo Library. 29 | UIFileSharingEnabled 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UIRequiredDeviceCapabilities 36 | 37 | armv7 38 | arkit 39 | 40 | UIStatusBarHidden 41 | 42 | UIStatusBarStyle 43 | UIStatusBarStyleLightContent 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | UIInterfaceOrientationPortraitUpsideDown 50 | 51 | UISupportedInterfaceOrientations~ipad 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationPortraitUpsideDown 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ARRecorder/ReplaySelectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Itty Bitty Apps Pty Ltd. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | protocol ReplaySelectionViewControllerDelegate: class { 8 | 9 | func replaySelectionViewController(_ viewController: ReplaySelectionViewController, didFinishWithReplayURL replayURL: URL?) 10 | 11 | } 12 | 13 | final class ReplaySelectionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIPopoverPresentationControllerDelegate { 14 | 15 | @IBOutlet var tableView: UITableView! 16 | 17 | weak var delegate: ReplaySelectionViewControllerDelegate? 18 | 19 | private var fileURLs = try! ReplayStorage.findReplayOverlays() 20 | 21 | override func viewWillAppear(_ animated: Bool) { 22 | super.viewWillAppear(animated) 23 | 24 | self.popoverPresentationController?.delegate = self 25 | } 26 | 27 | @IBAction func dismiss(_ sender: Any) { 28 | self.delegate?.replaySelectionViewController(self, didFinishWithReplayURL: nil) 29 | self.presentingViewController?.dismiss(animated: true) 30 | } 31 | 32 | // MARK: - UITableViewDataSource 33 | 34 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return self.fileURLs.count 36 | } 37 | 38 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: "FileCell", for: indexPath) 40 | cell.textLabel!.text = self.fileURLs[indexPath.row].lastPathComponent 41 | return cell 42 | } 43 | 44 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 45 | let url = self.fileURLs.remove(at: indexPath.row) 46 | try! ReplayStorage.deleteReplay(at: url) 47 | tableView.deleteRows(at: [indexPath], with: .automatic) 48 | } 49 | 50 | // MARK: - UITableViewDelegate 51 | 52 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 53 | let url = self.fileURLs[indexPath.row] 54 | self.delegate?.replaySelectionViewController(self, didFinishWithReplayURL: url) 55 | self.presentingViewController?.dismiss(animated: true) 56 | } 57 | 58 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 59 | return .delete 60 | } 61 | 62 | // MARK: - UIPopoverPresentationControllerDelegate 63 | 64 | func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool { 65 | self.delegate?.replaySelectionViewController(self, didFinishWithReplayURL: nil) 66 | return true 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /ARRecorder/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon-40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon-60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon-58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon-87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon-80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon-120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-121.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon-180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "icon-20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon-41.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon-29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon-59.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon-42.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon-81.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon-76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon-152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon-167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "icon-1024.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ This project is not actively maintained and due to use of private SPI may not work with newer releases of iOS. ⚠️ 2 | 3 | ---- 4 | 5 | # AR Recorder icon AR Recorder 6 | 7 | This project demonstrates how ARKit's private SPI can be used to record and replay AR sessions, thus enabling a convenient development workflow and test automation. 8 | 9 | > **Disclaimer:** Functionality exposed and used in this project is private SPI. It's not guaranteed to be reliable or stay available in any form in future versions of ARKit. It definitely cannot be used in production versions of apps distributed on the App Store. 10 | 11 | ## How to build 12 | 13 | Clone the repository and open `ARRecorder.xcodeproj` in Xcode 10 or newer. Configure automatic codesigning by opening project settings, _ARRecorder_ target, _General_, and configuring the _Team_ setting in the _Signing_ section. 14 | 15 | ## How to use 16 | 17 | The app starts a normal AR session once launched. Tap **Record** to start recording the session to a local file. Then tap **Finish** to complete the recording and continue a normal session. To replay a previously recorded session, tap **Replay**, then select the file. To stop the replay at any time, tap **╳**. 18 | 19 | During both normal, recording and replay sessions, tap anywhere to place a virtual cube in the scene at the estimated physical location that corresponds to your touch. Note that this won't be recorded into the replay file: you can interact with the session differently during recording and replay. 20 | 21 | To delete a recorded file, tap **Replay** and swipe left on a file row, then tap **Delete**. You can also access all session recordings using the Files app by selecting _On My iPhone/iPad_ location, where ARRecorder's documents container will show up. The app is also configured to allow File Sharing via iTunes. 22 | 23 | ## SPI declaration 24 | 25 | Relevant SPI classes and methods are annotated across a few headers like [ARRecordingTechnique.h](ARRecorder/ARRecordingTechnique.h) and [ARReplaySensorProtocol.h](ARRecorder/ARReplaySensorProtocol.h) (please see `ARKit Private API` group in Xcode project for the full list). Their signature and presumed function have been observed as of ARKit 3.0. 26 | 27 | Note that depending on the iOS version, either `ARReplaySensor` or `ARReplaySensorPublic` class is used to load replays. See `ARConfiguration.makeReplayConfiguration(replayURL:)` method in [MainViewController.swift](ARRecorder/MainViewController.swift) for an example of how that can be done. 28 | 29 | ## Supported devices 30 | 31 | All iOS 11.3+ devices with A9 chip or newer are supported. This includes: 32 | 33 | - iPhone SE 34 | - iPhone 6S, 6S Plus or newer 35 | - iPad (2017, 5th generation) or newer 36 | 37 | The project can be modified to support a wider range of hardware by replacing session's world tracking configuration with an orientation tracking configuration. 38 | 39 | ## Licensing 40 | 41 | This work is licensed under a BSD 3-Clause License. 42 | -------------------------------------------------------------------------------- /ARRecorder/ARRecordingTechnique.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARKit 3 | // Copyright © 2016-2019 Apple Inc. All rights reserved. 4 | // 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /// Value: `com.apple.arkit.session.record.filepath` 11 | extern NSString * const ARSessionRecordingFilePathDefaultsKey; 12 | 13 | @class ARRecordingTechnique; 14 | 15 | API_AVAILABLE(ios(11.3)) 16 | @protocol ARRecordingTechniqueDelegate 17 | 18 | /** 19 | Notifies the recording technique delegate that recording was finished, passing the error if it has occured while finishing recording. 20 | */ 21 | - (void)technique:(ARRecordingTechnique *)technique didFinishWithResult:(nullable NSError *)result; 22 | 23 | @end 24 | 25 | @interface ARTechnique: NSObject 26 | @end 27 | 28 | /** 29 | Captures sensor data of the session it's executed in to a file on disk, creating a "replay" of it. 30 | */ 31 | @interface ARRecordingTechnique: ARTechnique 32 | 33 | /** 34 | Recording destination file URL this technique was configured with. 35 | */ 36 | @property (nonatomic, strong, readonly) NSURL *outputFileURL; 37 | /** 38 | If set to YES, the receiver will export the recorded replay to the Photos Library when recording finishes. 39 | @note Authorization to access Photo Library will be requested automatically, but Info.plist of the application must be configured with the Photo Library Usage Description if this property is set to YES. 40 | */ 41 | @property (nonatomic, assign) BOOL shouldSaveVideoInPhotosLibrary; 42 | 43 | /** 44 | Delegate of the receiver, notified when recording finishes. 45 | */ 46 | @property (nonatomic, weak) id recordingTechniqueDelegate API_AVAILABLE(ios(11.3)); 47 | 48 | /** 49 | Asynchronously finishes processing and recording the replay, and exports the resulting video file to the Photo Library if `shouldSaveVideoInPhotosLibrary` is set. 50 | */ 51 | - (void)finishRecording; 52 | 53 | @end 54 | 55 | #pragma mark - Recording Configuration 56 | 57 | @interface ARConfiguration () 58 | 59 | /** 60 | Returns an `ARConfiguration` object that can be used to run an `ARSession` that will record a "replay" of its sensor data (including the video feed) to a file on disk. An `ARRecordingTechnique` object is returned by reference to allow additional customization. 61 | 62 | @param templateConfiguration Configuration object used to define which sensor data will be recorded in the replay. 63 | @param recordingTechnique Upon return, contains a `ARRecordingTechnique` object created to perform the recording as part of the configuration. 64 | @param fileURL URL to save the replay file to. If nil, the configuration will use the value of `ARSessionRecordingFilePathDefaultsKey` user default as the destination, or if that's not specified either, a temporary file. 65 | @return Configuration object which will record a replay of the session it's run on. 66 | */ 67 | + (ARConfiguration *)recordingConfigurationWithConfiguration:(ARConfiguration *)templateConfiguration recordingTechnique:(ARRecordingTechnique * _Nullable * _Nonnull)recordingTechnique fileURL:(nullable NSURL *)fileURL; 68 | /** 69 | Calls `+recordingConfigurationWithConfiguration:recordingTechnique:fileURL:` passing nil for `fileURL` parameter. 70 | */ 71 | + (ARConfiguration *)recordingConfigurationWithConfiguration:(ARConfiguration *)templateConfiguration recordingTechnique:(ARRecordingTechnique * _Nullable * _Nonnull)recordingTechnique; 72 | 73 | @end 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /ARRecorder/ARReplaySensorProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARKit 3 | // Copyright © 2016-2019 Apple Inc. All rights reserved. 4 | // 5 | 6 | #import 7 | #import "ARReplaySensorDelegate.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | Reads a "replay" of previously captured sensor data and intermediate processing results from a file on disk, and poses as a sensor that produces this data in a session. 13 | @note This is a partial interface of this protocol: more functionality is available internally. The protocol itself was introduced in iOS 13, however most of its functionality was available prior to that in ARReplaySensor class. 14 | */ 15 | @protocol ARReplaySensorProtocol 16 | 17 | /**@property (nonatomic,readonly) BOOL finishedReplaying; 18 | Returns whether the sensor was initialized in manual replay mode. 19 | */ 20 | @property (nonatomic, assign, readonly, getter=isReplayingManually) BOOL replayingManually API_AVAILABLE(ios(11.3)); 21 | 22 | /** 23 | Returns whether the sensor was initialized in synchronous (deterministic) replay mode. 24 | */ 25 | @property (nonatomic, assign, readonly, getter=isSynchronousMode) BOOL synchronousMode API_AVAILABLE(ios(12.0)); 26 | 27 | /** 28 | Defines the speed at which manual sensor replay advances to the current target frame (after a call to `advanceToFrameIndex:`) as a multiplier of normal speed. If set to 1 or a non-positive value, replay advances at normal speed. Default value is taken from `ARReplaySensorFilePathAdvanceFramesPerSecondMultiplierUserDefaultsKey` if it's specified. Has no effect if the sensor is not configured for manual replay. 29 | */ 30 | @property (nonatomic, assign) float advanceFramesPerSecondMultiplier API_AVAILABLE(ios(11.3)); 31 | 32 | /** 33 | Returns whether the sensor currently simulates an interruption in data stream. 34 | */ 35 | @property (nonatomic, assign, readonly) BOOL interrupted API_AVAILABLE(ios(11.3)); 36 | 37 | /** 38 | Returns whether the replay is finished. 39 | */ 40 | @property (nonatomic, assign, readonly) BOOL finishedReplaying API_AVAILABLE(ios(13.0)); 41 | 42 | /** 43 | Model identifier of the device that the replay was recorded on. 44 | */ 45 | @property (nonatomic, strong, readonly, nullable) NSString *deviceModel; 46 | 47 | /** 48 | OS version identifier that the replay was recorded on. For replays recorded on versions prior to iOS 13, this returns nil. 49 | */ 50 | @property (nonatomic, strong, readonly, nullable) NSString *osVersion API_AVAILABLE(ios(13.0)); 51 | 52 | /** 53 | ARKit version identifier that the replay was recorded with. For replays recorded on versions prior to ARKit 3 (iOS 13), this returns nil. 54 | */ 55 | @property (nonatomic, strong, readonly, nullable) NSString *arkitVersion API_AVAILABLE(ios(13.0)); 56 | 57 | /** 58 | Delegate object notified of the changes in replay state. 59 | */ 60 | @property (nonatomic, weak) id replaySensorDelegate API_AVAILABLE(ios(11.0)); 61 | 62 | /** 63 | Initializes a replay sensor with recorded data at the specified location. 64 | @param filePath Path to the recorded replay data file. 65 | @discussion On iOS 11.3+ this initializer delegates to `initWithSequenceURL:manualReplay:` passing NO for `manualReplay` parameter. 66 | */ 67 | - (instancetype)initWithDataFromFile:(NSString *)filePath API_AVAILABLE(ios(11.0)); 68 | 69 | /** 70 | Initializes a replay sensor with recorded data at the specified location. 71 | @param sequenceURL URL of the recorded replay data file. 72 | @param manualReplay If YES, enables a "manual" mode where replay needs to be advanced by calling `advanceFrame` and/or `advanceToFrameIndex:` methods instead of it automatically playing to the end. 73 | @discussion On iOS 12.0+ this initializer delegates to `initWithSequenceURL:manualReplay:synchronousMode:` passing a default value for `synchronousMode` taken from `ARReplaySensorSynchronousModeUserDefaultsKey` if it's set, and NO otherwise. 74 | */ 75 | - (instancetype)initWithSequenceURL:(NSURL *)sequenceURL manualReplay:(BOOL)manualReplay API_AVAILABLE(ios(11.3)); 76 | 77 | /** 78 | Initializes a replay sensor with recorded data at the specified location. 79 | @param sequenceURL URL of the recorded replay data file. 80 | @param manualReplay If YES, enables a "manual" mode where replay needs to be advanced by calling `advanceFrame` and/or `advanceToFrameIndex:` methods instead of it automatically playing to the end. 81 | @param synchronousMode If YES, forces the session into a "deterministic" mode which (presumably) gathers session technique results at specific time intervals, which makes session behavior and output more predictable and repeatable. 82 | */ 83 | - (instancetype)initWithSequenceURL:(NSURL *)sequenceURL manualReplay:(BOOL)manualReplay synchronousMode:(BOOL)synchronousMode API_AVAILABLE(ios(12.0)); 84 | 85 | /** 86 | Advances the replay to the next frame. 87 | @discussion Has no effect if the sensor is not configured for manual replay. 88 | */ 89 | - (void)advanceFrame API_AVAILABLE(ios(11.3)); 90 | 91 | /** 92 | Automatically advances the replay to the frame with the specified index, then pauses. 93 | @discussion This method can be called immediately after the receiver is initialized. If `ARReplaySensorFilePathAdvanceToFrameUserDefaultsKey` is set, this is done automatically with the value of that user default. The speed at which the replay is advanced is controlled by `advanceFramesPerSecondMultiplier` property; by default, the replay is advanced at the speed it was recorded with. Has no effect if the sensor is not configured for manual replay. 94 | */ 95 | - (void)advanceToFrameIndex:(NSInteger)frameIndex API_AVAILABLE(ios(11.3)); 96 | 97 | /** 98 | Simulates an interruption in sensor data stream. Called automatically when application enters background. 99 | */ 100 | - (void)interrupt API_AVAILABLE(ios(11.3)); 101 | 102 | /** 103 | Resumes the sensor data stream after simulating its interruption. Called automatically when application enters foreground. 104 | */ 105 | - (void)endInterruption API_AVAILABLE(ios(11.3)); 106 | 107 | /** 108 | Stops the replay. 109 | */ 110 | - (void)stop; 111 | 112 | @end 113 | 114 | #pragma mark - Replay Configuration 115 | 116 | @interface ARConfiguration () 117 | 118 | /** 119 | Returns an `ARConfiguration` object that can be used to run an `ARSession` that will "replay" previously recorded sensor data (including the video feed) instead of using real hardware. 120 | 121 | @param templateConfiguration Configuration object used to define which sensor data from the replay will be used. Data required by this configuration must be a subset of the data required and provided by the template configuration that the replay was originally recorded with. Ideally, these configurations should be identical. 122 | @param replaySensor `ARReplaySensor` instance providing the sensor data to replay. 123 | @param resultClasses Allows customizing the technique result classes that will be replayed. This requires having access to these (private) classes, so pass nil for this parameter to get default behavior. 124 | @return Configuration object which will replay sensor data in the session it's run on. 125 | */ 126 | + (ARConfiguration *)replayConfigurationWithConfiguration:(ARConfiguration *)templateConfiguration replaySensor:(id)replaySensor replayingResultDataClasses:(nullable NSSet *)resultClasses API_AVAILABLE(ios(11.3)); 127 | 128 | @end 129 | 130 | NS_ASSUME_NONNULL_END 131 | -------------------------------------------------------------------------------- /ARRecorder/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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /ARRecorder/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Itty Bitty Apps Pty Ltd. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import SceneKit 7 | import ARKit 8 | 9 | final class MainViewController: UIViewController, ARSCNViewDelegate, ARReplaySensorDelegate, ReplaySelectionViewControllerDelegate { 10 | 11 | @IBOutlet var sceneView: ARSCNView! 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | self.sceneView.delegate = self 17 | self.sceneView.showsStatistics = true 18 | self.sceneView.debugOptions = [ ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints ] 19 | 20 | // Start in Idle state 21 | self.transition(to: .idle) 22 | } 23 | 24 | override func viewDidAppear(_ animated: Bool) { 25 | super.viewDidAppear(animated) 26 | 27 | if self.state == .idle { 28 | // Start the normal session once the view initially appears 29 | self.transition(to: .normal) 30 | self.sceneView.session.run(.makeBaseConfiguration()) 31 | } 32 | } 33 | 34 | // MARK: - State 35 | 36 | fileprivate enum State: Equatable { 37 | case idle 38 | case normal 39 | case recording(ARRecordingTechnique) 40 | case loadingReplay 41 | case replaying 42 | case replayFinished 43 | } 44 | 45 | private var state = State.idle 46 | 47 | private func transition(to state: State) { 48 | switch self.state { 49 | case .recording(let technique): 50 | // Finish recording a replay when transitioning from Recording state 51 | technique.finishRecording() 52 | default: 53 | break 54 | } 55 | 56 | self.state = state 57 | self.title = state.navigationTitle 58 | 59 | // Update bar buttons 60 | let (left, right) = self.makeBarButtonItems(for: state) 61 | self.navigationItem.leftBarButtonItems = left 62 | self.navigationItem.rightBarButtonItems = right 63 | } 64 | 65 | private func makeBarButtonItems(for state: State) -> (left: [UIBarButtonItem], right: [UIBarButtonItem]) { 66 | switch state { 67 | case .idle, .loadingReplay: 68 | return ([], []) 69 | case .normal: 70 | return ([ self.makeStartRecordingButton() ], [ self.makeStartReplayButton() ]) 71 | case .recording: 72 | return ([ self.makeStopRecordingButton() ], []) 73 | case .replaying, .replayFinished: 74 | return ([ self.makeStopReplayButton() ], []) 75 | } 76 | } 77 | 78 | // MARK: - Actions 79 | 80 | @objc private func startRecording(_ sender: Any) { 81 | // Transition to Recording state and start a new session with a recording configuration 82 | let (configuration, technique) = ARConfiguration.makeRecordingConfiguration() 83 | self.transition(to: .recording(technique)) 84 | self.sceneView.session.run(configuration, options: [ .resetTracking, .removeExistingAnchors ]) 85 | } 86 | 87 | private func makeStartRecordingButton() -> UIBarButtonItem { 88 | return UIBarButtonItem(title: NSLocalizedString("Record", comment: "Bar button title"), style: .plain, target: self, action: #selector(startRecording(_:))) 89 | } 90 | 91 | @objc private func stopRecording(_ sender: Any) { 92 | // Transition to Normal state to stop recording, and resume the session with a normal configuration without resetting tracking/anchors 93 | self.transition(to: .normal) 94 | self.sceneView.session.run(.makeBaseConfiguration()) 95 | } 96 | 97 | private func makeStopRecordingButton() -> UIBarButtonItem { 98 | return UIBarButtonItem(title: NSLocalizedString("Finish", comment: "Bar button title"), style: .plain, target: self, action: #selector(stopRecording(_:))) 99 | } 100 | 101 | @objc private func startReplay(_ sender: UIBarButtonItem!) { 102 | // Instantiate and present the replay selector; selected replay will be started in the delegate callback 103 | let navigationController = self.storyboard!.instantiateViewController(withIdentifier: "ReplaySelectionScene") as! UINavigationController 104 | navigationController.modalPresentationStyle = .popover 105 | navigationController.modalTransitionStyle = .crossDissolve 106 | navigationController.popoverPresentationController?.barButtonItem = sender 107 | 108 | let replaySelectionViewController = navigationController.viewControllers.first as! ReplaySelectionViewController 109 | replaySelectionViewController.delegate = self 110 | 111 | self.present(navigationController, animated: true) 112 | } 113 | 114 | private func makeStartReplayButton() -> UIBarButtonItem { 115 | return UIBarButtonItem(title: NSLocalizedString("Replay", comment: "Bar button title"), style: .plain, target: self, action: #selector(startReplay(_:))) 116 | } 117 | 118 | @objc private func stopReplay(_ sender: Any) { 119 | // Transition back to Normal state to stop the playback, and reset the session with a normal configuration 120 | self.transition(to: .normal) 121 | self.sceneView.session.run(.makeBaseConfiguration(), options: [ .resetTracking, .removeExistingAnchors ]) 122 | } 123 | 124 | private func makeStopReplayButton() -> UIBarButtonItem { 125 | return UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(stopReplay(_:))) 126 | } 127 | 128 | // MARK: - Custom anchor placement 129 | 130 | private var exampleAnchor: ARAnchor? 131 | 132 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 133 | super.touchesEnded(touches, with: event) 134 | 135 | // If an anchor already exists, remove it 136 | if let anchor = self.exampleAnchor { 137 | self.sceneView.session.remove(anchor: anchor) 138 | self.exampleAnchor = nil 139 | return 140 | } 141 | 142 | // Otherwise, determine a point on an existing or estimated plane via hit-testing the touch location 143 | guard let point = touches.first?.location(in: self.view) else { 144 | return 145 | } 146 | guard let hitResult = self.sceneView.hitTest(point, types: [ .estimatedHorizontalPlane, .existingPlaneUsingExtent ]).first else { 147 | return 148 | } 149 | 150 | // Add an anchor at hit-tested point 151 | let anchor = ARAnchor(transform: hitResult.worldTransform) 152 | self.exampleAnchor = anchor 153 | self.sceneView.session.add(anchor: anchor) 154 | } 155 | 156 | // MARK: - ARSCNViewDelegate 157 | 158 | func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { 159 | if anchor == self.exampleAnchor { 160 | // Represent a custom anchor with a red box 161 | let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0) 162 | box.firstMaterial?.diffuse.contents = UIColor.red 163 | node.addChildNode(SCNNode(geometry: box)) 164 | } 165 | } 166 | 167 | // MARK: - ARReplaySensorDelegate 168 | 169 | func replaySensorDidFinishLoadingFrames(_ framesCount: UInt) { 170 | // ARReplaySensor calls both this and the "modern" callback on iOS 13; there's no need to handle both 171 | if #available(iOS 13, *) { return } 172 | 173 | print("Replay sensor loaded \(framesCount) frames.") 174 | 175 | DispatchQueue.main.async { 176 | guard case .loadingReplay = self.state else { 177 | return 178 | } 179 | 180 | // Once replay sensor finishes loading, transition to the Replaying state 181 | self.transition(to: .replaying) 182 | } 183 | } 184 | 185 | func replaySensorDidFinishLoading(withStartTimestamp startTimestamp: TimeInterval, endTimestamp: TimeInterval) { 186 | print("Replay sensor loaded frames from \(String(format: "%.3f", startTimestamp))s to \(String(format: "%.3f", endTimestamp))s.") 187 | 188 | DispatchQueue.main.async { 189 | guard case .loadingReplay = self.state else { 190 | return 191 | } 192 | 193 | // Once replay sensor finishes loading, transition to the Replaying state 194 | self.transition(to: .replaying) 195 | } 196 | } 197 | 198 | func replaySensorDidFinishReplayingData() { 199 | print("Replay finished.") 200 | 201 | DispatchQueue.main.async { 202 | guard case .replaying = self.state else { 203 | return 204 | } 205 | 206 | // Once replay data is exhausted, transition to Replay Finished state and pause the session to stop consuming system resources 207 | self.transition(to: .replayFinished) 208 | self.sceneView.session.pause() 209 | } 210 | } 211 | 212 | // MARK: - ReplaySelectionViewControllerDelegate 213 | 214 | func replaySelectionViewController(_ viewController: ReplaySelectionViewController, didFinishWithReplayURL replayURL: URL?) { 215 | guard let url = replayURL else { 216 | return 217 | } 218 | 219 | let (configuration, sensor) = ARConfiguration.makeReplayConfiguration(replayURL: url) 220 | sensor.replaySensorDelegate = self 221 | 222 | // Transition to Loading Replay state and start a replay session; delegate callback from the replay sensor will inform when loading finishes 223 | self.sceneView.session.pause() 224 | self.transition(to: .loadingReplay) 225 | self.sceneView.session.run(configuration, options: [ .resetTracking, .removeExistingAnchors ]) 226 | } 227 | 228 | } 229 | 230 | // MARK: - Private extensions 231 | 232 | private extension ARConfiguration { 233 | 234 | static func makeBaseConfiguration() -> ARConfiguration { 235 | let configuration = ARWorldTrackingConfiguration() 236 | configuration.planeDetection = [ .horizontal, .vertical ] 237 | 238 | return configuration 239 | } 240 | 241 | static func makeRecordingConfiguration(replayURL: URL? = ReplayStorage.makeNewReplayURL()) -> (ARConfiguration, ARRecordingTechnique) { 242 | var recordingTechnique: ARRecordingTechnique? 243 | let configuration = self.recordingConfiguration(with: .makeBaseConfiguration(), recordingTechnique: &recordingTechnique, fileURL: replayURL) 244 | guard let technique = recordingTechnique else { 245 | preconditionFailure("Expecting recording technique to be returned!") 246 | } 247 | 248 | return (configuration, technique) 249 | } 250 | 251 | static func makeReplayConfiguration(replayURL: URL) -> (ARConfiguration, ARReplaySensorProtocol) { 252 | let replaySensor: ARReplaySensorProtocol 253 | if #available(iOS 13, *) { 254 | let modernReplaySensor = ARReplaySensorPublic(sequenceURL: replayURL, manualReplay: false) 255 | if modernReplaySensor.arkitVersion != nil { 256 | // This is a replay made on iOS 13 or later 257 | replaySensor = modernReplaySensor 258 | } else { 259 | // This is a replay made on iOS 12 or earlier – have to use the legacy API for it 260 | replaySensor = ARReplaySensor(sequenceURL: replayURL, manualReplay: false) 261 | } 262 | } else { 263 | replaySensor = ARReplaySensor(sequenceURL: replayURL, manualReplay: false) 264 | } 265 | 266 | let replayConfiguration = self.replayConfiguration(with: .makeBaseConfiguration(), replaySensor: replaySensor, replayingResultDataClasses: nil) 267 | return (replayConfiguration, replaySensor) 268 | } 269 | 270 | } 271 | 272 | private extension MainViewController.State { 273 | 274 | var navigationTitle: String? { 275 | switch self { 276 | case .idle: 277 | return NSLocalizedString("Session Idle", comment: "Session state title") 278 | case .normal: 279 | return nil 280 | case .recording: 281 | return NSLocalizedString("RECORDING", comment: "Session state title") 282 | case .loadingReplay: 283 | return NSLocalizedString("Loading…", comment: "Session state title") 284 | case .replaying: 285 | return NSLocalizedString("REPLAYING", comment: "Session state title") 286 | case .replayFinished: 287 | return NSLocalizedString("Replay Finished", comment: "Session state title") 288 | } 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /ARRecorder.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7D21844221548288002AFD28 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D21844121548288002AFD28 /* AppDelegate.swift */; }; 11 | 7D21844621548288002AFD28 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D21844521548288002AFD28 /* MainViewController.swift */; }; 12 | 7D21844921548288002AFD28 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D21844721548288002AFD28 /* Main.storyboard */; }; 13 | 7D21844B21548289002AFD28 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D21844A21548289002AFD28 /* Assets.xcassets */; }; 14 | 7D21844E21548289002AFD28 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D21844C21548289002AFD28 /* LaunchScreen.storyboard */; }; 15 | 7D2184572154855B002AFD28 /* ReplaySelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2184552154855B002AFD28 /* ReplaySelectionViewController.swift */; }; 16 | 7D2184582154855B002AFD28 /* ReplayStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2184562154855B002AFD28 /* ReplayStorage.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 7D21843E21548288002AFD28 /* ARRecorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ARRecorder.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7D21844121548288002AFD28 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 7D21844521548288002AFD28 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 23 | 7D21844821548288002AFD28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 7D21844A21548289002AFD28 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 7D21844D21548289002AFD28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 7D21844F21548289002AFD28 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 7D2184552154855B002AFD28 /* ReplaySelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaySelectionViewController.swift; sourceTree = ""; }; 28 | 7D2184562154855B002AFD28 /* ReplayStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplayStorage.swift; sourceTree = ""; }; 29 | 7D21845A21548663002AFD28 /* ARRecordingTechnique.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARRecordingTechnique.h; sourceTree = ""; }; 30 | 7D21845B21548663002AFD28 /* ARReplaySensor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARReplaySensor.h; sourceTree = ""; }; 31 | 7D21845C21548803002AFD28 /* ARRecorder-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ARRecorder-Bridging-Header.h"; sourceTree = ""; }; 32 | 7D25217B23921EAB008E98C0 /* ARReplaySensorDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARReplaySensorDelegate.h; sourceTree = ""; }; 33 | 7D25217C23922234008E98C0 /* ARReplaySensorProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARReplaySensorProtocol.h; sourceTree = ""; }; 34 | 7D25217D23922962008E98C0 /* ARReplaySensorPublic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARReplaySensorPublic.h; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 7D21843B21548288002AFD28 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 7D21843521548287002AFD28 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 7D21844021548288002AFD28 /* ARRecorder */, 52 | 7D21843F21548288002AFD28 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 7D21843F21548288002AFD28 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 7D21843E21548288002AFD28 /* ARRecorder.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 7D21844021548288002AFD28 /* ARRecorder */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 7D21844121548288002AFD28 /* AppDelegate.swift */, 68 | 7D21845921548655002AFD28 /* ARKit Private API */, 69 | 7D21845C21548803002AFD28 /* ARRecorder-Bridging-Header.h */, 70 | 7D21844A21548289002AFD28 /* Assets.xcassets */, 71 | 7D21844F21548289002AFD28 /* Info.plist */, 72 | 7D21844C21548289002AFD28 /* LaunchScreen.storyboard */, 73 | 7D21844721548288002AFD28 /* Main.storyboard */, 74 | 7D21844521548288002AFD28 /* MainViewController.swift */, 75 | 7D2184552154855B002AFD28 /* ReplaySelectionViewController.swift */, 76 | 7D2184562154855B002AFD28 /* ReplayStorage.swift */, 77 | ); 78 | path = ARRecorder; 79 | sourceTree = ""; 80 | }; 81 | 7D21845921548655002AFD28 /* ARKit Private API */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 7D21845A21548663002AFD28 /* ARRecordingTechnique.h */, 85 | 7D21845B21548663002AFD28 /* ARReplaySensor.h */, 86 | 7D25217B23921EAB008E98C0 /* ARReplaySensorDelegate.h */, 87 | 7D25217C23922234008E98C0 /* ARReplaySensorProtocol.h */, 88 | 7D25217D23922962008E98C0 /* ARReplaySensorPublic.h */, 89 | ); 90 | name = "ARKit Private API"; 91 | sourceTree = ""; 92 | }; 93 | /* End PBXGroup section */ 94 | 95 | /* Begin PBXNativeTarget section */ 96 | 7D21843D21548288002AFD28 /* ARRecorder */ = { 97 | isa = PBXNativeTarget; 98 | buildConfigurationList = 7D21845221548289002AFD28 /* Build configuration list for PBXNativeTarget "ARRecorder" */; 99 | buildPhases = ( 100 | 7D21843A21548288002AFD28 /* Sources */, 101 | 7D21843B21548288002AFD28 /* Frameworks */, 102 | 7D21843C21548288002AFD28 /* Resources */, 103 | ); 104 | buildRules = ( 105 | ); 106 | dependencies = ( 107 | ); 108 | name = ARRecorder; 109 | productName = ARRecorder; 110 | productReference = 7D21843E21548288002AFD28 /* ARRecorder.app */; 111 | productType = "com.apple.product-type.application"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | 7D21843621548287002AFD28 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | LastSwiftUpdateCheck = 1000; 120 | LastUpgradeCheck = 1000; 121 | ORGANIZATIONNAME = "Itty Bitty Apps Pty Ltd"; 122 | TargetAttributes = { 123 | 7D21843D21548288002AFD28 = { 124 | CreatedOnToolsVersion = 10.0; 125 | LastSwiftMigration = 1000; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 7D21843921548287002AFD28 /* Build configuration list for PBXProject "ARRecorder" */; 130 | compatibilityVersion = "Xcode 9.3"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 7D21843521548287002AFD28; 138 | productRefGroup = 7D21843F21548288002AFD28 /* Products */; 139 | projectDirPath = ""; 140 | projectRoot = ""; 141 | targets = ( 142 | 7D21843D21548288002AFD28 /* ARRecorder */, 143 | ); 144 | }; 145 | /* End PBXProject section */ 146 | 147 | /* Begin PBXResourcesBuildPhase section */ 148 | 7D21843C21548288002AFD28 /* Resources */ = { 149 | isa = PBXResourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 7D21844E21548289002AFD28 /* LaunchScreen.storyboard in Resources */, 153 | 7D21844B21548289002AFD28 /* Assets.xcassets in Resources */, 154 | 7D21844921548288002AFD28 /* Main.storyboard in Resources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXResourcesBuildPhase section */ 159 | 160 | /* Begin PBXSourcesBuildPhase section */ 161 | 7D21843A21548288002AFD28 /* Sources */ = { 162 | isa = PBXSourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 7D2184582154855B002AFD28 /* ReplayStorage.swift in Sources */, 166 | 7D21844621548288002AFD28 /* MainViewController.swift in Sources */, 167 | 7D21844221548288002AFD28 /* AppDelegate.swift in Sources */, 168 | 7D2184572154855B002AFD28 /* ReplaySelectionViewController.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin PBXVariantGroup section */ 175 | 7D21844721548288002AFD28 /* Main.storyboard */ = { 176 | isa = PBXVariantGroup; 177 | children = ( 178 | 7D21844821548288002AFD28 /* Base */, 179 | ); 180 | name = Main.storyboard; 181 | sourceTree = ""; 182 | }; 183 | 7D21844C21548289002AFD28 /* LaunchScreen.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | 7D21844D21548289002AFD28 /* Base */, 187 | ); 188 | name = LaunchScreen.storyboard; 189 | sourceTree = ""; 190 | }; 191 | /* End PBXVariantGroup section */ 192 | 193 | /* Begin XCBuildConfiguration section */ 194 | 7D21845021548289002AFD28 /* Debug */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 201 | CLANG_CXX_LIBRARY = "libc++"; 202 | CLANG_ENABLE_MODULES = YES; 203 | CLANG_ENABLE_OBJC_ARC = YES; 204 | CLANG_ENABLE_OBJC_WEAK = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INFINITE_RECURSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | CODE_SIGN_IDENTITY = "iPhone Developer"; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = dwarf; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_TESTABILITY = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu11; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 247 | MTL_FAST_MATH = YES; 248 | ONLY_ACTIVE_ARCH = YES; 249 | SDKROOT = iphoneos; 250 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 251 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 252 | }; 253 | name = Debug; 254 | }; 255 | 7D21845121548289002AFD28 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 262 | CLANG_CXX_LIBRARY = "libc++"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_ENABLE_OBJC_WEAK = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 279 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | CODE_SIGN_IDENTITY = "iPhone Developer"; 288 | COPY_PHASE_STRIP = NO; 289 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 290 | ENABLE_NS_ASSERTIONS = NO; 291 | ENABLE_STRICT_OBJC_MSGSEND = YES; 292 | GCC_C_LANGUAGE_STANDARD = gnu11; 293 | GCC_NO_COMMON_BLOCKS = YES; 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 301 | MTL_ENABLE_DEBUG_INFO = NO; 302 | MTL_FAST_MATH = YES; 303 | SDKROOT = iphoneos; 304 | SWIFT_COMPILATION_MODE = wholemodule; 305 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 306 | VALIDATE_PRODUCT = YES; 307 | }; 308 | name = Release; 309 | }; 310 | 7D21845321548289002AFD28 /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | CLANG_ENABLE_MODULES = YES; 315 | CODE_SIGN_STYLE = Automatic; 316 | DEVELOPMENT_TEAM = ""; 317 | INFOPLIST_FILE = ARRecorder/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/Frameworks", 321 | ); 322 | PRODUCT_BUNDLE_IDENTIFIER = com.ittybittyapps.ARRecorder; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SWIFT_OBJC_BRIDGING_HEADER = "ARRecorder/ARRecorder-Bridging-Header.h"; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | SWIFT_VERSION = 4.2; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Debug; 330 | }; 331 | 7D21845421548289002AFD28 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 335 | CLANG_ENABLE_MODULES = YES; 336 | CODE_SIGN_STYLE = Automatic; 337 | DEVELOPMENT_TEAM = ""; 338 | INFOPLIST_FILE = ARRecorder/Info.plist; 339 | LD_RUNPATH_SEARCH_PATHS = ( 340 | "$(inherited)", 341 | "@executable_path/Frameworks", 342 | ); 343 | PRODUCT_BUNDLE_IDENTIFIER = com.ittybittyapps.ARRecorder; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SWIFT_OBJC_BRIDGING_HEADER = "ARRecorder/ARRecorder-Bridging-Header.h"; 346 | SWIFT_VERSION = 4.2; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | }; 349 | name = Release; 350 | }; 351 | /* End XCBuildConfiguration section */ 352 | 353 | /* Begin XCConfigurationList section */ 354 | 7D21843921548287002AFD28 /* Build configuration list for PBXProject "ARRecorder" */ = { 355 | isa = XCConfigurationList; 356 | buildConfigurations = ( 357 | 7D21845021548289002AFD28 /* Debug */, 358 | 7D21845121548289002AFD28 /* Release */, 359 | ); 360 | defaultConfigurationIsVisible = 0; 361 | defaultConfigurationName = Release; 362 | }; 363 | 7D21845221548289002AFD28 /* Build configuration list for PBXNativeTarget "ARRecorder" */ = { 364 | isa = XCConfigurationList; 365 | buildConfigurations = ( 366 | 7D21845321548289002AFD28 /* Debug */, 367 | 7D21845421548289002AFD28 /* Release */, 368 | ); 369 | defaultConfigurationIsVisible = 0; 370 | defaultConfigurationName = Release; 371 | }; 372 | /* End XCConfigurationList section */ 373 | }; 374 | rootObject = 7D21843621548287002AFD28 /* Project object */; 375 | } 376 | --------------------------------------------------------------------------------