Swift completion handlers - using escaped closure? - ios

Hi i am a beginner studying swift and would like to know what to use when making an api request. What is the modern and professional way?
is it using an escaping closure like so:
func getTrendingMovies(completion: #escaping (Result< [Movie], Error >) -> Void) {
guard let url = URL(string: "\(Constants.baseUrl)/trending/all/day?api_key=\.(Constants.API_KEY)") else {return}
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _,
error in
guard let data = data, error == nil else {
return
}
do {
let results = try JSONDecoder().decode(TrendingMoviesResponse.self, from:
data)
completion(.success(results.results))
} catch {
completion(.failure(error))
}
}
task.resume()
}
or should i make an api request without escaping closure while using a sort of delegate like so:
func performRequest(with urlString: String){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
delegate?.didFailWithError(error: error!)
return
}
if let safeData = data{
// created parseJson func
if let weather = parseJSON(safeData){
delegate?.didUpdateWeather(self,weather: weather)
}
}
}
task.resume()
} else {
print("url is nil")
}
}

I agree with matt, the modern and professional way is async/await
func getTrendingMovies() async throws -> [Movie] {
let url = URL(string: "\(Constants.baseUrl)/trending/all/day?api_key=\(Constants.API_KEY)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(TrendingMoviesResponse.self, from: data).results
}

Related

How to print the api response to the console? Xcode 14

I am learning about API's and decided to practice using them by writing a simple function to call an api and print the response. The issues I am having is that the response is not printing to the console. I am also new to Swift but watched a couple of tutorials, which lead me to write this basic skeleton code.
import Foundation
struct Posts: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
func fetch() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
return
}
do {
let posts = try JSONDecoder().decode(Posts.self, from: data)
print(posts) //Doesn't print the response
}
catch {
print(error)
}
}
task.resume()
}
fetch()
func fetch() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
return
}
do {
let posts = try JSONDecoder().decode([Posts].self, from: data)
print(posts)
}
catch {
print(error)
}
}
task.resume()
}

Rxswift PublishRelay.flatMapLatest with networking call, but no response

I'm new to Rxswift Here is my View Model code:
let test = reloadRelay
.asObservable()
//2
.flatMapLatest({ apiClient.requestData(type: rankingType) })
.map({ $0.results })
.asDriver { (error) -> Driver<[ArticleResult]> in
errorRelay.accept((error as? ErrorResult)?.localizedDescription ?? error.localizedDescription)
return Driver.just([])
}
This is very typical and simple ViewModel with logics to get data and send the View layer.
However, when I initialise the ViewModel. The networking call (2) never happened.
Here is my networking code:
func requestData(type: ArticlesRankingType) -> Observable<Articles> {
let urlString = "https://api.nytimes.com/svc/mostpopular/v2/\(type.rawValue)/7.json?api-key=dylOnQnYUzEF1B9MTYYHM0MyffMPBZRi"
let urlRequest = URLRequest(url: URL(string: urlString)!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error {
observer.onError(error)
return
}
if let data = data ,let responseCode = response as? HTTPURLResponse {
do {
let decoder = JSONDecoder()
if responseCode.statusCode == 200 {
let value = try decoder.decode(Articles.self, from: data)
return observer.onNext(value)
}
}
catch let parseJSONError {
observer.onError(error!)
print("error on parsing request to JSON : \(parseJSONError)")
}
}
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
Here is how I call my output in View:
articlesViewModel.output
.articles.asDriver(onErrorJustReturn: [])
.drive(tableView.rx.items(cellIdentifier: "aCell", cellType: ArticlesCell.self))
articlesViewModel
.input
.reload
.asDriver(onErrorJustReturn: ())
.drive()
Can someone tell what's wrong with my code? Thanks

How can I unit test a network request using a local json file?

I'm trying to figure out the best way to unit test a network request. My initial thought was to create a local file with the JSON response for testing purposes but that doesn't seem to be working. See my code below.
I wanna test that I can get a non-nil array back from the completion handler in the function below.
class APIClient {
let downloader = JSONDownloader() // just a class that creates a new data task
// what I want to test
func getArticles(from url: URL?, completion: #escaping([Article]?, Error?) -> ()) {
guard let url = url else { return }
let request = URLRequest(url: url)
let task = downloader.createTask(with: request) { json, error in
DispatchQueue.main.async {
// parse JSON
...
completion(articles, nil)
}
}
task.resume()
}
}
I tried testing as shown below to no avail.
func testArticleResponseIsNotNil() {
let bundle = Bundle(for: APIClientTests.self)
guard let path = Bundle.path(forResource: "response-articles", ofType: "json", inDirectory: bundle.bundlePath) else {
XCTFail("Missing file: response-articles.json")
return
}
let url = URL(fileURLWithPath: path)
var articles: [Article]?
let expectation = self.expectation(description: "Articles")
let client = APIClient()
client.getArticles(from: url) { response, error in
articles = response
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
XCTAssertNotNil(articles)
}
Any ideas on how exactly I should test this function?
Edit: This is the JSONDownloader class.
class JSONDownloader {
let session: URLSession
init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}
convenience init() {
self.init(configuration: .default)
}
typealias JSON = [String: AnyObject]
func createTask(with request: URLRequest, completion: #escaping(JSON?, Error?) -> ()) -> URLSessionDataTask {
let task = session.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 200 {
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? JSON
completion(json, nil)
} catch { completion(nil, error) }
} else { completion(nil, error) }
} else { completion(nil, error) }
}
return task
}
}

