can't set data to the variable when i parse json - ios

I am currently trying to set data I get from parsing my json to a variable in my ViewController from RawDataCategory file, which decodes json.
Here is how I call a static method in CategoryViewController
class CategoryViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var categoryProducts: [Datum]? // this is the variable i want with data i get
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
Datum.fetchProducts { (categoryProductsFromJSON) -> () in // here i call a static function.. i will paste the code below
self.categoryProducts = categoryProductsFromJSON //here i set value
print(self.categoryProducts?[0].name) // if i try to print the value in the scope i can access it. and it shows with no problem
}
print(categoryProducts?[0].name) //here is the problem. when i get out of the scope it return nil.
}
}
I can access the variable in the scope but somehow when i try to call it outside it prints nil.
Here is the static function (method) i call to get decoded json:
static func fetchProducts(_ completionHandler: #escaping ([Datum]) -> ()) {
guard let url = URL(string: "http://localhost:8888/dayhandan/public/api/v1/category/1") else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
if error != nil {
print("error")
return
}
do {
print("no error so far")
guard let data = data else { return }
let rawData = try JSONDecoder().decode(CategoryRawData.self, from: data)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(rawData.data ?? [])
})
} catch let err {
print(err)
}
})
task.resume()
}
I don't get any errors while i decode json. In fact I can pass it to my ViewContoller. There are no errors overall. categoryProducts just becomes nil when i call it outside the scope.
Is this a good way to use decoded data. if it is can someone help me to solve the issue. or can someone point at a good way to use decoded data. Thanks for your time

You should only try to use the result of fetchProducts in the completion handler because the completion handler will be executed at some point in the future. Anything outside of the completion handler will be executed immediately.
Reload your tableView from within the completion handler:
Datum.fetchProducts { (categoryProductsFromJSON) -> () in
self.categoryProducts = categoryProductsFromJSON
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Then you should have some logic to populate the tableView:
extension CategoryViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categoryProducts?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// something like this
guard let category = categoryProducts?[indexPath.row] else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell.titleLabel?.text = category.title
return cell
}
}

Related

How to send Json Data to Table View Array? Swift

