Populating UIView with results of NSURL - ios

I am using an API that returns JSON which is parsed into a String. I want to then use this string as a label on another view controller. I tried using completion blocks but I can't get it to work, it continuously returns "Fatal error: expected optional returned nil" but I can't figure out where. I'm assuming it has something to do with asynchronous calls not allowing the popup class to populate with the String that the API hasn't returned yet.
func summarizeArticle(finished: () -> Void) {
let formatText = (searchText.text?.replacingOccurrences(of: " ", with: "_"))!
articleNameFormatted = ("https://en.wikipedia.org/wiki/" + formatText)
let request : NSMutableURLRequest = NSMutableURLRequest()
request.url = NSURL(string: urlString + articleNameFormatted) as URL!
request.httpMethod = "POST"
let url = request.url
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print(error!)
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
articleSummary = parsedData["sm_api_content"] as! String
print(articleSummary)
} catch let error as NSError {
print(error)
}
}
}.resume()
finished()
}
#IBAction func summarizeButtonPressed(_ sender: Any) {
self.view.endEditing(true)
summarizeArticle{
let popupVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopupVC") as! PopupViewController
popupVC.view.frame = self.view.frame
self.addChildViewController(popupVC)
self.view.addSubview(popupVC.view)
popupVC.didMove(toParentViewController: self)
}
}
The second view controller then uses this function which is supposed to grab the text from a global variable set by the previous functions:
override func viewDidLoad() {
super.viewDidLoad()
popupView.layer.cornerRadius = 10.0
popupView.clipsToBounds = true
summaryText.text = articleSummary
view.backgroundColor = UIColor.black.withAlphaComponent(0.0)
self.showAnimation()
}

Instead of making articleSummary a global variable, try this:
1) Make articleSummary local to your summarizeArticle() function.
2) In the class definition for your view controller with identifier "sbPopupVC", add an instance variable called something like articleSummaryText.
3) Get rid of the completion handler on summarizeArticle() and move all the code that was in there to the completion block of your URLSession API call.
4) In that completion block, right before self.addChildViewController(popupVC), add this line: popupVC.articleSummaryText = articleSummary
5) Finally, in viewDidLoad() of your popupVC, set summaryText.text = articleSummaryText.

Related

Getting data before navigating to a page

I am trying to load some data from API and then navigate to some page.
The issue is that it navigates to the page before it finishes loading the data.
I need the data to be loaded and then move to the page
What I am doing is:
func getData(){
var serviceCenter : ServiceCenter?
var serviceCenterid : Int?
print("AM HERE")
let link: String = ""
guard let Requesturl = URL(string: link) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: Requesturl)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
print(responseData)
guard let aViewController = UIStoryboard(storyboard: .mainStoryboard).instantiateViewController(withIdentifier: String(describing: aViewController.self)) as? aViewController else {
return
}
aViewController.selectedServiceCenterID = serviceCenterID
let navController = UINavigationController(rootViewController: aViewController)
let controllerview = AppDelegate.topViewController()
controllerview?.present(navController, animated: true, completion: nil)
do {
guard let receivedData = try JSONSerialization.jsonObject(with: responseData,options: []) as? [String: Any] else {
print("Could not get JSON from responseData as dictionary")
return
}
print(responseData)
guard let data = receivedData["data"] as? [String: Any] else {
print("Could not get status from JSON")
return
}
guard let id = data["serviceCenterId"] as? Int else {
print(error)
return
}
serviceCenterid = id
print(serviceCenterid)
} catch {
print("error parsing response from POST on /public/api/login_customer")
return
}
}
task.resume()
}
What I want to print in my ViewController :--
override func viewDidLoad() {
super.viewDidLoad()
print("DEEPLINK")
print(selectedServiceCenter)
.....
}
what I am getting is:
> AM HERE
> 10041 bytes
> before
> nil
> 10041 bytes
> 26349 --> servicecenterid
my problem is that selectedservicecenter is empty because it navigates before data is loaded! How to make the data to be loaded first and then navigate after everything is completed above?
In your method, the data fetch is asynchronous . As you placed the code to navigate after the task.resume(), which means after the data fetch call is initiated, the next line that gets executed is your navigation code.
What you need to do is, you need to place the navigation code inside the response block, after you print(responseData) inside the do-catch block.
Note: Make sure you execute the navigation code on main thread.
Write function with callback handler and in call back navigate to desired viewController this will solve your problem.

