Andrew
Andrew

Reputation: 43

Delegate becomes nil in Operation of urlSession. How to keep delegate variable in separate thread?

I'm using an OperationQueue to upload files one by one to a remote server using URLSession.dataTask. A delegate is used to update a progress bar but after implementing OperationQueue my delegate becomes nil. It worked without OperationQueues. Looking at the stack while the program is running I don't see my progress bar's view controller. It's been a few days and I still can't quite figure it out. I'm guessing the view controller is getting deallocated but I'm not sure how to prevent it from getting deallocated. Thank you.

I have my delegate set to self in NetWorkViewController but inside my NetworkManager class's urlSession(didSendBodyData), the delegate becomes nil. The delegate is not weak and is a class variable.

However, my delegate becomes none-nil again within the completion block of my BlockOperation. This works for dismissing the ViewController via delegation. But the delegate is nil when I'm trying to update inside urlSession(didSendBodyData)...

UPDATE 10/30/2018

It seems that my urlSessions delegates are on a separate thread and is enqueued to the main thread when called but I lose reference to my custom delegate that updates the UI. I'm trying to read more about multithreading but any help would be appreciated!

UPDATE 2 10/30/2018

Solution found The issue was that I was creating another instance of NetworkManager inside each operation. This causes the delegate to be nil because a new instance of NetworkManager is being created for each operation. The fix is to pass self from the original NetworkManager so the delegate is retained.

uploadFiles

    func uploadFiles(item: LocalEntry) {
        let mainOperation = UploadMainFileOperation(file: item)
        // This is where I need to give the operation its 
        // networkManager so the proper delegate is transferred.
        mainOperation.networkManager = self
        mainOperation.onDidUpload = { (uploadResult) in
            if let result = uploadResult {
            self.result.append(result)
            }
        }
        if let lastOp = queue.operations.last {
            mainOperation.addDependency(lastOp)
        }
        queue.addOperation(mainOperation)

    ....
    ....

        let finishOperation = BlockOperation { [unowned self] in
            self.dismissProgressController()
            for result in self.result {
                print(result)
            }
            self.delegate?.popToRootController()
        }
        if let lastOp = queue.operations.last {
            finishOperation.addDependency(lastOp)
        }
        queue.addOperation(finishOperation)

        queue.isSuspended = false
    }

UploadMainFileOperation

class UploadMainFileOperation: NetworkOperation {
    let file: LocalEntry
    // First issue is here. I re-declared another NetworkManager that doesn't have its delegate properly set.
    private let networkManager = NetworkManager() 

    // I have since have this class receive the original networkManager after it's declared.
    var networkManager: NetworkManager?

    var onDidUpload: ((_ uploadResult: String?) -> Void)!

    init(file: LocalEntry) {
        self.file = file
    }

    override func execute() {
        uploadFile()
    }

    private func uploadFile() {

        networkManager.uploadMainFile(item: file) {
            (httpResult) in
            self.onDidUpload(httpResult)
            self.finished(error: "upload main")
        }
    }
}

urlSession(didSendBodyData)

func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
    // This is wrong.
    let uploadProgress: Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
    updateDelegateWith(progress: uploadProgress)
    // This is the correct way for my situation.
    // Because each operation on the queue is on a separate thread. I need to update the UI from the main thread.
    DispatchQueue.main.async {
        let uploadProgress: Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
       self.updateDelegateWith(progress: uploadProgress)
    }
}

updateDelegateWith(progress: Float)

func updateDelegateWith(progress: Float) {
    delegate?.uploadProgressWith(progress: progress)
}

NetworkManagerViewController where the progress bar lives

class NetworkViewController: UIViewController, NetWorkManagerDelegate {

var localEntry: LocalEntry?

var progressBackground = UIView()

var progressBar = UIProgressView()

func uploadProgressWith(progress: Float) {
    progressBar.progress = progress
    view.layoutSubviews()
}

deinit {
    print("deallocate")
}

override func viewDidLoad() {
    super.viewDidLoad()

    let networkManager = NetworkManager()
    networkManager.delegate = self
    networkManager.uploadFiles(item: self.localEntry!)
....
....
}

}

Upvotes: 3

Views: 406

Answers (3)

Kamran
Kamran

Reputation: 15238

With the latest code shared, i would suggest to keep NetworkManager instance at the class level instead of a function level scope as this will ensure the networkManager instance is not deallocated.

class NetworkViewController: UIViewController, NetWorkManagerDelegate {

var localEntry: LocalEntry?

var progressBackground = UIView()

var progressBar = UIProgressView()

let networkManager = NetworkManager()

func uploadProgressWith(progress: Float) {
    progressBar.progress = progress
    view.layoutSubviews()
}

deinit {
    print("deallocate")
}

override func viewDidLoad() {
    super.viewDidLoad()

    networkManager.delegate = self
    networkManager.uploadFiles(item: self.localEntry!)
}

...

Also, you need to be careful for retain-cycles that cause memory leaks. To avoid retain cycles, you need to declare your delegate variable as weak.

Upvotes: 1

Andrew
Andrew

Reputation: 43

As user @Kamran pointed out, I was creating a class level instance of networkManager inside UploadMainFileOperation. The issue has been fixed by changed that variable to an Optional and giving it an instance of NetworkManager, as self ,that was queueing up the operations. The code blocks as been updated with comments of the correct code along with the incorrect code.

Upvotes: 1

herzi
herzi

Reputation: 824

If you set a delegate and later it becomes nil, this means your delegate has been deallocated.

I would recommend to create an (empty) deinit in your delegate class and set a breakpoint for the debugger in that method. This will help you find out where you're losing the reference to said delegate.

You can probably avoid this by assigning your delegate to a property of one of your classes or make it a strong reference in one of your completion blocks.

Upvotes: 0

Related Questions