From a7915e031c10993603dd3dd92819dca72343b818 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 26 Jun 2025 16:24:11 -0700 Subject: [PATCH 1/3] Added feature to share scans via ios Share Sheet --- StrayScanner.xcodeproj/project.pbxproj | 6 ++ StrayScanner/Helpers/ShareUtility.swift | 60 ++++++++++++++++ StrayScanner/Views/InformationView.swift | 11 +-- StrayScanner/Views/SessionDetail.swift | 92 ++++++++++++++++++++++-- 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 StrayScanner/Helpers/ShareUtility.swift diff --git a/StrayScanner.xcodeproj/project.pbxproj b/StrayScanner.xcodeproj/project.pbxproj index e8258ae..af99333 100644 --- a/StrayScanner.xcodeproj/project.pbxproj +++ b/StrayScanner.xcodeproj/project.pbxproj @@ -66,6 +66,8 @@ 38E969CB2572608E00054CC4 /* NewSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E969CA2572608E00054CC4 /* NewSession.swift */; }; 38FB730F2572A9FA007D9CB0 /* RecordSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FB730E2572A9FA007D9CB0 /* RecordSessionViewController.swift */; }; 38FB73162572AF63007D9CB0 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 38FB73152572AF63007D9CB0 /* Shaders.metal */; }; + 709259192E0B79AB00A7B62E /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; + 7092591A2E0B79AB00A7B62E /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -119,6 +121,7 @@ 38FB73152572AF63007D9CB0 /* Shaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = ""; }; 38FB731F2573EABB007D9CB0 /* ShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShaderTypes.h; sourceTree = ""; }; 38FB73242573ECE2007D9CB0 /* BridgeHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgeHeader.h; path = StrayScanner/BridgeHeader.h; sourceTree = ""; }; + 709259182E0B79AB00A7B62E /* ShareUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareUtility.swift; sourceTree = ""; }; AC52749A3B5AED09C9753120 /* Pods-StrayScanner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StrayScanner.debug.xcconfig"; path = "Target Support Files/Pods-StrayScanner/Pods-StrayScanner.debug.xcconfig"; sourceTree = ""; }; DBC054EDDA47FB8A717AA671 /* Pods_StrayScanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StrayScanner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -244,6 +247,7 @@ 38694C6B26D2C66F00546EA1 /* Helpers */ = { isa = PBXGroup; children = ( + 709259182E0B79AB00A7B62E /* ShareUtility.swift */, 38694C6C26D2C66F00546EA1 /* DatasetEncoder.swift */, 38694C6D26D2C66F00546EA1 /* ConfidenceEncoder.swift */, 38694C6E26D2C66F00546EA1 /* VideoEncoder.swift */, @@ -513,6 +517,7 @@ 38694C7626D2C66F00546EA1 /* ConfidenceEncoder.swift in Sources */, 386E277825991163007D023B /* RecordButton.swift in Sources */, 385999BF25616F2B00F3F681 /* SceneDelegate.swift in Sources */, + 709259192E0B79AB00A7B62E /* ShareUtility.swift in Sources */, 38694C8026D2C66F00546EA1 /* DepthEncoder.swift in Sources */, 38C17B13259BE1DA006B3FDA /* Recording+CoreDataProperties.swift in Sources */, 38C17B12259BE1DA006B3FDA /* Recording+CoreDataClass.swift in Sources */, @@ -543,6 +548,7 @@ 3863FE6A25FCBB0E00C1DA4F /* RecordButton.swift in Sources */, 3807421027BBD07D003194C1 /* PngEncoder.mm in Sources */, 3863FE6C25FCBB0E00C1DA4F /* SceneDelegate.swift in Sources */, + 7092591A2E0B79AB00A7B62E /* ShareUtility.swift in Sources */, 38694C8126D2C66F00546EA1 /* DepthEncoder.swift in Sources */, 3863FE6E25FCBB0E00C1DA4F /* Recording+CoreDataProperties.swift in Sources */, 3863FE6F25FCBB0E00C1DA4F /* Recording+CoreDataClass.swift in Sources */, diff --git a/StrayScanner/Helpers/ShareUtility.swift b/StrayScanner/Helpers/ShareUtility.swift new file mode 100644 index 0000000..8a35c27 --- /dev/null +++ b/StrayScanner/Helpers/ShareUtility.swift @@ -0,0 +1,60 @@ +// +// ShareUtility.swift +// StrayScanner +// +// Created by Claude on 6/24/25. +// + +import Foundation +import Compression + +/// Utility class for creating shareable archives from recording datasets +class ShareUtility { + + /// Creates a shareable ZIP archive from a recording's dataset + /// - Parameter recording: The recording to create a ZIP archive for + /// - Returns: URL of the created ZIP file + static func createShareableArchive(for recording: Recording) async throws -> URL { + guard let sourceDirectory = recording.directoryPath() else { + throw NSError(domain: "ShareError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to get recording directory path"]) + } + + let tempDirectory = FileManager.default.temporaryDirectory + let archiveName = "\(recording.name ?? "Recording")_\(recording.id?.uuidString.prefix(8) ?? "unknown").zip" + let archiveURL = tempDirectory.appendingPathComponent(archiveName) + + // Remove existing archive if it exists + try? FileManager.default.removeItem(at: archiveURL) + + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + try createZipArchive(sourceDirectory: sourceDirectory, destinationURL: archiveURL) + continuation.resume(returning: archiveURL) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private static func createZipArchive(sourceDirectory: URL, destinationURL: URL) throws { + let coordinator = NSFileCoordinator() + var error: NSError? + + coordinator.coordinate(readingItemAt: sourceDirectory, options: [.forUploading], error: &error) { (zipURL) in + do { + _ = zipURL.startAccessingSecurityScopedResource() + defer { zipURL.stopAccessingSecurityScopedResource() } + + try FileManager.default.copyItem(at: zipURL, to: destinationURL) + } catch { + print("Failed to create zip: \(error)") + } + } + + if let error = error { + throw error + } + } +} diff --git a/StrayScanner/Views/InformationView.swift b/StrayScanner/Views/InformationView.swift index ca25c53..2169bec 100644 --- a/StrayScanner/Views/InformationView.swift +++ b/StrayScanner/Views/InformationView.swift @@ -26,13 +26,16 @@ This app lets you record video and depth datasets using the camera and LIDAR sca heading("Transfering Datasets To Your Desktop Computer") bodyText(""" -The recorded datasets can be exported by connecting your device to it with the lightning cable. +The recorded datasets can be exported in several ways: -On Mac, you can access the files through Finder. In the sidebar, select your device. Under the "Files" tab, you should see an entry for Stray Scanner. Expand it, then drag the folders to the desired location. There is one folder per dataset, each named after a random alphanumerical hash. +1. Share directly from the app: Tap the "Share" button inside any recording to export it as a TAR archive via AirDrop, email, or save to Files. -On Windows, you can access the files through iTunes. +2. Connect your device with the lightning cable: + • On Mac, access files through Finder sidebar > your device > "Files" tab > Stray Scanner + • On Windows, access files through iTunes + • Drag folders to your desired location (one folder per dataset) -Alternatively, you can access the data in the Files app under "Browse > On My iPhone > Stray Scanner" and export them to another app or move them to your iCloud drive. +3. Use the Files app: Browse > On My iPhone > Stray Scanner, then export to another app or iCloud drive. """) } Group { diff --git a/StrayScanner/Views/SessionDetail.swift b/StrayScanner/Views/SessionDetail.swift index 2723554..4144f64 100644 --- a/StrayScanner/Views/SessionDetail.swift +++ b/StrayScanner/Views/SessionDetail.swift @@ -9,6 +9,7 @@ import SwiftUI import AVKit import CoreData +import Foundation class SessionDetailViewModel: ObservableObject { private var dataContext: NSManagedObjectContext? @@ -45,6 +46,10 @@ struct SessionDetailView: View { @ObservedObject var viewModel = SessionDetailViewModel() var recording: Recording @Environment(\.presentationMode) var presentationMode: Binding + @State private var showingShareSheet = false + @State private var tempPackageURL: URL? + @State private var isCreatingPackage = false + @State private var player: AVPlayer? let defaultUrl = URL(fileURLWithPath: "") @@ -55,16 +60,52 @@ struct SessionDetailView: View { Color("BackgroundColor") .edgesIgnoringSafeArea(.all) VStack { - let player = AVPlayer(url: recording.absoluteRgbPath() ?? defaultUrl) - VideoPlayer(player: player) + VideoPlayer(player: player ?? AVPlayer(url: defaultUrl)) .frame(width: width, height: height) .padding(.horizontal, 0.0) - Button(action: deleteItem) { - Text("Delete").foregroundColor(Color("DangerColor")) + .onAppear { + if player == nil { + player = AVPlayer(url: recording.absoluteRgbPath() ?? defaultUrl) + } + } + + HStack(spacing: 20) { + Button(action: shareItem) { + HStack { + if isCreatingPackage { + ProgressView() + .scaleEffect(0.8) + } else { + Image(systemName: "square.and.arrow.up") + } + Text(isCreatingPackage ? "Preparing..." : "Share") + .fixedSize() + } + .foregroundColor(isCreatingPackage ? .gray : .blue) + .frame(minWidth: 100) + } + .disabled(isCreatingPackage) + + Button(action: deleteItem) { + Text("Delete").foregroundColor(Color("DangerColor")) + } } + .padding(.top, 20) } .navigationBarTitle(viewModel.title(recording: recording)) .background(Color("BackgroundColor")) + .sheet(isPresented: $showingShareSheet) { + if let packageURL = tempPackageURL { + ShareSheet(activityItems: [packageURL]) { activityType, completed, returnedItems, activityError in + DispatchQueue.main.async { + // Clean up temporary package after sharing + try? FileManager.default.removeItem(at: packageURL) + tempPackageURL = nil + showingShareSheet = false + } + } + } + } } } @@ -72,9 +113,50 @@ struct SessionDetailView: View { viewModel.delete(recording: recording) self.presentationMode.wrappedValue.dismiss() } + + func shareItem() { + isCreatingPackage = true + + Task { + do { + let packageURL = try await ShareUtility.createShareableArchive(for: recording) + await MainActor.run { + tempPackageURL = packageURL + isCreatingPackage = false + showingShareSheet = true + } + } catch { + await MainActor.run { + isCreatingPackage = false + print("Failed to create package: \(error)") + } + } + } + } } - +struct ShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + let applicationActivities: [UIActivity]? = nil + let completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? + + init(activityItems: [Any], completionHandler: UIActivityViewController.CompletionWithItemsHandler? = nil) { + self.activityItems = activityItems + self.completionWithItemsHandler = completionHandler + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: applicationActivities + ) + controller.completionWithItemsHandler = completionWithItemsHandler + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { + } +} struct SessionDetailView_Previews: PreviewProvider { static var recording: Recording = { () -> Recording in From f653e45f1dcc28075e99da6486b932922579db08 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 26 Jun 2025 17:46:57 -0700 Subject: [PATCH 2/3] Minor project ref cleanup, corrected outdated info TAR->ZIP --- StrayScanner.xcodeproj/project.pbxproj | 2 -- StrayScanner/Views/InformationView.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/StrayScanner.xcodeproj/project.pbxproj b/StrayScanner.xcodeproj/project.pbxproj index af99333..567c3c9 100644 --- a/StrayScanner.xcodeproj/project.pbxproj +++ b/StrayScanner.xcodeproj/project.pbxproj @@ -67,7 +67,6 @@ 38FB730F2572A9FA007D9CB0 /* RecordSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FB730E2572A9FA007D9CB0 /* RecordSessionViewController.swift */; }; 38FB73162572AF63007D9CB0 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 38FB73152572AF63007D9CB0 /* Shaders.metal */; }; 709259192E0B79AB00A7B62E /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; - 7092591A2E0B79AB00A7B62E /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -548,7 +547,6 @@ 3863FE6A25FCBB0E00C1DA4F /* RecordButton.swift in Sources */, 3807421027BBD07D003194C1 /* PngEncoder.mm in Sources */, 3863FE6C25FCBB0E00C1DA4F /* SceneDelegate.swift in Sources */, - 7092591A2E0B79AB00A7B62E /* ShareUtility.swift in Sources */, 38694C8126D2C66F00546EA1 /* DepthEncoder.swift in Sources */, 3863FE6E25FCBB0E00C1DA4F /* Recording+CoreDataProperties.swift in Sources */, 3863FE6F25FCBB0E00C1DA4F /* Recording+CoreDataClass.swift in Sources */, diff --git a/StrayScanner/Views/InformationView.swift b/StrayScanner/Views/InformationView.swift index 2169bec..03eebfb 100644 --- a/StrayScanner/Views/InformationView.swift +++ b/StrayScanner/Views/InformationView.swift @@ -28,7 +28,7 @@ This app lets you record video and depth datasets using the camera and LIDAR sca bodyText(""" The recorded datasets can be exported in several ways: -1. Share directly from the app: Tap the "Share" button inside any recording to export it as a TAR archive via AirDrop, email, or save to Files. +1. Share directly from the app: Tap the "Share" button inside any recording to export it as a ZIP archive via AirDrop, email, or save to Files. 2. Connect your device with the lightning cable: • On Mac, access files through Finder sidebar > your device > "Files" tab > Stray Scanner From f41d2ff258a33602caa3021eb0df6c22a79712ce Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 3 Jul 2025 20:22:15 -0700 Subject: [PATCH 3/3] Fix ShareUtility in debug target --- StrayScanner.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/StrayScanner.xcodeproj/project.pbxproj b/StrayScanner.xcodeproj/project.pbxproj index 567c3c9..7cfbe00 100644 --- a/StrayScanner.xcodeproj/project.pbxproj +++ b/StrayScanner.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ 38FB730F2572A9FA007D9CB0 /* RecordSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FB730E2572A9FA007D9CB0 /* RecordSessionViewController.swift */; }; 38FB73162572AF63007D9CB0 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 38FB73152572AF63007D9CB0 /* Shaders.metal */; }; 709259192E0B79AB00A7B62E /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; + 70CF35492E17803700276207 /* ShareUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709259182E0B79AB00A7B62E /* ShareUtility.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -547,6 +548,7 @@ 3863FE6A25FCBB0E00C1DA4F /* RecordButton.swift in Sources */, 3807421027BBD07D003194C1 /* PngEncoder.mm in Sources */, 3863FE6C25FCBB0E00C1DA4F /* SceneDelegate.swift in Sources */, + 70CF35492E17803700276207 /* ShareUtility.swift in Sources */, 38694C8126D2C66F00546EA1 /* DepthEncoder.swift in Sources */, 3863FE6E25FCBB0E00C1DA4F /* Recording+CoreDataProperties.swift in Sources */, 3863FE6F25FCBB0E00C1DA4F /* Recording+CoreDataClass.swift in Sources */,