Background task in iOS action extension
Asked Answered
E

4

15

I'm working on an action extension for an app which prepares some data for the user and then uses MailCore2 to send this information to a SMTP server. Preparing data is done very fast, but sending the email might take some time (depending on its size). That's why I'm looking for a way to handle the sending activities in the background. But this results in some problems with different solutions:

  1. Using URLSession. This is the first way to go for handling big uploads or downloads in an iOS extension. But the problem is that MailCore2 does not use URLSessions to transmit the data to the mail server. So this is not usable. Or is it possible to somehow 'wrap' this call in an URLSession?

  2. Using UNUserNotificationCenter and send a local notification from the extension after data preparation. The idea is to start the sending task in the main app after receiving this notification. Problem: the notifications userNotificationCenter didReceive method is only called, when the user clicks the notification. A simple badge notification does not call the delegate method.

  3. Using Background Fetch. This is working fine in the simulator, but on an iOS device there is an error when connecting to the SMTP server. I came across the same HELO problem as described in github.com/MailCore/mailcore2/issues/252. For this issue, a solution should be MCOSMTPSession.isUseHeloIPEnabled = true, but this might not work for all servers. So also not a perfect solution.

Any ideas, how to handle such a task? Or how to deal with one of the solutions mentioned above?

Edit: as this question does not receive any answers yet: what is unclear or which additional information is required?

