From 01501e5b1b8920b8c0f52e8fc0cbe003b09a56c0 Mon Sep 17 00:00:00 2001
From: Stephen B <158498287+StephenDev0@users.noreply.github.com>
Date: Mon, 8 Dec 2025 21:20:09 -0500
Subject: [PATCH] Remove continued processing, add PiP support
Eliminates all continued background processing logic and related settings, including the ContinuedProcessingManager and its usage. Adds Picture in Picture (PiP) support for script execution using the Pipify package, updates settings and UI to reflect PiP instead of continued processing, and refactors related code paths to use PiP for script execution feedback.
---
DebugWidget/DebugWidgetBundle.swift | 2 +-
StikDebug.xcodeproj/project.pbxproj | 17 ++
.../xcshareddata/swiftpm/Package.resolved | 11 +-
StikJIT/Info.plist | 5 -
StikJIT/JSSupport/RunJSView.swift | 52 ++---
StikJIT/StikJITApp.swift | 15 +-
.../ContinuedProcessingManager.swift | 177 ------------------
StikJIT/Utilities/Extensions.swift | 2 -
StikJIT/Views/HomeView.swift | 15 +-
StikJIT/Views/SettingsView.swift | 32 +---
10 files changed, 67 insertions(+), 261 deletions(-)
delete mode 100644 StikJIT/Utilities/ContinuedProcessingManager.swift
diff --git a/DebugWidget/DebugWidgetBundle.swift b/DebugWidget/DebugWidgetBundle.swift
index 10b39be..77bf06e 100644
--- a/DebugWidget/DebugWidgetBundle.swift
+++ b/DebugWidget/DebugWidgetBundle.swift
@@ -11,7 +11,7 @@ import SwiftUI
@main
struct StikDebugWidgetBundle: WidgetBundle {
var body: some Widget {
- // Both widgets enabled: Favorites uses enable-jit URL scheme (with continued processing handled in-app),
+ // Both widgets enabled: Favorites uses enable-jit URL scheme (with PiP/script handled in-app),
// System Apps uses launch-app URL scheme for non-debug launch behavior.
FavoritesWidget()
SystemAppsWidget()
diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj
index 966ad40..85f47b5 100644
--- a/StikDebug.xcodeproj/project.pbxproj
+++ b/StikDebug.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; };
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */; };
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; };
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; };
@@ -153,6 +154,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 17C744F02E20BED000834F17 /* Pipify in Frameworks */,
DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */,
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */,
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */,
@@ -270,6 +272,7 @@
packageProductDependencies = (
68D569BD2E1B415700A5BA36 /* CodeEditorView */,
68D569BF2E1B415700A5BA36 /* LanguageSupport */,
+ 17C744EF2E20BED000834F17 /* Pipify */,
DCBA85852E3897BD00E88C06 /* StikImporter */,
68E714E52E6AA2B00025610F /* ZIPFoundation */,
);
@@ -387,6 +390,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */,
+ 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */,
DCBA85842E3897BD00E88C06 /* XCRemoteSwiftPackageReference "StikImporter" */,
68E714E42E6AA2B00025610F /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
);
@@ -997,6 +1001,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
+ 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/hugeBlack/swiftui-pipify";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchakravarty/CodeEditorView";
@@ -1024,6 +1036,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ 17C744EF2E20BED000834F17 /* Pipify */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */;
+ productName = Pipify;
+ };
68D569BD2E1B415700A5BA36 /* CodeEditorView */ = {
isa = XCSwiftPackageProductDependency;
package = 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */;
diff --git a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index ce50db7..7da08b0 100644
--- a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "0c22ab9d76f71ac04ee3ae6420154b1551511330120649c0ec71bbeb02f33fd5",
+ "originHash" : "ac6355f0d394d08e98cd8bc09669552151ca717b83a414b5708b301c9ba767e4",
"pins" : [
{
"identity" : "codeeditorview",
@@ -28,6 +28,15 @@
"version" : "1.0.2"
}
},
+ {
+ "identity" : "swiftui-pipify",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/hugeBlack/swiftui-pipify",
+ "state" : {
+ "branch" : "main",
+ "revision" : "a1ec2fd1781c8289bff1a8b3f664dcf21c910efb"
+ }
+ },
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
diff --git a/StikJIT/Info.plist b/StikJIT/Info.plist
index aabccb5..28a008c 100644
--- a/StikJIT/Info.plist
+++ b/StikJIT/Info.plist
@@ -15,14 +15,9 @@
- BGTaskSchedulerPermittedIdentifiers
-
- $(PRODUCT_BUNDLE_IDENTIFIER).continuedProcessingTask.script
-
UIBackgroundModes
audio
- processing
ITSAppUsesNonExemptEncryption
diff --git a/StikJIT/JSSupport/RunJSView.swift b/StikJIT/JSSupport/RunJSView.swift
index b55a1a0..8e994e7 100644
--- a/StikJIT/JSSupport/RunJSView.swift
+++ b/StikJIT/JSSupport/RunJSView.swift
@@ -20,8 +20,6 @@ class RunJSViewModel: ObservableObject {
var debugProxy: OpaquePointer?
var remoteServer: OpaquePointer?
var semaphore: dispatch_semaphore_t?
- private var progressTimer: DispatchSourceTimer?
- private var reportedProgress: Double = 0
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
self.pid = pid
@@ -37,7 +35,6 @@ class RunJSViewModel: ObservableObject {
func runScript(data: Data, name: String? = nil) throws {
let scriptContent = String(data: data, encoding: .utf8)
scriptName = name ?? "Script"
- startContinuedProcessing(withTitle: scriptName)
let getPidFunction: @convention(block) () -> Int = {
return self.pid
@@ -91,10 +88,8 @@ class RunJSViewModel: ObservableObject {
if let exception = self.context?.exception {
self.logs.append(exception.debugDescription)
}
- let success = self.context?.exception == nil && !self.executionInterrupted
- self.stopContinuedProcessing(success: success)
self.logs.append("Script Execution Completed")
- self.logs.append("Background processing finished. You can dismiss this view.")
+ self.logs.append("You are safe to close the PIP Window.")
}
}
@@ -206,37 +201,26 @@ class RunJSViewModel: ObservableObject {
guard let context else { return }
context.exception = JSValue(object: message, in: context)
}
+}
+
+struct RunJSViewPiP: View {
+ @Binding var model: RunJSViewModel?
+ @State private var logs: [String] = []
+ private let timer = Timer.publish(every: 0.034, on: .main, in: .common).autoconnect()
- private func startContinuedProcessing(withTitle title: String) {
- guard ContinuedProcessingManager.shared.isSupported,
- UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return }
- stopProgressTimer()
- reportedProgress = 0.05
- ContinuedProcessingManager.shared.begin(title: title, subtitle: "Script execution in progress")
- ContinuedProcessingManager.shared.updateProgress(reportedProgress)
- let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
- timer.schedule(deadline: .now() + 5, repeating: 5)
- timer.setEventHandler { [weak self] in
- guard let self else { return }
- self.reportedProgress = min(0.9, self.reportedProgress + 0.1)
- ContinuedProcessingManager.shared.updateProgress(self.reportedProgress)
- if self.reportedProgress >= 0.9 {
- self.stopProgressTimer()
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(logs.suffix(6).indices, id: \.self) { index in
+ Text(logs.suffix(6)[index])
+ .font(.system(size: 12))
+ .foregroundStyle(.white)
}
}
- timer.resume()
- progressTimer = timer
- }
-
- private func stopContinuedProcessing(success: Bool) {
- stopProgressTimer()
- ContinuedProcessingManager.shared.updateProgress(1.0)
- ContinuedProcessingManager.shared.finish(success: success)
- }
-
- private func stopProgressTimer() {
- progressTimer?.cancel()
- progressTimer = nil
+ .padding()
+ .onReceive(timer) { _ in
+ logs = model?.logs ?? []
+ }
+ .frame(width: 300, height: 150)
}
}
diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift
index 2323148..4329230 100644
--- a/StikJIT/StikJITApp.swift
+++ b/StikJIT/StikJITApp.swift
@@ -15,17 +15,9 @@ private func registerAdvancedOptionsDefault() {
let os = ProcessInfo.processInfo.operatingSystemVersion
// Enable advanced options by default on iOS 19/26 and above
let enabled = os.majorVersion >= 19
- let defaults = UserDefaults.standard
- defaults.register(defaults: ["enableAdvancedOptions": enabled])
- defaults.register(defaults: [UserDefaults.Keys.txmOverride: false])
- if defaults.object(forKey: UserDefaults.Keys.enableContinuedProcessing) == nil {
- let legacyPiP = defaults.object(forKey: "enablePiP") as? Bool
- if let legacyPiP {
- defaults.set(legacyPiP, forKey: UserDefaults.Keys.enableContinuedProcessing)
- } else {
- defaults.register(defaults: [UserDefaults.Keys.enableContinuedProcessing: enabled])
- }
- }
+ UserDefaults.standard.register(defaults: ["enableAdvancedOptions": enabled])
+ UserDefaults.standard.register(defaults: ["enablePiP": enabled])
+ UserDefaults.standard.register(defaults: [UserDefaults.Keys.txmOverride: false])
}
// MARK: - Welcome Sheet
@@ -527,7 +519,6 @@ struct HeartbeatApp: App {
init() {
registerAdvancedOptionsDefault()
- ContinuedProcessingManager.shared.configureIfNeeded()
newVerCheck()
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
diff --git a/StikJIT/Utilities/ContinuedProcessingManager.swift b/StikJIT/Utilities/ContinuedProcessingManager.swift
deleted file mode 100644
index ac86cac..0000000
--- a/StikJIT/Utilities/ContinuedProcessingManager.swift
+++ /dev/null
@@ -1,177 +0,0 @@
-//
-// ContinuedProcessingManager.swift
-// StikJIT
-//
-// Created by Codex on 11/20/24.
-//
-
-import Foundation
-import BackgroundTasks
-
-final class ContinuedProcessingManager {
- static let shared = ContinuedProcessingManager()
- private let handler: ContinuedProcessingHandling
-
- private init() {
- if #available(iOS 26.0, *) {
- handler = ModernContinuedProcessingHandler()
- } else {
- handler = NoopContinuedProcessingHandler()
- }
- }
-
- var isSupported: Bool { handler.isSupported }
-
- func configureIfNeeded() {
- handler.configureIfNeeded()
- }
-
- func cancelPendingTasks() {
- handler.cancelPendingTasks()
- }
-
- func begin(title: String, subtitle: String) {
- handler.begin(title: title, subtitle: subtitle)
- }
-
- func updateProgress(_ fraction: Double) {
- handler.updateProgress(fraction)
- }
-
- func finish(success: Bool) {
- handler.finish(success: success)
- }
-}
-
-private protocol ContinuedProcessingHandling: AnyObject {
- var isSupported: Bool { get }
- func configureIfNeeded()
- func cancelPendingTasks()
- func begin(title: String, subtitle: String)
- func updateProgress(_ fraction: Double)
- func finish(success: Bool)
-}
-
-private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling {
- var isSupported: Bool { false }
- func configureIfNeeded() {}
- func cancelPendingTasks() {}
- func begin(title: String, subtitle: String) {}
- func updateProgress(_ fraction: Double) {}
- func finish(success: Bool) {}
-}
-
-@available(iOS 26.0, *)
-private final class ModernContinuedProcessingHandler: ContinuedProcessingHandling {
- private let scheduler = BGTaskScheduler.shared
- private let taskIdentifier: String
- private var didRegister = false
- private var activeTask: BGContinuedProcessingTask?
- private let queue = DispatchQueue(label: "com.stikdebug.continuedProcessing",
- qos: .utility)
- private var pendingMetadata: (title: String, subtitle: String)?
-
- init() {
- let bundleID = Bundle.main.bundleIdentifier ?? "com.stik.sj"
- taskIdentifier = "\(bundleID).continuedProcessingTask.script"
- }
-
- var isSupported: Bool { true }
-
- func configureIfNeeded() {
- guard !didRegister else { return }
- scheduler.register(forTaskWithIdentifier: taskIdentifier, using: nil) { [weak self] task in
- guard let continuedTask = task as? BGContinuedProcessingTask else {
- task.setTaskCompleted(success: false)
- return
- }
- self?.handle(task: continuedTask)
- }
- didRegister = true
- }
-
- func begin(title: String, subtitle: String) {
- guard UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return }
- configureIfNeeded()
- var reserved = false
- queue.sync {
- if activeTask == nil && pendingMetadata == nil {
- pendingMetadata = (title: title, subtitle: subtitle)
- reserved = true
- }
- }
- guard reserved else { return }
- // Clear any stale request that might block new submissions.
- scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
- let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier,
- title: title,
- subtitle: subtitle)
- request.strategy = .queue
- do {
- try scheduler.submit(request)
- LogManager.shared.addInfoLog("Requested continued processing: \(title)")
- } catch {
- LogManager.shared.addWarningLog("Unable to request continued processing: \(error.localizedDescription)")
- queue.async { [weak self] in
- self?.pendingMetadata = nil
- }
- }
- }
-
- func cancelPendingTasks() {
- queue.async { [weak self] in
- guard let self else { return }
- if let task = activeTask {
- task.setTaskCompleted(success: false)
- activeTask = nil
- }
- pendingMetadata = nil
- scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
- }
- }
-
- func updateProgress(_ fraction: Double) {
- queue.async { [weak self] in
- guard let task = self?.activeTask else { return }
- let clamped = max(0.0, min(1.0, fraction))
- task.progress.totalUnitCount = max(task.progress.totalUnitCount, 100)
- task.progress.completedUnitCount = Int64(Double(task.progress.totalUnitCount) * clamped)
- }
- }
-
- func finish(success: Bool) {
- queue.async { [weak self] in
- guard let self else { return }
- if let task = self.activeTask {
- task.progress.completedUnitCount = task.progress.totalUnitCount
- task.setTaskCompleted(success: success)
- self.activeTask = nil
- } else if pendingMetadata != nil {
- scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
- }
- pendingMetadata = nil
- }
- }
-
- private func handle(task: BGContinuedProcessingTask) {
- queue.async { [weak self] in
- guard let self else { return }
- activeTask = task
- if let metadata = pendingMetadata {
- task.updateTitle(metadata.title, subtitle: metadata.subtitle)
- }
- if task.progress.totalUnitCount == 0 {
- task.progress.totalUnitCount = 100
- }
- task.progress.completedUnitCount = 1
- task.expirationHandler = { [weak self] in
- self?.handleExpiration()
- }
- }
- }
-
- private func handleExpiration() {
- LogManager.shared.addWarningLog("Continued processing expired early")
- finish(success: false)
- }
-}
diff --git a/StikJIT/Utilities/Extensions.swift b/StikJIT/Utilities/Extensions.swift
index 4104bc8..234eeeb 100644
--- a/StikJIT/Utilities/Extensions.swift
+++ b/StikJIT/Utilities/Extensions.swift
@@ -23,8 +23,6 @@ extension UserDefaults {
enum Keys {
/// Forces the app to treat the current device as TXM-capable so scripts always run.
static let txmOverride = "overrideTXMForScripts"
- /// Controls whether BGContinuedProcessingTask should be used to keep scripts alive in the background.
- static let enableContinuedProcessing = "enableContinuedProcessing"
/// Tracks whether an external device profile is currently active.
static let usingExternalDevice = "UsingExternalDevice"
}
diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift
index fa8a023..6b494d8 100644
--- a/StikJIT/Views/HomeView.swift
+++ b/StikJIT/Views/HomeView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import UniformTypeIdentifiers
+import Pipify
import UIKit
import WidgetKit
import Combine
@@ -49,7 +50,9 @@ struct HomeView: View {
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false
+ @AppStorage("enablePiP") private var enablePiP = true
@State var scriptViewShow = false
+ @State private var pipRequired = false
@AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js"
@State var jsModel: RunJSViewModel?
@@ -298,6 +301,12 @@ struct HomeView: View {
triggeredByURLScheme: false)
}
}
+ .pipify(isPresented: Binding(
+ get: { pipRequired && enablePiP },
+ set: { pipRequired = $0 }
+ )) {
+ RunJSViewPiP(model: $jsModel)
+ }
.sheet(isPresented: $scriptViewShow) {
NavigationView {
if let jsModel {
@@ -1496,6 +1505,9 @@ struct HomeView: View {
if ProcessInfo.processInfo.hasTXM, let sd = scriptData {
callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script")
if triggeredByURLScheme { usleep(500000) }
+ pipRequired = true
+ } else {
+ pipRequired = false
}
let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } }
@@ -1518,8 +1530,9 @@ struct HomeView: View {
}
}
isProcessing = false
- }
+ pipRequired = false
}
+ }
private func launchSystemApp(item: SystemPinnedItem) {
guard !launchingSystemApps.contains(item.bundleID) else { return }
diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift
index 26b0c66..ad344aa 100644
--- a/StikJIT/Views/SettingsView.swift
+++ b/StikJIT/Views/SettingsView.swift
@@ -13,7 +13,7 @@ struct SettingsView: View {
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false
@AppStorage("enableAdvancedBetaOptions") private var enableAdvancedBetaOptions = false
@AppStorage("enableTesting") private var enableTesting = false
- @AppStorage(UserDefaults.Keys.enableContinuedProcessing) private var enableContinuedProcessing = false
+ @AppStorage("enablePiP") private var enablePiP = false
@AppStorage(UserDefaults.Keys.txmOverride) private var overrideTXMDetection = false
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@@ -429,7 +429,8 @@ struct SettingsView: View {
.font(.headline)
.foregroundColor(.primary)
- continuedProcessingToggle
+ Toggle("Picture in Picture", isOn: $enablePiP)
+ .tint(accentColor)
Toggle(isOn: $overrideTXMDetection) {
VStack(alignment: .leading, spacing: 2) {
Text("Always Run Scripts")
@@ -444,7 +445,7 @@ struct SettingsView: View {
}
.onChange(of: enableAdvancedOptions) { _, newValue in
if !newValue {
- enableContinuedProcessing = false
+ enablePiP = false
enableAdvancedBetaOptions = false
enableTesting = false
}
@@ -454,13 +455,6 @@ struct SettingsView: View {
enableTesting = false
}
}
- .onChange(of: enableContinuedProcessing) { _, newValue in
- if newValue {
- ContinuedProcessingManager.shared.configureIfNeeded()
- } else {
- ContinuedProcessingManager.shared.cancelPendingTasks()
- }
- }
}
}
@@ -516,24 +510,6 @@ struct SettingsView: View {
}
}
- private var continuedProcessingToggle: some View {
- Group {
- if ContinuedProcessingManager.shared.isSupported {
- Toggle("Allow Continued Processing", isOn: $enableContinuedProcessing)
- .tint(accentColor)
- } else {
- VStack(alignment: .leading, spacing: 4) {
- Toggle("Allow Continued Processing", isOn: .constant(false))
- .tint(accentColor)
- .disabled(true)
- Text("Requires iOS 26 or later.")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
- }
- }
-
private var helpCard: some View {
glassCard {
VStack(alignment: .leading, spacing: 14) {