Get data from URL as separate function [duplicate]

This question already has answers here:
function with dataTask returning a value
(4 answers)
Closed 5 years ago.
I try to create function to get data from URL:
func getStringFromUrl(urlString: String) -> String {
if let requestURL = URL(string: urlString) {
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: requestURL, completionHandler: { (data, response, error) in
if let data = data {
do {
let str = String(data: data, encoding: String.Encoding.utf8)
return str
}
catch let error as NSError {
print ("error = \(error)")
}
}
else {
print ("error = \(error)")
}
})
task.resume()
}
}
But I got this error: unexpected non-void return value in void function
How can I create a separate function to get data from Url?
In your code you have:
let str = String(data: data, encoding: String.Encoding.utf8)
return str
Which is inside a closure block which is not defined to return anything. Because the function session.dataTask is an asynchronous task, it won't return straight away. You should use a completion block/closure to get the response when it returns. Also bear in mind that it might not return, so the string needs to be optional. See the code below.
func getStringFromUrl(urlString: String, completion: #escaping (_ str: String?) -> Void) {
if let requestURL = URL(string: urlString) {
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: requestURL, completionHandler: { (data, response, error) in
if let data = data {
do {
let str = String(data: data, encoding: String.Encoding.utf8)
completion(str)
}
catch let error as NSError {
print ("error = \(error)")
completion(nil)
}
}
else {
print ("error = \(error)")
completion(nil)
}
})
task.resume()
}
}
EDIT: Usage
getStringFromUrl(urlString: "http://google.com") { str in
if let text = str {
// you now have string returned
}
}

How to extract a value from function in swift

So I've written a function in swift which gives me a numeric value from JSON api. My question is how can I take the value from function so I can use it in more practical means.
override func viewDidLoad() {
super.viewDidLoad()
getJSON()
}
func getJSON(){
let url = NSURL(string: baseURL)
let request = NSURLRequest(URL: url!)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if error == nil{
let swiftyJSON = JSON(data: data!)
let usdPrice = swiftyJSON["bpi"]["USD"]["rate"].doubleValue
print(usdPrice)
}else{
print("There was an error!")
}
let usdPrice gets me the value so how can I take that from the function getJSON() and do something with it, for example attribute it to some label in Main.storyboard
Unfortunately the other answers are incorrect. Just returning a value will not work because you are getting the value from the completion closure of dataTaskWithRequest.
Having the statement return usdPrice should be a compiler error because the completion closure does not have a return value.
You'll need to add your own completion closure to getJSON that gets the double as a parameter.
override func viewDidLoad() {
super.viewDidLoad()
getJSON { (usdPrice) -> Void in
// do something with usdPrice
print(usdPrice)
}
}
func getJSON(completion: (Double) -> Void) {
let url = NSURL(string: baseURL)
let request = NSURLRequest(URL: url!)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if error == nil{
let swiftyJSON = JSON(data: data!)
let usdPrice = swiftyJSON["bpi"]["USD"]["rate"].doubleValue
completion(usdPrice)
} else {
print("There was an error!")
}
}
Your function needs to return the value as a result of its execution. For more details check out this: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Functions.html#//apple_ref/doc/uid/TP40014097-CH10-ID160
You have to have a return value for the function. The code below should work.
func getJSON() -> Double {
let url = NSURL(string: baseURL)
let request = NSURLRequest(URL: url!)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
var usdReturnValue : Double = 0.0
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if error == nil{
let swiftyJSON = JSON(data: data!)
let usdPrice = swiftyJSON["bpi"]["USD"]["rate"].doubleValue
print(usdPrice)
usdReturnValue = usdPrice
}else{
print("There was an error!")
}
}
return usdReturnValue
}

Resources