I have a delegate which contains the following function:
func didFailToConnect(_ interface) {
}
and also i have an asynchronous call:
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error != nil {
}
if let placemarks = placemarks {
}
}
what i want to achieve is to see if the 2 calls has been finished and inform the user with an alert.Which is the best way to implement this i'm thinking of using DispatchGroup but because the delegate implementation is coming from a third party framework i don't exactly where to put the setup code of dispatch for example:
group.enter()
and then:
func didFailToConnect(_ interface) {
group.leave()
}
Any ideas?
Related
I am following Stanfords' CS193p Developing Apps for iOS online course.
It is using the Grand Central Dispatch (GCD) API for a demo of multithreading.
But they noted, that
"GCD has been mostly replaced by Swift's new built-in async API as of WWDC 2021".
So I wanted to learn how the code from the Lecture would look like after updating it to use this new API.
After watching Apple's WWDC videos, it seems to me like
DispatchQueue.global(qos: .userInitiated).async { } is replaced in this new async API with Task { } or Task(priority: .userInitiated) {}, but I'm not sure, what has DispatchQueue.main.async { } been replaced with?
So, my questions are:
Am I correctly assuming, that DispatchQueue.global(qos: .userInitiated).async { } has been replaced with Task(priority: .userInitiated) {}
What has DispatchQueue.main.async { } been replaced with?
Please help, I want to learn this new async-await API.
Here's the code from the Lecture, using old GCD API:
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
The whole function (in case you need to see more code):
private func fetchBackgroundImageDataIfNecessary() {
backgroundImage = nil
switch emojiArt.background {
case .url(let url):
// fetch the url
backgroundImageFetchStatus = .fetching
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
case .imageData(let data):
backgroundImage = UIImage(data: data)
case .blank:
break
}
}
If you really are going to do something slow and synchronous, Task.detached is a closer analog to GCD’s dispatching to a global queue. If you just use Task(priority: ...) { ... } you are leaving it to the discretion of the concurrency system to decide which thread to run it on. (And just because you specify a lower priority does not guarantee that it might not run on the main thread.)
For example:
func fetchAndUpdateUI(from url: URL) {
Task.detached { // or specify a priority with `Task.detached(priority: .background)`
let data = try Data(contentsOf: url)
let image = UIImage(data: data)
await self.updateUI(with: image)
}
}
And if you want to do the UI update on the main thread, rather than dispatching it back to the main queue, you would simply add the #MainActor modifier to the method that updates the UI:
#MainActor
func updateUI(with image: UIImage?) async {
imageView.image = image
}
That having been said, this is a pretty unusual pattern (doing the network request synchronously and creating a detached task to make sure you don't block the main thread). We would probably use URLSession’s new asynchronous data(from:delegate:) method to perform the request asynchronously. It offers better error handling, greater configurability, participates in structured concurrency, and is cancelable.
In short, rather than looking for one-to-one analogs for the old GCD patterns, use the concurrent API that Apple has provided where possible.
FWIW, in addition to the #MainActor pattern shown above (as a replacement for dispatching to the main queue), you can also do:
await MainActor.run {
…
}
That is roughly analogous to the dispatching to the main queue. In WWDC 2021 video Swift concurrency: Update a sample app, they say:
In Swift’s concurrency model, there is a global actor called the main actor that coordinates all operations on the main thread. We can replace our DispatchQueue.main.async with a call to MainActor’s run function. This takes a block of code to run on the MainActor. …
But he goes on to say:
I can annotate functions with #MainActor. And that will require that the caller switch to the main actor before this function is run. … Now that we've put this function on the main actor, we don’t, strictly speaking, need this MainActor.run anymore.
I am writing a Safari app extension and want to fetch the URL for the active page in my view controller.
This means nested completion handlers to fetch the window, to fetch the tab, to fetch the page, to access its properties. Annoying but simple enough. It looks like this:
func doStuffWithURL() {
var url: URL?
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage { (page) in
page?.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
}
}
}
}
// NOW DO STUFF WITH THE URL
NSLog("The URL is \(String(describing: url))")
}
The obvious problem is it does not work. Being completion handlers they will not be executed until the end of the function. The variable url will be nil, and the stuff will be done before any attempt is made to get the URL.
One way around this is to use a DispatchQueue. It works, but the code is truly ugly:
func doStuffWithURL() {
var url: URL?
let group = DispatchGroup()
group.enter()
SFSafariApplication.getActiveWindow { (window) in
if let window = window {
group.enter()
window.getActiveTab { (tab) in
if let tab = tab {
group.enter()
tab.getActivePage { (page) in
if let page = page {
group.enter()
page.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
group.leave()
}
}
group.leave()
}
}
group.leave()
}
}
group.leave()
}
// NOW DO STUFF WITH THE URL
group.notify(queue: .main) {
NSLog("The URL is \(String(describing: url))")
}
}
The if blocks are needed to know we are not dealing with a nil value. We need to be certain a completion handler will return, and therefore a .leave() call before we can call a .enter() to end up back at zero.
I cannot even bury all that ugliness away in some kind of getURLForPage() function or extension (adding some kind of SFSafariApplication.getPageProperties would be my preference) as obviously you cannot return from a function from within a .notify block.
Although I tried creating a function using queue.wait and a different DispatchQueue as described in the following answer to be able to use return…
https://stackoverflow.com/a/42484670/2081620
…not unsurprisingly to me it causes deadlock, as the .wait is still executing on the main queue.
Is there a better way of achieving this? The "stuff to do," incidentally, is to update the UI at a user request so needs to be on the main queue.
Edit: For the avoidance of doubt, this is not an iOS question. Whilst similar principles apply, Safari app extensions are a feature of Safari for macOS only.
Thanks to Larme's suggestions in the comments, I have come up with a solution that hides the ugliness, is reusable, and keep the code clean and standard.
The nested completion handlers can be replaced by an extension to the SFSafariApplication class so that only one is required in the main body of the code.
extension SFSafariApplication {
static func getActivePageProperties(_ completionHandler: #escaping (SFSafariPageProperties?) -> Void) {
self.getActiveWindow { (window) in
guard let window = window else { return completionHandler(nil) }
window.getActiveTab { (tab) in
guard let tab = tab else { return completionHandler(nil) }
tab.getActivePage { (page) in
guard let page = page else { return completionHandler(nil) }
page.getPropertiesWithCompletionHandler { (properties) in
return completionHandler(properties)
}
}
}
}
}
}
Then in the code it can be used as:
func doStuffWithURL() {
SFSafariApplication.getActivePageProperties { (properties) in
if let url = properties?.url {
// NOW DO STUFF WITH THE URL
NSLog("URL is \(url))")
} else {
// NOW DO STUFF WHERE THERE IS NO URL
NSLog("URL ERROR")
}
}
}
I've written a function that's supposed to return the HTML string that makes up a WKWebview. However, the completion handler is never called, and the project freezes indefinitely. I've also already adopted the WKScriptMessageHandler protocol so that's not the problem.
public func getHTML() -> String {
var result = ""
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: {(html: Any?, error: Error?) in
if (error != nil) {
print(error!)
}
result = html as! String
group.leave()
})
}
group.wait()
print("done waiting")
return result
}
I've found several examples on how to get the html, like here, but I don't want to merely print, I want to be able to return its value. I'm not experienced with DispatchQueues, but I do know for that WKWebView's evaluateJavaScript completion handler always runs on the main thread
I am writing the iOS application using swift 4.2. I am making a service call to logout user.
I need to know where to use main thread (DispatchQueue.main.async).
Here is my code:
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
switch result {
case let (.success(didLogout)):
didLogout ? self.handleSuccessfullLogout() : self.handleLogoutError(with: nil)
case let (.failure(error)):
self.handleLogoutError(with: error)
}
}
}
logoutUseCase?.logout() makes a service call and returns #escaping completion. Should I use DispatchQueue.main.async on this whole handleLogoutCellTap() function or just in a handling segment?
Move the control to main thread wherever you're updating the UI after receiving the response of logout.
If handleSuccessfullLogout() and handleLogoutError(with:) methods perform any UI operation, you can embed the whole switch statement in DispatchQueue.main.async, i,e.
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
DispatchQueue.main.async { //here.....
switch result {
//rest of the code....
}
}
}
}
I'm using DispatchGroup.enter() and leave() to process a helper class's reverseG async function. Problem is clear, I'm using mainViewController's object to call mainViewControllers's dispatchGroup.leave() in helper class! Is there a way to do it?
Same code works when reverseG is declared in the main view controller.
class Geo {
var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, _ completion: #escaping (CLPlacemark) -> ()) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { (placemarks, error) in
if let error = error {
print("error: \(error.localizedDescription)")
}
if let placemarks = placemarks, placemarks.count > 0 {
let placemark = placemarks.first!
completion(placemark) // set ViewController's properties
} else {
print("no data")
}
obj.dispatchGroup.leave() // ** ERROR **
}
}
}
Function call from main view controller
dispatchGroup.enter()
Geo.reverseG(coordinates, setValues) // completionHandler: setValues
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
Every leave call must have an associated enter call. If you call leave without having first called enter, it will crash. The issue here is that you're calling enter on some group, but reverseG is calling leave on some other instance of ViewController. I'd suggest passing the DispatchGroup as a parameter to your reverseG method. Or, better, reverseG shouldn't leave the group, but rather put the leave call inside the completion handler that reserveG calls.
dispatchGroup.enter()
Geo.reverseG(coordinates) { placemark in
defer { dispatchGroup.leave() }
guard let placemark = placemark else { return }
// use placemark here, e.g. call `setValues` or whatever
}
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
And
class Geo {
// var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { placemarks, error in
if let error = error {
print("error: \(error.localizedDescription)")
}
completion(placemarks?.first)
// obj.dispatchGroup.leave() // ** ERROR **
}
}
}
This keeps the DispatchGroup logic at one level of the app, keeping your classes less tightly coupled (e.g. the Geo coder doesn't need to know whether the view controller uses dispatch groups or not).
Frankly, I'm not clear why you're using dispatch group at all if there's only one call. Usually you'd put whatever you call inside the completion handler, simplifying the code further. You generally only use groups if you're doing a whole series of calls. (Perhaps you've just simplified your code snippet whereas you're really doing multiple calls. In that case, a dispatch group might make sense. But then again, you shouldn't be doing concurrent geocode requests, suggesting a completely different pattern, altogether.
Passed dispatchGroup as parameter with function call and it worked.
Geo.reverseG(coordinates, dispatchGroup, setValues)
my two cents to show how can work:
(maybe useful for others..)
// Created by ing.conti on 02/02/21.
//
import Foundation
print("Hello, World!")
let r = AsyncRunner()
r.runMultiple(args: ["Sam", "Sarah", "Tom"])
class AsyncRunner{
static let shared = AsyncRunner()
let dispatchQueue = DispatchQueue(label: "MyQueue", qos:.userInitiated)
let dispatchGroup = DispatchGroup.init()
func runMultiple(args: [String]){
let count = args.count
for i in 0..<count {
dispatchQueue.async(group: dispatchGroup) { [unowned self] in
dispatchGroup.enter()
self.fakeTask(arg: args[i])
}
}
_ = dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
func fakeTask(arg: String){
for i in 0..<3 {
print(arg, i)
sleep(1)
}
dispatchGroup.leave()
}
}