slow
slow

Reputation: 825

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()

    }

}

Upvotes: 1

Views: 1149

Answers (1)

Marcus
Marcus

Reputation: 2321

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!

Upvotes: 2

Related Questions