Skip to content

Added feature to share scans via ios Share Sheet #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions StrayScanner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
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 */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -119,6 +120,7 @@
38FB73152572AF63007D9CB0 /* Shaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = "<group>"; };
38FB731F2573EABB007D9CB0 /* ShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShaderTypes.h; sourceTree = "<group>"; };
38FB73242573ECE2007D9CB0 /* BridgeHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgeHeader.h; path = StrayScanner/BridgeHeader.h; sourceTree = "<group>"; };
709259182E0B79AB00A7B62E /* ShareUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareUtility.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DBC054EDDA47FB8A717AA671 /* Pods_StrayScanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StrayScanner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -244,6 +246,7 @@
38694C6B26D2C66F00546EA1 /* Helpers */ = {
isa = PBXGroup;
children = (
709259182E0B79AB00A7B62E /* ShareUtility.swift */,
38694C6C26D2C66F00546EA1 /* DatasetEncoder.swift */,
38694C6D26D2C66F00546EA1 /* ConfidenceEncoder.swift */,
38694C6E26D2C66F00546EA1 /* VideoEncoder.swift */,
Expand Down Expand Up @@ -513,6 +516,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 */,
Expand Down
60 changes: 60 additions & 0 deletions StrayScanner/Helpers/ShareUtility.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
11 changes: 7 additions & 4 deletions StrayScanner/Views/InformationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ZIP 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 {
Expand Down
92 changes: 87 additions & 5 deletions StrayScanner/Views/SessionDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import SwiftUI
import AVKit
import CoreData
import Foundation

class SessionDetailViewModel: ObservableObject {
private var dataContext: NSManagedObjectContext?
Expand Down Expand Up @@ -45,6 +46,10 @@ struct SessionDetailView: View {
@ObservedObject var viewModel = SessionDetailViewModel()
var recording: Recording
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var showingShareSheet = false
@State private var tempPackageURL: URL?
@State private var isCreatingPackage = false
@State private var player: AVPlayer?

let defaultUrl = URL(fileURLWithPath: "")

Expand All @@ -55,26 +60,103 @@ 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
}
}
}
}
}
}

func deleteItem() {
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<ShareSheet>) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
controller.completionWithItemsHandler = completionWithItemsHandler
return controller
}

func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ShareSheet>) {
}
}

struct SessionDetailView_Previews: PreviewProvider {
static var recording: Recording = { () -> Recording in
Expand Down