Refreshing view after api call in OnOpenUrl view modifier - ios

I have this very basic piece of code in one of my Views:
.onOpenURL(perform: { url in
avm.handleRedirect(viewContext,url: url) })
where avm is defined as
#ObservedObject var avm = AccountsViewModel.makeAccountsViewModel()
avm has this basic property:
#Published var isLoading = true
That when set, my view listens to and shows a loading spinner. This works in all other situations, except that outlined below.
The handleRedirect function looks as follows:
func handleRedirect(_ context: NSManagedObjectContext, url: URL) {
debugPrint("Handling redirect")
let url = URLComponents(string: url.absoluteString)!
let code = url.queryItems?.first(where: { $0.name == "code" })?.value
trueLayerClient.getAccessToken(code: code!) { res in
if res == nil {
debugPrint("accessTokenResponse was nil")
}
self.api.storeTokens(
str: API.StoreTokensRequest(
userID: "***",
authToken: res!.accessToken,
refreshToken: res!.refreshToken,
apiKey: "***"
), finished: { success in
if success{
debugPrint("was successful")
DispatchQueue.main.async{
self.isLoading = false
}
} else {
debugPrint("store token failed")
}
})
}
}
I'm triggering this onOpenUrl using a universal Link I have setup.
When I hit the link, my app opens from the background and I see the following logs:
Handling Redirect
Was successful
However, the app never goes into a loading state. Furthermore, the UI becomes "blocked" and I have to hard kill the app to be able to press anything.
Once I do reopen the app, the new state (which I fetch via api and store in CoreData) is reflected in the view.
At first I thought my API was responding too quickly, but I put a 5 second sleep in it and I still see the UI get blocked but with the same logs (just further apart).
I would appreciate any help on this.
If helpful, my storeTokens API call looks like this:
func storeTokens(str: StoreTokensRequest , finished: #escaping (Bool)->Void) {
let u = URL(string:"\(self.baseURL)/\(storeTokenPath)")!
var req = URLRequest(url: u)
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpMethod = "POST"
let reqBody = str
debugPrint("req bodyed")
guard let encoded = try? JSONEncoder().encode(reqBody) else {
debugPrint("failed to encode req")
finished(false)
return
}
req.httpBody = encoded
debugPrint("encoded")
let task = URLSession.shared.dataTask(with: req) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
debugPrint("wasnt http rep")
finished(false)
return
}
debugPrint("status code is: \(httpResponse.statusCode)")
if httpResponse.statusCode < 299 {
debugPrint("About to call true")
finished(true)
return
}
}
task.resume()
}
}
I see all the logs in this function in my output too, I just left them out for succinctness.

I have tried an implementation similar to yours. My "onOpenUrl()" method is waking up, if necessary, my app, and passing the url to my app, and the app is updating its state correctly.
Basic check.
Forget for a moment "onOpenUrl". Implement inside your view this method:
.onAppear {
viewModel.onAppear()
}
where inside your view model play with the "isLoading" property. For example,
func onAppear(){
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("- loading complete")
self.isLoading = false
}
}
Does the view update (the activity loader) accordingly?

Related

How to publish changes from the background thread Swift UI GET Request