Passing JSON Response From HTTP Request to Another ViewController in Swift 3

I'm new to iOS and was hoping someone would be willing to help me out with an issue I'm having
Say I have 2 Views in my storyboard:
- View1: Has 1 text box
- View2: Has 1 Label
Each being respectively controlled by a ViewControllers:
- FirstViewController
- SecondViewController
My app would send the text of the textbox in View1 as an HTTP (POST) request to an API, and would display on View2 the result which is sent back in JSON format.
My approach is to use the prepare(for segue:,Sender:), however I am having a hard time returning the JSON response from Task() in order to send it to SecondViewController via a Segue.
class ResultViewController: UIViewController {
#IBOutlet var text_input: UITextField!
Let api_url = (the api url)
func makeRequest(voucher_number:String, redemption_code:String){
let json: [String: Any] = [
"input" : text_input.text
]
let request_json = try? JSONSerialization.data(withJSONObject: json)
let url:URL = URL(string: api_url)!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
request.httpBody = request_json
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(
data, response, error) in
guard let _:Data = data, let _:URLResponse = response , error == nil else {
return
}
let json: Any?
do
{
json = try JSONSerialization.jsonObject(with: data!, options: [])
}
catch
{
}
guard let server_response = json as? [String: Any] else
{
return
}
//This is where I think the return should take place
//but can't figure out how
})
task.resume()
}
}
I know I would need to modify my func declaration by adding the return syntax, but I can't figure out how to return data in the first place :P so I skipped this part for the time being.
I would then do the following to send the response to SecondViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "firstSegue" {
if let resultViewController = segue.destination as? SecondViewController {
if (text_input.text != nil && redemption_code.text != nil) {
if let json_response = makeRequest() {
SecondViewController.request_result = json_response
}
// request_result is the variable in
// SecondViewController that will store the data
// being passed via the segue.
}
}
}
}
I know my code may not be the best practice for what I'm trying to achieve. And I'm open to suggestions to tackle a different approach, as long as it's not too advanced for me.
Cheers
Notifications are a good way to forward JSON data out of completion handler blocks, like:
NotificationCenter.default.post(name: Notification.Name(rawValue:"JSON_RESPONSE_RECEIVED"), object: nil, userInfo: server_response)
Register and handle the notification in FirstViewController:
NotificationCenter.default.addObserver(self, selector: #selector(FirstViewController.json_Response_Received(_:)), name:NSNotification.Name(rawValue: "JSON_RESPONSE_RECEIVED"), object: nil)
(in viewDidLoad()) and:
func json_Response_Received(_ notification:Notification) {
responseDictionary = (notification as NSNotification).userInfo as! [String:AnyObject];
self.performSegue(withIdentifier: "SegueToSecondController", sender: self)
}
Then you can pass responseDictionary to SecondViewController in:
override func prepare(for segue:UIStoryboardSegue, sender:Any?) {
if (segue.identifier == "SegueToSecondController") {
secondViewController = segue.destinationViewController as! SecondViewController
secondViewController.response = responseDictionary
}
}

Performing Segue after Login

So I'm designing an application where, like most apps, takes users to the "home page" after a successful login. However, I can't quite figure out how to get it to work. The code for my Login page is as follows:
import UIKit
class LoginVC: UIViewController {
#IBOutlet weak var usernameTxt: UITextField!
#IBOutlet weak var passwordTxt: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//#IBAction func userLogin(_ sender: AnyObject) {
#IBAction func userLogin(_ sender: AnyObject) {
// if textboxes are empty
if usernameTxt.text!.isEmpty || passwordTxt.text!.isEmpty {
// red placeholders
usernameTxt.attributedPlaceholder = NSAttributedString(string: "Username", attributes: [NSForegroundColorAttributeName: UIColor.red])
passwordTxt.attributedPlaceholder = NSAttributedString(string: "Password", attributes: [NSForegroundColorAttributeName: UIColor.red])
} else {
// shortcuts
let username = usernameTxt.text!.lowercased()
let password = passwordTxt.text!
// send request to mysql db
// Create a user in the mySQL db
// the exclamation at the end means we insist to launch it
// url to php file
let url = NSURL(string: "https://cgi.soic.indiana.edu/~team7/login.php")!
// request to the file
let request = NSMutableURLRequest(url: url as URL)
// method to pass data to this file via the POST method
request.httpMethod = "POST"
// what occurs after the question mark in the url
// body to be appended to url from values in textboxes
let body = "username=\(username)&password=\(password)"
// appends body to request that will be sent
request.httpBody = body.data(using: String.Encoding.utf8)
// launching
URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data:Data?, response:URLResponse?, error:Error?) -> Void in
if error == nil {
// get main queue in code process to communicate back
DispatchQueue.main.async(execute: {
// do this unless some error which is caught by catch
do {
// get json result
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
// guard let is the same thing as if let
// asign json to new variable in secure way
// original guard let used
guard let parseJSON = json else {
print("Error while parsing")
return
}
// get id from parseJSON dictionary
let id = parseJSON["id"] as? String
// if there is some id value
if id != nil && response != nil {
print(parseJSON)
// successfully logged in
//let userID = parseJSON["id"] as! String
//let userN = parseJSON["username"] as! String
//let eMail = parseJSON["email"] as! String
//print(parseJSON["username"] ?? String.self)
//let myVC = self.storyboard?.instantiateViewController(withIdentifier: "RetrievalVC") as! RetrievalVC
//myVC.id_Outlet.text = userID
//myVC.full_Outlet.text = userN
//myVC.email_Outlet.text = eMail
//
//self.navigationController?.pushViewController(myVC, animated: true)
}
} catch {
print("Caught an error \(error)")
}
})
// if unable to process request
} else {
print("error: \(error)")
}
}).resume()
//performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
}
}
}
I am trying to use
performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
In order to perform the segue but I'm not sure where in the code it should go.
Any suggestions or changes I need to make to the code?
It depends on back end logic.I assume that parseJSON["id"] is returned only if user is verified. So you can use this
let id = parseJSON["id"] as? String
// if there is some id value
if id != nil {
// perform segue here
}
You can perform a segue when error is nil and you are response contains data...
if id != nil && response != nil {
performSegue(withIdentifier: "loginSuccess", sender: LoginVC.self)
}

