Making looped HTTP requests

54 views Asked by At

I am writing a translator app that connects with an online translation API. Due to size limitations in the API, I have written my program to send text one sentence at a time, and then join the translations together. I have looped

let serialQueue = DispatchQueue(label: "translationQueue")
        
        for line in lines {
          serialQueue.async {
            print("line is: " + line)
            
            var jpText = String(line)
            
            if jpText.isEmpty {
              jpText = "\n"
            }
            
            let escapedStr = jpText.addingPercentEncoding(withAllowedCharacters: (NSCharacterSet.urlQueryAllowed))
            let urlStr:String = ("https://api.mymemory.translated.net/get?q="+escapedStr!+"&langpair="+langStr!)
            let url = URL(string: urlStr)
            
            // Creating Http Request
            let request = NSURLRequest(url: url!)
            
            
            // If empty, don't feed to translator.
            if escapedStr!.isEmpty {
              //translatedLines.append("\n")
              self.enTextView.text = translatedLines
            }
            
            else {
              let configuration = URLSessionConfiguration.default
              configuration.waitsForConnectivity = true
              let defaultSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
              var dataTask: URLSessionDataTask?
              
              let group = DispatchGroup()
              group.enter()
              
              dataTask?.cancel()
              
              dataTask = defaultSession.dataTask(with: request as URLRequest) { [weak self] data, response, error in
                
                if let error = error {
                  // self?.errorMessage += "DataTask error: " + error.localizedDescription + "\n"
                  print("DataTask error: " + error.localizedDescription + "\n")
                  
                } else if
                  let data = data,
                  let response = response as? HTTPURLResponse,
                  response.statusCode == 200 {
                  let jsonDict: NSDictionary!=((try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as! NSDictionary)
                  
                  // if(jsonDict.value(forKey: "responseStatus") as! NSNumber == 200){
                  let responseData: NSDictionary = jsonDict.object(forKey: "responseData") as! NSDictionary
                  
                  group.leave()
                  
                  var translatedString = String()
                  translatedString = responseData.object(forKey: "translatedText") as! String
                  
                  let data = translatedString.data(using: .utf8)
                  
                  let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
                    .documentType: NSAttributedString.DocumentType.html,
                    .characterEncoding: String.Encoding.utf8.rawValue
                  ]
                  
                  guard let attributedString = try? NSAttributedString(data: data!, options: options, documentAttributes: .none) else {
                    return
                  }
                  
                  let decodedString = attributedString.string
                  print("decoded: " + decodedString)
                  
                  translatedLines.append(decodedString)
                  translatedLines.append("\n")
                  
                  
                  DispatchQueue.main.async {
                    self?.enTextView.text = translatedLines
                  }
                }
              }
              
              dataTask?.resume()
              group.wait()
            }
          }
        }

But the translation output comes out in a random order. I broadly understand that there are concurrent requests being sent. But what can I do in my for-loop to make sure the entire send/receive happens before moving to the next iteration?

1

There are 1 answers

2
Vadim Belyaev On

I would use the following technique:

  1. Preallocate an array for translation results. A result may be either a success (with associated translated text) or a failure (with associated error type). Swift's Result type comes in very handy for this use case.
  2. Assign each data task an index (essentially, an index in the array created on step 1).
  3. Execute all data tasks simultaneously, coordinating them using a DispatchGroup.
  4. When completed either with success or failure, each task writes its result to the array.
  5. When all data tasks have been completed, the dispatch group will notify the main queue and execute our completing block where we can update the UI.

Here's a quick playground code loosely based on yours that uses the tecnhique I described above:

let lines = ["one", "two", "three", "four"]

enum TranslationError: Error {
    case neverRequested
    case countNotBuildUrl
    case dataTaskError(localizedDescription: String)
    case responseDidNotContainData
    case parsingError
}

let dispatchGroup = DispatchGroup()
var translationResults = [Result<String, TranslationError>](repeating: .failure(.neverRequested), count: lines.count)
for i in 0..<lines.count {
    dispatchGroup.enter()
    let line = lines[i]
    var urlComponents = URLComponents(string: "https://api.mymemory.translated.net/get")!
    urlComponents.queryItems = [
        URLQueryItem(name: "q", value: line),
        URLQueryItem(name: "langpair", value: "en|fr")
    ]
    guard let url = urlComponents.url else {
        translationResults[i] = .failure(.countNotBuildUrl)
        dispatchGroup.leave()
        continue
    }

    let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in
        print("Data task #\(i) finished.")
        defer {
            dispatchGroup.leave()
        }
        if let error = error {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.dataTaskError(localizedDescription: error.localizedDescription))
            }
            return
        }
        guard let data = data else {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.responseDidNotContainData)
            }
            return
        }
        guard let jsonDict = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary,
            let responseData = jsonDict.value(forKey: "responseData") as? NSDictionary,
            let translatedLine = responseData.value(forKey: "translatedText") as? String else
        {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.parsingError)
            }
            return
        }
        DispatchQueue.main.async {
            translationResults[i] = .success(translatedLine)
        }
    }
    print("Starting data task #\(i)...")
    dataTask.resume()
}
dispatchGroup.notify(queue: .main, execute: {
    // Handle errors and update the UI
    print(translationResults)
})

A few notes:

  1. It's important that all data task completion blocks update the translationResults array on the same queue, that's why I'm calling DispatchQueue.main.async every time. Not doing it may result in weird crashes because arrays are not thread-safe in Swift.
  2. This technique does not allow to incrementally update the UI line by line as the translations come in, but it's possible to add something like that.
  3. In a real app I would refactor this code and split it into several methods with more focused responsibilities. However to make it easier to understand the idea I decided not to do it.
  4. Error handling in my code is also very basic just to show the idea and keep it show, and it doesn't include some other checks I'd do in a real app.
  5. I'm pretty sure there are better and more elegant ways to achieve the same result so I'm looking forward to see if someone comes up with a better implementation.