I have set up my app such that I use UserDefaults to store a users login info (isLoggedIn, account settings). If a user is logged in and exits out of the application, and then relaunches the app, I would like them to be returned to the home page tab.
This functionality works; however, for some reason, on relaunch the home page has a getRequest that should be carried out. Instead, the screen goes white. This request and the loading involved works when I navigate from the login, but not when I relaunch the app. I get this warning:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
In looking at other stack overflow posts, the common sentiment seems to be to wrap any type of change in a dispatchqueue.main.async; however, this does not seem to work for me.
import SwiftUI
struct StoresView: View {
#ObservedObject var request = Request()
#Environment(\.imageCache) var cache: ImageCache
#EnvironmentObject var carts: Carts
init() {
getStores()
}
var body: some View {
NavigationView {
List(self.request.stores) { store in
NavigationLink(destination: CategoryHome(store: store).environmentObject(self.carts)) {
VStack(alignment: .leading) {
Text(store.storeName)
.font(.system(size: 20))
}
}
}.navigationBarTitle(Text("Stores").foregroundColor(Color.black))
}
}
func getStores() {
DispatchQueue.main.async {
self.request.getStoresList() { stores, status in
if stores != nil {
self.request.stores = stores!
}
}
}
}
}
get stores call in Request class
class Request: ObservableObject {
#Published var stores = [Store]()
let rest = RestManager()
func getStoresList(completionHandler: #escaping ([Store]?, Int?)-> Void) {
guard let url = URL(string: "###################") else { return }
self.rest.makeRequest(toURL: url, withHttpMethod: .GET, useSessionCookie: false) { (results) in
guard let response = results.response else { return }
if response.httpStatusCode == 200 {
guard let data = results.data else { return}
let decoder = JSONDecoder()
guard let stores = try? decoder.decode([Store].self, from: data) else { return }
completionHandler(stores, response.httpStatusCode)
} else {
completionHandler(nil, response.httpStatusCode)
}
}
}
Make Request from RestManager, I included the make request because I've seen some others use shared dataPublishing tasks, but I may not have used it correctly when trying to use it. Any advice or help would be appreciated. Thanks!
func makeRequest(toURL url: URL,
withHttpMethod httpMethod: HttpMethod, useSessionCookie: Bool?,
completion: #escaping (_ result: Results) -> Void) {
DispatchQueue.main.async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
// fetches cookies and puts in appropriate header and body attributes
guard let request = self?.prepareRequest(withURL: targetURL, httpBody: httpBody, httpMethod: httpMethod, useSessionCookie: useSessionCookie) else
{
completion(Results(withError: CustomError.failedToCreateRequest))
return
}
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
print(response)
completion(Results(withData: data,
response: Response(fromURLResponse: response),
error: error))
}
task.resume()
}
}
You seem to be trying to call the function in the Main tread instead of setting the stores property. Calling request. getStoresList is already in the main thread once the call is made you enter the background thread from there you need to come back to the main thread once the URLSession is complete. You need to make the UI modification in the Main thread instead of the background tread as the error clearly state. Here's what you need to do to fix this issue:
func getStores() {
self.request.getStoresList() { stores, status in
DispatchQueue.main.async {
if stores != nil {
self.request.stores = stores!
}
}
}
}

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.

Swift: downloading data from url causes semaphore_wait_trap freeze

