0

I have a file for a BackgroundSession class

class BackgroundSession: NSObject {
  static let shared = BackgroundSession()

  static let identifier = "com.***.bg"

  private var session: URLSession!

  var savedCompletionHandler: (() -> Void)?

  private override init() {
    super.init()

    let configuration = URLSessionConfiguration.background(withIdentifier: BackgroundSession.identifier)
    session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
  }

  func start(_ request: URLRequest) {
    session.downloadTask(with: request).resume()
  }
}

extension BackgroundSession: URLSessionDelegate {
  func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
      self.savedCompletionHandler?()
      self.savedCompletionHandler = nil
    }
  }
}

extension BackgroundSession: URLSessionTaskDelegate {
  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error {
      // handle failure here
      print("\(error.localizedDescription)")
    }
  }
}

extension BackgroundSession: URLSessionDownloadDelegate {
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    do {
      let data = try Data(contentsOf: location)
      let json = try JSONSerialization.jsonObject(with: data)

      print("\(json)")
      // do something with json
    } catch {
      print("\(error.localizedDescription)")
    }
  }
}

I am listening for background location updates to come in later on

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    print("didUpdateLocations")
    if locations.first != nil {
      let lastLocation = locations.last
      self.lastLocation = lastLocation
      print("doing background work")
      self.getUserData()
      if PubnubController.pubnubChannel != nil {
        PubnubController.sharedClient.publish(["action": "newCoordinates", "data": ["coordinates": ["latitude": lastLocation?.coordinate.latitude, "longitude": lastLocation?.coordinate.longitude]]], toChannel: PubnubController.pubnubChannel!, compressed: false)
      }
    }
  }

self.getUserData() looks like this

func getUserData() {
    print("getUserData")
    if (self.userId != -1 && self.userAuthToken != nil) {
      let httpUrl: String = "https://api.***.com/dev/users/\(self.userId)"
      guard let url = URL(string: httpUrl) else {
        return
      }
      var request = URLRequest(url: url)
      request.setValue(self.userAuthToken, forHTTPHeaderField: "Authorization")
      let session = BackgroundSession.shared
      session.start(request)
    }
  }

In my ExtensionDelegate.swift I have the typical func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)

with a for loop and switch set with a case for WKURLSessionRefreshBackgroundTask that looks like this

case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
              print("WKURLSessionRefreshBackgroundTask")
                // Be sure to complete the URL session task once you’re done.
                urlSessionTask.setTaskCompletedWithSnapshot(false)

In my controller, I also have pasted the function the class is supposed to call

func application(_ application: WKExtension, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    print("handleEventsForBackgroundURLSession")
    BackgroundSession.shared.savedCompletionHandler = parseUserData
  }

It seems that both the delegate function and this pasted function are not being called with my data. I'm having a really hard time trying to understand this background URLSession flow

Note the BackgroundSession class came from this Stackoverflow question

URLSession.datatask with request block not called in background

Jordan
  • 2,393
  • 4
  • 30
  • 60

1 Answers1

0

This handleEventsForBackgroundURLSession is an iOS pattern. This is a method of the UIApplicationDelegate protocol. You can’t just add that to some random controller. It’s only applicable for your iOS app's UIApplicationDelegate.

For watchOS, I suspect the idea is the same, except rather than calling the completion handler that iOS provides, supply your own completion handler to the BackgroundSession that calls the setTaskCompletedWithSnapshot of the WKURLSessionRefreshBackgroundTask:

func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
    // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
    for task in backgroundTasks {
        // Use a switch statement to check the task type
        switch task {
        case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
            // Be sure to complete the URL session task once you’re done.
            BackgroundSession.shared.savedCompletionHandler = {
                urlSessionTask.setTaskCompletedWithSnapshot(false)
            }
        ...
        }
    }
}

But, effectively, the idea is the same. We’re deferring the setTaskCompletedWithSnapshot until urlSessionDidFinishEvents(forBackgroundURLSession:) is called.


If you want the BackgroundSession to call your controller’s parser, you can specify a protocol for that interface:

protocol Parser: class {
    func parse(_ data: Data)
}

You can then give your BackgroundSession a property to keep track of the parser:

weak var parser: Parser?

You can have the didFinishDownloadingTo call the parser:

extension BackgroundSession: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            let data = try Data(contentsOf: location)
            parser?.parse(data)
        } catch {
            os_log(.error, log: log, "Error retrieving data for %{public}@: %{public}@", downloadTask.originalRequest?.url?.absoluteString ?? "Unknown request", error.localizedDescription)
        }
    }
}

You can then have your controller (or whatever) (a) conform to this protocol; (b) implement the parse(_:) method of that protocol; and (c) specify itself as the parser:

BackgroundSession.shared.parser = self
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I'm going to attempt implementing this but if there is data and other methods that need to be called that are in my controller, what is the best way to work with those if it's always being called outside of the context of the controller? – Jordan Aug 09 '19 at 17:08
  • so now I'm getting a bunch of `No such file or directory` errors on the `BackgroundSession` class. I am using the second example you showed, I added a `print` right before setting the `savedCompletionHandler` and it's not being printed at all, neither is the print statement in the `didFinishDownloadingTo`. I'm not sure this is working at all :/ – Jordan Aug 09 '19 at 17:41
  • There are a number of ways of doing that, but don’t use the `savedTask` or `savedCompletionHandler` for that. Reserve that for just telling the `WKExtensionDelegate` that you’re done. See expanded answer on how you might let the `BackgroundSession` know who should do the parsing. – Rob Aug 09 '19 at 17:47
  • @Jordan - Re “no such file”, are you doing any asynchronous dispatching? When `didFinishDownloadingTo` returns, it deletes the temporary files supplied to the `location` URL. And if you do any asynchronous dispatching, it will immediately return and delete the file, possibly doing so before you go to grab the file yourself asynchronously. Make sure to (a) either immediately convert to `Data` and supply that to the parser, or (b) move the supplied temporary file to some location of your choosing. – Rob Aug 09 '19 at 17:50
  • so far I'm not really doing anything with the data I'm just logging in the various parts to see if it's all being called – Jordan Aug 09 '19 at 17:52
  • I'm also not really familiar with Protocols so I'm a little lost in this area – Jordan Aug 09 '19 at 17:54