Why app is blocked by semaphore? - ios

I have the following function that suppose to return [CIImage] for my purpose - displaying some metadata of photos in tableView.
func getCIImages() -> [CIImage] {
var images = [CIImage]()
let assets = PHAsset.fetchAssetsWithMediaType(.Image, options: nil)
for i in 0..<assets.count {
guard let asset = assets[i] as? PHAsset else {fatalError("Cannot cast as PHAsset")}
let semaphore = dispatch_semaphore_create(0)
asset.requestContentEditingInputWithOptions(nil) { contentEditingInput, _ in
//Get full image
guard let url = contentEditingInput?.fullSizeImageURL else {return}
guard let inputImage = CIImage(contentsOfURL: url) else {return}
images.append(inputImage)
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
return images
}
but it stucks in semaphore wait and didn't go further. I have walked through many tutorials but other variants of GCD don't works. I think it's because of simulator, I don't know, can't test on real device. Please help.

guards inside requestContentEditingInputWithOptions callback closure prevents signal sent to semaphore.
In such cases (when you need cleanup actions) it is good to use defer. In your case:
asset.requestContentEditingInputWithOptions(nil) { contentEditingInput, _ in
defer { dispatch_semaphore_signal(semaphore) }
//Get full image
guard let url = contentEditingInput?.fullSizeImageURL else {return}
guard let inputImage = CIImage(contentsOfURL: url) else {return}
images.append(inputImage)
}
UPDATE
Apart from cleanup bug there is another one. Completion closure of requestContentEditingInputWithOptions called on main thread. Which means that if you blocking main thread with semaphore: completion closure is blocked form executing as well. To fix blocked semaphore issue you need call getCIImages on a different thread than main.
Anyway making asynchronous things synchronous is wrong. You should think of different approach.

Related

kingfisher causing main thread issue

So the app works in test mode but as soon as I went to build for release I got this main thread issue.
UIImageView.image must be used from main thread only
According the the error I am not calling something on the main thread, yet the line it has thrown the thread error at is blank (see screenshot)
So I can only guess what they talking about is the code directly under that line?
code
#objc func nowplaying(){
let jsonURLString = "https://api.drn1.com.au/station/playing"
guard let feedurl = URL(string: jsonURLString) else { return }
URLSession.shared.dataTask(with: feedurl) { (data,response,err)
in
guard let data = data else { return }
do{
let nowplaying = try JSONDecoder().decode(Nowplayng.self, from: data)
nowplaying.data.forEach {
DispatchQueue.main.async {
self.artist.text = nowplaying.data.first?.track.artist
self.song.text = nowplaying.data.first?.track.title
}
print($0.track.title)
if var strUrl = nowplaying.data.first?.track.imageurl {
strUrl = strUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
//MusicPlayer.shared.nowplaying(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
MusicPlayer.shared.getArtBoard(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
}
}
I can only guess it is because kingfisher wants a loading picture or something. But unclear?
It is your responsibility to call Kingfisher's UI-extension methods on UI thread.
Before:
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
After:
DispatchQueue.main.async {
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
}
I believe you could always run your code in main thread in your own processor, by using:
king fisher process image on download thread so that can cause this problem
public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image?
return DispatchQueue.main.sync {
let image = ... // Your code needs to be performed in UI thread
return image
}
}

Swift iOS -How to continue running a DispatchSemaphore combined with DispatchGroup()

vc1 pushes on vc2 and in vc2.
In vc2 I have an array of strings that I convert to urls which eventually go through a URLSession and the images returned get converted to images with filters. Once I get the filtered image in the URLSession callback I save it to cache.
I want the filtered images that are returned to be displayed in the exact order that is in the array of strings. I followed this answer and use DispatchGroup + Semaphores and everything works fine, the order of the filtered images are returned in the exact order.
The problem I ran into is if the user returns back to vc1 then pushes on vc2, once the code below runs and sees that the cache has the first filtered image ("str1"), it adds it to the filteredArray and jumps to dispatchGroup.notify. The filtered images associated with "str1" from the array is the only thing that is appended and the images from "str2" and "str3" aren't.
The cache might or might not have purged the filtered images associated with "str2", "str3" and if it did purge them then the loop should continue to the URLSession callback but it doesn't. But if it didn't purge them it also doesn't add them. That is where my problem occurs, the loop stops running once the cache is hit. This is my first time using a semaphore so I'm not sure where the issues is arising from.
What I did to get around the problem was I simply removed the cache and everything works fine but that also means I don't get the benefit of using the cache.
let imageCache = NSCache<AnyObject, AnyObject>()
let arrOfStringsAsUrls = ["str1", "str2", "str3", "maybe more strings..."]
var filteredArray = [UIImage]()
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)
dispatchQueue.async {
for str in arrOfStringsAsUrls {
dispatchGroup.enter()
guard let url = URL(string: str) else {
dispatchSemaphore.signal()
dispatchGroup.leave()
return
}
if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage {
self?.filteredArray.append(cachedImage)
dispatchSemaphore.signal()
dispatchGroup.leave()
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
dispatchSemaphore.signal()
dispatchGroup.leave()
return
}
guard let data = data, let image = UIImage(data: data) else {
dispatchSemaphore.signal()
dispatchGroup.leave()
return
}
DispatchQueue.main.async{ [weak self] in
let filteredImage = self?.addFilterToImage(image)
self?.imageCache.setObject(image, forKey: str as AnyObject)
self?.filteredArray.append(filteredImage)
dispatchSemaphore.signal()
dispatchGroup.leave()
}
}).resume()
dispatchSemaphore.wait()
}
dispatchGroup.notify(queue: dispatchQueue) { in
// reloadData() and do some other stuff on the main queue
}
}
During the first push of the second VCs all images are not in cache so no guard else triggers and also if let cachedImage = imageCache... so everything goes smoothly , after the next push the cache has images stored so this if let triggers
if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage {
....
return
}
and since you put a return statement , it will cause the exit of the for-loop and function , causing the subsequent catches of images to end which triggers the disptach group notify as there is an equal count of enter / leave at that moment , what you really need is when there is an image stored in cache or url is wrong to stop it's fetch with session and the best thing for this job inside a for-loop is continue keyword which will skip the current iteration and jumps to the next one
guard let url = URL(string: str) else {
dispatchSemaphore.signal()
dispatchGroup.leave()
continue
}
if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage {
self?.filteredArray.append(cachedImage)
dispatchSemaphore.signal()
dispatchGroup.leave()
continue
}

Swift iOS -DispatchGroup with URLSession is locking other parts of app that it is not located in

I have an array of up to 6 images. I use a loop to loop through all of the images, turn them into metadata, send the metadata to Storage and then when done I send the url strings to Firebase Database.
I'm using DispatchGroup to control the loop as the Url is changed to Data so I can send the data to Firebase Storage.
If this loop is happening in tabOne, if i go back and forth to tabTwo or tabThree, when the loop finishes and the alert appears, tabTwo is temporarily locked or tabThree gets temporarily locked for around 2-3 seconds. I cannot figure out where I'm going wrong?
I'm not sure if it makes a difference but I'm using a custom alert instead of the UIAlertController. It's just some UIViews and a button, it's nothing special so I didn't include the code.
var urls = [URL]()
picUUID = UUID().uuidString
dict = [String:Any]()
let myGroup = DispatchGroup()
var count = 0
for url in urls{
myGroup.enter() // enter group here
URLSession.shared.dataTask(with: url!, completionHandler: {
(data, response, error) in
guard let data = data, let _ = error else { return }
DispatchQueue.main.async{
self.sendDataToStorage("\(self.picUUID)_\(self.count).jpg", picData: data)
self.count += 1
}
}).resume()
// send dictionary data to firebase when loop is done
myGroup.notify(queue: .main) {
self.sendDataToFirebaseDatabase()
self.count = 0
}
}
func sendDataToStorage(_ picId: String, picData: Data?){
dict.updateValue(picId, forKey:"picId_\(count)")
let picRef = storageRoot.child("pics")
picRef.putData(picData!, metadata: nil, completion: { (metadata, error) in
if let picUrl = metadata?.downloadURL()?.absoluteString{
self.dict.updateValue(picUrl, forKey:"picUrl_\(count)")
self.myGroup.leave() // leave group here
}else{
self.myGroup.leave() // leave group if picUrl is nil
}
}
}
func sendDataToFirebaseDatabase(){
let ref = dbRoot.child("myRef")
ref.updateChildValues(dict, withCompletionBlock: { (error, ref) in
displaySuccessAlert()
}
}
I don't know much about Firebase, but you are dispatching your sendDataToFirebaseDatabase method to main queue which probably explains why your UI becomes unresponsive.
Dispatch sendDataToFirebaseDatabase to a background queue and only dispatch your displaySuccessAlert back to main queue.

Modify Global Variable Inside Closure (Swift 4)

I am trying to modify the global variable currentWeather (of type CurrentWeather) using this function, which is meant to update said variable with the information retrieved from the URL and return a bool signifying its success. However, the function is returning false, as currentWeather is still nil. I recognize that the dataTask is asynchronous, and that the task is running in the background parallel to the application, but I don't understand what this means for what I'm trying to accomplish. I also am unable to update currentWeather after the do block, as weather is no longer recognized after exiting the block. I did try using "self.currentWeather", but was told it was an unresolved identifier (perhaps because the function is also global, and there is no "self"?).
The URL is not currently valid because I took out my API key, but it is working as expected otherwise, and my CurrentWeather struct is Decodable. Printing currentWeatherUnwrapped is also consistently successful.
I did look around Stack Overflow and through Apple's official documentation and was unable to find something that answered my question, but perhaps I wasn't thorough enough. I'm sorry if this is a duplicate question. Direction to any further relevant reading is also appreciated! I apologize for the lack of conformity to best coding practices - I'm not very experienced at this point. Thank you all so much!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}
When you do an asynchronous call like this, your function will return long before your dataTask will have any value to return. What you need to do is use a completion handler in your function. You can pass it in as a parameter like this:
func getCurrentWeather(completion: #escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
Then calling that function looks like this:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})
All you need is a closure.
You cant have synchronous return statement to return the response of web service call which in itself is asynchronous in nature. You need closures for that.
You can modify your answer as below. Because you have not answered to my question in comment I have taken liberty to return the wether object rather than returning bool which does not make much sense.
func getCurrentWeather (completion : #escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
Assuming currentWeather is a static variable in your CurrentWeather class you can update your global variable as well as return the actual data to caller as shown above
EDIT:
As pointed out by Duncan in comments below, the above code executes the completion block in background thread. All the UI operations must be done only on main thread. Hence its very much essential to switch the thread before updating the UI.
Two ways :
1- Make sure you execute the completion block on main thread.
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
This will make sure that whoever uses your getCurrentWeather in future need not worry about switching thread because your method takes care of it. Useful if your completion block contains only the code to update UI. Lengthier logic in completion block with this approach will burden the main thread.
2 - Else In completion block that you pass as a parameter to getCurrentWeather whenever you update UI elements make sure you wrap those statements in
DispatchQueue.main.async {
//your code to update UI
}
EDIT 2:
As pointed out by Leo Dabus in comments below, I should have run completion block rather than guard let url = URL(string: jsonUrlString) else { return false } That was a copy paste error. I copied the OP's question and in a hurry din realize that there is a return statement.
Though having a error as a parameter is optional in this case and completely depends on how you designed your error handling model, I appreciate the idea suggested by Leo Dabus which is more general approach and hence updating my answer to have error as a parameter.
Now there are cases where we may need to send our custom error as well for example if guard let data = data else { return } returns false rather than simply calling return you may need to return a error of your own which says invalid input or something like that.
Hence I have taken a liberty to declare a custom errors of my own and you can as well use the model to deal with your error handling
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : #escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
You fundamentally misunderstand how async functions work. You function returns before the URLSession's dataTask has even begun to execute. A network request may take multiple seconds to complete. You ask it to fetch some data for you, give it a block of code to execute ONCE THE DATA HAS DOWNLOADED, and then go on with your business.
You can be certain that the line after the dataTask's resume() call will run before the new data has loaded.
You need to put code that you want to run when the data is available inside the data task's completion block. (Your statement print(currentWeatherUnwrapped) will run once the data has been read successfully.)
As you pointed out, the data ask is async, meaning you do not know when it will be completed.
One option is to modify your wrapper function getCurrentWeather to be async as well by not providing a return value, but instead a callback/closure. Then you will have to deal with the async nature somewhere else though.
The other option which is what you probably want in your scenario is to make the data task synchronous like so:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
The wait function can take parameters, which can define a timeout. https://developer.apple.com/documentation/dispatch/dispatchgroup Otherwise your app could be stuck waiting forever. You will then be able to define some action to present that to the user.
Btw I made a fully functional weather app just for learning, so check it out here on GitHub https://github.com/erikmartens/NearbyWeather. Hope the code there can help you for your project. It's also available on the app store.
EDIT: Please understand that this answer is meant to show how to make async calls synchronous. I am not saying this is good practice for handling network calls. This is a hacky solution for when you absolutely must have a return value from a function even though it uses async calls inside.

How to perform segue after 5 files download request completed in swift3

I am trying to perform segue after downloading.
I tried BlockOperation firstly, but I failed.
Below is my code.
let operationQ = OperationQueue()
let taskArray: [BlockOperation]
for index in 0..<songsList.count {
let eachSong = songsList[index]
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = NSHomeDirectory() + "/Documents/"
let fileURL = URL(fileURLWithPath: documentsURL.appending("song\(index).m4a"))
print("song\(index).m4a is downloading")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
taskArray.append(BlockOperation(block: let task = {
Alamofire.download(eachSong, to: destination).response { _ in
// print(response.response)
}
}))
taskArray[4].completionBlock = { performSegue(withIdentifier: "NextVC", sender: self) }
I want to set a array of [BlockOperation] firstly.
Then, trying to append Alamofire.download in this array, but failed.
I am not sure which part got wrong, maybe each block need a different name?
Please help me.
The tasks do not always terminate in the same order that you pushed them into the queue.
You also must use UI operations on the main thread!
See this post How is it possible to perform multiple Alamofire requests that are finished one after another?
You can also read this blog post for an example of using BlockOperation

Resources