In method A, get data from callback in method B? - swift

I have a function that is build to get the latest items from a API. There are several other ones, with different functionality, but they all work the same. It looks like this:
func getLatest(pageNumber: Int) -> Array<Any>{
let urlRequest = URL(string: baseUrl + latestUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> Void in
// We have the data from doRequest stored in data, but now what?!
})
return allData
}
I also have a async method that handles the requests. That one looks like this:
func doRequest(url: URL, completion: #escaping ([[ApiItem]]) -> ()){
var allItems = [[ApiItem]]()
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: AnyObject]
let results = json["items"] as? [AnyObject]
for r in results!{
let item = ApiItem(json: r as! [String: Any])
allItems.append([item])
}
completion(allItems)
} catch let jsonError{
print("JSON error: \(jsonError)")
}
}.resume()
The doRequest function works absolutely fine. It gets the data, parses the JSON and send it back to getLatest --> requestedData. The problem right now is, is that getLatest() is a function that needs to return the data that is stored in the data variable of requestedData.
How can I make it so, that the getLatest() function returns the data that is stored in the data in requestedData()?

So I've fixed it by doing this:
In the first method, the one that actually needs the data from the API, I added this:
let trendingData = restApiManager.getLatest(pageNumber: 0, completion: { data -> Void in
let item = data[indexPath.row]
let url = NSURL(string: item.still)
let data = NSData(contentsOf: url as! URL)
if data != nil {
cell.image.image = UIImage(data:data! as Data)
}
})
The getLatest() method looks like this:
func getLatest(pageNumber: Int, completion: #escaping ([ApiItem]) -> ()) {
let urlRequest = URL(string: baseUrl + trendingUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> Void in
// We have the data from doRequest stored in data
var requestedData = data
completion(requestedData)
})
}
And finally, the doRequest() method looks like this:
func doRequest(url: URL, completion: #escaping ([ApiItem]) -> ()){
var allItems = [ApiItem]()
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: AnyObject]
let results = json["items"] as? [AnyObject]
for r in results!{
let item = ApiItem(json: r as! [String: Any])
allItems.append(item)
}
completion(allItems)
} catch let jsonError{
print("JSON error: \(jsonError)")
}
}.resume()
}

What I would do is use a Singleton in which I can store the Data
class DataManager:NSObject
{
static let instance = DataManager()
override private init(){}
var items:[ApiItem] = []
}
Then in your first method I would do this:
func getLatest(pageNumber: Int){
let urlRequest = URL(string: baseUrl + latestUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> items in
// We have the data from doRequest stored in data, but now what?!
DataManager.instance.items = items
})
}
This is how I usually go about this kind of situations. There may be better options though...

Related

Parse Server Swift Logic for grabbing all items contained in array

I am using parse server with Swift. I am trying to get all matches that where the key "username" is contained in commentUserArray. Comment userArray displays 3 names ["username","username", "username2"].
The query grabs only two values as opposed to three as this looks up only for those two usernames "username" and "username1". However, I need all 3 instances of this. Please help.
var commentImgUrlArray: [NSString] = []
func getPics(_ completionHandler: #escaping () -> Void) {
let query = PFQuery(className: "_User")
query.whereKey("username", containedIn: commentUserArray)
query.findObjectsInBackground(block: { (objects: [PFObject]?, error: Error?) in
if let objects = objects {
for object in objects {
if error == nil {
let imageFile = object["profilePic"] as? PFFileObject
let imageFileString = imageFile?.url as! String
if let url = URL(string: imageFileString) {
let data = try? Data(contentsOf: url)
if let imageData = data {
self.commentImgUrlArray.append(imageFileString as NSString)
print(self.commentImgUrlArray)
}
}
}
}
completionHandler()
}
})
}
So after some trial and error, I have come across the solution.
Delete the original query.contained in as that overrides the rest of the formula.
Ideally, I want to run one query. Not however many on in commentuserarray. As a result, the for in loop should be placed inside the query.find -->
need to grab object for that specific user. Therefore, let pfuser = object["username"] as String
reverse the array to get the proper order --
func getPics(_ completionHandler: #escaping () -> Void) {
let query = PFQuery(className: "_User")
query.findObjectsInBackground(block: { (objects: [PFObject]?, error:
Error?) in
if let objects = objects {
for object in objects {
if error == nil {
for user in self.commentUserArray {
let pfuser = object["username"] as! String
if pfuser == user {
let imageFile = object["profilePic"] as? PFFileObject
let imageFileString = imageFile?.url as! String
if let url = URL(string: imageFileString) {
let data = try? Data(contentsOf: url)
if let imageData = data {
self.commentImgUrlArray.append(imageFileString as
NSString)
print(self.commentImgUrlArray)
}
}
}
}
}
}
self.commentImgUrlArray.reverse()
completionHandler()
}
})
}

do not know how to get the result of completion

