Pavlos
07/05/2025, 11:08 AMBGAppRefreshTaskRequest
and a BGProcessingTaskRequest
), triggering it via //e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"{my_work_name}"]
works successfully. It just doesn't run by itself at all, I have the app running in 2 iPhones for 3+ days, with opening the app every day a lot of times, nothing.
Also nothing significant is being logged at Console.app in my Mac, the work just never starts, there aren't any exceptions or crashes at all. Also every setting is enabled successfully at both phones. It just doesn't run. If the iOS enforces that "because of low usage", then how is a new installer of my app supposed to use this functionality at all?
I would love your thoughts and if anyone has made it work:
Code continues in the thread 🧵👇Pavlos
07/05/2025, 11:08 AMstruct ComposeView: UIViewControllerRepresentable {
func registerBackgroundTasks() {
print("[Network Checker]: Attempting to register background tasks")
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.bluesky.followers.analyzer.apprefresh",
using: nil
) { task in
print("[Network Checker] Swift handler triggered for app refresh task")
// Call your Kotlin handler here
NotificationService_iosKt.handleAppRefreshTask(task: task)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.bluesky.followers.analyzer.processing",
using: nil
) { task in
print("[Network Checker] Swift handler triggered for processing task")
// Call your Kotlin handler here
NotificationService_iosKt.handleProcessingTask(task: task)
}
print("[Network Checker]: Registered background tasks")
}
func makeUIViewController(context: Context) -> UIViewController {
registerBackgroundTasks()
return MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}
Pavlos
07/05/2025, 11:08 AMPavlos
07/05/2025, 11:10 AMprivate const val BACKGROUND_APP_REFRESH_TASK =
"com.bluesky.followers.analyzer.apprefresh"
private const val BACKGROUND_PROCESSING_TASK =
"com.bluesky.followers.analyzer.processing"
actual fun showLocalNotification(title: String, message:
...
...
...
}
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
actual fun startNetworkCheckerWorker(hoursInterval: Long?) {
println("[Network Checker]: Start to initial submit the background tasks")
startAppRefreshTask()
startProcessingTask()
}
/**
* Gets triggered via swift code.
*/
fun handleAppRefreshTask(task: BGTask) {
val scope = CoroutineScope(Dispatchers.Default)
/**
* Set expiration handler
*/
task.expirationHandler = {
println("[Network Checker]: App Refresh task expired")
scope.cancel()
task.setTaskCompletedWithSuccess(false)
}
scope.launch {
try {
checkForFollowerChanges(20.seconds)
/**
* Reschedule the next background task using the user's selected repeat hours
*/
startAppRefreshTask()
task.setTaskCompletedWithSuccess(true)
} catch (e: Exception) {
Firebase.crashlytics.recordException(e)
task.setTaskCompletedWithSuccess(false)
}
}
}
/**
* Gets triggered via swift code.
*/
fun handleProcessingTask(task: BGTask) {
val scope = CoroutineScope(Dispatchers.Default)
/**
* Set expiration handler
*/
task.expirationHandler = {
println("[Network Checker]: Processing task expired")
scope.cancel()
task.setTaskCompletedWithSuccess(false)
}
scope.launch {
try {
checkForFollowerChanges(5.minutes)
/**
* Reschedule the next background task using the user's selected repeat hours
*/
startProcessingTask()
task.setTaskCompletedWithSuccess(true)
} catch (e: Exception) {
Firebase.crashlytics.recordException(e)
task.setTaskCompletedWithSuccess(false)
}
}
}
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun startAppRefreshTask() {
println("[Network Checker]: Start to submit the app refresh task")
/**
* Scheduled EVERY 3 HOUR if nothing else defined.
*/
val request = BGAppRefreshTaskRequest(BACKGROUND_APP_REFRESH_TASK).apply {
val futureTime =
NSDate().timeIntervalSinceReferenceDate + (DEFAULT_SMALL_INTERVAL * 3600.0)
earliestBeginDate = NSDate(timeIntervalSinceReferenceDate = futureTime)
}
println("[Network Checker]: BGAppRefreshTaskRequest was created - begin date: ${request.earliestBeginDate}")
val scheduler = BGTaskScheduler.sharedScheduler()
/**
* Check if a same task is already scheduled before submitting
*/
scheduler.getPendingTaskRequestsWithCompletionHandler { tasks ->
val hasPendingAppRefreshTask =
tasks?.any { it is BGAppRefreshTaskRequest } == true
if (hasPendingAppRefreshTask) {
val task =
tasks.find { it is BGAppRefreshTaskRequest } as? BGAppRefreshTaskRequest
println(
"[Network Checker]: App Refresh Task already scheduled, " +
"with identifier: ${task?.identifier}} - " +
"with description: ${task?.earliestBeginDate}} - " +
"with earliest begin date: ${task?.earliestBeginDate}} " +
"- Skipping submission"
)
} else {
println("[Network Checker]: No pending App Refresh Task found, submitting new one")
/**
* Submit new requests
*/
val errorPtr = nativeHeap.alloc<ObjCObjectVar<NSError?>>()
.apply { value = null }
scheduler.submitTaskRequest(request, errorPtr.ptr)
errorPtr.value?.let { error ->
println("[Network Checker]: Scheduler error for app refresh ${error.localizedDescription}")
Firebase.crashlytics.recordException(
Exception(
"[Network Checker]: Scheduler error for app refresh ${error.localizedDescription}"
)
)
}
nativeHeap.free(errorPtr)
println("[Network Checker]: Submitted new app refresh task...")
/**
* Verify the task was rescheduled
*/
scheduler.getPendingTaskRequestsWithCompletionHandler { tasks ->
val hasNewAppRefreshTask =
tasks?.any { it is BGAppRefreshTaskRequest } == true
if (hasNewAppRefreshTask) {
println("[Network Checker]: App Refresh Task successfully scheduled")
} else {
println("[Network Checker]: WARNING: No pending App Refresh Task found after submission!")
}
}
}
}
}
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun startProcessingTask() {
println("[Network Checker]: Start to submit the processing task")
/**
* Scheduled EVERY 6 HOUR if nothing else defined.
*/
val request = BGProcessingTaskRequest(BACKGROUND_PROCESSING_TASK).apply {
val futureTime =
NSDate().timeIntervalSinceReferenceDate + (DEFAULT_BIG_INTERVAL * 3600.0)
earliestBeginDate = NSDate(timeIntervalSinceReferenceDate = futureTime)
}
println("[Network Checker]: BGProcessingTaskRequest was created - begin date: ${request.earliestBeginDate}")
val scheduler = BGTaskScheduler.sharedScheduler()
/**
* Check if a same task is already scheduled before submitting
*/
scheduler.getPendingTaskRequestsWithCompletionHandler { tasks ->
val hasPendingProcessingTask =
tasks?.any { it is BGProcessingTaskRequest } == true
if (hasPendingProcessingTask) {
val task =
tasks.find { it is BGProcessingTaskRequest } as? BGProcessingTaskRequest
println(
"[Network Checker]: Processing Task already scheduled, " +
"with identifier: ${task?.identifier}} - " +
"with description: ${task?.earliestBeginDate}} - " +
"with earliest begin date: ${task?.earliestBeginDate}} " +
"- Skipping submission"
)
} else {
println("[Network Checker]: No pending Processing Task found, submitting new one")
/**
* Submit new requests
*/
val errorPtr = nativeHeap.alloc<ObjCObjectVar<NSError?>>()
.apply { value = null }
scheduler.submitTaskRequest(request, errorPtr.ptr)
errorPtr.value?.let { error ->
println("[Network Checker]: Scheduler error for processing: ${error.localizedDescription}")
Firebase.crashlytics.recordException(
Exception(
"[Network Checker]: Scheduler error for processing: ${error.localizedDescription}"
)
)
}
nativeHeap.free(errorPtr)
println("[Network Checker]: Submitted new processing task...")
/**
* Verify the task was rescheduled
*/
scheduler.getPendingTaskRequestsWithCompletionHandler { tasks ->
val hasNewProcessingTask =
tasks?.any { it is BGProcessingTaskRequest } == true
if (hasNewProcessingTask) {
println("[Network Checker]: Processing Task successfully scheduled")
} else {
println("[Network Checker]: WARNING: No pending Processing Task found after submission!")
}
}
}
}
}
private suspend fun checkForFollowerChanges(maxExecutionTime: Duration) {
...
...
...
}