How to pass a JSON decoded Array to another VC on swift - json

well Im trying to pass an array of articles which came from a news API to another VC so I will be able to display them on my TableView, now the problem is that the tableview is loaded before I updated my array, is there a way to update the array before the tableview is loaded?
MY CODE:
MY NewsViewController Code:
import UIKit
import Foundation
class NewsViewController: UITableViewController,NewsProtocol {
func didUpdateNewsArr(arr: [Articles]) {
newsArr = arr
print("I'm loaded first!!")
// print(newsArr[0].title)
}
var newsArr = [Articles]()
var newsManager = NewsManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = "Hot News"
newsManager.newsProtocolDelegate = self
newsManager.performRequest()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
// MARK: - UITableView Delegate&DataSource Methods:
extension NewsViewController {
// Return the number of rows for the table.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return newsArr.count
}
// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch a cell of the appropriate type.
let cell = tableView.dequeueReusableCell(withIdentifier: "newsCell", for: indexPath)
// Configure the cell’s contents.
cell.textLabel!.text = "Cell text"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
my networking file:
import Foundation
import UIKit
protocol NewsProtocol {
func didUpdateNewsArr(arr: [Articles])
}
struct NewsManager {
let newsURL = "https://newsapi.org/v2/everything?q=apple&from=2020-10-19&to=2020-10-19&sortBy=popularity&apiKey=009a08a56d664f1b92986e9cce27767b"
var newsProtocolDelegate: NewsProtocol?
func performRequest() {
if let url = URL(string: newsURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
if error != nil {
print(error)
return
}
if let safeData = data {
self.parseJSON(newsData: safeData)
}
}
task.resume()
}
}
func parseJSON(newsData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(NewsData.self, from: newsData)
let articlesArr = decodedData.articles
newsProtocolDelegate?.didUpdateNewsArr(arr: articlesArr)
} catch {
print("")
}
}
}

I've solved the problem :) thanks fellas, all was needed was tableView.reload() to reload the cells, due to the fact that the JSON request took a while and the tableView was made up before the request, I've used DispatchQue :)

Related

Get data from API call and show another viewController on UItableView row click

I'm new to Swift, so I ask for help. I show list of characters on tableview using API call.When user click one row, I want to call API ,get data and show it in new viewController
API call url looks like :
https://rickandmortyapi.com/api/character/{user_clicked_row_charactor_id}
Sample response for id = 3
{
"id": 3,
"name": "Summer Smith",
"status": "Alive",
"species": "Human",
"type": "",
"gender": "Female",
}
And so it is necessary for each of the characters. Tell or direct how to implement this?
Sorry for my bad English.
My code:
class UsersTableViewController: UITableViewController {
var characters = [Results]()
override func viewDidLoad() {
super.viewDidLoad()
LoadCharacters()
}
func LoadCharacters() {
let urlString = "https://rickandmortyapi.com/api/character/"
if let url = URL(string: urlString)
{
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, responce, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJson(usersData: safeData)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
task.resume()
self.tableView.reloadData()
}
}
func parseJson(usersData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(JSONData.self, from: usersData)
characters = decodedData.results
print(decodedData.results[0].name)
} catch {
print(error)
}
}
struct JSONData: Decodable {
let results: [Results]
}
struct Results: Decodable {
let name: String
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return characters.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "user", for: indexPath)
let guys = characters[indexPath.row]
cell.textLabel?.text = guys.name
return cell
}
}
If I understand your question properly, you’d need to implement the didSelectRowAt of the table view method then pass the necessary data to the new view controller:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let character = characters[indexPath.row]
let charactersViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "characterVC") as! CharacterDetailViewController
charactersViewController.character = character
present(charactersViewController.character, animated: true)
}
For this to work, you will need to have another view controller to show the character once tapped on. Give that view controller the Storyboard ID of "characterVC" in the Identity inspector. The view controller will also need a variable to receive the character object, perhaps:
class CharacterDetailViewController: UIViewController {
var character: Character?
override func viewDidLoad() {
super.viewDidLoad()
if let character = character {
// show the character on the view
}
}
}

Display callback array in UITableView, currently showing as empty

