I want to show the JSON data grabbed from a server on a Table View. The problem is, I can't get it to show up on it. I have tried several different methods and searched a lot to find a solution, but I can't.
My code (all of it) is shown below and I hope somebody can help me out. Thanks in advance.
import UIKit
import Alamofire
import SwiftyJSON
class MasterViewController: UITableViewController {
var tableTitle = [String]()
var tableBody = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
getJSON()
}
func getJSON(){
Alamofire.request(.GET, "http://announcement.vassy.net/api/AnnouncementAPI/Get/").responseJSON { (Response) -> Void in
// checking if result has value
if let value = Response.result.value {
let json = JSON(value)
for anItem in json.array! {
let title: String? = anItem["Title"].stringValue
let body: String? = anItem["Body"].stringValue
self.tableTitle.append(title!)
self.tableBody.append(body!)
}
}
}
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// Table View Stuff
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableTitle.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! TableViewCell
// cell config
cell.title!.text = tableTitle[indexPath.row]
cell.body!.text = tableBody[indexPath.row]
return cell
}
}
The Alamofire network request is asynchronous, meaning you can't know when the result will come back.
The problem here is that you reload the tableView outside the scope of the Alamofire request, so it is executed before the data comes back.
The reload should happen in the same scope, and on the main thread, for example:
func getJSON(){
Alamofire.request(.GET, "http://announcement.vassy.net/api/AnnouncementAPI/Get/").responseJSON { (Response) -> Void in
// checking if result has value
if let value = Response.result.value {
let json = JSON(value)
for anItem in json.array! {
let title: String? = anItem["Title"].stringValue
let body: String? = anItem["Body"].stringValue
self.tableTitle.append(title!)
self.tableBody.append(body!)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
}
I think #Eric said almost everything in his answer, nevertheless, not it's a good decision in design keep the code for make the network request in your same UITableViewController this keep a couple between two things that are independents and change for differents reasons.
My advice is separate the two parts of the code decoupling the dependency between your two layers. In this way when you need to change anything related with your networking request handler you don't need to change it in any place where you make the same request, it's an advice!!!.
In case you want to do it, you can use closures to hanlde the async behaviour of Alamofire passgin the completionHandlerinside the wrapper you make to handle the networking requests, for example, let's define a simple wrapper using the singleton pattern (it's just for the purpose of explain the sample, you can handle it as you want).
import AlamofireImage
import SwiftyJSON
class NetworkHandler {
/// The shared instance to define the singleton.
static let sharedInstance = RequestManager()
/**
Private initializer to create the singleton instance.
*/
private init() { }
func getJSON(completionHandler: (json: JSON?, error: NSError?) -> Void) {
Alamofire.request(.GET, http://announcement.vassy.net/api/AnnouncementAPI/Get/).responseJSON { response in
switch(response.result) {
case .Success(let value):
let json = JSON(value)
completionHandler(json: json, error: nil)
case .Failure(let error):
completionHandler(json: nil, error: error)
}
}
}
}
Then in your UITableViewController you can call the new wrapper to Alamofire in this way:
class MasterViewController: UITableViewController {
var tableTitle = [String]()
var tableBody = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NetworkHandler.sharedInstance.getJSON { [weak self] (json, error) -> Void in
// request was made successful
if error == nil {
for anItem in json.array! {
let title: String? = anItem["Title"].stringValue
let body: String? = anItem["Body"].stringValue
self.tableTitle.append(title!)
self.tableBody.append(body!)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
}
// rest of your code
}
In the above way you keep the code decoupled, it's a good design.
I hope this help you
Related
I have already parsed JSON and showing in tableView which is working fine. Now my question is how will i save data offline and show when internet is not available offline using Core Data. I am working in Swift 3. If anyone can help me with screenshot it will be great help.
Below is my Code for fetching json and showing on tableView :
import UIKit
import SystemConfiguration
struct CellData {
var name:String
var address:String
public init(name:String,address:String){
self.name = name
self.address = address
}
}
///ViewController
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
#IBOutlet weak var tableViewData: UITableView!
var arrayData = [CellData]()
override func viewDidLoad() {
super.viewDidLoad()
if Reachability.isConnectedToNetwork(){
print("Internet Connection Available!")
fetchServerData()
}else{
let alert = UIAlertController(title: "No Internet connection", message: "Please ensure you are connected to the Internet", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
print("Internet Connection not Available!")
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MyCellData
cell.lblTop.text = "π\(arrayData[indexPath.row].name)"
cell.lblBottom.text = arrayData[indexPath.row].address
return cell
}
func fetchServerData(){
let prs = [
"author_id": "1780",
"get_deals_author": "1" as String
]
Service.StartWithoutLoading(prs as [String : AnyObject]?, onCompletion: { result in
let json = result as? NSDictionary
if let data = json as? [String:Any]{
if let err = data["status"] as? String, err == "success"{
if let data = data["result"] as? [Any]{
var arrayData = [CellData]()
for sectionObj in data{
if let sectionObjVal = sectionObj as? [String:Any]{
if let name_deal = sectionObjVal["name"] as? String{
if let address_deal = sectionObjVal["address"] as? String{
let dataValue = CellData.init(name: name_deal, address: address_deal)
arrayData.append(dataValue)
}
}
}
}
DispatchQueue.main.async { () -> Void in
self.arrayData.removeAll()
self.arrayData = arrayData
self.tableViewData.reloadData()
}
}
}
}
})
}
}
For Core Data, you need to create the entities you need in CoreData model .xcdatamodeld. Click on Add Entity and name your entity. Then add attributes which you require to save.
You can see this link on how to create the entities and attributes. After creating everything, we can write a CoreDataStack and a manager class or we can directly use the code pre-written in AppDelegate when we check on Core Data when creating a project. I'll here use the CoreDataStack class.
Here is the class
import Foundation
import CoreData
class CoreDataStack: NSObject {
static let moduleName = "YourProject"
static let shared = CoreDataStack()
private override init() {
super.init()
_ = self.persistentContainer
}
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: CoreDataStack.moduleName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
print("Coordinator URL - \(storeDescription)")
})
return container
}()
}
Now we can make a manager class to insert the data. Let's say your entity is Person and its attributes are name and address
Here is the CoreDataManager class to insert, update, fetch data.
import UIKit
import CoreData
class CoreDataManager: NSObject {
class func addRecord(object:[String:Any]) {
let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: CoreDataStack.shared.persistentContainer.viewContext) as! Person
person.name = object["name"] as? String
person.address = object["address"] as? String
CoreDataStack.shared.saveContext()
}
class func getRecords() -> [Person]? {
let request:NSFetchRequest<Person> = Person.fetchRequest()
do {
let results = try CoreDataStack.shared.persistentContainer.viewContext.fetch(request)
return results
} catch {
print(error.localizedDescription)
}
return nil
}
}
You can call addRecord method in your ViewController class and it will save your data. I recommend that you pass the complete array and then add in core data and finally call saveContext().
Finally you can use getRecords to get all records.
I can't put data to the cells, I searched in different tutorials and it should work , I checked in debug area and data are downloaded but doesn't exist in cells, I tried also with custom cells but it doesn't work too. I have not got any error message, simply empty cells. Do you know maybe what can cause this issue? I spended much time for searching solution but I can't find anything, on every tutorial people do this similar to me.
struct Country: Decodable {
let name: String
}
class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var countries = [Country]()
var liczba = Int()
#IBOutlet var tv: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
json() {
self.tv.reloadData()
}
tv.dataSource = self
tv.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return liczba
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = countries[indexPath.row].name
return cell
}
func json (completed: #escaping()->()) {
let jsonUrl = "https://restcountries.eu/rest/v2/all"
let url = URL(string: jsonUrl)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
do {
self.countries = try JSONDecoder().decode([Country].self, from: data!)
let numer = self.countries.count
self.liczba = numer
}
catch {
print("error")
}
}.resume()
}
}
It seems that you are having a problem with the sequentiality of your code. You are calling reloadData on your tableview but at that moment you don't have set the datasource yet.
try this:
tv.dataSource = self
tv.delegate = self
json() {
self.tv.reloadData()
}
You are never calling the completed closure of your json method, so self.tv.reloadData() will never be executed.
Add completed() after self.liczba = numer.
Since you are not returning anything from the closure I recommend to delete the completion handler and reload the table view on the main thread within the closure.
And don't forget to handle a potential error
func json() {
let jsonUrl = "https://restcountries.eu/rest/v2/all"
let url = URL(string: jsonUrl)
URLSession.shared.dataTask(with: url!) { [unowned self] (data, response, error) in
if let error = error { print(error); return }
do {
self.countries = try JSONDecoder().decode([Country].self, from: data!)
self.liczba = self.countries.count
DispatchQueue.main.async {
self.tv.reloadData()
}
}
catch {
print("error")
}
}.resume()
}
and call the method in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
tv.dataSource = self // I'd connect datasource and delegate in Interface Builder
tv.delegate = self
json()
}
i started to learn IOS development using swift 3.0.i built a simple app to call web api to query data from server database. i can get the json data and parsed it into string array. the App can print the array, but it cannot show in the tableview. it confused me several days and i searched some examples and answers on internet but still couldn't work out it.
My codes as below:
class LocationTableViewController: UITableViewController {
var names: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
//ββββββββββββββββββββββββββget the data from web api and using json parsingββββββββββββββββββββββββ
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: "http://XXXXXXX/api/mylocations")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.localizedDescription)
} else {
do {
var jsonResult: NSMutableArray = NSMutableArray()
let jsonArray = try JSONSerialization.jsonObject(with: data!, options:JSONSerialization.ReadingOptions.allowFragments) as! NSArray
jsonResult = jsonArray.mutableCopy() as! NSMutableArray
var jsonElement: NSDictionary = NSDictionary()
for i in 0..<jsonResult.count {
jsonElement = jsonResult[i] as! NSDictionary
if let name = jsonElement["Name"] as? String
{
// print(id)
// print(name)
// print(address)
// print(latitude)
// print(longitude)
// print("-------")
self.names.append(name)
}
// self.tableView.reloadData()
// print(self.names)
}
print(self.names)
// can print the string array data like [βname1β,βname2β,βname3β]
} catch {
print("error in JSONSerialization")
}
}
})
task.resume()
//-------------- βββ result is [] it seems the above code didn't put the string array to names.ββββββββββββββ
print(self.names)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count;
}
internal override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cellIdentifier = "cell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for:
indexPath as IndexPath) as UITableViewCell
// Configure the cell...
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Can anyone help me have a look?
Put self.tableView.reloadData() after print print(self.names).
At the point where you have commented...
result is [] it seems the above code didn't put the string array to
names
This line of code is being executed before the data has been downloaded within the completion handler, so we wouldn't expect to see anything here. You will note that it is working on the other print that you have within the completion handler.
The tableView.reloadData() at the end of the completion handler should be working.
Are you sure that you have the delegates set up correctly for the tableView? What do you see if you comment out the download task, and simply set
names = ["Tom", "Dick", "Harry"]
within viewDidLoad ? If that doesn't work, it's a problem with the delegates.
I am trying to load an exercises JSON into a table view, i have a service function that gets the data from a source as JSON, and a view controller with a table that I want to load the info into. There are no errors in the code however the table loads blank rows, the debug section shows the JSON data just fine via a print command. Im a beginner so im sure im missing a core element, but cant work it out!
api service
class ApiService {
static var swiftyJsonVar:JSON?
class func getExerciseData() {
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar ?? nil)
}
}
}
View Controller
class ExerciseDatabaseController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var ExerciseSearchField: UISearchBar!
#IBOutlet weak var ExercisesTableView: UITableView!
var arrRes = [[String:AnyObject]]() // Array of dictionary
override func viewDidLoad() {
super.viewDidLoad()
let arrRes = ApiService.getExerciseData()
if let resData = ApiService.swiftyJsonVar?["exercise"].arrayObject {
self.arrRes = resData as! [[String:AnyObject]]
}
if self.arrRes.count > 0 {
self.ExercisesTableView.reloadData()
}
print(arrRes)
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrRes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var dict = arrRes[indexPath.row]
cell.textLabel?.text = dict["name"] as? String
cell.detailTextLabel?.text = dict["description"] as? String
return cell
}
You should be loading your JSON asynchronously, which means you should have a closure in the method that makes your alamofire call.
class ApiService {
class func getExerciseData(completion: #escaping ([[String: AnyObject]]) -> ()) {
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
guard let jsonResponse = responseData.result.value else {
//possibly put some sort of protection, or what you want to do if there is not a response here
return
}
//instead of creating a variable for swiftyJsonVar in this class,
//you want to use a completion to send the array of dictionaries to the tableview asynchronously,
//that way it doesn't load blank
//I'm not super familiar with swifty json(sorry). What I normally do is below.
let swiftyJsonVar = JSON(jsonResponse)
guard let dictArray = swiftyJsonVar["exercise"].arrayObject as? [[String: AnyObject]] else {
//some sort of protection here if this fails
return
}
completion(dictArray)
}
}
So now we have made our asynchronous call(generally you want to do this whenever you are displaying information visually from an internet call that was not already preloaded/saved somewhere in app).
Next, we want to display this information in our tableview upon tableview load.
class ExerciseDatabaseController: UIViewController, UITableViewDataSource, UITableViewDelegate {
//these should start with lower cases(exerciseSearchField), never uppercased
#IBOutlet weak var ExerciseSearchField: UISearchBar!
#IBOutlet weak var ExercisesTableView: UITableView!
var arrRes = [[String:AnyObject]]() // Array of dictionary
override func viewDidLoad() {
super.viewDidLoad()
//you said you would use these delegates up top when you created the class, so you have to set them
ExercisesTableView.delegate = self
ExercisesTableView.dataSource = self
fetchData()
// Do any additional setup after loading the view.
}
//this method will make the api call
//you'll notice that if you set breakpoints, it will reach the end of the method before hitting self?.arrRes = dictArray
//this is normal and how asynchronous calls work, look into tableview threading for a deeper explanation of why that is. It is super important to understand threading in iOS
//once it gets data back from the API call, it will go to the main thread and tell the tableview to reload with that data
func fetchData() {
ApiService.getExerciseData { [weak self] (dictArray) in
self?.arrRes = dictArray
print(self?.arrRes)
if self?.arrRes.count > 0 {
DispatchQueue.main.async {
self?.ExercisesTableView.reloadData()
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrRes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var dict = arrRes[indexPath.row]
cell.textLabel?.text = dict["name"] as? String
cell.detailTextLabel?.text = dict["description"] as? String
return cell
}
You'll see I used [weak self] above. For more of an explanation of why that is necessary with asynchronous internet calls/whenever using closures, you can read here:
http://krakendev.io/blog/weak-and-unowned-references-in-swift
There are a lot of other resources for reading about weak and strong references/parent child stuff in iOS with a quick google search. Also, pursue researching asynchronous/synchronous in iOS. Both of these topics are incredibly important to learn when beginning.
Reload your tableView once the JSON data from your asynchronous request is received. So your
self.ExercisesTableView.reloadData()
will go inside
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar ?? nil)
}
}
I'm new to iOS programming, so my question might not be complicated but I'm still struggling to find the best solution. Any help will be highly appreciated!
I'm trying to send GET request every time the user opens the app. I wrote the function loadMenu() that collects the data from a json file on the server and populates the table in the app.
The problem is that if I update the json file, it's not reflected in the app. If feels like the loadMenu() part of the code is just ignored.
Here's my code:
import UIKit
class TableViewControllerNew: UITableViewController {
var names = [String]()
var mealDescription = [String]()
var price = [Double]()
override func viewDidLoad() {
super.viewDidLoad()
loadMenu()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
var new = NSUserDefaults.standardUserDefaults().objectForKey("names") as! [String]
print("test: \(new.count)")
return new.count
}
func loadMenu() {
print("viewDidLoad works")
// Send HTTP GET
let myUrl = NSURL(string: "http://localhost/myPi/selection/wheyStationSelection.json");
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "GET";
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
dispatch_async(dispatch_get_main_queue()) {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let meals = json["meals"] as? [[String: AnyObject]] {
for meal in meals {
if let name = meal["name"] as? String {
self.names.append(name)
//NSUserDefaults.standardUserDefaults().setObject(self.names, forKey: "test1") as! [String]
//var new = NSUserDefaults.standardUserDefaults().objectForKey("test1") as? [String]
//print(new)
}
if let mealDescription = meal["mealDescription"] as? String {
self.mealDescription.append(mealDescription)
}
if let price = meal["price"] as? Double {
self.price.append(price)
}
}
}
} catch {
print("error serializing JSON: \(error)")
}
NSUserDefaults.standardUserDefaults().setObject(self.names, forKey: "test1") as! [String]
//print(self.names)
//print(self.mealDescription)
//print(self.price)
}
}).resume()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdenifier = "MealTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdenifier, forIndexPath: indexPath) as! MealTableViewCell
var new = NSUserDefaults.standardUserDefaults().objectForKey("names") as! [String]
let name = new[indexPath.row]
//print(name)
cell.mealNameLabel.text = name
return cell
}
#david is right, if you place your loadMenu() method in viewDidAppear() it will be called each and every time your view appears. You can read more about the various lifecycle phases of a UIViewController here
One other thing. It is not clear to me whether your loadMenu() isn't called every time or whether you are just not seeing the updated content.
I can see that you are not reloading your table view when the JSON has been updated. Therefore your TableView don't know that any updates has occurred and will not render again and you won't see any updates.
Therefore, right after this line:
NSUserDefaults.standardUserDefaults().setObject(self.names, forKey: "test1")
You should tell your tableView to reload itself like so:
tableView.reloadData()
So you have:
NSUserDefaults.standardUserDefaults().setObject(self.names, forKey: "test1")
tableView.reloadData()
That should cause all your "render the TableView" methods to be called again, but this time with the new data and you should see the updated content.
Hope this helps you.
If you call loadMenu() in viewDidAppear(animated:) instead of viewDidLoad(), then it will be called every time you leave your app and reopen it.