I am having trouble to use the result of a completion handler.
I am getting this error "Cannot convert value of type '()' to expected argument type"
struct SearchCollectionViewModel {
let name: String
let previewURL: String?
var image:UIImage?
let dataController = DataController()
}
extension SearchCollectionViewModel {
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
if let url = result.previewURL {
let imgData = preview(with: url, completion: { data -> Data? in
guard let data = data as? Data else { return nil }
return data
})
self.image = UIImage(data: imgData)
}
}
private func preview(with url: String, completion: #escaping (Data) -> Data?) {
dataController.download(with: url) { data, error in
if error == nil {
guard let imageData = data else { return }
DispatchQueue.main.async {
_ = completion(imageData)
}
}
}
}
}
A couple of observations:
You cannot “return” a value that is retrieved asynchronously via escaping closure.
The closure definition (Data) -> Data? says that the closure not only will be passed the Data retrieved for the image, but that the closure will, itself, return something back to preview. But it’s obviously not doing that (hence the need for _, as in _ = completion(...)). I’d suggest you change that to (Data?) -> Void (or use the Result<T, U> pattern).
I’d suggest renaming your Result type as there’s a well-known generic called Result<Success, Failure> for returning .success(Success) or .failure(Failure). This is a pattern that we’ve used for a while, but is formally introduced in Swift 5, too. See SE-0235.
Your codebase can have its own Result type, but it’s an invitation for confusion later down the road if and when you start adopting this Result<T, U> convention.
You really shouldn’t be initiating asynchronous process from init, but instead invoke a method to do that.
Personally, I’d move the conversion to UIImage into the DataController, e.g.
extension DataController {
func downloadImage(with url: URL, completion: #escaping (UIImage?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
let image = data.flatMap { UIImage(data: $0) }
completion(image, error)
}
task.resume()
}
}
So, I might suggest you end up with something like:
class SearchCollectionViewModel {
let name: String
let previewURL: String?
let dataController = DataController()
var image: UIImage?
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
}
}
extension SearchCollectionViewModel {
func preview(with url: String, completion: #escaping (UIImage?) -> Void) {
guard let urlString = previewURL, let url = URL(string: urlString) else {
completion(nil)
return
}
dataController.downloadImage(with: url) { [weak self] image, error in
DispatchQueue.main.async {
self?.image = image
completion(image)
}
}
}
}

Array is null after setting data in it

I have a JSON request that gets data from the Darksky API, I get the data properly and it is showing on the screen. However, When i'm trying to set the data from the array I get from the JSON call in another array, it stays empty.
This is my code:
just declaring the array:
var mForecastArray = [Weather]()
this is the function that calls the API:
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}
The weird part is that it does work, and the data do shows on screen, but still, mForecastArray seems null.
This is the API call itself:
static func forecast(withLocation location: String, completion: #escaping ([Weather]) -> ()){
let url = basePath + location
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
var forecastArray: [Weather] = []
if let data = data{
do{
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any]{
if let dailyForecast = json["daily"] as? [String:Any]{
if let dailyData = dailyForecast["data"] as? [[String:Any]]{
for dataPoint in dailyData{
if let weatherObject = try? Weather(json: dataPoint){
forecastArray.append(weatherObject)
}
}
}
}
}
}catch{
print(error.localizedDescription)
}
completion(forecastArray)
}
}
task.resume()
}
It's a visual asynchronous illusion.
The static method forecast works asynchronously.
Most likely your code looks like
getForecast()
print(self.mForecastArray)
This cannot work because the array is populated much later.
Move the print line into the completion handler of the static method
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
print(self.mForecastArray)
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}

Access a dictionary (JSON format) in a function with a flexible variable

I can't find a solution for my programming issue. I want to create a function which will access a dictionary (data is coming from the internet) an I need the following code very often:
if let job_dict = json["data"] as? [String:Any] {
It would be great to be more flexible and to change the ["data"] part to a variable or something like that:
func get_JSON(Link: String, Value: String) -> [Double] {
let url = Link
let request = URLRequest(url: URL(string: url)!)
let myUrl = URL(string: basePath)!
var ValuestoReturn = [Double]()
let task = URLSession.shared.dataTask(with: myUrl) { (data, response, error) in
if let data = data {
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] {
print(Value.description)
if let job_dict = json[Value.description] as? [String:Any] {
print(job_dict)
}
}
} catch {
}
}
}
task.resume()
json[Value.description] is always wrong and the json["data"] thing is always true.
Don't use Value.Description use just Value
print(Value)
if let job_dict = json[Value] as? [String:Any] {...}
P.D: Don't use "Value" for a variable's name. The first letter in uppercase is for types. You can use value instead.

Returning data from async call that takes multiple params in Swift function

I was trying to create a post method so I could reuse it further in my code.
I saw this example Returning data from async call in Swift function that gives partial solution to my problem but don't know how to call the function once I define it.
This is the function I am trying to call:
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) {
(data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
This is the caller function:
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
//in the line below I get the error message, extra argument "request" in call.
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
You cannot make loginPostRequest return NSDictionary because you are making async call with what you need is to create completion block same way you have create with postRequest method also from Swift 3 you need to use URLRequest with mutable var object instead of NSMutableURLRequest you need to also change the postRequest function's request argument type to URLRequest so latter no need to convert NSMutableURLRequest to URLRequest and use Swift type dictionary instead of NSDictionary
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key as! String) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
Now simply changed the argument type of request to URLRequest from NSMutableURLRequest in method postRequest
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) { (data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
Now when you call this loginPostRequest you are having response in completion block of it.
Functions that receive a closure as parameter can be called like any other functions:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false, completionHandler: { postRequestStatus in
// ... code that will run once the request is done
})
If the closure is the last parameter you can pass it outside the parenthesis:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false) { postRequestStatus in
// ... code that will run once the request is done
})
You can check the Swift book to learn more about closures and functions.
By the way, your postRequest method looks weird, I haven't checked deeply into it, but for instance I believe although url is one of the parameters it isn't actually used. Some other answer pointed other problems into that function.