Test request body with OHHTTPStubs in Swift - ios

I'm trying to test the request body sent captured with OHHTTPStubs but it seems buggy returning because the
request.httpBody is nil.
I found this info about this problem Testing for the request body in your stubs. But I'm pretty new in iOS development and don't know how to access to OHHTTPStubs_HTTPBody in Swift. How can I do this?

I guess rough equivalent in Swift will be following:
import OHHTTPStubs.NSURLRequest_HTTPBodyTesting
...
stub(isMethodPOST() && testBody()) { _ in
return OHHTTPStubsResponse(data: validLoginResponseData, statusCode:200, headers:nil)
}).name = "login"
public func testBody() -> OHHTTPStubsTestBlock {
return { req in
let body = req.ohhttpStubs_HTTPBody()
let bodyString = String.init(data: body, encoding: String.Encoding.utf8)
return bodyString == "user=foo&password=bar"
}
}
So, more precisely, you can access OHHTTPStubs_HTTPBody by calling ohhttpStubs_HTTPBody() method inside OHHTTPStubsTestBlock.

What worked for me is the following:
func testYourStuff() {
let semaphore = DispatchSemaphore(value: 0)
stub(condition: isScheme(https)) { request in
if request.url!.host == "blah.com" && request.url!.path == "/blah/stuff" {
let data = Data(reading: request.httpBodyStream!)
let dict = Support.dataToDict(with: data)
// at this point of time you have your data to test
// for example dictionary as I have
XCTAssertTrue(...)
} else {
XCTFail()
}
// flag that we got inside of this block
semaphore.signal()
return OHHTTPStubsResponse(jsonObject: [:], statusCode:200, headers:nil)
}
// this code will be executed first,
// but we still need to wait till our stub code will be completed
CODE to make https request
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
}
// convert InputStream to Data
extension Data {
init(reading input: InputStream) {
self.init()
input.open()
let bufferSize = 1024
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
while input.hasBytesAvailable {
let read = input.read(buffer, maxLength: bufferSize)
self.append(buffer, count: read)
}
buffer.deallocate(capacity: bufferSize)
input.close()
}
}
Credits to this person for converting InputStrem to Data: Reading an InputStream into a Data object

Related

Reading and parsing a file from dropbox in swift

I am following a tutorial on how to read and parse a csv file from dropbox in swift. However, the tutorial is 4 years old and my code is not compiling in swift5. The code example is copied below and the link to the original video tutorial is here https://www.youtube.com/watch?v=O6AKHAXpji0
I am getting two errors.
Error 1:
on the let request = line of callFileFromWeb(){}
'NSURL' is not implicitly convertible to 'URL'; did you mean to use 'as' to explicitly convert?
Error 2:
and on let session = ... within the httpGet(){}
'NSURLSession' has been renamed to 'URLSession'
When I try to implement the proposed fix for error two then I get another error
Cannot call value of non-function type 'URLSession`
Any ideas what should I be adjusting for it to work in swift5?
var items:[(days:String, city:String, inches: String)]?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
callFileFromWeb()
}
func callFileFromWeb(){
let request = NSMutableURLRequest(URL: NSURL(string: "https://dl.dropboxusercontent.com/u/2813968/raindata.txt")!)
httpGet(request){
(data, error) -> Void in
if error != nil {
print(error)
} else {
print(data)//PRINTING ALL DATA TO CONSOLE
let delimiter = ":"
self.items = []
let lines:[String] = data.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
for line in lines {
var values:[String] = []
if line != "" {
values = line.componentsSeparatedByString(delimiter)
// Put the values into the tuple and add it to the items array
print(values[2])//PRINTING LAST COLUMN
let item = (days: values[0], city: values[1], inches: values[2])
self.items?.append(item)
}}//all good above
// self.AddDataToDatabase()
}//there was an error
}//end of request
}//end of get data from web and load in database
func httpGet(request: NSURLRequest!, callback: (String, String?) -> Void) {
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
if error != nil {
callback("", error!.localizedDescription)
} else {
let result = NSString(data: data!, encoding:
NSASCIIStringEncoding)!
callback(result as String, nil)
}
}
task.resume()
}
The final goal is to be able to read a file from drop box. The file updates weakly, so when users launch the app they always have access to the most updated version of the file, rather than having to re download the app when the file updates. Is this the correct approach to do this?
A framework that proved useful in past projects with parsing CSV files is:
CSwiftV -> https://github.com/Daniel1of1/CSwiftV
Updated: 9/23/2020
Let me demonstrate it by refactoring your callFileFromWeb():
func callFileFromWeb() {
let dropboxURL = URL(string: "https://dl.dropboxusercontent.com/u/2813968/raindata.txt")
URLSession.shared.dataTask(with: dropboxURL!) { data, response, error in
guard let urlData = data, error == nil else {
return
}
let unparsedCSV = String(data: urlData, encoding: String.Encoding.utf8) ?? "Year,Make,Model,Description,Price\r\n1997,Ford,E350,descrition,3000.00\r\n1999,Chevy,Venture,another description,4900.00\r\n"
let csv = CSwiftV(with: unparsedCSV)
let rows = csv.rows
var iteration = 0
for row in rows {
// Assuming you want the last row
if iteration == rows.count - 1 {
let item = (days: row[0], city: row[1], inches: row[2])
self.items?.append(item)
}
iteration += 1
}
}
}
One other thing, remember that you need to download CSSwiftV from Github and copy the original CSwiftV.swift file into your project.
Hope this helps!