In my application, button tapping downloads data from an Internet site. The site is a list of links containing binary data. Sometimes, the first link may not contain the proper data. In this case, the application takes the next link in the array and gets data from there. The links are correct.
The problem I have is that frequently (not always though) the application freezes for seconds when I tap on the button. After 5-30 seconds, it unfreezes and downloading implements normally. I understand, something is blocking the main thread. When stopping the process in xCode, I get this (semaphore_wait_trap noted):
This is how I do it:
// Button Action
#IBAction func downloadWindNoaa(_ sender: UIButton)
{
// Starts activity indicator
startActivityIndicator()
// Starts downloading and processing data
// Either use this
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async
{
self.downloadWindsAloftData()
}
}
// Or this - no difference.
//downloadWindsAloftData()
}
}
func downloadWindsAloftData()
{
// Creates a list of website addresses to request data: CHECKED.
self.listOfLinks = makeGribWebAddress()
// Extract and save the data
saveGribFile()
}
// This downloads the data and saves it in a required format. I suspect, this is the culprit
func saveGribFile()
{
// Check if the links have been created
if (!self.listOfLinks.isEmpty)
{
/// Instance of OperationQueue
queue = OperationQueue()
// Convert array of Strings to array of URL links
let urls = self.listOfLinks.map { URL(string: $0)! }
guard self.urlIndex != urls.count else
{
NSLog("report failure")
return
}
// Current link
let url = urls[self.urlIndex]
// Increment the url index
self.urlIndex += 1
// Add operation to the queue
queue.addOperation { () -> Void in
// Variables for Request, Queue, and Error
let request = URLRequest(url: url)
let session = URLSession.shared
// Array of bytes that will hold the data
var dataReceived = [UInt8]()
// Read data
let task = session.dataTask(with: request) {(data, response, error) -> Void in
if error != nil
{
print("Request transport error")
}
else
{
let response = response as! HTTPURLResponse
let data = data!
if response.statusCode == 200
{
//Converting data to String
dataReceived = [UInt8](data)
}
else
{
print("Request server-side error")
}
}
// Main thread
OperationQueue.main.addOperation(
{
// If downloaded data is less than 2 KB in size, repeat the operation
if dataReceived.count <= 2000
{
self.saveGribFile()
}
else
{
self.setWindsAloftDataFromGrib(gribData: dataReceived)
// Reset the URL Index back to 0
self.urlIndex = 0
}
}
)
}
task.resume()
}
}
}
// Processing data further
func setWindsAloftDataFromGrib(gribData: [UInt8])
{
// Stops spinning activity indicator
stopActivityIndicator()
// Other code to process data...
}
// Makes Web Address
let GRIB_URL = "http://xxxxxxxxxx"
func makeGribWebAddress() -> [String]
{
var finalResult = [String]()
// Main address site
let address1 = "http://xxxxxxxx"
// Address part with type of data
let address2 = "file=gfs.t";
let address4 = "z.pgrb2.1p00.anl&lev_250_mb=on&lev_450_mb=on&lev_700_mb=on&var_TMP=on&var_UGRD=on&var_VGRD=on"
let leftlon = "0"
let rightlon = "359"
let toplat = "90"
let bottomlat = "-90"
// Address part with coordinates
let address5 = "&leftlon="+leftlon+"&rightlon="+rightlon+"&toplat="+toplat+"&bottomlat="+bottomlat
// Vector that includes all Grib files available for download
let listOfFiles = readWebToString()
if (!listOfFiles.isEmpty)
{
for i in 0..<listOfFiles.count
{
// Part of the link that includes the file
let address6 = "&dir=%2F"+listOfFiles[i]
// Extract time: last 2 characters
let address3 = listOfFiles[i].substring(from:listOfFiles[i].index(listOfFiles[i].endIndex, offsetBy: -2))
// Make the link
let addressFull = (address1 + address2 + address3 + address4 + address5 + address6).trimmingCharacters(in: .whitespacesAndNewlines)
finalResult.append(addressFull)
}
}
return finalResult;
}
func readWebToString() -> [String]
{
// Final array to return
var finalResult = [String]()
guard let dataURL = NSURL(string: self.GRIB_URL)
else
{
print("IGAGribReader error: No URL identified")
return []
}
do
{
// Get contents of the page
let contents = try String(contentsOf: dataURL as URL)
// Regular expression
let expression : String = ">gfs\\.\\d+<"
let range = NSRange(location: 0, length: contents.characters.count)
do
{
// Match the URL content with regex expression
let regex = try NSRegularExpression(pattern: expression, options: NSRegularExpression.Options.caseInsensitive)
let contentsNS = contents as NSString
let matches = regex.matches(in: contents, options: [], range: range)
for match in matches
{
for i in 0..<match.numberOfRanges
{
let resultingNS = contentsNS.substring(with: (match.rangeAt(i))) as String
finalResult.append(resultingNS)
}
}
// Remove "<" and ">" from the strings
if (!finalResult.isEmpty)
{
for i in 0..<finalResult.count
{
finalResult[i].remove(at: finalResult[i].startIndex)
finalResult[i].remove(at: finalResult[i].index(before: finalResult[i].endIndex))
}
}
}
catch
{
print("IGAGribReader error: No regex match")
}
}
catch
{
print("IGAGribReader error: URL content is not read")
}
return finalResult;
}
I have been trying to fix it for the past several weeks but in vain. Any help would be much appreciated!
let contents = try String(contentsOf: dataURL as URL)
You are calling String(contentsOf: url) on the main thread (main queue). This downloads the content of the URL into a string synchronously The main thread is used to drive the UI, running synchronous network code is going to freeze the UI. This is a big no-no.
You should never call readWebToString() in the main queue. Doing DispatchQueue.main.async { self.downloadWindsAloftData() } exactly put the block in the main queue which we should avoid. (async just means "execute this later", it is still executed on Dispatch.main.)
You should just run downloadWindsAloftData in the global queue instead of main queue
DispatchQueue.global(qos: .default).async {
self.downloadWindsAloftData()
}
Only run DispatchQueue.main.async when you want to update the UI.
Your stack trace is telling you that it's stopping at String(contentsOf:), called by readWebToString, called by makeGribWebAddress.
The problem is that String(contentsOf:) performs a synchronous network request. If that request takes any time, it will block that thread. And if you call this from the main thread, your app may freeze.
Theoretically, you could just dispatch that process to a background queue, but that merely hides the deeper problem, that you are doing a network request with an API that is synchronous, non-cancellable, and offers no meaningful error reporting.
You really should doing asynchronous requests with URLSession, like you have elsewhere. Avoid using String(contentsOf:) with remote URL.

