More Code More Code - 1 month ago 16
iOS Question

Dispatch group notifies before the work is done

I'm new to Apple's GCD and having a problem with

DispatchGroup
.
So, I'm trying to attach a post to a map after the post is fully initialized. Inside
post.init
, there is a
URLSession
that downloads a
UIImage
from a url. But even before the download completes,
group.notify
gets fired off. I cannot really find what the reason is. Below is the code. I would really appreciate any advice or help! Thanks.

// ViewController.swift
let group = DispatchGroup
...
...

group.enter()
DispatchQueue.global(qos: .userInitiated).async {
post = Post(values: post)
self.posts[postId] = post
group.leave()
}

group.notify(queue: DispatchQueue.main, execute: {
print("notify:: \(post?.picture)") // This prints out nil, when it shouldn't.
self.addPostToMap(post!, at: location!)
})

// Post.swift
class Post {
var picture: UIImage?
var thumbnail: UIImage?
init(values: [String: Any]) {
...
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
DispatchQueue.main.async {
self.picture = UIImage(data: data!)
self.thumbnail = Util.resizeImage(image: self.picture!, targetSize: CGSize(width: 50, height: 50))
}

}).resume()

}

}

Answer

The completion handler in init(values:) will only be triggered when the task completes, whenever that may be. However, after your call to .resume() code execution will continue, meaning that init will exit and your group.notify block will get called immediately.

You could handle this in various ways. One would be to set up a delegate protocol for your Post class and set the ViewController as that delegate. In you completion handler you could then call a function - for the sake of argument call it didFinish() - which will tell the view controller that the task had finished. You could wrap your notification in that function.

Doing things this way, however, I would take the dataTask out of the init function. The reason for this is that delegates are typically declared as implicitly unwrapped optionals and set from the creating class. As such, when you initialise the class, there is a chance that the delegate may not refer to anything when you call your delegate callback.

So the structure would be as follows:

// ViewController.swift
class ViewController : UIViewController, PostDelegate {
    let group = DispatchGroup
    ...
    ...

    group.enter()
    DispatchQueue.global(qos: .userInitiated).async {
        post = Post(values: post)
        post.delegate = self
        post.getImage() // New function
        self.posts[postId] = post
        group.leave()
    }

    ....

    func didFinish() -> Void {
        group.notify(queue: DispatchQueue.main, execute: {
            print("notify:: \(post?.picture)")
            self.addPostToMap(post!, at: location!)
    })

    ....

Protocol PostDelegate {
    func didFinish() -> Void
}

// Post.swift
class Post {
    var delegate : PostDelegate!
    var picture: UIImage?
    var thumbnail: UIImage?
    init(values: [String: Any]) {
    ...
    }

    func getImage() -> Void {
        URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in 
            DispatchQueue.main.async {
                self.picture = UIImage(data: data!)
                self.thumbnail = Util.resizeImage(image: self.picture!, targetSize: CGSize(width: 50, height: 50))
                if delegate != nil {
                    delegate.didFinish() // Tell the delegate you are done
                }
            }

        }).resume()

    }

}

Hope that makes sense, and more, importantly, that it helps!

Comments