How to send api response to UIViewController using Combine - ios

I am updating my collectionView with a response from my api using Combine to provide real time info. My api returns NSArray which is working but for some strange reasons inside my SearchAPI class I can receive the response and print it out in the console print("Our array ", searchArray) but can't sink to my UIViewController and update my collectionView accordingly. print("value ", value) Value is always empty
import Foundation
import Combine
class SearchAPI {
static let shared = SearchAPI()
func fetchData(url: String, category: String, queryString: String) -> Future<NSArray, Error>{
var searchArray: NSArray = []
let urlString = url
print("url come ", urlString)
guard let url = URL(string: urlString) else {
fatalError()
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let data = data, error == nil else {
return
}
do{
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray
if let responseJson = json {
searchArray = responseJson
print("Our array", searchArray)
}
}
}
task.resume()
return Future { promixe in
DispatchQueue.main.async {
promixe(.success(searchArray))
}
}
}
}
//In my UIViewController
var observers: [AnyCancellable] = []
let action = PassthroughSubject<NSArray, Never>()
var category = "tv"
var queryString = ""
private var models: NSArray = []
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if isMovieSelected {
btnMoviesBottomBorder.backgroundColor = .secondaryPink
btnAlbumsBottomBorder.backgroundColor = .systemGray
btnAlbumsBottomBorder.backgroundColor = .systemGray
}
txtSearch.searchTextField.addTarget(self, action: #selector(searchItem), for: .editingChanged)
}
#objc func searchItem(){
moviesView.alpha = 0
albumsView.alpha = 0
booksView.alpha = 0
lblSrchResults.alpha = 1
queryString = txtSearch.text!.replacingOccurrences(of: " ", with: "%20")
print("who is calling ", queryString)
let url = "https://endpoint?category=\(category)&query=\(queryString)"
SearchAPI.shared.fetchData(url: url, category: category, queryString: queryString)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print(error)
}
}, receiveValue: { [weak self] value in
print("value ", value)
self?.models = value
self?.searchCollectionView!.reloadData()
}).store(in: &observers)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Fetched ", models.count)
return models.count
}

In your fetchData function, you create a dataTask to run the URL request. At some point later on that dataTask will complete and return an array.
Then you create a Future and send some code off to run on the main queue. That code has nothing inside of it to make it wait until the dataTask completes.
The code inside the Future is going to run "right away" and complete before a value has been assigned to searchArray.
You need to change your Future so that it only resolves (only completes) after the dataTask is done. Just tweaking your code a bit it looks something like:
func fetchData(url: String, category: String, queryString: String) -> Future<NSArray, Error>
{
return Future { promixe in
var searchArray: NSArray = []
let urlString = url
print("url come ", urlString)
guard let url = URL(string: urlString) else {
fatalError()
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data,
error == nil else {
promixe(.failure(error!))
return
}
do{
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray
if let responseJson = json {
searchArray = responseJson
print("Our array", searchArray)
promixe(.success(searchArray))
}
}
}
task.resume()
}
}
I typed this into a playground, but I did not run it, so additional changes may be necessary.
All of the action now happens inside the future. Once you are in a future block then you should call its resolution function (in your code called promixe) for all success or failure cases.
I changed your code so that promixe is called ONLY when the data task completes (successfully or unsuccessfully).
For your code you should probably also:
Be sure to call proximate, once, on every path through your Futures block. In particular you need to catch the error that JSONSerialization might give you and called promixe to report the error. Otherwise your future may not complete if data comes back from the server, but it cannot be parsed.
Change your code to use Swift style JSON instead of using JSONSerialization that way you will have an [SomeType] instead of NSArray. Use the type system to your advantage to reduce the possibility of errors.
Instead of doing queryString = txtSearch.text!.replacingOccurrences(of: " ", with: "%20") use
txtSearch.text!.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed). It will cover more cases if the user types
something unexpected.

Related

SwiftUI Login Issue