Where to place a completionHandler when inside loops?

I'm using a completionHandler in this function, however it's nested within several for loops (below). The problem is the handler where it is now gets called every time the loop it's in runs, whereas I only want the handler to pass in the Set when the entire function has completed processing. If I place it outside of the loop, then it gets called too early and is empty. What should I do here?
Right now when I print to the console to test it prints:
Set item 1
Set item 1, 2
Set item 1, 2, 3 etc.
struct RekoRequest {
public func getRekos(rekoType: rekoCategory, handler: #escaping (Set<String>) -> Void) {
var urls = [NSURL]()
var IDs = Set<String>()
TwitterRequest().fetchTweets(searchType: "things") { result in
guard let tweets = result as? [TWTRTweet] else {print("Error in getRekos receiving tweet results from TwitterRequest.fetchTweets"); return}
for tweet in tweets {
let types: NSTextCheckingResult.CheckingType = .link
let detector = try? NSDataDetector(types: types.rawValue)
guard let detect = detector else { print("NSDataDetector error"); return }
let matches = detect.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, (text.characters.count)))
for match in matches {
if let url = match.url {
guard let unwrappedNSURL = NSURL(string: url.absoluteString) else {print("error converting url to NSURL");return}
//Show the original URL
unwrappedNSURL.resolveWithCompletionHandler {
guard let expandedURL = URL(string: "\($0)") else {print("couldn't covert to expandedURL"); return}
guard let urlDomain = expandedURL.host else { print("no host on expandedURL"); return }
switch urlDomain {
case "www.somesite.com":
let components = expandedURL.pathComponents
for component in components {
if component == "dp" {
guard let componentIndex = components.index(of: component) else {print("component index error"); return}
let IDIndex = componentIndex + 1
let ID = components[IDIndex]
//Filter out Dups and add to Set
IDs.insert(ID)
handler(IDs)
print(ID) //this prints multiple sets of IDs, I only want one when the function is finished completely
}
}
break;
default:
break;
}
}
} else { print("error with match.url") }
} //for match in matches loop
} //for tweet in tweets loop
}
}
}
// Create an extension to NSURL that will resolve a shortened URL
extension NSURL
{
func resolveWithCompletionHandler(completion: #escaping (NSURL) -> Void)
{
let originalURL = self
let req = NSMutableURLRequest(url: originalURL as URL)
req.httpMethod = "HEAD"
URLSession.shared.dataTask(with: req as URLRequest)
{
body, response, error in completion(response?.url as NSURL? ?? originalURL)
}
.resume()
}
}
Call your completion handler after the for loop.
for component in components {
if component == "dp" {
...
}
}
handler(IDs)
Important: The handler should be called outside of the for loop, but within the TwitterRequest().fetchTweets() trailing closure.
Approaches to handling an empty set
Your IDs are being initialized to an empty set. Only after meeting certain conditions within your for loop are values being inserted into this set. If these conditions aren't met, then your IDs set will be empty.
If this is undesirable, then you will have to either make changes to your completion handler or alter your conditional logic so that you always get a non-empty set.
One approach might be to have optional set in your callback. Something like:
(Set<String>?) -> Void
If IDs are empty, then callback with a nil and have your calling code handle the possibility of a nil set.
Another approach might be to create an enum to encapsulate your result and use this in your callback. Something like:
Enum
enum Result {
case success(Set<String>)
case failure
}
Callback
handler: (Result) -> Void
Usage
handler(.success(IDs))
// or
handler(.failure)
Calling Code
getReckos(rekoType: .someType) { result in
switch result {
case .success(let IDs):
// Use IDs
case .failure:
// Handle no IDs
}
}

Swift (iOS), waiting for all images to finish downloading before returning

I am writing a Swift iOS app (my first, so please bear with me) where I use Swifter HTTP server to process various requests. One such request is an HTTP POST with a JSON array specifying images to download from the web (and do some other stuff, not pertinent to the issue at hand).
I use Alamofire to download the images (this works fine), but I am looking for good (preferably simple) way to wait for all the images to finish downloading before returning a response to the POST request above (since the response has to contain JSON indicating the result, including any failed downloads).
What is a good way to accomplish this (preferably w/o blocking the main thread)?
Here are some snippets to illustrate:
public func webServer(publicDir: String?) -> HttpServer {
let server = HttpServer()
server.POST["/images/update"] = { r in
let images = ...(from JSON array in body)
let updateResult = ImageUtil.updateImages(images)
let resultJson: String = Mapper().toJSONString(updateResult, prettyPrint: true)!
if updateResult.success {
return .OK(.Text(resultJson))
}
return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](updateResult.errorMessage.utf8)) })
}
}
static func updateImages(images: [ImageInfo]) -> UpdateResult {
let updateResult = UpdateResult()
for image in images {
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.validate()
.response{_, _, _, error in
if let error = error {
Log.error?.message("Error downloading file \(image.imageUrl) to \(image.fileName): \(error)")
} else {
updateResult.filesDownloaded++
Log.info?.message("Downloaded file \(image.imageUrl) to \(image.fileName)")
}}
}
return updateResult // It obviously returns before any images finish downloading. I need to wait until all images have downloaded before I can return an accurate result.
}
Update 1/23/2016, using dispatcher per bbum
This is an attempt to use the dispatcher mechanism, but the call to updateImages still return right away (even when using dispatch_sync).
How can I await the completion of all downloads before returning my HTTP response to the caller?
public func webServer(publicDir: String?) -> HttpServer {
let server = HttpServer()
server.POST["/images/update"] = { r in
let imageDownloader = ImageDownloader()
imageDownloader.updateimageFiles(adFilesOnServer)
let resultJson: String = Mapper().toJSONString(imageDownloader.updateResult, prettyPrint: true)!
if imageDownloader.updateResult.success {
return .OK(.Text(resultJson))
}
return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](imageDownloader.updateResult.errorMessage.utf8)) })
}
}
class ImageDownloader {
var updateResult = AdUpdateResult()
private var imageFilesOnServer = [ImageFile]()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.imageDirectory, isDirectory: true)
private let semaphore = dispatch_semaphore_create(4)
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func updateimageFiles(imageFilesOnServer: [ImageFile]) {
self.imageFilesOnServer = imageFilesOnServer
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
for serverFile in imageFilesOnServer {
downloadImageFileFromServer(serverFile)
}
dispatch_sync(downloadQueue) {
dispatch_sync(dispatch_get_main_queue()) {
print("done") // It gets here before images have downloaded.
}
}
}
private func downloadImageFileFromServer(serverFile: ImageFile) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.validate()
.response { _, _, _, error in
if let error = error {
Log.error?.message("Error downloading file \(serverFile.imageUrl) to \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
Log.info?.message("Downloaded file \(serverFile.imageUrl) to \(serverFile.fileName)")
}
dispatch_semaphore_signal(self.semaphore)
}
}
}
First, you really don't want to be firing off a request-per-image without some kind of a throttle. Semaphores work well for that sort of thing.
Secondly, you need to basically count the number of operations outstanding and then fire a completion handler when they are all done. Or, if new operations can be started at any time, you'll probably want to group operations.
So, pseudo code:
sema = dispatch_semaphore_create(4) // 4 being # of concurrent operations allowed
serialQ = dispatch_queue_create(.., SERIAL)
dispatch_async(serialQ) {
dispatch_semaphore_wait(sema, FOREVER) // will block if there are 4 in flight already
for image in images {
downloader.downloadAsync(image, ...) { // completion
dispatch_semaphore_signal(sema) // signal that we are done with one
... handle downloaded image or error ...
... add downloaded images to downloadedImages ...
}
}
}
dispatch_async(serialQ) {
// since serialQ is serial, this will be executed after the downloads are done
dispatch_async(main_queue()) {
yo_main_queue_here_be_yer_images(... downloadedImages ...)
}
}

Resources