I have a function that connects to an endpoint and returns a JSON result which I save into an array and using a callback function. Everything seems to be configured correctly. But no data is showing in the tables when the simulator runs. I'm not sure why? I was under the impression I would just need to run tableView.reloadData() but this doesn't do anything
class ViewConversionsTableViewController: UITableViewController {
/* codesToConvert: The Country code and Country sent from the previous view used to connect to the endpoint
* arraysToDisplay: The total list of countries and their corresponding values to be displayed
*/
var codesToConvert = [String]()
var arraysToDisplay = [String]()
override func viewDidLoad() {
super.viewDidLoad()
calculateRate(value: codesToConvert) {(structuredArray) in
self.arraysToDisplay.append(contentsOf: structuredArray)
}
tableView.reloadData()
}
func calculateRate(value: [String], completionHandler: #escaping (_ structuredArray: [String])->()){
var structuredArray = [String]()
let url = URL(string: "domain.com")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString = "pairs=\(value[0] + value[2])&pairs=\(value[2]+value[0])"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
do {
var arrayToSend = [String]()
let jsonResult = try JSONSerialization.jsonObject(with: data!)
let results = jsonResult as! [String: Any]
for values in results{
arrayToSend.append(values.key)
arrayToSend.append("\(values.value)")
}
arrayToSend.append(value[1])
arrayToSend.append(value[3])
// Structure the array to the required format
structuredArray.append(arrayToSend[1] + " " + String(arrayToSend[0].prefix(3)))
structuredArray.append(arrayToSend[4])
structuredArray.append(arrayToSend[3])
structuredArray.append(arrayToSend[5] + " · " + String(arrayToSend[2].prefix(3)))
completionHandler(structuredArray)
} catch {
print(error)
}
}
task.resume()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arraysToDisplay.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ConversionsCell
cell.Amount1.text = arraysToDisplay[0]
cell.currencyName1.text = arraysToDisplay[1]
cell.Amount2.text = arraysToDisplay[2]
cell.currencyName2.text = arraysToDisplay[3]
return cell
}
}
You should've reloaded the tableview inside the completion block.
override func viewDidLoad() {
super.viewDidLoad()
calculateRate(value: codesToConvert) {(structuredArray) in
self.arraysToDisplay.append(contentsOf: structuredArray)
DispatchQueue.main.async {
tableView.reloadData()
}
}
}
This is occurred because your tableView load the data when the data network call process haven't completed yet.
put your tableView.reloadData inside the closure and it must be on main thread. So it should be like this:
override func viewDidLoad() {
super.viewDidLoad()
calculateRate(value: codesToConvert) {(structuredArray) in
self.arraysToDisplay.append(contentsOf: structuredArray)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

Request with Alamofire

I'm starting on swift and I'm trying to bring a list of cars with alamofire but it is not bringing
The code executes without throwing errors but not the list
I see a blank table view
https://uploaddeimagens.com.br/imagens/simulator_screen_shot_-_iphone_8_-_2019-10-20_at_16-48-23-png
(sorry... editing with the write code)
My classes
struct HotelData: Codable {
let results: [Results]
}
struct Results: Codable {
let smallDescription: String
let price: Price
let gallery: [ImageHotel]
let name: String
let address: Address
var getRandonImage: ImageHotel {
let index = Int(arc4random_uniform(UInt32(gallery.count)))
return gallery[index]
}
}
==============
My manager
class func getHotels(onComplete: #escaping (HotelData?) -> Void) {
AF.request(path).responseJSON { (response) in
guard let jsonData = response.data else { return }
guard let hotelData = try? JSONDecoder().decode(HotelData.self, from: jsonData)
else {
onComplete(nil)
return
}
onComplete(hotelData)
return
}
}
}
==============
Cell
func prepareCell(with hotel: Results){
lbName.text = hotel.name
lbSmallDescription.text = hotel.smallDescription
lbPrice.text = "R$ \(hotel.price.amount)"
lbAdress.text = hotel.address.city
}
==============
TableView
class HotelTableViewController: UITableViewController {
var hotels: [Results] = []
let hotelManager = HotelManager()
override func viewDidLoad() {
super.viewDidLoad()
loadHotels()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return hotels.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HotelTableViewCell
let hotel = hotels[indexPath.row]
cell.prepareCell(with: hotel)
return cell
}
func loadHotels() {
HotelManager.getHotels { (hotelData) in
if let hotelData = hotelData {
self.hotels += hotelData.results
}
}
}
}
Your direct issue is that you need to call reloadData() if you want your UITableView to look at your downloaded data and load the appropriate cells. You can put in the completion handler of your network call, or in a didSet on your hotels property.
Additionally, you shouldn't use responseJSON for Decodable types, it's redundant. Instead you should use responseDecodable and pass the type you want to parse: responseDecodable(of: HotelData.self) { response in ... }.

Loading tableView after JSON only

I realize the tableView.reloadData() get's called in the JSON completion block to reload the tableView with the data received; I was wondering if there was a way to load the tableView only after this completion block has finished. What is happening is that the tableView first loads empty with default cells and a few seconds later the reloadData() method gets called inside the completion block and the tableView reloads and the data appears with the custom cells. I want to load the tableView ONLY when and after the data is received. What approach can I take? Is there a way to load the view only after this is completed? I basically don't want to have the user look at a blank table for a few seconds and wait for the data to appear. Here is my viewController code and simple structs to hold the model for the data.
viewController:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
var users = [User]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let recieved = try JSONDecoder().decode([User].self, from: data)
self.users = recieved
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let error as NSError {
print("Error: \(error.description)")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.name.text = users[indexPath.row].name
cell.eMail.text = users[indexPath.row].email
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
Structs:
struct User: Decodable {
let id: Int
let name: String
let username: String
let email: String
let company: Company
}
struct Company: Decodable {
let name: String
let catchPhrase: String
let bs: String
}
Since you're essentially waiting for a network call before the data can be displayed, why not display a spinner or activity indicator on a view on top of the tableview then dismiss this when the data has been parsed successfully (or handle any errors). The alternative could be to request the data before the view is loaded in another class.
I think you can add activity indicator in your UITableView. So User will not see only blank UITableView. Or you can add background image in your UITableView, You can show it if the data is still empty and hide it after JSON decoded.
for reference background image in UITableView, you can see here
As per your suggestions; here is the route I took using an activityIndicator. I set a UIView onto of the tableView, then added an activityIndicator on top of that UIView and I also added a simple UILabel next to the activityIndicator with the string "Loading". I used propertyAnimator inside the JSON task after data had been received and after reloading the tableView, then stopping activityIndicator, fading out the UIView to show the tableView and then removing the UIView from the superView. Here is the code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
var users = [User]()
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var loadingView: UIView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let recievedUsers = try JSONDecoder().decode([User].self, from: data)
self.users = recievedUsers
DispatchQueue.main.async {
self.tableView.reloadData()
if self.loadingView.alpha == 1.0 {
self.activityIndicator.stopAnimating()
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0,
delay: 0.0,
options: [],
animations: {
self.loadingView.alpha = 0.0
},
completion: { (position) in
if position == .end {
self.loadingView.removeFromSuperview()
}
})
}
}
} catch let error as NSError {
print("Error: \(error.description)")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.name.text = users[indexPath.row].name
cell.eMail.text = users[indexPath.row].email
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}

