How to put JSON response to a tableview - arrays

I have a model Users and in that model
static func getUsers() -> Single<[Users]> {
let path = API.Path.users.details
let params: [String: Any] = [
"utcOffset": TimeZone.current.utcOffset
]
return API.requestMany(path: path, method: .get, parameters: params)
}
i declare it like this
let sus = [Users]()
And in my cellForRowAt i have this
Users
.getUsers()
.do(onError: { [weak self] error in
print(error.localizedDescription)
})
.do(onSuccess: { [weak self] userNames in
print(userNames)
cell.name.text = userNames[indexPath.row].name
})
.subscribe()
.disposed(by: rx.disposeBag)
That code is not being called so my tableview is empty but when i tried putting it in ViewDidLoad its being called and triggering my print statement print(userNames)

Your code isn't being called because nothing is calling the function that you put the code in. It works in viewDidLoad because that function is being called.
Based on your description I would expect code that looks like this in your viewDidLoad()
Users.getUsers()
.asObservable()
.bind(to: tableView.rx.items) { tableView, index, user in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: IndexPath(index: index)) as! MyTableViewCell
cell.configure(for: user)
return cell
}
.disposed(by: disposeBag)
Based on your comments, I've come to understand that you only want to use Rx for your network calls. In that case, the solution should look like this:
final class ExampleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
private var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
_ = User.getUsers()
.subscribe(
onSuccess: { [weak self] in
self?.users = $0
self?.tableView.reloadData()
},
onError: { error in
print("handle network error here")
}
)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyTableViewCell
cell.configure(for: users[indexPath.row])
return cell
}
}
final class MyTableViewCell: UITableViewCell {
func configure(for user: User) {
print("configure your cell here")
}
}
struct User {
static func getUsers() -> Single<[User]> { fatalError("Implement me") }
}

Related

parsing nested arrays to populate uitableview in swift

