How to pass API data to table view cells - ios

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()
}
}
}

Related

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 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()
}

Why does my TableView only show the last image loaded in every cell? (swift)

The problem I have have at the moment is properly displaying images in a tableView cell. My images are saved to Firebase, and I can easily retrieve both images.
When I try to display each image in its own table view cell they quickly load both images and the end result is the last image displaying in both cells instead of two different images.
I believe the issue is either with my cellForRowAt IndexPath or how I am calling the data from Firebase.
This is my Main TableView View Controller
import UIKit
import Firebase
import FirebaseStorage
import FirebaseDatabase
class CardDesignViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
//passing a value to another page with thie var
var IdvalueTitle = ""
var db:Firestore!
//PropertiesCell2 is pointing to a swift file containing my dictionary for the outlets
var propertiesArray3 = [PropertiesCell2]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
extension CardDesignViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return propertiesArray3.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let spot = propertiesArray3[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "TableView2") as! TableView2
cell.app? = "\(spot.templateOption)"
cell.IDvalueHidden?.text = "\(spot.IDvalueHidden)"
cell.templateOption.layer.cornerRadius = 12
cell.templateOption.layer.masksToBounds = true
return cell
}
I have put my outlets for the cells into a file called "TableView2". This is where I call the data for the images from Firebase in a func called getTemplates() and use that within "var app: String!"
import Foundation
import UIKit
import Firebase
import FirebaseDatabase
import FirebaseStorage
class TableView2: UITableViewCell {
#IBOutlet weak var templateOption: UIImageView!
#IBOutlet weak var IDvalueHidden: UILabel!
func styleTheCells2(cells: Cell2) {
templateOption.image = cells.templateOption
IDvalueHidden.text = cells.IDvalueHidden
}
var app: String! {
didSet {
self.getTemplates()
}
}
func getTemplates() {
let db = Firestore.firestore()
db.collection("Card Templates").getDocuments { (snapshot, err) in
if err != nil {
return
} else {
for document in (snapshot?.documents)! {
if let picURL = document.data()["Template"] as? String {
let url = URL(string: picURL)
print(picURL)
DispatchQueue.global().async {
do{
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.templateOption.image = UIImage(data: data)
}
} catch {
}
}
}
}
}
}
}
I've attached a picture as well of the end result when I run this code. I get the same image in both cells however, when I look at the debug area I can see that both images were accessed twice.
This is my simulator when I run this code. Im looking to have two different images in the cells rather than the one picture in both:
My debugger shows both image urls being pulled twice consecutively and the last image pulled (the green image) shows in both cells:
You are fetching the images from Firebase Storage each time UITableViewCell is being presented via getTemplates() function
Since you have 2 images in Firebase, I am assuming 'propertiesArray3' has 2 elements in it.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return propertiesArray3.count
}
Each time it goes through the Firebase and prints out all the URLs in the db. As the numberOfRowsInSection is 2, the Image URL are being printed twice.
The for loop ends at the last element each time and sets the last URL as the image.
func getTemplates() {
let db = Firestore.firestore()
db.collection("Card Templates").getDocuments { (snapshot, err) in
if err != nil {
return
} else {
for document in (snapshot?.documents)! {
if let picURL = document.data()["Template"] as? String {
let url = URL(string: picURL)
print(picURL)
DispatchQueue.global().async {
do{
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.templateOption.image = UIImage(data: data)
}
} catch {
}
}
}
}
}
}
Hope it helps
For a basic approach to start with, you can try something like this -
Declare an array to store the URL
var urlArray: [URL] = []
Fetch the URLs in viewDidLoad()
let db = Firestore.firestore()
db.collection("Card Templates").getDocuments { (snapshot, err) in
if err != nil {
return
} else {
for document in (snapshot?.documents)! {
if let picURL = document.data()["Template"] as? String {
let url = URL(string: picURL)
// ADD ALL THE URLs TO THE NEW ARRAY
urlArray.append(url)
}
}
tableView.reloadData()
}
}
Remove getTemplates() from UITableViewCell
Edit the tableView Delegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return urlArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let spot = propertiesArray3[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "TableView2") as! TableView2
let url = urlArray[indexPath.row]
do{
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
cell.templateOption.image = UIImage(data: data)
}
} catch {
}
cell.app? = "\(spot.templateOption)"
cell.IDvalueHidden?.text = "\(spot.IDvalueHidden)"
cell.templateOption.layer.cornerRadius = 12
cell.templateOption.layer.masksToBounds = true
return cell
}
You most clear content based Views of cell in prepareForReuse function
override func prepareForReuse() {
super.prepareForReuse()
// remove image from imageView
templateOption.image = nil
}

Use data on JSON with cells

