I'm new to swift and I can't get my head around this issue.
I can successfully make JSON fetch from an API, but data on the Table View only load after I click a button. Calling the same function on the viewDidLoad didn't load the data when the app is opening.
I tried lot of solutions, but I can't find where the fault is
here's the main view controller code:
import UIKit
struct ExpenseItem: Decodable {
let name: String
let amount: String
let id: String
let timestamp: String
let description: String
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBAction func fetchData(_ sender: Any) {
ds()
self.eTW.reloadData()
}
#IBOutlet weak var eTW: UITableView!
var allExpenses = [ExpenseItem]()
let expensesURL = "https://temp.cuttons.com/json/expenses.json"
override func viewDidLoad() {
super.viewDidLoad()
eTW.delegate = self
eTW.dataSource = self
ds()
self.eTW.reloadData()
}
func parseJSON(data: Data) -> [ExpenseItem] {
var e = [ExpenseItem]()
let decoder = JSONDecoder()
do {
e = try decoder.decode([ExpenseItem].self, from: data)
} catch {
print(error.localizedDescription)
}
return e
}
func ds() {
if let url = URL(string: expensesURL){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, session, error) in
if error != nil {
print("some error happened")
} else {
if let content = data {
self.allExpenses = self.parseJSON(data: content)
}
}
}
task.resume()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.allExpenses.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = eTW.dequeueReusableCell(withIdentifier: "expense", for: indexPath) as! myCellTableViewCell
cell.name.text = self.allExpenses[indexPath.row].name
if let am = Double(self.allExpenses[indexPath.row].amount) {
if (am > 0) {
cell.amount.textColor = .green
} else {
cell.amount.textColor = .red
}
}
cell.amount.text = self.allExpenses[indexPath.row].amount
return cell
}
thanks
L.
You have to reload the table view after receiving the data asynchronously.
Remove the line in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
eTW.delegate = self
eTW.dataSource = self
ds()
}
and add it in ds
func ds() {
if let url = URL(string: expensesURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if let error = error {
print("some error happened", error)
} else {
self.allExpenses = self.parseJSON(data: data!)
DispatchQueue.main.async {
self.eTW.reloadData()
}
}
}
task.resume()
}
}
Do not reload data after the api call in viewDidLoad(). Do it inside of the completion block after you parse the JSON into the object you need (assuming it's successful like you said).
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, session, error) in
guard error == nil else {
print("some error happened")
return
}
guard let data = data else {
print("bad JSON")
return
}
self.allExpenses = self.parseJSON(data: data)
DispatchQueue.main.async {
self.eTW.reloadData()
}
}
task.resume()
Also, use guard let instead of if let if possible.
Related
I am wanting the search bar to return the title for each object in the Array. I believe the problem is in my for loop but I am not completed sure. Hopefully, you guys are able to tell me what I am doing wrong.
I am searching through an API. This is the array I am attempting to search through
struct ProductResponse: Decodable {
let results: [SearchResults]
}
struct SearchResults: Decodable {
let title: String
let id:Int
}
I created a for loop to run through each object in the array and get the title and id.
func fetchProduct(productName: String) {
let urlString = "\(searchURL)&query=\(productName)&number=25"
performRequest(urlString: urlString)
}
func performRequest(urlString: String) {
// Create a URL
if let url = URL(string: urlString) {
//Create a URLSession
let session = URLSession(configuration: .default)
// Give the session a task
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJSON(productTitle: safeData)
}
}
// Start the task
task.resume()
}
}
func parseJSON(productTitle: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(ProductResponse.self, from: productTitle)
print(decodedData.results)
} catch {
print(error)
}
}
}
I created this function to reload the data in my tableview. When I search for the product results in the Search Bar, my tableview doesn't return anything. The goal is to have my tableview return each result in a tableview cell
var listOfProducts = [SearchResults]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
self.navigationItem.title = "\(self.listOfProducts.count) Products found"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
productSearch.delegate = self
}
func downloadJSON() {
guard let downloadURL = url else { fatalError("Failed to get URL")}
URLSession.shared.dataTask(with: downloadURL) { (data, Response, error) in
print("downloaded")
}.resume()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return listOfProducts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let product = listOfProducts[indexPath.row].title
cell.textLabel?.text = product
return cell
}
}
extension ProductsTableViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let searchBarText = searchBar.text {
searchRequest.fetchProduct(productName: searchBarText)
}
}
}
This is the result
enter image description here
If everything goes well and You got the data under "decodedData.results" of "parseJSON" method, And I saw "decodedData.results" and "listOfProducts" both variables are the same type of SearchResults. So you can just add the one line of under "parseJSON" method as follows:-
func parseJSON(productTitle: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(ProductResponse.self, from: productTitle)
print(decodedData.results)// it have some array data of type SearchResults
self.listOfProducts = decodedData.results
self.tableView.reloadData()
} catch {
print(error)
}
}
I'm trying to display images that comes from an API. The images are inside an URL and I want to fill a Table View with all the array, but it shows only one image at the Table View.
Here's my code:
struct Autos {
let Marca:String
let Modelo:String
let Precio:String
let RutaImagen:String
init?(_ dict:[String:Any]?){
guard let _dict = dict,
let marca=_dict["Marca"]as?String,
let modelo=_dict["Modelo"]as?String,
let precio=_dict["Precio"]as?String,
let rutaImagen=_dict["RutaImagen"]as?String
else { return nil }
self.Marca = marca
self.Modelo = modelo
self.Precio = precio
self.RutaImagen = rutaImagen
}
}
var arrAutos = [Autos]()
func getImage(from string: String) -> UIImage? {
// Get valid URL
guard let url = URL(string: string)
else {
print("Unable to create URL")
return nil
}
var image: UIImage? = nil
do {
// Get valid data
let data = try Data(contentsOf: url, options: [])
// Make image
image = UIImage(data: data)
}
catch {
print(error.localizedDescription)
}
return image
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 9
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "carsCell", for: indexPath) as! CarsDetailTableViewCell
let url = URL(string: "http://ws-smartit.divisionautomotriz.com/wsApiCasaTrust/api/autos")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
guard let dataResponse = data, error == nil else {
print(error?.localizedDescription ?? "Response Error")
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with: dataResponse, options: []) as? NSArray
self.arrAutos = jsonResponse!.compactMap({ Autos($0 as? [String:String])})
DispatchQueue.main.async {
// Get valid string
let string = self.arrAutos[indexPath.row].RutaImagen
if let image = self.getImage(from: string) {
// Apply image
cell.imgCar.image = image
}
cell.lblBrand.text = self.arrAutos[indexPath.row].Marca
cell.lblPrice.text = self.arrAutos[indexPath.row].Precio
cell.lblModel.text = self.arrAutos[indexPath.row].Modelo
}
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
return cell
}
The JSON serialization is working fine, because the other data is showed correctly at the table view, the issue is with the image, because in the table view only appears one image, the other rows are empty. Does anyone have an advise?
I think you should download your full data before loading tableview and reload tableview in the completion handler. Call loadData() method in your viewDidLoad().
fileprivate func loadData() {
let url = URL(string: "http://ws-smartit.divisionautomotriz.com/wsApiCasaTrust/api/autos")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
guard let dataResponse = data, error == nil else {
print(error?.localizedDescription ?? "Response Error")
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with: dataResponse, options: []) as? NSArray
self.arrAutos = jsonResponse!.compactMap({ Autos($0 as? [String:String])})
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
For loading images in tableView cell, download the image in background thread and then update the imageView in the main thread.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "carsCell", for: indexPath) as! CarsDetailTableViewCell
// Get valid string
let string = self.arrAutos[indexPath.row].RutaImagen
//print(string)
cell.lblBrand.text = self.arrAutos[indexPath.row].Marca
cell.lblPrice.text = self.arrAutos[indexPath.row].Precio
cell.lblModel.text = self.arrAutos[indexPath.row].Modelo
let url = URL(string: string)
if url != nil {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if data != nil {
cell.imgCar.image = UIImage(data:data!)
}
}
}
}
return cell
}
Hope this will work.
Swift 4 Question
Json wont parse into individual tableview cell titles
I am trying to get my UITableView title's to display the outputted printed json shown here
https://i.imgur.com/yuOWW74.png
However when I run the program the output shows
https://i.imgur.com/A00t6rE.png
My current snippet of code for getting the data into the title object is
func fetchPlayerStats(completion: #escaping (Result<[beatMaps], Error>) -> ()) {
let urlString = "https://osu.ppy.sh/api/get_beatmaps?&k=983e993af59aa75b73d21cd42b4dfe96db068802"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
if let err = err {
completion(.failure(err))
return
}
do {
let playerInfo = try JSONDecoder().decode([beatMaps].self, from: data!)
completion(.success(playerInfo))
} catch let jsonError {
completion(.failure(jsonError))
}
}.resume()
}
func start() {
fetchPlayerStats { (res) in
switch res {
case .success(let playerInfo):
playerInfo.forEach({ (info) in
print(info.title)
DispatchQueue.main.async {
self.titleLabel.text = info.title
}
})
case .failure(let err):
print("failed", err)
}
}
}
and my tableviewcell class is
class BeatMapCell: UITableViewCell {
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.setCellShadow()
return view
}()
let pictureImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
iv.backgroundColor = .red
return iv
}()
let titleLabel: UILabel = {
let label = UILabel()
label.text = "Name"
label.textColor = UIColor.darkGray
label.font = UIFont.boldSystemFont(ofSize: 16)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUp()
}
Thank you!
You should call this method inside the vc and make an array as the dataSource of the table , then refresh it in callback
var tableArr = [BeatMaps]() // start class/struct names with capital letter
func start() {
fetchPlayerStats { (res) in
switch res {
case .success(let playerInfo):
self.tableArr = playerInfo
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
case .failure(let err):
print("failed", err)
}
}
}
currently you load the array multiple times for every cell and because of the forEach it sets the last item title to the tableView's label
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! BeatMapCell
cell.label.text = tableArr[indexPath.row].title
return cell
}
I gotta parse and discover an elegant way to work with my parsed data and also use it to populate correctly UIElements, such as, UITableViews, UICollectionViews and etc. I'll post below how I'm parsing and also the JSON file.
import Foundation
struct Contents : Decodable {
let data : [Content]
}
struct Content : Decodable {
let id : Int
let descricao : String
let urlImagem : String
}
API Response file:
import Foundation
var audioBook = [Content]()
func getAudiobooksAPI() {
let url = URL(string: "https://alodjinha.herokuapp.com/categoria")
let session = URLSession.shared
guard let unwrappedURL = url else { print("Error unwrapping URL"); return }
let dataTask = session.dataTask(with: unwrappedURL) { (data, response, error) in
guard let unwrappedDAta = data else { print("Error unwrapping data"); return }
do {
let posts2 = try JSONDecoder().decode(Contents.self, from: unwrappedDAta)
audioBook = posts2.data
} catch {
print("Could not get API data. \(error), \(error.localizedDescription)")
}
}
dataTask.resume()
}
TableView file:
How can I populate using the data I parsed? I really have difficult on doing that. I need an explanation to guide me on how to do it using an elegant way and also as a Senior Developer.
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
getAudiobooksAPI()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return ?????
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell
???????
return cell
}
}
JSON:
JSON Model
Just update your API Service with completion Handler
func getAudiobooksAPI(_ completion:#escaping ([Content])->Void) {
let url = URL(string: "https://alodjinha.herokuapp.com/categoria")
let session = URLSession.shared
guard let unwrappedDAta = data else { completion([Content]()); return}
let dataTask = session.dataTask(with: unwrappedURL) { (data, response, error) in
guard let unwrappedDAta = data else { print("Error unwrapping data"); return }
do {
let posts2 = try JSONDecoder().decode(Contents.self, from: unwrappedDAta)
completion(posts2.data)
} catch {
print("Could not get API data. \(error), \(error.localizedDescription)")
completion([Content]())
}
}
dataTask.resume()
}
And Use Like that
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet var collectionView: UICollectionView!
var dataSource: [Content] = [Content]()
override func viewDidLoad() {
super.viewDidLoad()
getAudiobooksAPI { (data) in
DispatchQueue.main.async {
self.dataSource = data
self.collectionView.reloadData()
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell
let content = self.dataSource[indexPath.item]
return cell
}
}
I am able to download a json file from a server and put it into a TableView each time the App is opened. (see code below)
However, I also want to allow offline access using the last file downloaded. So essentially I am having trouble working out how to save the downloaded file for local access offline. Any help (especially examples) much appreciated.
class KalkanTableController: UITableViewController {
var TableData:Array< String > = Array < String >()
override func viewDidLoad() {
super.viewDidLoad()
get_data_from_url("http://ftp.MY FILE LOCATION/kalkanInfoTest.json")
if let path = Bundle.main.path(forResource: "kalkanInfoTest", ofType: "json") {
do {
let jsonData = try NSData(contentsOfFile: path, options: NSData.ReadingOptions.mappedIfSafe)
do {
let jsonResult: NSDictionary = try JSONSerialization.jsonObject(with: jsonData as Data, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
} catch {}
} catch {}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = TableData[indexPath.row]
return cell
}
func get_data_from_url(_ link:String)
{
let url:URL = URL(string: link)!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url)
request.httpMethod = "GET"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(
data, response, error) in
guard let _:Data = data, let _:URLResponse = response , error == nil else {
return
}
self.extract_json(data!)
})
task.resume()
}
func extract_json(_ data: Data)
{
let json: Any?
do
{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch
{
return
}
guard let data_list = json as? NSArray else
{
return
}
if let item_list = json as? NSArray
{
for i in 0 ..< data_list.count
{
if let item_obj = item_list[i] as? NSDictionary
{
if let item_name = item_obj["kalkanInfo"] as? Stri
{
TableData.append(item_name)
print(item_name)
}
}
}
}
DispatchQueue.main.async(execute: {self.do_table_refresh()})
}
func do_table_refresh()
{
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
return
})
}
}
You can try to save your json locally using these methods to save on disk
override func viewDidLoad() {
super.viewDidLoad()
if readKalkanDataFromDisk() {
extract_json(readKalkanDataFromDisk())
} else {
get_data_from_url("http://ftp.MY FILE LOCATION/kalkanInfoTest.json")
}
}
func get_data_from_url(_ link:String)
{
let url:URL = URL(string: link)!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url)
request.httpMethod = "GET"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(
data, response, error) in
guard let _:Data = data, let _:URLResponse = response , error == nil else {
return
}
saveKalkanDataOnDisk(kalkanData: data!)
self.extract_json(data!)
})
task.resume()
}
func saveKalkanDataOnDisk(kalkanData: Data) {
do {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsURL.appendingPathComponent("Kalkan.json")
try kalkanData.write(to: fileURL, options: .atomic)
} catch { }
}
func readKalkanDataFromDisk() -> Data? {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let filePath = documentsURL.appendingPathComponent("Kalkan.json").path
if FileManager.default.fileExists(atPath: filePath), let data = FileManager.default.contents(atPath: filePath) {
return data
}
return nil
}