From 58c4b8e5581f92ad30d43c70024b326901f72569 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Tue, 25 Aug 2026 18:51:03 +0530 Subject: [PATCH] Add SwiftUI upload example; consolidate apps under examples/ --- .gitignore | 8 + examples/README.md | 11 + examples/swiftui/README.md | 55 ++++ .../project.pbxproj | 304 ++++++++++++++++++ .../contents.xcworkspacedata | 0 .../AccentColor.colorset/Contents.json | 4 + .../AppIcon.appiconset/Contents.json | 4 + .../Assets.xcassets/Contents.json | 3 + .../SwiftUIUploadExample/ContentView.swift | 91 ++++++ .../SwiftUIUploadExample/FastPixAPI.swift | 53 +++ .../swiftui/SwiftUIUploadExample/Info.plist | 14 + .../Secrets.example.swift | 11 + .../UploadExampleApp.swift | 14 + .../SwiftUIUploadExample/UploadManager.swift | 152 +++++++++ {example => examples/uikit}/README.md | 0 .../UploadExample.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 0 .../uikit}/UploadExample/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../uikit}/UploadExample/Info.plist | 0 .../uikit}/UploadExample/SceneDelegate.swift | 0 .../UploadExample/UploadViewController.swift | 0 26 files changed, 731 insertions(+) create mode 100644 .gitignore create mode 100644 examples/README.md create mode 100644 examples/swiftui/README.md create mode 100644 examples/swiftui/SwiftUIUploadExample.xcodeproj/project.pbxproj rename {example/UploadExample.xcodeproj => examples/swiftui/SwiftUIUploadExample.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/swiftui/SwiftUIUploadExample/Assets.xcassets/Contents.json create mode 100644 examples/swiftui/SwiftUIUploadExample/ContentView.swift create mode 100644 examples/swiftui/SwiftUIUploadExample/FastPixAPI.swift create mode 100644 examples/swiftui/SwiftUIUploadExample/Info.plist create mode 100644 examples/swiftui/SwiftUIUploadExample/Secrets.example.swift create mode 100644 examples/swiftui/SwiftUIUploadExample/UploadExampleApp.swift create mode 100644 examples/swiftui/SwiftUIUploadExample/UploadManager.swift rename {example => examples/uikit}/README.md (100%) rename {example => examples/uikit}/UploadExample.xcodeproj/project.pbxproj (100%) create mode 100644 examples/uikit/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename {example => examples/uikit}/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => examples/uikit}/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (100%) rename {example => examples/uikit}/UploadExample/AppDelegate.swift (100%) rename {example => examples/uikit}/UploadExample/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {example => examples/uikit}/UploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => examples/uikit}/UploadExample/Assets.xcassets/Contents.json (100%) rename {example => examples/uikit}/UploadExample/Info.plist (100%) rename {example => examples/uikit}/UploadExample/SceneDelegate.swift (100%) rename {example => examples/uikit}/UploadExample/UploadViewController.swift (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc53db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Swift Package Manager / Xcode build output +.build/ +DerivedData/ +*.xcuserstate +xcuserdata/ + +# Credentials — never commit real keys (see Secrets.example.swift) +examples/swiftui/SwiftUIUploadExample/Secrets.swift diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4fbe176 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# FastPix iOS Uploads — Examples + +Standalone example apps for the [FastPix iOS Uploads SDK](../). Each folder is a +self-contained Xcode project — copy one and run it. + +| Example | UI | Picks from | Notes | +|---------|-----|-----------|-------| +| [`uikit/`](uikit) | UIKit | Files (paste a signed URL) | Original example. | +| [`swiftui/`](swiftui) | SwiftUI | Photos | Mints the upload URL in-app; best-effort background upload. | + +Both do chunked upload with live progress and pause / resume / cancel. diff --git a/examples/swiftui/README.md b/examples/swiftui/README.md new file mode 100644 index 0000000..f55cfe0 --- /dev/null +++ b/examples/swiftui/README.md @@ -0,0 +1,55 @@ +# FastPix iOS Uploads — SwiftUI Example + +A minimal **SwiftUI** app that mints a signed upload URL from FastPix, picks a +video, and chunk-uploads it with live progress and **pause / resume / cancel**. +Upload state is driven by an `ObservableObject` (`UploadManager`). + +This is separate from the UIKit example in [`../uikit`](../uikit). + +## Run it + +1. **Add your credentials.** Copy the template and paste a token + secret key + (Dashboard → Settings → Access Tokens): + + ```bash + cd examples/swiftui/SwiftUIUploadExample + cp Secrets.example.swift Secrets.swift # Secrets.swift is gitignored + ``` + +2. **Open and run:** + + ```bash + open examples/swiftui/SwiftUIUploadExample.xcodeproj + ``` + + Pick the `SwiftUIUploadExample` scheme + a simulator (or your device, with a + signing team set), then ⌘R. The project references the SDK locally (`../..`), + so it builds against the code in this repo. + +3. Tap **Choose Video…**, pick a clip. The app calls create-upload, then uploads. + Watch the progress bar and event log; use **Pause / Resume / Cancel**. + +## What "background upload" means here + +Tap upload, then background the app — it keeps uploading for the short window +iOS grants (`beginBackgroundTask`), typically tens of seconds to a couple of +minutes, then iOS suspends it. + +**Why not longer, like the Android example?** Android has a *foreground service* +that keeps your process running while backgrounded. iOS has no equivalent — it +suspends your process within seconds. The only way a large transfer truly +survives suspension is a **background `URLSession`** (`URLSessionConfiguration.background`), +where a system daemon does the transfer. That has to be configured *inside* the +SDK (it currently uses `URLSessionConfiguration.default`), and a background +session can't upload in-memory `Data` chunks the way this SDK does — so it's an +SDK change, out of scope for an example. This app does the honest best-effort: +`beginBackgroundTask` around the active upload. + +## Files + +- `UploadExampleApp.swift` — `@main` app, owns the `UploadManager`. +- `ContentView.swift` — the screen: pick, progress, pause/resume/cancel, log. +- `UploadManager.swift` — `ObservableObject` bridging SDK delegates → `@Published` + state; holds the `beginBackgroundTask` assertion. +- `FastPixAPI.swift` — the create-upload POST call. +- `Secrets.swift` — your credentials (gitignored). diff --git a/examples/swiftui/SwiftUIUploadExample.xcodeproj/project.pbxproj b/examples/swiftui/SwiftUIUploadExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9834c9d --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample.xcodeproj/project.pbxproj @@ -0,0 +1,304 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 1B0000000000000000000001 /* UploadExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000001 /* UploadExampleApp.swift */; }; + 1B0000000000000000000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000002 /* ContentView.swift */; }; + 1B0000000000000000000003 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000003 /* UploadManager.swift */; }; + 1B0000000000000000000004 /* FastPixAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000004 /* FastPixAPI.swift */; }; + 1B0000000000000000000005 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000005 /* Secrets.swift */; }; + 1B0000000000000000000006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000006 /* Assets.xcassets */; }; + 1B0000000000000000000010 /* fp-swift-upload-sdk in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0000000000000000000002 /* fp-swift-upload-sdk */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1A0000000000000000000001 /* UploadExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadExampleApp.swift; sourceTree = ""; }; + 1A0000000000000000000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 1A0000000000000000000003 /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; + 1A0000000000000000000004 /* FastPixAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastPixAPI.swift; sourceTree = ""; }; + 1A0000000000000000000005 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; + 1A0000000000000000000006 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1A0000000000000000000007 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1A0000000000000000000008 /* Secrets.example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.example.swift; sourceTree = ""; }; + 1A0000000000000000000009 /* SwiftUIUploadExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIUploadExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A0000000000000000000002 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B0000000000000000000010 /* fp-swift-upload-sdk in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A0000000000000000000001 = { + isa = PBXGroup; + children = ( + 2A0000000000000000000002 /* SwiftUIUploadExample */, + 2A0000000000000000000003 /* Products */, + 2A0000000000000000000004 /* Frameworks */, + ); + sourceTree = ""; + }; + 2A0000000000000000000002 /* SwiftUIUploadExample */ = { + isa = PBXGroup; + children = ( + 1A0000000000000000000001 /* UploadExampleApp.swift */, + 1A0000000000000000000002 /* ContentView.swift */, + 1A0000000000000000000003 /* UploadManager.swift */, + 1A0000000000000000000004 /* FastPixAPI.swift */, + 1A0000000000000000000005 /* Secrets.swift */, + 1A0000000000000000000008 /* Secrets.example.swift */, + 1A0000000000000000000006 /* Assets.xcassets */, + 1A0000000000000000000007 /* Info.plist */, + ); + path = SwiftUIUploadExample; + sourceTree = ""; + }; + 2A0000000000000000000003 /* Products */ = { + isa = PBXGroup; + children = ( + 1A0000000000000000000009 /* SwiftUIUploadExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 2A0000000000000000000004 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4A0000000000000000000001 /* SwiftUIUploadExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5A0000000000000000000002 /* Build configuration list for PBXNativeTarget "SwiftUIUploadExample" */; + buildPhases = ( + 3A0000000000000000000001 /* Sources */, + 3A0000000000000000000002 /* Frameworks */, + 3A0000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftUIUploadExample; + packageProductDependencies = ( + 7A0000000000000000000002 /* fp-swift-upload-sdk */, + ); + productName = SwiftUIUploadExample; + productReference = 1A0000000000000000000009 /* SwiftUIUploadExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4A0000000000000000000002 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 4A0000000000000000000001 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 5A0000000000000000000001 /* Build configuration list for PBXProject "SwiftUIUploadExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A0000000000000000000001; + packageReferences = ( + 7A0000000000000000000001 /* XCLocalSwiftPackageReference "iOS-Uploads" */, + ); + productRefGroup = 2A0000000000000000000003 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4A0000000000000000000001 /* SwiftUIUploadExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3A0000000000000000000003 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B0000000000000000000006 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A0000000000000000000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B0000000000000000000001 /* UploadExampleApp.swift in Sources */, + 1B0000000000000000000002 /* ContentView.swift in Sources */, + 1B0000000000000000000003 /* UploadManager.swift in Sources */, + 1B0000000000000000000004 /* FastPixAPI.swift in Sources */, + 1B0000000000000000000005 /* Secrets.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6A0000000000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6A0000000000000000000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6A0000000000000000000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = TRG6Q7877U; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SwiftUIUploadExample/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fastpix.SwiftUIUploadExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6A0000000000000000000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = TRG6Q7877U; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SwiftUIUploadExample/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fastpix.SwiftUIUploadExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5A0000000000000000000001 /* Build configuration list for PBXProject "SwiftUIUploadExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6A0000000000000000000001 /* Debug */, + 6A0000000000000000000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5A0000000000000000000002 /* Build configuration list for PBXNativeTarget "SwiftUIUploadExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6A0000000000000000000003 /* Debug */, + 6A0000000000000000000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 7A0000000000000000000001 /* XCLocalSwiftPackageReference "iOS-Uploads" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7A0000000000000000000002 /* fp-swift-upload-sdk */ = { + isa = XCSwiftPackageProductDependency; + productName = "fp-swift-upload-sdk"; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4A0000000000000000000002 /* Project object */; +} diff --git a/example/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/swiftui/SwiftUIUploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to examples/swiftui/SwiftUIUploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..3be300b --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,4 @@ +{ + "colors" : [ { "idiom" : "universal" } ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2d72247 --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,4 @@ +{ + "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/Contents.json b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..d8b757a --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/Assets.xcassets/Contents.json @@ -0,0 +1,3 @@ +{ + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/examples/swiftui/SwiftUIUploadExample/ContentView.swift b/examples/swiftui/SwiftUIUploadExample/ContentView.swift new file mode 100644 index 0000000..09f330d --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/ContentView.swift @@ -0,0 +1,91 @@ +import SwiftUI +import PhotosUI +import UniformTypeIdentifiers + +struct ContentView: View { + @EnvironmentObject private var manager: UploadManager + @State private var pickerItem: PhotosPickerItem? + @State private var pickError: String? + + var body: some View { + NavigationView { + Form { + Section("Video") { + PhotosPicker(selection: $pickerItem, matching: .videos) { + Label(manager.fileName.isEmpty ? "Choose Video…" : manager.fileName, + systemImage: "film") + } + .disabled(manager.isActive) + if let pickError { + Text(pickError).foregroundColor(.red).font(.footnote) + } + } + + Section("Progress") { + ProgressView(value: manager.progress) + HStack { + Text(manager.phase.rawValue) + Spacer() + Text("\(Int(manager.progress * 100))%").monospacedDigit() + } + .font(.subheadline) + .foregroundColor(.secondary) + + HStack { + Button("Pause") { manager.pause() }.disabled(!manager.canPause) + Spacer() + Button("Resume") { manager.resume() }.disabled(!manager.canResume) + Spacer() + Button("Cancel", role: .destructive) { manager.cancel() } + .disabled(!manager.isActive) + } + .buttonStyle(.bordered) + } + + if !manager.log.isEmpty { + Section("Event Log") { + ForEach(Array(manager.log.enumerated()), id: \.offset) { _, line in + Text(line).font(.caption).foregroundColor(.secondary) + } + } + } + } + .navigationTitle("FastPix Upload") + } + .navigationViewStyle(.stack) + .onChange(of: pickerItem) { newItem in + guard let newItem else { return } + Task { await load(newItem) } + } + } + + private func load(_ item: PhotosPickerItem) async { + pickError = nil + do { + // Copies the picked video into our temp dir; the SDK reads it from there. + guard let movie = try await item.loadTransferable(type: Movie.self) else { + pickError = "Couldn't load that video." + return + } + manager.start(fileURL: movie.url) + } catch { + pickError = "Couldn't load the video: \(error.localizedDescription)" + } + } +} + +// Transferable that lands the picked video as a file URL in our temp dir. +struct Movie: Transferable { + let url: URL + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(contentType: .movie) { movie in + SentTransferredFile(movie.url) + } importing: { received in + let dest = FileManager.default.temporaryDirectory + .appendingPathComponent(received.file.lastPathComponent) + try? FileManager.default.removeItem(at: dest) + try FileManager.default.copyItem(at: received.file, to: dest) + return Movie(url: dest) + } + } +} diff --git a/examples/swiftui/SwiftUIUploadExample/FastPixAPI.swift b/examples/swiftui/SwiftUIUploadExample/FastPixAPI.swift new file mode 100644 index 0000000..2832d20 --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/FastPixAPI.swift @@ -0,0 +1,53 @@ +import Foundation + +// Mints a signed upload URL from FastPix's Direct Upload API. +// POST https://api.fastpix.com/v1/on-demand/upload (Basic auth: base64(token:secretKey)) +// Response: data.url is the signed PUT URL the SDK uploads chunks to. +enum FastPixAPI { + + struct SignedUpload { + let url: String + let uploadId: String + } + + enum APIError: LocalizedError { + case missingCredentials + case badResponse(Int) + case malformed + + var errorDescription: String? { + switch self { + case .missingCredentials: return "Set your token/secret in Secrets.swift." + case .badResponse(let code): return "create-upload failed (HTTP \(code))." + case .malformed: return "create-upload returned an unexpected response." + } + } + } + + static func createUpload() async throws -> SignedUpload { + guard Secrets.token != "YOUR_FASTPIX_TOKEN", !Secrets.token.isEmpty, !Secrets.secretKey.isEmpty else { + throw APIError.missingCredentials + } + + var request = URLRequest(url: URL(string: "https://api.fastpix.com/v1/on-demand/upload")!) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let basic = Data("\(Secrets.token):\(Secrets.secretKey)".utf8).base64EncodedString() + request.setValue("Basic \(basic)", forHTTPHeaderField: "Authorization") + request.httpBody = try JSONSerialization.data(withJSONObject: [ + "corsOrigin": "*", + "pushMediaSettings": ["accessPolicy": "public", "maxResolution": "2160p"] + ]) + + let (data, response) = try await URLSession.shared.data(for: request) + guard let http = response as? HTTPURLResponse else { throw APIError.malformed } + guard (200..<300).contains(http.statusCode) else { throw APIError.badResponse(http.statusCode) } + + guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let payload = root["data"] as? [String: Any], + let url = payload["url"] as? String else { + throw APIError.malformed + } + return SignedUpload(url: url, uploadId: payload["uploadId"] as? String ?? "") + } +} diff --git a/examples/swiftui/SwiftUIUploadExample/Info.plist b/examples/swiftui/SwiftUIUploadExample/Info.plist new file mode 100644 index 0000000..b67f256 --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/Info.plist @@ -0,0 +1,14 @@ + + + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/swiftui/SwiftUIUploadExample/Secrets.example.swift b/examples/swiftui/SwiftUIUploadExample/Secrets.example.swift new file mode 100644 index 0000000..451e5ef --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/Secrets.example.swift @@ -0,0 +1,11 @@ +// Template — copy this file to `Secrets.swift` and paste your credentials. +// +// cp Secrets.example.swift Secrets.swift +// +// `Secrets.swift` is gitignored so your keys never get committed. +// Get a token + secret key from https://dashboard.fastpix.com (Settings → Access Tokens). + +enum Secrets { + static let token = "YOUR_FASTPIX_TOKEN" + static let secretKey = "YOUR_FASTPIX_SECRET_KEY" +} diff --git a/examples/swiftui/SwiftUIUploadExample/UploadExampleApp.swift b/examples/swiftui/SwiftUIUploadExample/UploadExampleApp.swift new file mode 100644 index 0000000..ff52767 --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/UploadExampleApp.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@main +struct UploadExampleApp: App { + // One manager for the whole app — it owns the uploader and its state. + @StateObject private var manager = UploadManager() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(manager) + } + } +} diff --git a/examples/swiftui/SwiftUIUploadExample/UploadManager.swift b/examples/swiftui/SwiftUIUploadExample/UploadManager.swift new file mode 100644 index 0000000..5879e7c --- /dev/null +++ b/examples/swiftui/SwiftUIUploadExample/UploadManager.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import fp_swift_upload_sdk + +// Bridges the SDK's delegate callbacks into @Published state that SwiftUI observes. +// Also holds a UIApplication background-task assertion so an in-flight upload keeps +// running for the short window iOS grants after the app is backgrounded. +// +// ponytail: beginBackgroundTask ceiling ~30s–few min; large uploads still suspend. +// True background continuation needs a background URLSession, which lives in the SDK +// (URLSessionConfiguration.default) — out of scope here (examples don't modify the SDK). +@MainActor +final class UploadManager: NSObject, ObservableObject { + + enum Phase: String { + case idle = "Idle" + case preparing = "Preparing…" + case uploading = "Uploading" + case paused = "Paused" + case completed = "Completed 🎉" + case failed = "Failed" + case cancelled = "Cancelled" + } + + @Published private(set) var phase: Phase = .idle + @Published private(set) var progress: Double = 0 // 0…1 + @Published private(set) var fileName = "" + @Published private(set) var log: [String] = [] + + var isActive: Bool { phase == .preparing || phase == .uploading || phase == .paused } + var canPause: Bool { phase == .uploading } + var canResume: Bool { phase == .paused } + + private let uploader = Uploads() + private var bgTask: UIBackgroundTaskIdentifier = .invalid + + override init() { + super.init() + uploader.delegate = self + uploader.progressDelegate = self + uploader.errorDelegate = self + } + + // MARK: - Actions + + func start(fileURL: URL) { + guard !isActive else { return } + fileName = fileURL.lastPathComponent + progress = 0 + log = [] + phase = .preparing + beginBackgroundTask() + append("Requesting signed upload URL…") + + Task { + do { + let signed = try await FastPixAPI.createUpload() + append("Got signed URL. Starting upload of \(fileName).") + phase = .uploading + uploader.uploadFile(file: fileURL, endpoint: signed.url) + } catch { + fail(error.localizedDescription) + } + } + } + + func pause() { if canPause { uploader.pause() } } + func resume() { if canResume { uploader.resume() } } + func cancel() { + guard isActive else { return } + uploader.abort() + phase = .cancelled + endBackgroundTask() + } + + // MARK: - Helpers + + private func append(_ line: String) { log.append(line) } + + private func fail(_ message: String) { + append("Error: \(message)") + phase = .failed + endBackgroundTask() + } + + private func succeed() { + progress = 1 + append("Upload complete.") + phase = .completed + endBackgroundTask() + } + + private func beginBackgroundTask() { + endBackgroundTask() + bgTask = UIApplication.shared.beginBackgroundTask(withName: "FastPixUpload") { [weak self] in + // iOS is about to reclaim the time it granted us. + self?.endBackgroundTask() + } + } + + private func endBackgroundTask() { + guard bgTask != .invalid else { return } + UIApplication.shared.endBackgroundTask(bgTask) + bgTask = .invalid + } +} + +// MARK: - SDK delegates + +extension UploadManager: UploadsDelegate, UploadProgressDelegate, UploadSDKErrorDelegate { + + nonisolated func uploads(_ uploads: Uploads, didEmit event: UploadEvent) { + // SDK emits on the main thread already; hop to the actor for @Published safety. + Task { @MainActor in + switch event { + case .progress(let p): + self.progress = Double(p) + if self.phase == .uploading, p >= 0.999 { + // The SDK only fires .uploadsuccess for single-chunk uploads; + // treat full progress as done so multi-chunk uploads finish too. + self.succeed() + } + case .uploadsuccess: + self.succeed() + case .pause: + self.phase = .paused + case .resume: + self.phase = .uploading + case .chunkSuccess(let n, let total): + self.append("Chunk \(n)/\(total) uploaded.") + case .offline: + self.append("Network offline — waiting…") + case .online: + self.append("Network back online.") + case .chunkAttemptFailure(let n, let total, _, let attempt): + self.append("Chunk \(n)/\(total) retry (attempt \(attempt))…") + case .error(let error): + self.fail(error.localizedDescription) + case .chunkAttempt: + break + } + } + } + + nonisolated func didUpdateProgressText(_ text: String) {} + + nonisolated func uploadSDKDidFail(with error: String) { + // The SDK reports both hard failures and transient status text here + // (e.g. "Network is Online"); log it without forcing a failed state. + Task { @MainActor in self.append(error) } + } +} diff --git a/example/README.md b/examples/uikit/README.md similarity index 100% rename from example/README.md rename to examples/uikit/README.md diff --git a/example/UploadExample.xcodeproj/project.pbxproj b/examples/uikit/UploadExample.xcodeproj/project.pbxproj similarity index 100% rename from example/UploadExample.xcodeproj/project.pbxproj rename to examples/uikit/UploadExample.xcodeproj/project.pbxproj diff --git a/examples/uikit/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/uikit/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/examples/uikit/UploadExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/uikit/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to examples/uikit/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/examples/uikit/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 100% rename from example/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to examples/uikit/UploadExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/example/UploadExample/AppDelegate.swift b/examples/uikit/UploadExample/AppDelegate.swift similarity index 100% rename from example/UploadExample/AppDelegate.swift rename to examples/uikit/UploadExample/AppDelegate.swift diff --git a/example/UploadExample/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/uikit/UploadExample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from example/UploadExample/Assets.xcassets/AccentColor.colorset/Contents.json rename to examples/uikit/UploadExample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/example/UploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/uikit/UploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/UploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to examples/uikit/UploadExample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/UploadExample/Assets.xcassets/Contents.json b/examples/uikit/UploadExample/Assets.xcassets/Contents.json similarity index 100% rename from example/UploadExample/Assets.xcassets/Contents.json rename to examples/uikit/UploadExample/Assets.xcassets/Contents.json diff --git a/example/UploadExample/Info.plist b/examples/uikit/UploadExample/Info.plist similarity index 100% rename from example/UploadExample/Info.plist rename to examples/uikit/UploadExample/Info.plist diff --git a/example/UploadExample/SceneDelegate.swift b/examples/uikit/UploadExample/SceneDelegate.swift similarity index 100% rename from example/UploadExample/SceneDelegate.swift rename to examples/uikit/UploadExample/SceneDelegate.swift diff --git a/example/UploadExample/UploadViewController.swift b/examples/uikit/UploadExample/UploadViewController.swift similarity index 100% rename from example/UploadExample/UploadViewController.swift rename to examples/uikit/UploadExample/UploadViewController.swift