issue passing data to segue.destination within function

I'm trying to make 2 API calls on Segue invoke and ultimately pass Array of Data from Second Call to CollectionView. With first call I'm getting one value catID, which I need in order to make the other call:
let searchEndpoint: String = MY_ENDPOINT
// Add auth key
let serviceCallWithParams = searchEndpoint + "?PARAMETER"
guard let url = URL(string: serviceCallWithParams) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// setting up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// making the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// error check
guard error == nil else {
print("error")
print(error)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse JSON
do {
guard let catData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let data = catData["data"] as? [String: Any] {
if let array = data["categories"] as? [Any] {
if let firstObject = array.first as? [String: Any] {
if let catId = firstObject["catId"] as? Int {
getTitles(catId: catId)
}
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
And then getTitles function looks like this:
func getTitles(catId: Int) {
let catIdString = String(catId)
let titlesEndPoint: String = MY_ENDPOINT + catIdString
// Add auth key
let titlesEndPointWithParams = titlesEndPoint + "?PARAMETER"
guard let titlesUrl = URL(string: titlesEndPointWithParams) else {
print("Error: cannot create URL")
return
}
let titlesUrlRequest = URLRequest(url: titlesUrl)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: titlesUrlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on listCategoryTitles")
print(error)
return
}
// make sure we got data
guard let titlesData = data else {
print("Error: did not receive data")
return
}
// parse the JSON
do {
guard let allTitles = try JSONSerialization.jsonObject(with: titlesData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let titlesJson = allTitles["data"] as? [String: Any] {
if let titlesArray = titlesJson["titles"] as? Array<AnyObject> {
self.books = []
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
}
Now when I put:
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
outside function, in target controller I'm getting an empty array as output on first click, but on every next one I'm getting proper data.
When I try putting this inside function "getTitles" the output in CollectionViewController is "nil" every time.
Worth mentioning could be that I have "books" variable defined like so:
Main Controller:
var books: [Book]? = []
Collection Controller:
var books: [Book]?
and I have created type [Book] which is basically object with 3 string variables in separate struct.
All of the code above is encapsulated in
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
Any help/guideline would be much appreciated!
When you make api call it will execute in background means asynchronously where as prepare(for:sender:) will call synchronously.
Now from your question it is looks like that you have create segue in storyboard from Button to ViewController, so before you get response from your api you are moved to your destination controller, to solved your issue you need to create segue from your Source ViewController to Destination ViewController and set its identifier. After that inside getTitles(catId: Int) method after your for loop perform segue on the main thread.
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowResults", sender: nil)
}
After that inside your prepare(for:sender:) make changes like below.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
}
}

Use Post request response string in another method Swift

I am trying to use response of the post request in prepareForSegue function. i want to assign response value to destination viewController property. i can not do that because when i assign it in side closure brackets, does not assign before view the new window. because inside the closure code read, after view the new window.
as well as i try to assign saveInvoice function assign to a variable and use within prepareForSegue function. but that variable is NSUrlSessionDataTask type, so i could not use it further.
bellow i have mentioned my code segment
calling function
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let previewVC :PreviewViewController = segue.destinationViewController as! PreviewViewController
previewVC.invoiceSummary = "test hash key"
let invoiceSave = saveInvoice({ hash_key in
print(hash_key)
let test = hash_key
if test != ""
{
print("Success")
}
previewVC.sendersName = "sender view controller name"
})
print(invoiceSave)
}
This is the function which handdle the post request
func saveInvoice(completionHandler: (NSString) -> Void) -> NSURLSessionDataTask
{
let invoiceSummary = "Sample invoice summary"
let invoiceDate = "2015-11-20"
let invoiceConnectionID = "647193"
//let json = ["summary": invoiceSummary, "date": invoiceDate, "business_id": invoiceConnectionID]
let json = NSMutableDictionary()
json.setValue(invoiceSummary, forKey: "summary")
json.setValue(invoiceDate, forKey: "2015-11-20")
json.setValue(invoiceConnectionID, forKey: "business_id")
let data = try? NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions(rawValue: 0));
let request = NSMutableURLRequest(URL: NSURL(string: "https://livetest.somedomain.com/api/invs?auth_token=jmkm6ijijejf23kmkdd")!)
request.HTTPMethod = "POST"
request.addValue("application/json",forHTTPHeaderField: "Content-Type")
request.addValue("application/json",forHTTPHeaderField: "Accept")
request.HTTPBody = data
var outer = ""
let task = NSURLSession.sharedSession().dataTaskWithRequest(request){
data, response, error in
let responceJSon = JSON(data: data!)
//let text = self.passJson(responceJSon)
let invoice = responceJSon["invoice"]
let hash_key = invoice["hash_key"].stringValue
let statement_number = invoice["statement_no"].stringValue
let statement_summary = invoice["summary"].stringValue
let statement_date = invoice["date"].stringValue
let obj = ["hash_key": hash_key, "statement_no": statement_number, "summary": statement_summary, "date": statement_date]
self.invObject.append(obj)
//self.invObject.append(text as! [String : String])
outer = self.invObject[0]["statement_no"]!
print(outer)
if let hash_key = invoice["hash_key"].string{
completionHandler(hash_key)
return
}
}
task.resume()
return task
}
First of all the saveInvoice executes asynchronously so you won't have the value when the next controller loads but a while after.
To save the variable you need to do that inside the completion block. I guess it's the hash_key that is of interest here?
so you would instead do something like this
saveInvoice({ hash_key in
previewVC.hash_key = hash_key
previewVC.sendersName = "sender view controller name"
})
However, as mentioned above this executes asynchronously so I rather think it would be better to start the save and then execute the segue programatically when the completion handler is called from within saveInvoice
saveInvoice({ hash_key in
savedHashKey = hash_key
self.performSegueWithIdentifier(segueName, sender: self)
})
and then in prepareForSegue
let savedHashKey:String? = nil
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let previewVC :PreviewViewController = segue.destinationViewController as! PreviewViewController
previewVC.invoiceSummary = savedHashKey
savedHashKey = nil
}

Resources