i requested data from webserver by alamofire. i want to pass data to viewdidload but data in viewdidload is empty, please help me explain. thanks and sory for my english. this my code
class LiveScoreViewController: UIViewController
{
var matchData : JSON! = []
func loadLiveScore(section: String){
DNService.getLiveScore(section) { (JSON) -> () in
self.matchData = JSON[ ]
self.matchData = self.matchData["match"]
//print(self.matchData) -> is ok
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadLiveScore("LiveScore")
//print(self.matchData) -> is empty
}}
If DNService.getLiveScore is a webservice call, then you wont be able to get the matchData inside viewDidLoad, since the webservice call will take some time to complete, whatever you are trying to do with the matchData should be done in the completion block of DNService.getLiveScore most likely
If you want, you can put a print statement right after loadLiveScore in viewDidLoad and also in the completion block, and you will see the order of execution of the print statements is not what you are expecting
getLiveScore is asynchronous method. So you have to use completion handler to get the response. Make an handler for loadLiveScore
func loadLiveScore(section: String), handler: (JSON) -> ()) {
DNService.getLiveScore(section) { (JSON) -> () in
handler(JSON)
}
}
Call the method from your viewDidLoad as like:
override func viewDidLoad() {
super.viewDidLoad()
loadLiveScore("LiveScore") { json in
print(json) // parse JSON as you need
self.matchData = json["match"]
}
}}
Related
I have this that download the JSON and I have appended them into an array
func downloadDetails(completed: #escaping DownloadComplete){
Alamofire.request(API_URL).responseJSON { (response) in
let result = response.result
let json = JSON(result.value!).array
let jsonCount = json?.count
let count = jsonCount!
for i in 0..<(count){
let image = json![i]["urls"]["raw"].stringValue
self.imagesArray.append(image)
}
self.tableView.reloadData()
print(self.imagesArray)
completed()
}
}
I know that viewDidLoad loads first and viewDidAppear load later. I am trying to retrieve the array that contain all the informations but it's not showing on viewDidLoad as the array is empty but its showing on viewDidAppear with an array of ten and I dont know how to get that from viewDidAppear.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var imageList: Image!
var imagesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad " ,imagesArray)
setupDelegate()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
downloadDetails {
DispatchQueue.main.async {
}
print("viewDidAppear ", self.imagesArray)
}
}
The output is as follow
viewDidLoad []
viewDidAppear ["https://images.unsplash.com/photo-1464550883968-cec281c19761", "https://images.unsplash.com/photo-1464550838636-1a3496df938b", "https://images.unsplash.com/photo-1464550580740-b3f73fd373cb", "https://images.unsplash.com/photo-1464547323744-4edd0cd0c746", "https://images.unsplash.com/photo-1464545022782-925ec69295ef", "https://images.unsplash.com/photo-1464537356976-89e35dfa63ee", "https://images.unsplash.com/photo-1464536564416-b73260a9532b", "https://images.unsplash.com/photo-1464536194743-0c49f0766ef6", "https://images.unsplash.com/photo-1464519586905-a8c004d307cc", "https://images.unsplash.com/photo-1464519046765-f6d70b82a0df"]
What is the right way to access the array with information in it?
that's because the request is asynchronous it's not getting executed as serial lines of code , there should be a delay according to network status , besides you don't even call downloadDetails inside viewDidLoad
//
you also need to reload table here
downloadDetails { // Alamofire callback by default runs in main queue
self.tableView.reloadData()
}
Just call your method on
viewDidLoad()
And maybe you should learn more about iOS lifecycle, so you can spot the difference between viewDidLoad and viewDidAppear 😉
So currently, I'm facing some issues in regards to writing a GET request in my Data Model and calling it in my ViewController. I have no issues with writing a GET request and calling it from the ViewController if there is no Init() function.
You simply initialize the ViewController like so and call it in viewDidLoad
Class ViewController {
var dataModel = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
print(dataModel.info)
// Calls a completion handler, and won't print any info until the data is downloaded.
// retrieve the info from the data model.
}
}
But if in my DataModel I do something like
Class DataModel {
var info1: String!
var info2: String!
var info3: String!
init(info1: String, info2: String, info3: String) {
}
func downloadInfo(completed: #escaping downloadComplete) {
Alamofire.request(URL).responseJSON { response in
// GET request, parse data, and assign to variables
completed()
}
}
}
I can no longer initialize my ViewController without passing in these properties. But I can't call these properties until I download the data.
So I can longer do
Class ViewController {
var dataModel = DataModel()
}
but if I do
Class ViewController {
var dataModel: DataModel!
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
DataModel(dataModel.info1, dataModel.info2, dataModel.info3)(
// initialize after properties get downloaded.
}
I get unexpectedly returned nil because I didn't initialize in the beginning.
So I tried initializing with empty data because I can't get retrieve my real data until it gets downloaded.
Class ViewController {
var infoArray = [datModel]()
var dataModel = DataModel(info1: "", info2: "", info3: "")
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
let infoVar = DataModel(dataModel.info1, dataModel.info2, dataModel.info3)
self.infoArray.append(infoVar)
// append the datModel information into an array.
}
}
Technically this works, but am I doing it wrong, because this seems like a workaround, not a solution to very common task.
Lastly, the only other problem I receive is that I only get one object in the array, not the hundreds that there should be.
The easiest approach I would go for; is to make the properties optionals. It will alleviate you for having to do the work-around you have identified.
Class DataModel {
var info1: String?
var info2: String?
var info3: String?
}
And secondly, do make the download call async (with a callback as argument to the method) to allow the UI to load immediately. It will make the User experience a lot better!
I have static data (3 values) coming from CloudKit, and the problem is when I refresh the UITableView, I get 6 values instead of 3 values.
I'm not sure why it doesn't refresh and throw out old data from the Array, but instead it keeps the old data and adds the same data to it Array.
Initial UITableView set up:
func getData() {
cloud.getCloudKit { (game: [Game]) in
var teamColorArray = [String]()
for item in game {
let itemColor = item.teamColor
teamColorArray.append(itemColor)
print("TCA in: \(teamColorArray)")
}
self.teamColorArray = teamColorArray
self.tableView.reloadData()
}
}
Prints: ["CC0033", "FDB927", "E3263A"]
Refresh data when UIRefreshControl pulled:
#IBAction func refreshData(_ sender: Any) {
self.teamColorArray.removeAll()
getData()
self.refreshControl?.endRefreshing()
}
Prints: ["CC0033", "FDB927", "E3263A", "CC0033", "FDB927", "E3263A"]
I think I have it narrowed down to somehow game in the function getData() is incremented to a count of 6 items. I'm not sure why it wouldn't always stay at 3 items if it were pulling all new data from CloudKit, but maybe I'm not understanding that calling a completion handler doubles the values and maybe I need to removeAll inside of that? I'm just really not sure
Does anyone have anything they see I'm doing wrong, or anything they'd do to fix my code?
Thanks!
Might have to do with your async call to cloudkit. I'm not too familiar with refresh control but here is a way to maybe solve your problem and also make your code a little cleaner.
func getData(_ completion: () -> ()) {
teamColorArray.removeAll()
cloud.getCloudKit { [weak self] (game: [Game]) in
guard let unwrappedSelf = self else { return }
var updatedColorArray = [String]()
game.forEach { updatedColorArray.append($0.teamColor) }
unwrappedSelf.teamColorArray = updatedColorArray
completion()
}
}
now when you call getData it would look like this
getData {
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
self?.refreshControl?.endRefreshing()
}
}
you add weak self to remove the possibility of retain cycles
make sure your updating UI from the main thread
Call reloadData and endRefreshing when you know the array has been set properly
I put teamColorArray.removeAll() inside the getData function since it seems like you will need to call it everytime and it saves you from having to add it before the function everytime.
I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.
My Issue:
I am trying to load data from Server, through Alamofire, before SubViewController Load its view. After writing the code, I failed to solve the problem of Async Feature of Alamofire. The view is always be loaded in the SubViewController before Alamofire finished its job.
Part Of My Code:
ParentViewController:
Leading the way to SubViewController through PrepareForSegue().
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CellDetailSegue" {
if let indexPaths = self.dateCollectionView.indexPathsForSelectedItems() {
let subViewController = segue.destinationViewController as! SubViewConroller
}
}
SubViewController:
Test whether the data has been loaded by print() in the its viewDidLoad() and load the data by dataRequest() in viewWillAppear()
class SubViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var availablePeriods401 = [String]()
var availablePeriods403 = [String]()
var availablePeriods405 = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.dataRequest(self.availablePeriods401)
self.dataRequest(self.availablePeriods403)
self.dataRequest(self.availablePeriods405)
print(self.availablePeriods401.count)
print(self.availablePeriods403.count)
print(self.availablePeriods405.count)
}
func dataRequest(_ target: [String]) {
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON {
.
.
.
target = Result
}
}
}
Problem Description:
Three variables in the SubViewController can not be assigned the valid values after view was loaded.
Three Outputs' results are all 0.
But I can get valid count if I set print() in the dataRequest().
My Question:
How to make sure that Alamofire finishes its job?
Where Shall I put the Alamofire Request Function? viewWillApper()? viewDidApper()?
Should I even finished requesting job in ParentViewController's PrepareForSegue() ?
Please teach me how to solve this problem.
A big appreciation for your guide and time.
Ethan Joe
You should call Alamofire Request Function in viewDidLoad function. and you should reload table data when you got response from completion block(from where you print the data).
You can reload tableview like,
self.tableView.reloadData()
hope this will help :)
The first thing I noticed is that you are doing 3 asynchronous requests, not one. You could use a completion handler but which one? I think you have 2 options.
Nest the network calls so that the completion of one starts the next one. The downside to this approach is that they will run sequentially and if you add more, you have to continue nesting. An approach like this might be OK if you are only doing 2 calls but beyond that it will get more and more difficult.
Use a semaphore to wait until all the data is loaded from all the remote calls. Use the completion handler to signal the semaphore. If you are going to use this approach, then it must be done on a background thread because use of a semaphore will block the thread and you don't want that happening on the main thread.
These three calls will all happen simultaneously. And the functions will return even though AlamoFire has not completed.
self.dataRequest(self.availablePeriods401)
self.dataRequest(self.availablePeriods403)
self.dataRequest(self.availablePeriods405)
These will execute, whether AlamoFire has completed or not.
print(self.availablePeriods401.count)
print(self.availablePeriods403.count)
print(self.availablePeriods405.count)
Using semaphores would look something like this:
override func viewWillAppear(animated: Bool) {
// maybe show a "Please Wait" dialog?
loadMyData() {
(success) in
// hide the "Please Wait" dialog.
// populate data on screen
}
}
func loadMyData(completion: MyCompletionHandler) {
// Do this in an operation queue so that we are not
// blocking the main thread.
let queue = NSOperationQueue()
queue.addOperationWithBlock {
let semaphore = dispatch_semaphore_create(0)
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar1"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar2"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
Alamofire.request(.POST, "http://httpbin.org/get", parameters: ["foo": "bar3"]).responseJSON {
// This block fires after the results come back
// do something
dispatch_semaphore_signal(semaphore);
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
completion(true)
}
}
Apple Docs - Grand Central Dispatch
How to use semaphores
The question I have for you is what are you going to do if some, bit not all of the web calls fail?
First create a global bool variable with false
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CellDetailSegue" && boolVar {
if let indexPaths = self.dateCollectionView.indexPathsForSelectedItems() {
let subViewController = segue.destinationViewController as! SubViewConroller
}
}
call prepare segue with segue name and boolVar true from almofire block.