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
}
}
Related
I couldn't find a way to pass the data to the Child VC once i get the data through a UITextField from the Github API
protocol GithubManagerDelegate {
func didUpdateGithub(_ githubManager: GithubManager, github: GithubModel)
func didFailWithError(error: Error)
}
struct GithubManager {
let profileUrl = "https://api.github.com/users"
let clientId = // deleted this
let secretId = // deleted this
var delegate: GithubManagerDelegate?
func fetchProfile(profileName: String) {
let urlString = "\(profileUrl)/\(profileName)?client_id=\(clientId)&client_secret=\(secretId)"
performRequest(with: urlString)
}
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 {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let github = self.parseJSON(safeData) {
self.delegate?.didUpdateGithub(self, github: github)
print(github)
}
}
}
task.resume()
}
}
func parseJSON(_ githubData: Data) -> GithubModel? {
let decoder = JSONDecoder()
do {
let decodeData = try decoder.decode(GithubData.self, from: githubData)
let name = decodeData.name ?? "The user does not have a name"
let login = decodeData.login ?? "No username by this name." // TODO: Change to give a alert
let avatarUrl = decodeData.avatar_url
let blog = decodeData.blog ?? "No Blog"
let bio = decodeData.bio ?? "No Bio"
let location = decodeData.location ?? "No location"
let followers = decodeData.followers
let following = decodeData.following
let github = GithubModel(githubName: login, githubUser: name, githubAvatar: avatarUrl, githubBio: bio, githubLocation: location, githubBlog: blog, githubFollowers: followers, githubFollowing: following)
return github
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
}
In the Parent VC in the textFieldDidEndEditing I take the input text and use that to fetch the information from the GithubAPI
if let username = searchTextField.text {
DispatchQueue.main.async {
self.githubManager.fetchProfile(profileName: username)
}
}
Then in my Child VC I use the GithubManagerDelegate, where I use DispatchQueue where, to populate the labels with the information. But the information is empty, because I can't pass the to the child once the data is recieved.
func didUpdateGithub(_ githubManager: GithubManager, github: GithubModel) {
DispatchQueue.main.async {
self.usernameLabel.text = github.githubName
}
}
The way I go from the ParentVC to ChildVC:
navigationController?.pushViewController(profileVC, animated: true)
Hopefully I made myself clear what the problem is...
when you try to pass data between parent ViewController and child ViewController you should add child not navigation to the child.
in child ViewController : var passingName: String?
For Example
let child = #YourChildViewController insitiate it.
self.addChild(child)
child.passingName = passingName
self.view.addSubview(child.view)
child.didMove(toParent: self)
I hope this solution helps you
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.
I'm new to Swift, and I want to 1) run a function that extracts a value from a JSON array (this part works) and 2) pass that variable into another function which will play that URL in my audio player.
My issue: I can't access that string stored in a variable outside the first function. Luckily, there's a bunch of questions on this (example), and they say to establish a global variable outside the function and update it. I have tried this like so:
var audio = ""
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
self.audio = (foo["audio"] as? String)!
} else {
// item could not be found
}
}).resume()
print(audio) // no errors but doesn't return anything
I have confirmed the JSON extraction is working -- if I move that print(audio) inside the function, it returns the value. I just can't use it elsewhere.
I originally tried it without the self. but returned an error.
Is there a better way to store this string in a variable so I can use it in another function?
EDIT: Trying new approach based on Oleg's first answer. This makes sense to me based on how I understand didSet to work, but it's still causing a thread error with the play button elsewhere.
var audiotest = ""{
didSet{
// use audio, start player
if let audioUrl = URL(string: audiotest) {
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationUrl = documentsDirectoryURL.appendingPathComponent(audioUrl.lastPathComponent)
//let url = Bundle.main.url(forResource: destinationUrl, withExtension: "mp3")!
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl)
} catch let error {
print(error.localizedDescription)
}
} // end player
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.com/example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
if let foo = data_list.first(where: {$0["episode"] as? String == "Houston Preview"}) {
// do something with foo
self.audiotest = (foo["audio"] as? String)!
} else {
// item could not be found
}
print(self.audiotest)
}).resume()
The request for the data is asynchronous so the code that is inside the completionHandler block happens some time later (depending on the server or the timeout) , that’s why if you try to print outside the completionHandler actually the print func happens before you get the data.
There are couple of solution:
1. Add property observer to your audio property and start playing when it is set:
var audio = “”{
didSet{
// use audio, start player
}
}
2. Wrapping the request with a method that one of its parameters is a completion closure:
// the request
func fetchAudio(completion:(String)->()){
// make request and call completion with the string inside the completionHandler block i.e. completion(audio)
}
// Usage
fetchAudio{ audioString in
// dispatch to main queue and use audioString
}
Try this code. No need to take global variable if it is not being used in multiple function. you can return fetched URL in completion handler.
func getAudioUrl(completionHandler:#escaping ((_ url:String?) -> Void)) {
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
let audio = (foo["audio"] as? String)!
completionHandler(audio)
} else {
// item could not be found
completionHandler(nil)
}
}).resume()
}
func useAudioURL() {
self.getAudioUrl { (url) in
if let strUrl = url {
// perform your dependant operation
print(strUrl)
}else {
//url is nil
}
}
}
Here i had implemented pagination for the table view and items are loaded by using model class but here the loaded items are replacing with the new items and whenever it calls api it returns the new data and old data is overriding on it and displaying only 10 items at a time i am implementing it for first time can anyone help me how to resolve the issue ?
func listCategoryDownloadJsonWithURL(listUrl: String) {
let url = URL(string: listUrl)!
print(listUrl)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil { print(error!); return }
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel = ModelClass(dict: jsonObj as [String : AnyObject])
DispatchQueue.main.async {
guard let obj = self.listClassModel else { return }
let itemsCount = obj.items.count
print(itemsCount)
for i in 0..<itemsCount {
let customAttribute = obj.items[i].customAttribute
for j in 0..<customAttribute.count {
if customAttribute[j].attributeCode == "image" {
let baseUrl = "http://192.168.1.11/magento2/pub/media/catalog/product"
self.listCategoryImageArray.append(baseUrl + customAttribute[j].value)
print(self.listCategoryImageArray)
}
}
}
self.activityIndicator.stopAnimating()
self.activityIndicator.hidesWhenStopped = true
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
self.collectionView.isHidden = false
self.tableView.reloadData()
}
}
} catch {
print(error)
}
}
task.resume()
}
You are assigning your result data to model array, each time you call your API. This is the reason that your old data is getting replaced with new one. Rather than assigning, you should append the new data to your datasource array.
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel.append(contentsOf: ModelClass(dict: jsonObj as [String : AnyObject]))
Also make sure you initialize your array as an empty array first. (maybe in declaration or viewDidLoad) before calling API.
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)
}