Displaying JSON imageURL into UITableView using Alamofire and SwiftyJSON

I am currently trying to display the JSON data on to my UITableView. I've created a separate class which handles and declares the data being extracted from the API. I also created a custom cell to display the data. However I am not sure how to display the images of the API. I was able to display the title of the API using just a standard TableViewController without custom cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
cell.textLabel?.text = recipes = [indexPath.row].title
return cell
}
but when trying to create a custom cell the image uses an imageURL which works differently. Other tutorials and demo don't seem to be using Alamofire and SwiftyJSON.
I did read the documentation: https://github.com/SwiftyJSON/SwiftyJSON#integration but still not sure how to solve this issue. Please tell me how you'd get the API data to display on my table.
Class
import Foundation
import SwiftyJSON
class Recipe {
var title: String!
var image: URL?
init(json: JSON) {
self.title = json["title"].stringValue
self.image = json["image"].url
}
}
CustomCell
import UIKit
class RecipeCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var imgView: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
ViewController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
Alamofire.request(searchURL, method: .get, parameters: params, encoding: URLEncoding.default, headers: headers).response { [unowned self] response in
guard let data = response.data else { return }
let json = JSON(data: data)
for recipe in json.arrayValue {
let newRecipe = Recipe(json: recipe)
self.recipes.append(newRecipe)
}
for recipe in self.recipes {
print(recipe.title)
print(recipe.image)
}
}
}
// Display JSON Data into the tableView
func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
// What to write here?
return cell
}
Thank you in advance! :)
for this you have to make the extension of Imageview as
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() { () -> Void in
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
then you have to write cellForRowAt method as
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
let recipe = recipes[indexPath.row]
let imagePath = recipe.image
cell.titleLabel.text = recipe.title
cell. imgView.downloadedFrom(link: imagePath)
return cell
}