I am making an app where I am using API to create login interface.
In my LoginViewModel class I have following login function
import Foundation
class LoginViewModel: ObservableObject {
#Published var loginuser : Login?
// func login(username: String, password: String, callback : #escaping (Bool,LoginData,String) -> Void) {
func login(with params: [String: Any], completionHandler: #escaping (Bool,LoginData,String) -> Void) {
// let parameters: [String: Any] = [
// "email": username,
// "password": password
// ]
var semaphore = DispatchSemaphore (value: 0)
guard let url = URL(string: "https://xxxxxxxxx/v1//user/login")else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
//request.addValue("token", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: params)
} catch let error {
print(error.localizedDescription)
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
print(String(data: data, encoding: .utf8)!)
}//urlsession
task.resume()
}//login func
}
And in my view I have button whose action has following code
Button(action: {
let params = ["email": userName,
"password": password]
viewModel.login(with: params) { success, data, message in
if success==true {
UserDefaultsStore.isUserLoggedIn = true
// UserDefaultsStore.loginuser = login
showsAlert4 = true
print ("cat1")
}
else
{
print ("cat2")
}
}
}, label: {
Image("btn_arrow")
While I can confirm the API is being read fine as console puts out data from this line in func login
print(String(data: data, encoding: .utf8)!)
What I can not get to work is show "cat1" or "cat2" message in case of successful or unsuccessful login attempt. In fact this piece of code just never executes
if success==true {
UserDefaultsStore.isUserLoggedIn = true
// UserDefaultsStore.loginuser = login
showsAlert4 = true
print ("cat1")
}
else
{
print ("cat2")
}
Any idea where I am going wrong in this? Any help will be appreciated. Thanks
I have tried changing completionHandler: to callback: in func login but that changes nothing.
As someone in the comments mentioned, you are not calling your completionHandler at all. You could also drop the semaphore as it is not needed. Or maybe I am missing the point of the semaphore and you can elaborate on that.
import Foundation
class LoginViewModel: ObservableObject {
#Published var loginuser : Login?
func login(with params: [String: Any], completionHandler: #escaping (Bool,LoginData,String) -> Void) {
var semaphore = DispatchSemaphore (value: 0)
guard let url = URL(string: "https://xxxxxxxxx/v1//user/login")else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: params)
} catch let error {
print(error.localizedDescription)
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
completionHandler(false, nil, "Error") -> ON FAILURE
return
}
print(String(data: data, encoding: .utf8)!)
completionHandler(true, data, "") -> ON SUCCESS
}
task.resume()
}

Swift 5, RxSwift: Network request with RxSwift

I am starting to use RxSwift to make the service call.
This was my old code:
class Service: GraphQLService {
func graphQL(body: [String: Any?], onSuccess: #escaping (Foundation.Data) throws -> (), onFailure: #escaping (Error) -> ()) {
guard let urlValue = Bundle.main.urlValue else { return }
guard let url = URL(string: urlValue) else { return
print("Error with info.plist")
}
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
onFailure(error)
}
if let data = data {
do{
try onSuccess(data)
}
catch{
onFailure(error)
}
}
}.resume()
}
And here I do the function to get time deposits:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits(onSuccess: #escaping ([TimeDeposits]) -> (), onFailure: #escaping (Error) -> ()) {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body, onSuccess: { data in
let json = try? JSONDecoder().decode(GraphQLResponse.self, from: data)
onSuccess(json?.data?.account?.timeDeposits ?? [])
}, onFailure: onFailure)
}
And so far this is my code with RxSwift:
class Service: GraphQLService {
func graphQL(body: [String : Any?]) -> Observable<Foundation.Data> {
return Observable.create { observer in
let urlValue = Bundle.main.urlValue
let url = URL(string: urlValue ?? "")
let session = URLSession.shared
var request = URLRequest(url: url!)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
session.dataTask(with: request) { (data, response, error) in
if let error = error {
observer.onError(error)
}
if let data = data {
do{
try onSuccess(data)
observer.onNext(data)
}
catch{
//onFailure(error)
observer.onError(error)
print("Error: \(error.localizedDescription)")
}
}
}.resume()
return Disposables.create {
session.finishTasksAndInvalidate()
}
}
}
This is where I don't understand how in my getTimeDeposits () I can do the deserialization with try? JSONDecoder () ... with RxSwift without using onSuccess?
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body)
}
You can have getTimeDeposits() return an Observable as well and handle the deserialization in a map closure. A couple of other things.
RxCocoa already has a method on URLSession so you don't need to write your own.
I suggest reducing the amount of code you have in a function that makes the network request. You want to be able to test your logic for making the request without actually making it.
Something like this:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
return Service().graphQL(body: body)
.map { try JSONDecoder().decode(GraphQLResponse.self, from: $0).data?.account?.timeDeposits ?? [] }
}
}
class Service: GraphQLService {
func graphQL(body: [String: Any?]) -> Observable<Data> {
guard let urlValue = Bundle.main.urlValue else { fatalError("Error with info.plist") }
let request = urlRequest(urlValue: urlValue, body: body)
return URLSession.shared.rx.data(request: request) // this is in RxCocoa
}
func urlRequest(urlValue: String, body: [String: Any?]) -> URLRequest {
guard let url = URL(string: urlValue) else { fatalError("Error with urlValue") }
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
return request
}
}
If you don't want to use RxCocoa for some reason, here is the correct way to wrap the URLSession.dataTask method:
extension URLSession {
func data(request: URLRequest) -> Observable<Data> {
Observable.create { observer in
let task = self.dataTask(with: request, completionHandler: { data, response, error in
guard let response = response as? HTTPURLResponse else {
observer.onError(URLError.notHTTPResponse(data: data, response: response))
return
}
guard 200 <= response.statusCode && response.statusCode < 300 else {
observer.onError(URLError.failedResponse(data: data, response: response))
return
}
guard let data = data else {
observer.onError(error ?? RxError.unknown)
return
}
observer.onNext(data)
observer.onCompleted() // be sure to call `onCompleted()` when you are done emitting values.
// make sure every possible path through the code calls some method on `observer`.
})
return Disposables.create { task.cancel() } // don't forget to handle cancelation properly. You don't want to kill *all* tasks, just this one.
}
}
}
enum URLError: Error {
case notHTTPResponse(data: Data?, response: URLResponse?)
case failedResponse(data: Data?, response: HTTPURLResponse)
}