I've got this JSON from an API:
{
"oldest": "2019-01-24T00:00:00+00:00",
"activities": [
{
"message": "<strong>Henrik</strong> didn't resist a guilty pleasure at <strong>Starbucks</strong>.",
"amount": 2.5,
"userId": 2,
"timestamp": "2019-05-23T00:00:00+00:00"
},
{
"message": "<strong>You</strong> made a manual transfer.",
"amount": 10,
"userId": 1,
"timestamp": "2019-01-24T00:00:00+00:00"
}
]
}
It has a lote more activities. How can I access it and fill my cells with its data? So far I've got this code:
MainViewController:
struct Activities: Decodable {
var oldest: String
var activities: [Activity]
}
struct Activity: Decodable {
var message: String
var amount: Float
var userId: Int
var timestamp: String
}
class MainTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var activityList: [Activities] = []
var activity: [Activity] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.prefetchDataSource = self
let activitiesJSONURLString = "https://qapital-ios-testtask.herokuapp.com/activities?from=2016-05-23T00:00:00+00:00&to=2019-05-23T00:00:00+00:00"
guard let activitiesURL = URL(string: activitiesJSONURLString) else { return }
URLSession.shared.dataTask(with: activitiesURL) { (data, response, err) in
// perhaps check err
// also perhaps check response status 200 OK
guard let data = data else { return }
do {
// Activities
let activities = try JSONDecoder().decode(Activities.self, from: data)
} catch let jsonErr {
print("Error serializing json: ", jsonErr)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}.resume()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activityList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
return cell
}
// Prefetching
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// if indexPaths.contains(where: isLoadingCell) {
// viewModel.fetchModerators()
// }
}
}
But I think something is off. Or I have no clue on how to start. I could really use some help or any tips you can give me. Please and thank you!
First of all the naming of the structs is pretty confusing. Name the root object with something unrelated like Response or Root.
And we are going to decode the timestamps as Date
struct Root: Decodable {
var oldest: Date
var activities: [Activity]
}
struct Activity: Decodable {
var message: String
var amount: Float
var userId: Int
var timestamp: Date
}
Second of all as the data is received in all the conformance to UITableViewDataSourcePrefetching is pointless. Remove it and delete also the prefetchRowsAt method.
Declare only one data source array and name it activities
var activities = [Activity]()
and delete
var activityList: Activities!
In the completion handler of the data task decode Root and assign the activities array to the data source array
do {
// Activities
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let result = try decoder.decode(Root.self, from: data)
self.activities = result.activities
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("Error serializing json: ", error)
}
The table view data source methods are
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
let activity = activities[indexPath.row]
// assign the activity data to the UI for example
// cell.someLabel = activity.amount
return cell
}
Because you are using the activityList to determine the number of rows, I'm assuming that you want to use the data from activityList in order to populate your ActivityCells. That is, unless, you meant for activityList to be a single instance of Activities instead of an array of Activities, in which case you would likely use activityList.activities.count in order to determine the number of rows. In either case, lets just call the array of data you want to use to fill the cells activityList.
In this case, you should make sure to update activityList to the activities that you have fetched from the API. Once you have the activityList, you can then use reloadData which will trigger your table view delegate methods. In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) you can then use activityList in order to update the dequeued cell.
Something like this might be what you want:
class MainTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var activityList: Activities!
var activity: [Activity] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.prefetchDataSource = self
let activitiesJSONURLString = "https://qapital-ios-testtask.herokuapp.com/activities?from=2016-05-23T00:00:00+00:00&to=2019-05-23T00:00:00+00:00"
guard let activitiesURL = URL(string: activitiesJSONURLString) else { return }
URLSession.shared.dataTask(with: activitiesURL) { (data, response, err) in
// perhaps check err
// also perhaps check response status 200 OK
guard let data = data else { return }
do {
// Activities
let activities = try JSONDecoder().decode(Activities.self, from: data)
self.activityList = activities
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Error serializing json: ", jsonErr)
}
}.resume()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activityList.activities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
if let activity = activityList?[indexPath.row] {
// UPDATE CELL ACCORDING TO activity
}
return cell
}
}

swift: Retrieving images from "Parse"

Im new in Parse(parse.com). I have such kind of table in parse.com:
And I wanna retrieve these 3 images and put are in table view row. And here is my code:
class LeaguesTableViewController: UITableViewController {
var leagues = [PFObject]() {
didSet {
tableView.reloadData()
}
}
var leaguesImage = [NSData]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadData()
tableView.registerClass(LeaguesTableViewCell.self, forCellReuseIdentifier: "ReusableCell")
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return leagues.count
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 160
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCell", forIndexPath: indexPath) as! LeaguesTableViewCell
cell.leagueImage.image = UIImage(data: leaguesImage[indexPath.row])
cell.leagueNameLabel.text = leagues[indexPath.row]["name"] as? String
return cell
}
// MARK: Parse
func loadData() {
let query = PFQuery(className: "Leagues")
query.findObjectsInBackgroundWithBlock { (objects, error) in
if( objects != nil && error == nil) {
// List of leagues
for i in objects! {
self.leagues.append(i)
// Retrieve images
let imageFile = i["image"] as? PFFile
imageFile!.getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
self.leaguesImage.append(imageData)
}
}
}
}
} else if error != nil {
print("Error is: \(error)")
}
}
}
}
Here is my code and from my point of view is everything is ok. But I have error: Index out of the range. My leaguesImages array is empty. Thank you.
Your problem is that leagues and leaguesImages are getting out of sync. Once you retrieve the array from Parse, you are adding the leagues immediately, but leaguesImages are only being added after getDataInBackgroundWithBlock completes.
Instead of downloading the image data right away and storing it in a separate array, I would add a leagues property to your custom cell, and in there I would download the data and apply the image.
Populating an array like you are populating the leaguesImages array is a bad idea when the order matters, because you don't know which one will finish downloading first, maybe the second league image is the smallest, and it will be set as the image for the first league. (PS: image size is not the only thing that dictates how long a download will take)

Resources