This is the API response which I have parsed
{
"status": 0,
"message": "Friends found.",
"friends": [
{
"id": 52,
"meetings": [
{
"id": 47,
"meeting_with": "Bbb"
}
]
}
]
}
The model class
struct TotalMeetings: Decodable {
var status: Int
var message: String
var friends: [FriendDetail]?
}
struct FriendDetail: Decodable {
var id: Int
var meetings: [MeetingsDetail]
}
struct MeetingsDetail: Decodable {
var id: Int
var meeting_with: String
}
Im calling the API here and the call is successful.
var meetingssData :Friends!
let decoder = JSONDecoder()
do{
meetingsData = try decoder.decode(TotalMeetings.self, from: response.data!)
let meet = [self.meetingsData!.friends].compactMap({$0}).flatMap({$0})
print(meetingsData!)
}catch{
print(error)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! FriendsMeetingTVC
return cell
}
Please guide me how do I populate the tableview with the response coming from API call.
you have to declared array instance of type according to your need either FriendDetail or MeetingsDetail.
After fetching data from api parse it and store mapped data into an array instance. Then, reload table view.
numberOfRowsInSection method of tableview returns number of rows using your array count.
You can access value by using dot notation as shown in "cellForRowAt indexPath:" method.
class MeetingVC: UIViewController, UITableViewDelegate, UITableViewDatasource {
var friendsArr = [FriendDetail]()
func fetchFriendDetail() {
let decoder = JSONDecoder()
do {
let meetingsData = try decoder.decode(TotalMeetings.self, from: response.data!)
self.friendsArr = meetingsData.friends ?? []//[meetingsData.friends].compactMap({$0}).flatMap({$0})
print(self.friendsArr)
self.tableView.reloadData()
}catch{
print(error)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return friendsArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! FriendsMeetingTVC
cell.emailTxt.text = friendsArr[indexPath.row].email
return cell
}
}

How to implement userDefaults with a Global Variable array

I have an empty array set as a global variable that is populated with array items from a tableview. This is used to populate another tableview. This data needs to persist so that when the user returns to the app, their tableview data is in the same state they left it, i.e. populate with data from the array.
Though I've looked for dozens of tutorials and examples. I've also hacked at it myself to make it work and every time I reload the app, the array is empty. How can I get that global variable array to hold onto it's array data?
var sharedData = [String]()
This is my 1st VC where I have setup functions for the UserDefaults. And I've executed my saveArray() func every time a change is made to the array. I've then executed retrieveArray() func every time I need to load from the array.
import UIKit
var sharedData = [String]()
struct Keys {
static let arrayKey = "arrayKey"
}
let defaults = UserDefaults.standard
func saveArray() {
defaults.set(sharedData, forKey: Keys.arrayKey)
}
func retrieveArray() {
var savedData = defaults.object(forKey: Keys.arrayKey) as? [String] ?? []
savedData.append(contentsOf: sharedData)
}
class ViewController: UIViewController {
var effect:UIVisualEffect!
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet var tableView: UITableView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
#IBOutlet weak var visualEffectView: UIVisualEffectView!
let materialData = ["One", "Two", "Three", "Four"]
var searchMaterial = [String]()
var searching = false
#IBAction func favoritesButtonArrayUpdate(_ sender: UIBarButtonItem) {
print(sharedData)
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
saveArray()
retrieveArray()
print(sharedData)
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
print(self.materialData[indexPath.row], "selected!")
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let favorite = UITableViewRowAction(style: .default, title: "Favorite") { (action, indexPath) in
var data: String
if self.searching {
data = self.searchMaterial[indexPath.row]
} else {
data = self.materialData[indexPath.row]
}
sharedData.append(data)
saveArray()
print(sharedData)
}
favorite.backgroundColor = UIColor.orange
return [favorite]
}
}
This is my 2nd VC which displays the array data stored in the global variable array sharedData. I've again added all the func when making changes to the array and pulling data from the array.
import UIKit
class FavoritesViewController: UIViewController {
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
saveArray()
retrieveArray()
}
}
extension FavoritesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
retrieveArray()
return sharedData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
retrieveArray()
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.numberOfLines = 0
cell.textLabel?.text = sharedData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
sharedData.remove(at: indexPath.row)
saveArray()
tableView.beginUpdates()
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
}
The problem could be here:
let savedData: [String] = userDefaults.object(forKey: "arrayKey") as? [String] ?? []
Try changing it with:
let savedData: [String] = userDefaults?.object(forKey: "arrayKey") as? [String] ?? []
This is because UserDefaults must be unwrapped to refer to member object. Give it a try
Based on MrHim recommendations I removed the saveArray and retrieveArray func from the viewDidLoad of my first VC and left retrieveArray in viewDidLoad of my second VC. Having saveArray in my viewDidLoads was overwriting the array with empty data. I then needed to retrieve the array data in the proper place in my second VC. Then in my numberOfRowsInSection I removed retrieveArray.

populate tableview cell from complex api json using swift

I'm trying to parse API JSON and display it in UITableView, but the problem is I cannot access all array in this API.
struct RootResults: Codable {
var results: [results]
}
// MARK: - results
struct results: Codable {
var container_number: String?
var commodities: [commodities]
}
// MARK: - commodities
struct commodities: Codable {
var commodity_name_en: String?
var varieties: [varieties]
}
// MARK: - varieties
struct varieties: Codable {
var variety_name_en: String?
var variety_name_ar: String?
}
import UIKit
class ViewController: UIViewController {
#IBOutlet var resultsTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
abuseedoAPIget()
}
var arrData = [results]()
var arrResults = [commodities]()
func abuseedoAPIget(){
let urlJSON = "http://abuseedotrading.com/apps/api/acp/?key=4ea1e08dd9ab329bbdaa9e5b42939c04&query=list_containers"
guard let url = URL(string: urlJSON) else {return}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
guard error == nil else {return}
do {
let decoder = JSONDecoder()
let APIResponse = try decoder.decode(RootResults.self, from: data)
self.arrData = APIResponse.results
DispatchQueue.main.async{
self.resultsTable.reloadData()
}
} catch let error {
print("Failed to decode JSON:", error)
}
}.resume()
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
let dataa = arrData[indexPath.row]
cell.conLabel.text = dataa.container_number
cell.comLabel.text = dataa.commodity_name_en
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
For Swift to be able to decode the JSON response into objects you need to have a similar structure defined in Swift using objects.
Your RootResults object needs to implement Codable protocol and represent the JSON structure.
Below a part of the returned JSON:
{
"status": 200,
"runtime": 1.7315270900726,
"results_count": 13,
"results": [
{
"container_id": 36473,
"container_number": "MMAU1163814",
"shipment_id": 17359,
}
}
RootResults would look something like this:
struct RootResults: Codable {
let status: Int
let runtime: Float
let results_count: 13
let results: [Container]
}
struct Container: Codable {
let container_id: Int
let container_number: String
let shipment_id: Int
}
More information on Swift Codable
Swift codable
SO question about
In the function tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) you are accessing data on different levels.
The container_number -property is accessible when retrieving an object from results. The commodity_name_en is a "level" deeper and part-of the commodities -array. To access the first item in the commodities -array , try the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
let dataa = arrData[indexPath.row]
cell.conLabel.text = dataa.container_number
cell.comLabel.text = dataa.commodities[0].commodity_name_en
return cell
}
As Vadian mentions, it's common to start types(struct, class, enum) with a capital letter in Swift. Have a look at struct and classes documentation

