Alexey Guseynov Alexey Guseynov - 2 months ago 361
iOS Question

Resume NSUrlSession on iOS10

iOS 10 is going to be released soon so it worth to test applications for compatibility with it. During such test we've discovered that our app can't resume background downloads on iOS10. Code that worked well on previous versions does not work on new one, both on an emulator and on a device.

Instead of reducing our code to minimal working test case I've searched internet for NSUrlSession tutorials and tested them. Behaviour is the same: resuming works on previos versions of iOS but breaks on 10th.

Steps to Reproduce:




  1. Download a project form NSUrlSession tutorial
    https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started

  2. Direct link:
    http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip

  3. Build it and launch under iOS 10. Search something, for example
    "swift". Start a download and then hit a "Pause" and then "Resume"



Expected Results:



Download is resumed. You can check how it works with versions prior to iOS10.

Actual Results:



Download fails. In xcode console you can see:

2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.


More scenarios:



If you activate offline mode while a file is beeng downloaded you get

Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} {
NSLocalizedDescription = "unsupported URL";
}


when network is shut down and download never recovers when network is up again. Other use cases with pause, such as reboot, do not work either.

Additional investigation:



I've tried to check if returned resumeData is valid using code suggested in

How can I check that an NSData blob is valid as resumeData for an NSURLSessionDownloadTask?

but the target file is in place. Though resumeData format has changed and now file name is stored in NSURLSessionResumeInfoTempFileName and you have to append NSTemporaryDirectory() to it.

Beside that I've filled a bug report to apple, but they haven't replied yet.

The question (of Life, the Universe, and Everything):



Is resuming of NSUrlSession broken in all other apps? Can it be fixed on application side?

Answer

The resume data structure has been changed in iOS 10, before it was simply a plist, now it's an NSKeyArchived encoded NSDictionary with an unusual root of "NSKeyedArchiveRootObjectKey" instead of NSKeyedArchiveRootObjectKey constant which is "root" literally. I suspect this arose a bug, though as I checked, NSKeyedUnarchiver can also handle this scenario.

I detected that in beta 1 and filed a bug (no. 27144153 in case you want duplicate). Even I sent an email to "Quinn the Eskimo" (eskimo1 at apple dot com) whom is support guy of NSURLSession team, to confirm they received it and he said they got that and are aware of issue.

UPDATE: I finally figured out how to resolve this issue. Give data to correctResumeData() function and it will return usable resume data

func correctResumeData(data: NSData?) -> NSData? {
    let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
    let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"

    guard let data = data, let resumeDictionary = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
        return nil
    }

    resumeDictionary[kResumeCurrentRequest] = correctFuckingRequestData(resumeDictionary[kResumeCurrentRequest] as? NSData)
    resumeDictionary[kResumeOriginalRequest] = correctFuckingRequestData(resumeDictionary[kResumeOriginalRequest] as? NSData)

    let result = try? NSPropertyListSerialization.dataWithPropertyList(resumeDictionary, format: NSPropertyListFormat.XMLFormat_v1_0, options: NSPropertyListWriteOptions())
    return result
}

func correctRequestData(data: NSData?) -> NSData? {
    guard let data = data else {
        return nil
    }
    guard let archive = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
        return nil
    }
    // Rectify weird __nsurlrequest_proto_props objects to $number pattern
    var i = 0
    while archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_prop_obj_\(i)") != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
            dic.setObject(obj, forKey: "$\(i + 3)")
            dic.removeObjectForKey("__nsurlrequest_proto_prop_obj_\(i)")
            arr?[1] = dic
            archive["$objects"] = arr
        }
        i += 1
    }
    if archive["$objects"]?[1]["__nsurlrequest_proto_props"] != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, obj = dic["__nsurlrequest_proto_props"] {
            dic.setObject(obj, forKey: "$\(i + 3)")
            dic.removeObjectForKey("__nsurlrequest_proto_props")
            arr?[1] = dic
            archive["$objects"] = arr
        }
    }
    // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
    if archive["$top"]?["NSKeyedArchiveRootObjectKey"] != nil {
        archive["$top"]?.setObject(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey)
        archive["$top"]?.removeObjectForKey("NSKeyedArchiveRootObjectKey")
    }
    // Reencode archived object
    let result = try? NSPropertyListSerialization.dataWithPropertyList(archive, format: NSPropertyListFormat.BinaryFormat_v1_0, options: NSPropertyListWriteOptions())
    return result
}
Comments