Background task not calling class in Swift
Asked Answered
P

3

5

I have a background task that should return an array of courses that is then sent to the apple watch. I'm calling the task inside didReceiveMessage via WatchConnectivity.

The background task needs to perform a few operations such as opening the realm database, querying the results and accessing the documents directory before returning the response to the courses dictionary. The logic seems to work as the watch outputs its getting the course data. The problem is that I don't think the background task is actually calling the method getWatchCourses()

DispatchQueue.global().async {

    var foundCourses = [[String : Any]]()
    let application = UIApplication.shared
    backgroundTask = application.beginBackgroundTask(withName: "app.test.getCourses") {
        let watchModel = WatchCourseModel()
        let courses = watchModel.getWatchCourses()
        print("Courses: \(courses)")
        foundCourses.append(contentsOf: courses)
    }
    replyHandler(["response": foundCourses])
    application.endBackgroundTask(backgroundTask)
    backgroundTask = UIBackgroundTaskInvalid
}

This also doesn't work if the getWatchCourses() result is hardcoded. Can the app perform this logic when in the background or should it work?

It's also worth pointing out that nowhere online has this documented, They always refer to sending simple text responses back to the watch, not something processor intensive :(

Thanks

Paediatrics answered 12/7, 2018 at 9:57 Comment(0)
G
4

You are doing your operation in trailing closure which is used to clean up. And more thing that you are ending background task as soon as it starts so your closure will never call.

Here is working example how the background task works

 var backgroundTaskIdentifier:UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
 var updateTimer: Timer?

Now when you start your operation for an example running timer

  updateTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self,
                                     selector: #selector(calculateNextNumber), userInfo: nil, repeats: true)
  // register background task
  beginBackgroundTask()

and when You end the timer end the background task; should always end background task without fail

func beginBackgroundTask () {
    backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
      self?.endBackgroundTask() // It will call to cleanup 
    })
    assert(backgroundTaskIdentifier != UIBackgroundTaskInvalid)
  }

  func endBackgroundTask () {
    UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
    backgroundTaskIdentifier = UIBackgroundTaskInvalid
  }

So In your case

  • Register for background task
  • Query your database (perform your all operations)
  • When you finish call end background task

Note: Also take note of the time limit given by apple to perform your background task

EDIT

Make sure you have enabled background capability form project settings

Try like

    beginBackgroundTask()

    let watchModel = WatchCourseModel()
    let courses = watchModel.getWatchCourses()
    print("Courses: \(courses)")
    foundCourses.append(contentsOf: courses)
    endBackgroundTask()
Gift answered 12/7, 2018 at 11:7 Comment(9)
So I place my business logic inside beginBackgroundTask?Paediatrics
You should start background task along with your business logic. And when you finish with it you should call end background taskGift
Confused, where would the let courses = watchModel.getWatchCourses() etc go? Inside beginBackgroundTask() or somewhere else?Paediatrics
Copy paste the method for background task start and end. And then check the editGift
Background capability is enabled, Would I need to select a specific option?Paediatrics
@Paediatrics Nope for this you don't need any option required to enableGift
Ok, Weird I've set breakpoints and none of them are being hit even if I return a simple string which the watch receives. Nothing on the iOS simulator app gets called etc, not even print statementsPaediatrics
@Paediatrics :( please try in actual device.Gift
Looks like Realm cant get the users profile from closed app, need to thread safe this but that's not the issue above. Will fix this first and then mark this the answer with the realm issue sorted. Thanks!Paediatrics
E
3

If you check the documentation you will see that the full function prototype is beginBackgroundTask(withName:expirationHandler:)

The code you have specified via the trailing closure is the expirationHandler - it is the code that is called if you don't call endBackgroundTask before the permitted background execution time is exhausted. The expiration handler gives you a final chance to perform any clean up before your app is terminated for exceeding its background time.

Since you almost immediately call endBackgroundTask and return from the function, the expiration handler won't be called.

What you actually want is something like:

var foundCourses = [[String : Any]]()
let application = UIApplication.shared
backgroundTask = application.beginBackgroundTask(withName: "app.test.getCourses") {
DispatchQueue.global().async {
    let watchModel = WatchCourseModel()
    let courses = watchModel.getWatchCourses()
    print("Courses: \(courses)")
    foundCourses.append(contentsOf: courses)
    replyHandler(["response": foundCourses])
    application.endBackgroundTask(backgroundTask)
    backgroundTask = UIBackgroundTaskInvalid
}

This starts a background task, you then perform the work asynchronously and pass the result back to the watch and end the background task.

Eternal answered 12/7, 2018 at 10:59 Comment(7)
This solution just hangs with no response from the phone.Paediatrics
You need to set a breakpoint in your app and see what it is doing; You haven't shown getWatchCourses so I can't say whether it is doing the right thing.Eternal
I've ran getWatchCourses on the App to test its result and it outputs into the correct response. I feel there's a config or something inside project settings I may be missing as it should work.Paediatrics
Also breakpoints wont work when testing watch app response from background iOS device - I don't think.Paediatrics
Yes, you just need to tell the Xcode debugger to attach to your app process by name.Eternal
Ok, breakpoint telling me that Realm can't perform a query. This has helped as the query could be run when app is in foreground. Need to solve thisPaediatrics
I would start by checking file protection classes. Maybe your realm file can5 be opened when the device is lockedEternal
N
1

You can create a background task by using QOS type as .background

DispatchQueue.global(qos: .background).async {
        //background code
        var foundCourses = [[String : Any]]()
        let application = UIApplication.shared
        backgroundTask = application.beginBackgroundTask(withName: "app.test.getCourses") {
            let watchModel = WatchCourseModel()
            let courses = watchModel.getWatchCourses()
            print("Courses: \(courses)")
            foundCourses.append(contentsOf: courses)
        }
        replyHandler(["response": foundCourses])
        DispatchQueue.main.async {
            //your main thread
        }
    }
Newtonnext answered 12/7, 2018 at 12:8 Comment(1)
This won't fix the problem as it still executes the required code in the expiration handler. Also, .background is not a recommended QoS; that priority level can be starved; it is best to use .utility as a minimum QoS.Eternal

© 2022 - 2024 — McMap. All rights reserved.