问题
I need to perform a task periodically, once a day to be exact, so I implemented background fetch with minimumBackgroundFetchInterval
of 23 hours. It gets done when I simulate background fetch with my app in the foreground.
But when my app is in the background only the application(_ application:, performFetchWithCompletionHandler)
method gets called like it should be, and the urlSession(_ session:, downloadTask:, didFinishDownloadingTo:)
method either doesn't get called at all or gets called and then paused at some random point in execution.When app gets back in the foreground it continues executing.
This happens both on simulator and on a device.
My code is below, with both of the above mentioned functions.
var sviBrojevi = EncodingKontakt(provjeri: [])
var completionHandler: (UIBackgroundFetchResult) -> Void = { result in
return
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let container = persistentContainer
let context = container.viewContext
sviBrojevi = EncodingKontakt(provjeri: [])
let request: NSFetchRequest<TelefonskiBroj> = TelefonskiBroj.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "ime", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))]
do{
let matches = try context.fetch(request)
if matches.count > 0 {
for match in matches{
sviBrojevi.provjeri.append(FetchedContact(ime: match.ime!, brojevi: [match.broj!]))
}
}
}catch {
print("Could not load data!")
}
guard let url = URL(string: "") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("", forHTTPHeaderField: "Authorization")
let data = try? JSONEncoder().encode(sviBrojevi)
urlRequest.httpBody = data
let backgroundtask = urlSession.downloadTask(with: urlRequest)
backgroundtask.resume()
}
var numberOfContactsChanged = 0
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
var kontakti = DecodingKontakt(provjereno: [])
do{
let contents = try Data.init(contentsOf: location)
kontakti = try JSONDecoder().decode(DecodingKontakt.self, from: contents)
}catch let error{
print(error.localizedDescription)
completionHandler(.failed)
}
var promijenjeniBrojevi = [String]()
var brojac = 0
let sviKontakti = kontakti.provjereno
persistentContainer.performBackgroundTask { [weak self] (context) in
for index in sviKontakti.indices{
let contact = sviKontakti[index]
let number = self!.sviBrojevi.provjeri[index].brojevi[0] //GRESKA
let request: NSFetchRequest<TelefonskiBroj> = TelefonskiBroj.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "ime", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))]
request.predicate = NSPredicate(format: "ime = %@ AND broj = %@", contact.ime, number)
// request.returnsObjectsAsFaults = false
do{
let match = try context.fetch(request)
if match.count > 0 {
assert(match.count == 1, "AppDelegate.urlSession -- database inconsistency")
if match[0].operater != contact.brojevi[0]{//, match[0].operater != nil{
let obavjestenje = Obavjestenja(context: context)
obavjestenje.broj = number
obavjestenje.datumPromjene = Date()
obavjestenje.stariOperator = match[0].operater
obavjestenje.noviOperator = contact.brojevi[0]
obavjestenje.ime = match[0].ime
if let ime = match[0].ime {
promijenjeniBrojevi.append(ime)
}
let badgeNum = ImenikTableViewController.defaults.integer(forKey: "obavjestenja") + 1
ImenikTableViewController.defaults.set(badgeNum, forKey: "obavjestenja")
obavjestenje.sekcija = ""
brojac += 1
ImenikTableViewController.defaults.set(brojac, forKey: "obavjestenja")
}
match[0].operater = contact.brojevi[0]
match[0].vrijemeProvjere = Date()
}
}catch {
self?.completionHandler(.failed)
print("Could not load data!")
}
}
try? context.save()
if promijenjeniBrojevi.count > 0{
let center = UNUserNotificationCenter.current()
//create the content for the notification
let content = UNMutableNotificationContent()
content.title = "Operator"
content.sound = UNNotificationSound.default
content.badge = NSNumber(integerLiteral: promijenjeniBrojevi.count)
if promijenjeniBrojevi.count == 1{
content.body = "\(promijenjeniBrojevi[0]) je promijenio/la mrežu"
}else if promijenjeniBrojevi.count == 2{
content.body = "\(promijenjeniBrojevi[0]) i \(promijenjeniBrojevi[1]) su promijenili mrežu"
}else{
content.body = "\(promijenjeniBrojevi[0]) i drugi su promijenili mrežu"
}
//notification trigger can be based on time, calendar or location
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(5), repeats: false)
//create request to display
let request = UNNotificationRequest(identifier: "Obavjestenje", content: content, trigger: trigger)
//add request to notification center
center.add(request) { (error) in
if error != nil {
print("error \(String(describing: error))")
}
}
self?.completionHandler(.newData)
}
NotificationCenter.default.post(name: Notification.backFetch, object: nil)
}
}
回答1:
A couple of thoughts:
“I need to perform a task periodically, once a day to be exact” ... I understand that’s what you want, but the OS dictates the frequency (on the basis of how often the user fires up your app, how often there’s new data, etc.). If you need something to happen at a particular time, you may have to consider push notifications (which really isn’t intended for this purpose, either).
I see that you’ve defined your own
completionHandler
variable with your own block. That’s not how it works. You must haveperformFetchWithCompletionHandler
save the completion handler that the OS provided to you, and then call that. You’re never calling their completion handler closure, and as a result, you won’t participate in future background fetch.If you’re going to do a delegate-based
URLSession
, you should save their completion handler in your own ivar and call it (within 30 seconds), while the app is still running in the background.In your comments, you mention that
urlSession
is a backgroundURLSession
. That’s a completely different mechanism (to run requests while your app is suspended/terminated) not to be confused with “background fetch”, in which case your app is awaken and must completely within 30 seconds before your app is suspended again. Typically you’d use a non-backgroundURLSession
to fetch the data (not backgroundURLSession
, because that is slower).Now, if you’re retrieving so much data that that response might take too long to finish (more than 30 seconds), just use this initial non-background fetch only to see if there is data to fetch, and if you want, optionally use a second, background
URLSession
to initiate the fetch of all the data. But that’s more complicated, so I’d only do that if you don’t think you can reasonably finish the fetch of all the data in 30 seconds.
来源:https://stackoverflow.com/questions/54619493/downloadtask-gets-paused-while-executing-as-part-of-the-background-fetch-if-app