iOS - Swift : fetching data from database in main thread, not in background

In my iOS App i'm able to download data from a database, but actually all the operations are made in background and the main thread is still active, even the GUI. I also tried to make a 'sleep' with
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { ... }
With this delay everthing works fine, but it's not a good solution. How can i change my code to do this in the main thread? Possibly with loadingIndicator.
This is my code (checking if username exists):
func CheckIfUsernameExists(username : String, passwordFromDb : inout String, errorMsg : inout String)
{
//declare parameter as a dictionary which contains string as key and value combination. considering inputs are valid
var _errorMsg = ""
var _psw = ""
var parameters : [String : Any]?
parameters = ["username": username,
"action": "login"]
print(parameters!)
let session = URLSession.shared
let url = "http://www.thetestiosapp.com/LoginFunctions.php"
let request = NSMutableURLRequest()
request.url = URL(string: url)!
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField:"Accept")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField:"Content-Type")
do{
request.httpBody = try JSONSerialization.data(withJSONObject: parameters!, options: .sortedKeys)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let response = response {
let nsHTTPResponse = response as! HTTPURLResponse
let statusCode = nsHTTPResponse.statusCode
print ("status code = \(statusCode)")
}
if let error = error {
print ("\(error)")
}
if let data = data {
do{
_psw = self.parseJSON_CheckIfUsernameExists(data, errorMsg: &_errorMsg)
}
}
})
task.resume()
}catch _ {
print ("Oops something happened buddy")
errorMsg = "Usarname non recuperato (1)"
}
passwordFromDb = _psw
errorMsg = _errorMsg
}
You’re attempting to update passwordFromDb and errorMsg at the end of this method. But this is an asynchronous method and and those local variables _psw and _errorMsg are set inside the closure. Rather than trying to defer the checking of those variables some arbitrary three seconds in the future, move whatever “post request” processing you need inside that closure. E.g.
func CheckIfUsernameExists(username : String, passwordFromDb : inout String, errorMsg : inout String) {
//declare parameter as a dictionary which contains string as key and value combination. considering inputs are valid
let parameters = ...
let session = URLSession.shared
var request = URLRequest()
...
do {
request.httpBody = ...
let task = session.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
let statusCode = httpResponse.statusCode {
print ("status code = \(statusCode)")
}
guard let data = data else {
print (error ?? "Unknown error")
return
}
let password = self.parseJSON_CheckIfUsernameExists(data, errorMsg: &_errorMsg)
DispatchQueue.main.async {
// USE YOUR PASSWORD AND ERROR MESSAGE HERE, E.G.:
self.passwordFromDb = password
self.errorMsg = _errorMsg
// INITIATE WHATEVER UI UPDATE YOU WANT HERE
}
}
task.resume()
} catch _ {
print ("Oops something happened buddy")
errorMsg = "Usarname non recuperato (1)"
}
}

