Search code examples
iosswift5nsurlsessiondatatask

Is it true that there is no way at all to subclass URLSessionDataTask, and if so is there a way to add an associated property?


I've always wanted to do something like

class URLSessionDataTask_Plus: URLSessionDataTask {
    
    var obs: NSKeyValueObservation? = nil
    // store the progress observer there 
}

which would be very convenient. I don't know how to do it. If instead you do something like

///URLSessionDataTask, plus more!
class URLSessionDataTask_Plus {
    
    var task: URLSessionDataTask? = nil
    var obs: NSKeyValueObservation? = nil
}

there's really no advantage, as you anyway have to still maintain that variable externally (or maintain that whole object externally).

Progress observations are a real nuisance to hang on to as you never know how many you'll need or where.

Is there some way to achieve this concept? ->

class URLSessionDataTask_Plus: URLSessionDataTask {
    
    var obs: NSKeyValueObservation? = nil
    // store the progress observer there 
}

(I note that the same applies even if you use the futuristic async calls .. https://stackoverflow.com/a/78804162/294884 )


Solution

  • FTR, I've been trying out simply adding an objc_getAssociatedObject.

    public var ass_observer: NSKeyValueObservation?
    
    • it does seem to work well

    • as far as I can determine, I cough think it has no leaks

    Outline:

    extension URLSession {
        
        ///Timed data task. Basically a super-data-task which autonomously and
        ///invisibly does progress, timers, and any other features we need at this
        ///low level.
        static func tDataTask(
            with request: URLRequest,
            completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void
        ) {
            var _firstbyte = true // (NB is workaround for poor behavior of .progress(
            
            LoggingEtc("Waiting for first byte")
    
            let t = URLSession.shared.dataTask(with: request) { (d, r, e) in
                LoggingEtc("All data arrived")
                completionHandler(d, r, e)
            }
    
            // Setup logging, timers, etc
            // Use t.taskIdentifier as needed for uniqueness
            
            t.ass_observer = t.progress.observe(\.fractionCompleted) { progress, _ in
                if progress.fractionCompleted < 0.02 { return }
                if _firstbyte {
                    LoggingEtc("First byte arrived")
                    _firstbyte = false
                }
                ProgressEtc()
            }
            
            t.resume()
        }
    }
    

    and ...

    fileprivate var _key_to_ass_observer: UInt8 = 0
    extension URLSessionDataTask {
        public var ass_observer: NSKeyValueObservation? {
            get {
                return objc_getAssociatedObject(self, &_key_to_ass_observer) as? NSKeyValueObservation ?? nil
            }
            set(newValue) {
                objc_setAssociatedObject(self, &_key_to_ass_observer, newValue, .OBJC_ASSOCIATION_RETAIN)
            }
        }
    }
    

    Call thus from your API singleton - fire and forget, nothing to retain, etc.

    URLSession.tDataTask(with: request) { (data, response, error) in
        
        .. 1, error handling
        
        .. 2, parsing
        
        .. 3, update databases, convert bitcoin, whatever
        
        .. 4, report to whoever called you as relevant
        DispatchQueue.main.async { completionOnMain?(200, []) }
    }