Ephrayim answered 5/6, 2018 at 6:6 Comment(8)
Hi, I think what you are doing here can answer my question. Can you please share your experience?Cervicitis
Why not use MCOAttachment to send the attachement with your email? And setup your app to work in the background.Ways
How do you setup an app to work in the background? I already use MCOAttachment. And I tried to use one of the three solutions mentioned in the question. All of them are my attempt to realise a background activity. But none of them leads to a working results.Ephrayim
Question, is your original app in the background while extension is running?Canst
No the app is not running in the background (or at least, it's not guaranteed). The extension can access all the code/libs which is required to handle its task. The question is: how to run some of this code (= sending the mail) in the background, so that the user does not need to wait for the completion.Ephrayim
Why don't you make the sending of the mail using the containing app? The extension could let the containing app know that it has to send an email with the required data using an App Group, this implies that the app is running - but you can also start the containing app from extension if you want. Hope it helps!Anselm
@MihaiErős with "starting the containing app" you mean open the app in the foreground, right? do you know if it's possible to launch the app in the background without being visible?Ephrayim
@Ephrayim I am afraid opening the app in the background without being visible doesn't seem to be something that Apple would agree with, even if it is possible to be done, which I don't think either.Anselm
E
3

After many tests and failures, I found the following solution to execute a long performing task in the background of an extension. This works as expected, even if the extension is already finished:

func performTask()
{
    // Perform the task in background.
    let processinfo = ProcessInfo()
    processinfo.performExpiringActivity(withReason: "Long task") { (expired) in
        if (!expired) {
            // Run task synchronously.
            self.performLongTask()
        }
        else {
            // Cancel task.
            self.cancelLongTask()
        }
    }
}

This code uses ProcessInfo.performExpiringActivity() to execute the task in another thread. It’s important that the task in performLongTask() is executed synchronously. When the end of the block is reached, the thread will terminate and end the execution of your task.

A similar approach is also working in the main app. It's described in detail in a small summary for background tasks in iOS.

Ephrayim answered 31/3, 2020 at 7:45 Comment(4)
As for a Share Extension, what would the self reference above refer to that contains the function? Should I use ```class ShareViewController: UIViewController or should I create my own class without inherting from any Foundation objects?Vday
I should ask if this would work for any App Extension or at least for the Share Extension.Vday
For me, this works for a Share Extension as well as for an Action Extension. For both I use a custom class inheriting from UIViewController like you already mentioned class ShareViewController: UIViewController. So self should refer to your class object.Ephrayim
It appears as though you are not using BGTaskScheduler at all. Am I correct? I was thinking I was supposed to schedule the code in my Share Extension and the code for the background task performed would be in the main Target of the Containing App. I'm still going through this stackoverflow question completely. I don't know if any of the other answers would or would not work. I don't know whether the task I want to perform would require BGProcessingTask according to Apple's design intentions, but that's what I'm trying at the first.Vday
L
4

It seems like you can't schedule long running running tasks in background from an extension.Refer here(Some APIs Are Unavailable to App Extensions)

Among the solutions you suggested,my suggestion would be to try using silent push notifications ,you can call an api at the preparing data stage,then sent a silent push from server and perform the background task when the silent push arrives.

Leilani answered 18/3, 2019 at 8:54 Comment(1)
I heart of silent notifications. But as the answer of Vyacheslav, it requires an additional server to handle the push notifications. Any ideas, if local notifications (like from the alarm clock) can also be silent?Ephrayim
E
3

After many tests and failures, I found the following solution to execute a long performing task in the background of an extension. This works as expected, even if the extension is already finished:

func performTask()
{
    // Perform the task in background.
    let processinfo = ProcessInfo()
    processinfo.performExpiringActivity(withReason: "Long task") { (expired) in
        if (!expired) {
            // Run task synchronously.
            self.performLongTask()
        }
        else {
            // Cancel task.
            self.cancelLongTask()
        }
    }
}

This code uses ProcessInfo.performExpiringActivity() to execute the task in another thread. It’s important that the task in performLongTask() is executed synchronously. When the end of the block is reached, the thread will terminate and end the execution of your task.

A similar approach is also working in the main app. It's described in detail in a small summary for background tasks in iOS.

Ephrayim answered 31/3, 2020 at 7:45 Comment(4)
As for a Share Extension, what would the self reference above refer to that contains the function? Should I use ```class ShareViewController: UIViewController or should I create my own class without inherting from any Foundation objects?Vday
I should ask if this would work for any App Extension or at least for the Share Extension.Vday
For me, this works for a Share Extension as well as for an Action Extension. For both I use a custom class inheriting from UIViewController like you already mentioned class ShareViewController: UIViewController. So self should refer to your class object.Ephrayim
It appears as though you are not using BGTaskScheduler at all. Am I correct? I was thinking I was supposed to schedule the code in my Share Extension and the code for the background task performed would be in the main Target of the Containing App. I'm still going through this stackoverflow question completely. I don't know if any of the other answers would or would not work. I don't know whether the task I want to perform would require BGProcessingTask according to Apple's design intentions, but that's what I'm trying at the first.Vday
W
2

Judging from your response, your difficulties is that you need to allow background activities. You can do that by following a tutorial such as this one.

Ways answered 15/6, 2018 at 0:22 Comment(3)
Thank you for your answer! Background activities are already enabled for my app. I checked the tutorial. There are different background modes described. Two of them fit the contents/task I want to perform in the background: Executing Finite-Length Tasks - this one is not usable from an extension, because it requires access to UIApplication.shared. Another one is the Background fetch - this leads me to the problem I described in the question.Ephrayim
Did you find a solution to this issue ?Squill
@Squill check my answer above: https://mcmap.net/q/783987/-background-task-in-ios-action-extensionEphrayim
E
2

The most useful solution is using push notifications. The solution I used in my own application:

  1. Create a simple server which can retrieve a simple request: device token and time when you'd like to wake up your device.
  2. When the application goes to background mode send the request to the server to wake up the device later. E.g. 5 minutes/20 minutes, etc. func applicationWillEnterForeground(_ application: UIApplication) { inside AppDelegate.
  3. Don't forget to allow the application to work in the background as much as possible.

For example,

   @UIApplicationMain
    class AppDelegate: UIResponder {
    var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
    func application(_ application: UIApplication,
                         didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                         fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
            if UIApplication.shared.applicationState != .active {
                doFetch(completionHandler: completionHandler)
            } else {
                // foreground here
                completionHandler(UIBackgroundFetchResult.noData)
            }
        }
    func application(_ application: UIApplication,
                         performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
            doFetch(completionHandler: completionHandler)
        }
    private func doFetch(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    sendTheRequestToWakeApp()
    backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
                self?.endBackgroundTask()
            }
/// do work here
    completionHandler(UIBackgroundFetchResult.noData)
    }
    private func endBackgroundTask() {
            print("Background task ended.")
            UIApplication.shared.endBackgroundTask(backgroundTask)
            backgroundTask = UIBackgroundTaskInvalid
        }
    private func sendTheRequestToWakeApp() {
    /// Implement request using native library or Alamofire. etc.
    }
    }

on server side use simple time or loop.

Disadvantages,

  1. Internet is required
  2. It doesn't perfectly work. When battery is low the background mode is restricted.

Don't forget to setup the project:

proejct screenshot

Exhilarate answered 20/3, 2019 at 23:2 Comment(1)
It's a good idea. The only thing is the additional server which is required to send the push notifications. Currently it's a standalone app without additional requirements. Nevertheless, it may be possible to send the information to server using NSUrlSession. Do you know how NSUrlSession behaves when there is no internet connection? Will it retry or just quit the request?Ephrayim

© 2022 - 2024 — McMap. All rights reserved.