Getting a value from web call in Swift [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 4 years ago.
I am trying to get the returned value from a PHP script in Swift. However, it seems as though I keep getting the error:
Unexpectedly found nil while unwrapping an Optional value
Here is the class:
var value: String!
func run(idNumber: Int) {
let request = NSMutableURLRequest(url: URL(string: "https://mywebsite.com/file.php")!)
request.httpMethod = "POST"
let postString = "a=Hello"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
//answer = error;
}
let answerString = String(data: data!, encoding: String.Encoding.utf8)
self.value = answerString
}
task.resume()
}
func getValue() -> String{
return value
}
The error occurs when calling the getValue() function. However, when I print out the "answerString" as soon as it is created, it prints out successfully!
The functions are called here:
let access = ApiAccess()
access.run(idNumber: 0)
print(access.getValue())
Making a request is an asynchronous task. You need to wait the closure callback to be call before calling getValue.
You can add a closure callback to your run method. That way you will know when the request has finished and you can print the result:
var value: String!
func run(idNumber: Int, #escaping callback: () -> Void) {
let request = NSMutableURLRequest(url: URL(string: "https://mywebsite.com/file.php")!)
request.httpMethod = "POST"
let postString = "a=Hello"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
//answer = error;
}
let answerString = String(data: data!, encoding: String.Encoding.utf8)
self.value = answerString
callback()
}
task.resume()
}
func getValue() -> String{
return value
}
let access = ApiAccess()
access.run(idNumber: 0) {
print(access.getValue())
}
The issue is that the callback for URLSession.shared.dataTask() happens asynchronously, so you'll end up executing access.getValue() before your var value is ever set. This means that value is forcefully unwrapped before it has a value, which causes this error.
To workaround this, consider using promises, RxSwift, or similar async tools so that you only access values when available.
Refactor your run(idNumber:) function to take a completion handler:
func run(idNumber: Int, completion: (String?, Error?)-> Void ) {
let request = NSMutableURLRequest(url: URL(string: "https://mywebsite.com/file.php")!)
request.httpMethod = "POST"
let postString = "a=Hello"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
completion(nil, error)
}
let answerString = String(data: data!, encoding: String.Encoding.utf8)
self.value = answerString
completion(answerString, nil)
}
task.resume()
}
And call it like this:
let access = ApiAccess()
access.run(idNumber: 0) { result, error in
guard let result = result else {
print("No result. Error = \(error)")
return
}
print("result = \(result)")
}
(Or use Futures or Promises, as mentioned by #JohnEllmore in his answer)

calling a web service with httpMethod (POST) in swift 4.1

Let me describe view controller (page design) first. I have added one button and one label. And a function is created by me for the button. Here is the ViewController.swift file:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var btnHitWebService: UIButton!
#IBOutlet weak var lblResult: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnHitWebServiceClick(_ sender: Any) {
if (getTokenWithPostMethod() == true)
{
lblResult.text = "yes, we get token: " + g_token
}
else
{
lblResult.text = "there is an error, please try again later"
}
}
func getTokenWithPostMethod() -> Bool{
var funcResult = false
var request = URLRequest(url: URL(string: "https://apiABCDEFG.com/token")!)
request.httpMethod = "POST"
let postString = "grant_type=password&username=ABCDEF&password=ABCDEF"
request.httpBody = postString.data(using: .utf8)
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request)
{
data, response, error in
guard let data = data, error == nil else
{
// check for fundamental networking error
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200
{
// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
do
{
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String:AnyObject]
{
g_token = (jsonObject["access_token"] as? String)!
print(g_token)
g_token_type = (jsonObject["token_type"] as? String)!
print("\n")
print(g_token_type)
funcResult = true ;
}
}
catch let error as NSError
{
print(error)
}
}
task.resume()
return funcResult
}
}
Here is my problem: When I click button, it gives me a token in output screen in Xcode (print) but getTokenWithPostMethod() method returns false. Why does it return false? During the debug process, it jumps from let task = to task.resume(). Thanks! (by the way I can't share correct link, username and password. I hope these 3 are not necessary to fix this problem)
You are making an API call. It takes time to return the value. Here return statement will not wait for response. you should use either closure or delegate. You can try the given code:
func getTokenWithPostMethod(handler: #escaping (_ funcResult: Bool) -> Void){
var request = URLRequest(url: URL(string: "https://apiABCDEFG.com/token")!)
request.httpMethod = "POST"
let postString = "grant_type=password&username=ABCDEF&password=ABCDEF"
request.httpBody = postString.data(using: .utf8)
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request)
{
data, response, error in
var funcResult = false
if error == nil, let data = data {
do
{
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String:AnyObject]
{
g_token = (jsonObject["access_token"] as? String)!
print(g_token)
g_token_type = (jsonObject["token_type"] as? String)!
print("\n")
print(g_token_type)
funcResult = true
}
}
catch let error as NSError
{
print(error)
}
}
// edited
DispatchQueue.main.async {
handler(funcResult)
}
}
task.resume()
}
This will return after the response comes. Replace the calling of that method with :
#IBAction func btnHitWebServiceClick(_ sender: Any) {
getTokenWithPostMethod { (flag) in
if flag {
lblResult.text = "yes, we get token: " + g_token
} else {
lblResult.text = "there is an error, please try again later"
}
}
}
For more details please visit
It is returning false because, you have set var funcResult = false in starting, so before URLSession request to server and return some data in response only, your function returns the funcResult value.
you can add below code inside URLSession task body, immediately after try catch, instead of adding it inside btnHitWebServiceClick :
if (getTokenWithPostMethod() == true)
{
lblResult.text = "yes, we get token: " + g_token
}
else
{
lblResult.text = "there is an error, please try again later"
}

Resources