Couldn't use JSON Data in an viewController extension?

could anybody please let me know what I did wrong here?
I couldn't pass the JSON data from URLSession.shared.dataTask to outer extension
Nor could bring the
func numberOfSections .. etc to be used in the URLSession.shared.dataTask
Any help appreciated, thank you for your time.
Swift
struct getSubData: Decodable {
let id: Int
let name: String
}
struct Section: Decodable {
let id: Int
let name: String
let subData: [getSubData]
}
class indexViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://nabulsi.com/nabulsi_app/main_sections_v4.json"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!){(data, response, error) in
do {
let sections = try JSONDecoder().decode([Section].self, from: data!)
for section in sections {
print(section.name)
let sectionName = section.name
for data in section.subData {
print(data.name)
let subSectionName = data.name
}
}
} catch {
print("We got an error")
}
}.resume()
}
}
extension indexViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return sectionName.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sectionName[section].subSectionName?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = sectionName[indexPath.section].subSectionName?[indexPath.row]
return cell
}
}
JsonDecoder class has no chance to parse your data just because of the received JSON text from the server is not a valid format.
Here is your endpoint, open it in the web browser: https://nabulsi.com/nabulsi_app/main_sections_v4.json
And here is the JSON validator tool: https://jsonlint.com
Copy and paste to validate your server response to the JSON validator tool called jsonlint, and you will see where is your wrong.
Probably it's not your fault. It's about your backend developer if you didn't write by yourself. Contact him to fix the JSON format. After fixing the issue please notify me. I am gonna fix your parse codes if it's still not working as you expected.
EDIT: After fixing problem, the related codes here:
[0] - Parsed json values assigned to local variable.
class indexViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var sections: [Section] = []
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://nabulsi.com/nabulsi_app/main_sections_v4.json"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!){(data, response, error) in
do {
let sections = try JSONDecoder().decode([Section].self, from: data!)
self.sections = sections // [0]
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("We got an error")
}
}.resume()
}
}
Look at the code and follow these brief explanation:
[1] - Return every rows in section, from local array
[2] - Assigned the name variable to label for current row
[3] - Assigned the name variable to header for current section
extension indexViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let subDatas = sections[section].subData // [1]
return subDatas.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// [2]
let currentSection = sections[indexPath.section]
let currentSubdata = currentSection.subData[indexPath.row]
cell.textLabel?.text = currentSubdata.name
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 40))
view.backgroundColor = #colorLiteral(red: 1, green: 0.3653766513, blue: 0.1507387459, alpha: 1)
let lbl = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.width - 15, height: 40))
lbl.font = UIFont.systemFont(ofSize: 20)
lbl.text = sections[section].name // [3]
lbl.textAlignment = .right
view.addSubview(lbl)
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
}
Happy Coding! =]
Just take you section Array outside your function. Keep it as property of your controller.
class indexViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var sections: [Section] = []
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://nabulsi.com/nabulsi_app/main_sections_v4.json"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!) {
[weak self](data, response, error) in
do {
self?.sections = try JSONDecoder().decode([Section].self, from: data!)
for section in sections {
//your code goes here
}
}
catch {
print("We got an error")
}
}.resume()
}