I've been researching and wrecking my brain attempting to get my JSON data to load into my tableview. I've tried placing the data in a Variable & I'm able to see the data in the console when I print it, however unable to push it to my table view.
Am I doing something wrong on the data page or am I not properly accessing the data within the loop?
I've tried putting the loop in the viewdidload but haven't been successful either.
// ViewController
import Foundation
import UIKit
import SDWebImage
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var A = EntertainmentApi()
var data = [EntertainmentPageData]()
var AA = EntertainmentApi().userFeedPosts
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
func showTable() {
}
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
DispatchQueue.main.async {
self.entPostTableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
customCell1.profileDisplayName.text = AA[indexPath.row].postDisplayName
self.AA.forEach({ (EntertainmentPageData) in
customCell1.configue(with: EntertainmentPageData.postDisplayName, PostImage: EntertainmentPageData.imageURLString, PostDescription: EntertainmentPageData.postDescription)
})
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func item(for index: Int) -> EntertainmentPageData {
return data[index]
}
func numberOfItems() -> Int {
return data.count
}
}
//Data
import SwiftUI
import SDWebImage
public protocol EntertainmentPagePostItem {
/// The image for the card.
var imageURLString: String { get }
/// Rating from 0 to 5. If set to nil, rating view will not be displayed for the card.
var postDescription: String? { get }
/// Will be displayed in the title view below the card.
var postDisplayName: String { get }
}
public protocol EntertainmentPagePostDataSource: class {
/// CardSliderItem for the card at given index, counting from the top.
func item(for index: Int) -> EntertainmentPagePostItem
/// Total number of cards.
func numberOfItems() -> Int
}
struct HomePagePost: Codable {
var displayName: String
var cityStatus: String
var displayDescription: String
var displayImageURL: String
var lookingFor: String
var profileImager1: String?
var profileImager2: String?
var profileImager3: String?
var profileImager4: String?
}
struct EntertainmentPageData: Codable {
let postDisplayName: String
let imageURLString: String
let postDescription: String?
}
public class entPostFly: Codable {
let postDisplayName, imageURLString, postDescription: String
}
struct eItem: EntertainmentPagePostItem {
var postDisplayName: String
var imageURLString: String
var postDescription: String?
}
public class EntertainmentApi {
var userFeedPosts = [EntertainmentPageData]()
init() {
load()
}
func load() {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
self.userFeedPosts = entPostData
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getFeedPosts(completion: #escaping ([EntertainmentPageData]) -> () ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(entPostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
}
class Api {
func getHomePagePosts(completion: #escaping ([HomePagePost]) -> Void ) {
guard let apiURL = URL(string: "https://api.quickques.com/.....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let homePostData = try JSONDecoder().decode([HomePagePost].self, from: Data)
completion(homePostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getImageData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
func getTopMostViewController() -> UIViewController? {
var topMostViewController = UIApplication.shared.keyWindow?.rootViewController
while let presentedViewController = topMostViewController?.presentedViewController {
topMostViewController = presentedViewController
}
return topMostViewController
}
First you have an empty function showTable inside your viewDidLoad - This does nothing. Presumably it is something hanging around from your various attempts. Delete that.
As you have probably worked out, your network fetch operation is going to occur asynchronously and you need to reload the table view once the data has been fetched.
You have some code in viewDidLoad that kind of tries to do this, but it isn't related to the fetch operation. It is just dispatched asynchronously on the next run loop cycle; This is probably still before the data has been fetched.
However, even if the data has been fetched, it won't show up because you are assigning userFeedPosts from a second instance of your API object to AA at initialisation time. This array is empty and will remain empty since Swift arrays are value types, not reference types. When userFeedPosts is updated, AA will hold the original empty array.
To load the data you need to
Start a load operation when the view loads
Pass a completion handler to that load operation to be invoked when the load is complete
Reload your table view with the new data
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data = [EntertainmentPageData]()
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
EntertainmentAPI.getFeedPosts { result in
DispatchQueue.main.async { // Ensure UI updates on main queue
switch result {
case .error(let error):
print("There was an error: \(error)")
case .success(let data):
self.data = data
self.entPostTableView.reloadData
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
let post = data[indexPath.row)
customCell1.profileDisplayName.text = data[indexPath.row].postDisplayName
customCell1.configure(with: post.postDisplayName, PostImage: post.imageURLString, PostDescription: post.postDescription)
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
public class EntertainmentAPI {
static func getFeedPosts(completion: #escaping ((Result<[EntertainmentPageData],Error>) -> Void) ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task = URLSession.shared.dataTask(with: apiURL) { data, apiResponse, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
/// TODO - Invoke the completion handler with a .failure case
return
}
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(.success(entPostData))
}
catch {
completion(.failure(error))
}
}.resume()
}
}

How can I expand inner scope value in Swift

I started develop not long. So I have problem in my code. I want return data.count in TableView function. But I can't get value of data: [DataGroup] in db.collection(). It isn't have value out of db.collection() scope. So how can I get data from that scope?
class RecordGroupViewController: UIViewController, UITableViewDataSource {
let db = Firestore.firestore()
var data: [DataGroup] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchDataGroup()
}
func fetchDataGroup() {
db.collection("data").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
do {
let docuData = try JSONSerialization.data(withJSONObject: document.data(), options: [])
let decoder = JSONDecoder()
let groupData: DataGroup = try decoder.decode(DataGroup.self, from: data)
self.data.append(groupData)
} catch let error {
print("---> error: \(error.localizedDescription)")
}
}
}
}
print("\(self.recordGroups.count)")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
From what I understood of your question, you want to access your data variable's count.
You can do so from anywhere in your code using self.data.count
However, if you want your tableView to use this new value :
Link your tableview to your controller using an IBOutlet (or create a reference to it if you instantiate it programmatically)
After you have appended your data with the new data, call self.yourTableViewReference.reloadData(), this will make the tableView call your dataSource method numberOfRowsInSection with the newly appended data.

How to pass API data to table view cells

I'm having some trouble passing my API returned data to table view cells. I am appending the data to an array and then passing this array to the table view (as usual) to get the number of rows and data for the cells. When I print inside the function where I am appending, the titles are shown in the array. Outside they're not. Any idea? Relevant code below:
class ProductTableViewController: UITableViewController, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet var tabView: UITableView!
var filteredData = ["Title1"]
override func viewDidLoad() {
super.viewDidLoad()
getProducts { (products) in
for product in products {
self.filteredData.append(product.title)
}
}
}
func getProducts(completionHandler: #escaping([ProductDetail]) -> Void) {
let url = URL(string: "exampleAPIURL")!
let dataTask = URLSession.shared.dataTask(with: url) {data, _, _ in
guard let jsonData = data else { return }
do {
let decoder = JSONDecoder()
let productsResponse = try decoder.decode(Products.self, from: jsonData)
let productDetails = productsResponse.data
for name in productDetails {
self.filteredData.append(name.title)
}
completionHandler(productDetails)
}catch {
print(error.localizedDescription)
}
}
dataTask.resume()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if filteredData == nil {
return 1 }
else {
return filteredData.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as UITableViewCell
for name in filteredData {
if name != nil {
let product = filteredData[indexPath.row]
cell.textLabel?.text = product
} else {
cell.textLabel?.text = "name"
}
}
return cell
}
I am only receiving the hardcoded strings in the filteredData array when I run the simulator. Is there a different way to pass the JSON?
Many thanks!
Reload the table view after the data is collected:
getProducts { (products) in
for product in products {
self.filteredData.append(product.title)
}
self.tabView.reloadData()
}
After setting the array, you need to call self.tableView.reloadData() and invoke it on the main thread.
Also, its better to do the products API call from viewDidAppear as if the API call from viewDidLoad returns fast enough, operations on the view might fail. Also you might want to show some activity indicator.
override func viewDidAppear() {
super.viewDidLoad()
getProducts { (products) in
for product in products {
self.filteredData.append(product.title)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

How to implement UISearchBar to filter name or capital JSON using JSON Decoder in swift iOS application

How to implement UISearchBar to filter name or capital JSON using JSON Decoder in swift iOS application. I want to implement UISearchBar and search results or filter results using name from JSON Data.
import UIKit
Structure Created
struct jsonstruct:Decodable
{
let name:String
let capital:String
}
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate, UISearchBarDelegate, UISearchControllerDelegate, UISearchDisplayDelegate {
Creating Outlet for TableView and SearchBar
#IBOutlet var tableview: UITableView!
#IBOutlet var searchBar: UISearchBar!
Declaring JSON
var arrdata = [jsonstruct]()
Function for getting Data
func getdata()
{
let url = URL(string: "https://restcountries.eu/rest/v2/all")
URLSession.shared.dataTask(with: url!)
{
(data, response, error) in
do
{
if error == nil
{
self.arrdata = try
JSONDecoder().decode([jsonstruct].self, from: data!)
for mainarr in self.arrdata
{
print(mainarr.name,":",mainarr.capital as Any)
DispatchQueue.main.async
{
self.tableview.reloadData()
}
}
}
}
catch
{
print(error.localizedDescription)
}
}.resume()
}
TABLE VIEW
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.arrdata.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
cell.label1.text = "Name: \(arrdata[indexPath.row].name)"
cell.label2.text = "Capital: \(arrdata[indexPath.row].capital)"
return cell
}
OverRiding Function
override func viewDidLoad()
{
getdata()
}
You need to make two objects of data, one original data and other filtered data.
var filteredArrData = [jsonstruct]()
var arrdata = [jsonstruct]()
Than in your getData functions:
do {
self.arrdata = try JSONDecoder().decode([jsonstruct].self, from: data!)
self.filteredArrData = self.arrdata
}
Then in your table view delegate and data source:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.filteredArrData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
cell.label1.text = "Name: \(filteredArrData[indexPath.row].name)"
cell.label2.text = "Capital: \(filteredArrData[indexPath.row].capital)"
return cell
}
Than make filter function like this:
func applyFilters(textSearched: String) {
filteredArrData = arrdata.filter({ item -> Bool in
return item.name.lowercased().hasPrefix(textSearched.lowercased())
})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Then pass your string to this function and everything will work fine.
Make TextField with an IBAction of didbegin like below and create an array where you can have filtered data.
#IBAction func tfSearch(_ sender: UITextField) {
let filteredArray = yourArr.filter { $0.contains(sender.text) }
}
Assuming you are not caching all your data and the filtering is done live via an API. You will need to set an object or the viewcontroller as a delegate of the search bar(UISearchBarDelegate). Then use the searchText as the text for your API query.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
//call throttle that will call urlsession
}
Since one character is typed at a time we do not to call the API every time. You may need to use a throttler to make lesser API calls instead of sending character by character search. You might find this tutorial about throttling helpful: Simple Throttling in Swift .
Most REST APIs should have a filter feature and you could just easily append the typed name or capital.
https://restcountries.eu/rest/v2/name/append name here
https://restcountries.eu/rest/v2/capital/append capital here
This is an example networking code to fetch the results. Use the results to call another method safely on the main queue to reload the tableview.
if let url = URL(string: "https://restcountries.eu/rest/v2/country?q=name") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let results = try JSONDecoder().decode(YourCustomDecodeStruct.self, from: data)
//safely your data source and reload the tableview
} catch let error {
print(error)
}
}
}.resume()
}

Swift 3: Preload data before putting it into UITableView

I have an app that will fetch exactly 100 strings from an API and place them into a UITableView. I wish to first preload the data into an array and then, once the array is fully populated with the 100 entries, load the data into the table.
Due to the asynchronous API call, it seems like I am unable to load data into the array before the table view starts populating its cells. Mainly, I am having difficulty getting the data out of the closure in the first place.
This is the API call defined in an APIAgent class:
func getAPIData(_ requestType: String, completionHandler: #escaping (Data) -> ()) {
let requestURL: URL = URL(string : baseURL + requestType)!
let currentSession = URLSession.shared
let task = currentSession.dataTask(with: requestURL) { (data, response, error) in
completionHandler(data!)
}
task.resume()
}
This is how the UITableView uses it:
protocol AsyncHelper {
func getData(data: Any)
}
class TableViewController: UITableViewController, AsyncHelper {
var dataEntries: [String] = []
func getData(data: Data) {
let entry: String = String(describing: data)
dataEntries.append(entry)
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...100 {
apiAgent.getAPIData("entry" + String(i), entry: { entry in
self.getData(data: entry)
})
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EntryCell", for: indexPath) as! EntryCell
let entry: String = dataEntries[indexPath.row] // index out of range error
DispatchQueue.main.async {
// add Strings to cell here
}
return cell
}
}
So it appears that the cells are being generated before data gets populated into the dataEntries array. How do I prevent the UITableView from generating the cells until dataEntries is populated.
If you are going to use a closure you won't need a protocol. You could change your networking code to:
var songData = [Data]
func getAPIData(_ requestType: String, completionHandler: #escaping ([Data]) -> ()) {
let requestURL: URL = URL(string : baseURL + requestType)!
let currentSession = URLSession.shared
let task = currentSession.dataTask(with: requestURL) { (data, response, error) in
songData.append(data!)
if (songData.count == 100) {
completionHandler(songData)
}
}
task.resume()
}
This will make sure that your getData() and tableView.reloadData() will only be called once all 100 of your data elements have been loaded.
FYI - tableView.reloadData() will reload pretty much everything that has to deal with your table view. Your numberOfRows, numberOfSections, and cellForRow will all be called again. This will create the tableView over again using the updated dataEntries values
Try this :
override func viewDidLoad() {
super.viewDidLoad()
tblView.delegate = nil
tblView.dataSource = nil
for i in 1...100 {
apiAgent.getAPIData("entry" + String(i), entry: { entry in
self.getData(data: entry)
tblView.delegate = self
tblView.dataSource = self
tblView.reloadData()
})
}
}

Resources