Swift 5 : Escaping closure captures 'inout' parameter

I already have the response data that I received from the server. This response data have some bakers data.
Now I want to calculate the distance of the user and bakery and then store it in the same modal class. I have created a function for it. And as this function need to be used in 4,5 view controllers, my plan is to create as an extension of UIViewController
func getDistanceUserBakery(bakeryData : inout [BakeryRecord], completion : #escaping (Int?) -> () ) {
for index in 0...(bakeryData.count-1) {
//1
let googleApiAdd = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&"
//2
let origin = "origins=\(UserLocation.coordinates.latitude),\(UserLocation.coordinates.longitude)"
//3
let destination = "&destinations=\(bakeryData[index].location?.coordinates?[1] ?? 0.0),\(bakeryData[index].location?.coordinates?[0] ?? 0.0)"
//4
let googleKey = "&key=\(GOOGLE_KEY)"
//5
let url = googleApiAdd + origin + destination + googleKey
let request = URLRequest(url: URL(string: url)!)
//6 - this line is showing the error.
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
guard let data = data else {
completion(nil)
Toast.show(message: "Unable to calculate distance from user to bakery", controller: self)
return }
let stringResponse = String(data: data, encoding: .utf8)!
let dictData = stringResponse.convertToDictionary()
do {
let jsonData = try JSONSerialization.data(withJSONObject: dictData as Any, options: .prettyPrinted)
let decoder = JSONDecoder()
let model = try decoder.decode(GoogleDistance.self, from: jsonData)
bakeryData[index].disanceInMiles = model.rows?[0].elements?[0].distance?.text ?? "NaN"
completion(index)
} catch let parsingError {
print("Error data :", parsingError)
completion(nil)
}
}
task.resume()
}
This is how I call this function once I have received the data from my server,
self.getDistanceUserBakery(bakeryData: &self.bakeryData) { index in
if index != nil {
DispatchQueue.main.async {
// here I am thinking as the bakeryData will hold the new value for distanceInMiles, the collectionView will start showing up that result on reload.
self.resultCollection.reloadItems(at: [IndexPath(item: index!, section: 0)])
}
}
}
Now the Question:
As I know, when you pass parameters as inout, there values can be changed from inside your function, and those changes reflect in the original value outside the function.
But when I try the code , it says Escaping closure captures 'inout' parameter 'bakeryData'. In my code , //6 is producing the error.
How to fix this error?
As #Paulw11 suggested in comments,
Is BakeryData a struct? If so then simply make it a class. If you make
BakerData a class then the array contains reference types and you can
update the element's properties
I changed the struct to class and it did work.

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