Populating an UITableview from a struct

I have two viewControllers one called programlist that displays the list of tiles and populates a a suitable view.
the second viewController inputs the data. Issues implementing the callback due to an error in the prepareForsegue function. Getting the error "Instance member 'callback' cannot be used on type 'addWorkout'"
viewController 1 aka Programlist:
import UIKit
struct Item: Codable {
var title: String
var others: [String]
}
class ProgramList: UIViewController, UITableViewDataSource, UITableViewDelegate{
var Programs = [Item]()
#IBOutlet weak var programTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
load()
}
//saving current state of programs array
func save() {
guard let data = try? JSONEncoder().encode(Programs) else { return }
UserDefaults.standard.set(data, forKey: "notes")
}
//loading saved program array
func load() {
guard let loadedData = UserDefaults.standard.data(forKey: "notes") else { return }
do {
Programs = try JSONDecoder().decode([Item].self, from: loadedData)
programTableView.reloadData()
} catch { print(error) }
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Programs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.programTitle.text = Programs[indexPath.row].title
return cell
}
//Removing Item by swipping left & saving this newly established array
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCell.EditingStyle.delete {
Programs.remove(at: indexPath.row)
programTableView.reloadData()
save()
}
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toAddPage"{
workoutController.callback = { [weak self] string in
let entered = Item(title: string, others: ["hi"])
self?.programs.append(entered)
let indexPath = IndexPath(row: self?.programs.count - 1, section: 0)
self?.tableView.insertRows(at: [indexPath], with: .automatic)
self?.save()
}
}
}
}
}
}
viewController 2 aka addWorkout:
import UIKit
class addWorkout: UIViewController {
#IBOutlet weak var workoutTitle: UITextField!
var callback : ((String) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func saveWorkoutTitle(_ sender: Any) {
if !workoutTitle.text!.isEmpty {
callback?(workoutTitle.text!)
}
}
}
The main mistake is you are trying to save an array of Item – which is not supported anyway – to UserDefaults and read an array of String. That's a clear type mismatch.
To be able to save an array of a custom struct to UserDefaults adopt Codable to save the struct as JSON.
struct Item : Codable {
var title: String
var others: [String]
}
Further it's a very bad practice to declare a data source array outside of any class.
This is the ProgramList class with adjusted load and save methods and the data source array inside the class. The method viewDidAppear is not needed.
class ProgramList: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var programTableView: UITableView!
var programs = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
load()
}
//saving current state of programs array
func save() {
guard let data = try? JSONEncoder().encode(programs) else { return }
UserDefaults.standard.set(data, forKey: "notes")
}
//loading saved program array
func load() {
guard let loadedData = UserDefaults.standard.data(forKey: "notes") else { return }
do {
programs = try JSONDecoder().decode([Item].self, from: loadedData)
programTableView.reloadData()
} catch { print(error) }
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return programs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.programTitle.text = programs[indexPath.row].title
return cell
}
//Removing Item by swipping left & saving this newly established array
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
programs.remove(at: indexPath.row)
programTableView.deleteRows(at: [indexPath], with: .automatic)
save()
}
}
}
To share data between controllers use a closure as callback and pass the string
class AddWorkout: UIViewController {
#IBOutlet weak var workoutTitle: UITextField!
var callback : ((String) -> Void)?
#IBAction func saveWorkoutTitle(_ sender: Any) {
if !workoutTitle.text!.isEmpty {
callback?(workoutTitle.text!)
}
}
}
Back in ProgramList controller assign a closure to the callback property in prepareForSegue (or right before presenting the controller)
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toAddPage" {
let workoutController = segue.destination as! AddWorkout
workoutController.callback = { string in
let entered = Item(title: string, others: ["hi"])
self.programs.append(entered)
let indexPath = IndexPath(row: self.programs.count - 1, section: 0)
self.tableView.insertRows(at: [indexPath], with: .automatic)
self.save()
}
}
}

Resources