3

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!)
....
....
}

}

Andrew
  • 43
  • 7
  • From all the code, it is difficult to tell where the problem lies. Can you provide the minimal code that is causing the problem. – Kamran Oct 30 '18 at 06:12
  • @Kamran I have removed some code that I don't believe to be causing the issue. Thank you for the feedback. – Andrew Oct 30 '18 at 17:14
  • 1
    In the image named (`Setting delegate to self inside ViewController`), you didn't share this code. From where you are calling this code. In this code you are not setting delegate for `networkManager` and it seems you are not creating class level instance so because of both these issues, delegate will be `nil`. Secondly, in `UploadMainFileOperation` class, you are creating class level instance of `newtworkManager` but you are not setting delegate so this will also cause a nil `delegate`. – Kamran Oct 30 '18 at 18:58
  • @Kamran I just came upon the issue inside `UpLoadMainFileOperation` too! Besides that I also needed to wrap my delegate function call inside urlSession(didSendBodyData) with DispathQueue.main.async{ }. I've updated the post with the relevant code. – Andrew Oct 30 '18 at 19:07
  • Actually, you are not setting `delegate` anywhere so you will always get a `nil` `delegate`. It would be better to follow a quick tutorial on how to pass `data` using `delegate` – Kamran Oct 30 '18 at 19:12
  • 1
    @Kamran Thank you for the help! Following your suggestions the issue has been resolved. The `delegate` is now set in each `Operation` under the `uploadFiles` function. – Andrew Oct 30 '18 at 19:20
  • @Kamran I'm not sure how to mark your comment as the correct answer but I have created an answer in this post but I don't want to steal your credit. if you would like to post your comment as an answer, I would gladly mark it as the correct solution. – Andrew Oct 30 '18 at 19:28

3 Answers3

1

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.

Andrew
  • 43
  • 7
1

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.

Kamran
  • 14,987
  • 4
  • 33
  • 51
0

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.

herzi
  • 774
  • 6
  • 18
  • I added an empty deinit and stepped through the debugger. It is called after my urlSession(didSendBodyData), where my delegate function is being called. Can this order be deceiving? – Andrew Oct 30 '18 at 17:01