I'm pulling data from Parse into an array but need a way to reference that specific object directly for making changes to the data on the server (ex: deleting a specific entry off the app and having it delete on the server). Previously I was using an array filled with a PFQuery, which worked for pulling data down but not for making changes back up. I think creating a dictionary with [objectID : string of data needed] would work, so each data set currently in the array would always be paired directly to it's identifier. My issue is pulling both sets of data down (objectID and string of data) and matching them up in the dictionary. Any advice or help?
I changed the original array to a dictionary in the cellContent variable but otherwise the code is still set up for an array.
Thanks!
import UIKit
import Parse
var segueWorker = ""
class MyWorkersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var cellContent = [String: String]()
#IBAction func backButton(_ sender: Any) {
navigationController?.popViewController(animated: true)
performSegue(withIdentifier: "workersToMyFarm", sender: self)
}
#IBOutlet weak var tableView: UITableView!
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellContent.count
}
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = cellContent[indexPath.row]
return cell
}
override func viewDidAppear(_ animated: Bool) {
// Do any additional setup after loading the view.
self.cellContent.removeAll()
let query = PFQuery(className: "Workers")
query.findObjectsInBackground(block: { (objects1, error) in
if error != nil {
print(error!)
} else {
query.whereKey("username", equalTo: PFUser.current()?.username)
query.findObjectsInBackground(block: { (objects2, error) in
for object in objects2! {
self.cellContent.append(object["workerName"] as! String)
self.cellContent.sort()
self.tableView.reloadData()
}
}
)}
}
)}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
cellContent.remove(at: indexPath.row)
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRow(at: indexPath!)!
segueWorker = (currentCell.textLabel!.text!)
performSegue(withIdentifier: "toAddWorkers", sender: self)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Took some tinkering but finally got it to work by spreading out the variables and finally building the dictionary once I had all variables created. Also, had a lot of trouble with getting the objectId from Parse but seems to be working by simply calling object.objectId.
Code below for reference:
self.cellContent.removeAll()
let query = PFQuery(className: "Workers")
query.findObjectsInBackground(block: { (objects1, error) in
if error != nil {
print(error!)
} else {
query.whereKey("username", equalTo: PFUser.current()?.username)
query.findObjectsInBackground(block: { (objects2, error) in
for object in objects2! {
self.cellContent.append(object["workerName"] as! String)
// Needs to be objectId
let objectKey = object.objectId
let objectValue = object["workerName"] as! String
self.cellContentDict[objectKey!] = objectValue
print(self.cellContentDict)
self.cellContent.sort()
self.tableView.reloadData()
Related
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.
I am trying to load data from an API into my TableViewController but the first time it loads the data returns empty. I can't build a table because the data is empty.
import UIKit
import Kanna
class TableViewController: UITableViewController {
var country = [String]()
override func viewDidLoad() {
super.viewDidLoad()
gets()
print(country)// is empty view controller.
}
func gets(){
let url = "https://site"
let myURL = NSURL(string: url)
let URLTask = URLSession.shared.dataTask(with: myURL! as URL) {
myData, response, error in
guard error == nil else {return}
let myHTML = String(data: myData!, encoding: String.Encoding.utf8)
DispatchQueue.global(qos: .userInitiated).async {
if let doc = try? HTML(html: myHTML!, encoding: .utf8) {
DispatchQueue.main.async {
for fdata in doc.xpath("//*[#id='content']/table[3]") {
let i = fdata.text!
self.country.append(i)
}
}
}
}
}
URLTask.resume()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return country.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = country[indexPath.row]
return cell
}
I know that there are many similar problems. I tried different options but I didn't succeed.
Since the data is coming from a network request, the table will always be empty initially. You could fetch the data from the previous view controller (displaying some kind of loading indicator) and wait to present this one until the fetch has completed.
Or, if you are just looking to reload your table when the data comes in, try adding a self.tableView.reloadData() after the data has loaded:
...
DispatchQueue.main.async {
for fdata in doc.xpath("//*[#id='content']/table[3]") {
let i = fdata.text!
self.country.append(i)
}
self.tableView.reloadData()
}
...
Found it! Use the viewWillAppear (_animated: Bool) method that is called before the view appears on the screen.
override func viewWillAppear(_ animated: Bool) {
gets()
}
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()
}
}
}
I'm using a UITabBarController to create a contact list, but when I'm trying to save the array to load the data when I restart the app is giving me problems where the data isn't displayed. I'm using UserDefaults to save the data and the restore when the app is restarted.
In this code I sent data from a textfield to the array named list.
import UIKit
class NewContactoViewController: UIViewController {
#IBOutlet weak var input: UITextField!
#IBAction func add(_ sender: Any) {
if (input.text != "") {
list.append(input.text!)
UserDefaults.standard.set(list, forKey: "SavedValue")
input.text = ""
}
}
}
In this code I'm printing the data in a table, and trying to save it with user defaults.
import UIKit
var list = [String]()
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let x = UserDefaults.standard.object(forKey: "SavedValue") as? String {
return (x.count)
}
return (0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
if let x = UserDefaults.standard.dictionary(forKey: "SavedValue") as? String {
cell.textLabel?.text = [x[indexPath.row]]
}
return(cell)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
list.remove(at: indexPath.row)
myTableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
myTableView.reloadData()
}
#IBOutlet weak var myTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
You are saving an array of strings but you are reading a single string (or even a dictionary) which obviously cannot work. There is a dedicated method stringArray(forKey to read a string array.
Apart from the issue never read from UserDefaults to populate the data source in the table view data source and delegate methods, do it in viewDidLoad or viewWillAppear for example
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let savedArray = UserDefaults.standard.stringArray(forKey: "SavedValue") {
list = savedArray
}
myTableView.reloadData()
}
Put the data source array in the view controller. A global variable as data source is very bad programming habit.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var list = [String]()
...
In numberOfRowsInSection return the number of items in list and return is not a function, there are no parentheses
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
Same in cellForRow. Get the item from list and use reusable cells and again, return is not a function.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = list[indexPath.row]
return cell
}
Note :
Consider that UserDefaults is the wrong place to share data between view controllers. Use segues, callbacks or protocol / delegate.
I have a page with a TableView that fills each cell with a hardcoded UILabel of some text. I would like it to fill up with UILabels from a JSON that I get online.
Storyboard:
The code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var objects: NSMutableArray! = NSMutableArray()
override func viewDidLoad(){
super.viewDidLoad()
self.objects.add("iPhone")
self.objects.add("Apple Watch")
self.objects.add("Mac")
self.objects.add("Test")
self.tableView.reloadData()
}
override func didReceiveMemoryWarning(){
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int{
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return self.objects.count
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell{
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.titleLabel.text = self.objects.object(at: (indexPath as NSIndexPath).row) as? String
//cell.logButton.tag = (indexPath as NSIndexPath).row;
//cell.logButton.addTarget(self, action: #selector(ViewController.logAction(_:)), for: .touchUpInside)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath){
self.performSegue(withIdentifier: "showView", sender: self)
}
#IBAction func logAction(_ sender: UIButton) {
let titleString = self.objects[sender.tag] as? String
let firstActivityItem = "\(titleString!)"
let activityViewController : UIActivityViewController = UIActivityViewController(activityItems: [firstActivityItem], applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?){
if (segue.identifier == "showView"){
let upcoming: NewViewController = segue.destination as! NewViewController
let indexPath = self.tableView.indexPathForSelectedRow!
let titleString = self.objects.object(at: indexPath.row) as? String
upcoming.titleString = titleString
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
}
Once in simulator and I open up the page, the TableView will have four cells with the "Label" changing to whatever was adding to the objects array, which in this case is iPhone, Apple Watch, Mac, and Test. Rather than having those hardcoded, I would like to have the items loaded from a JSON file.
I have the same thing done with a PickerView, but I am struggling to figure out how to do it with this. Here is how it was done with my PickerView if it helps:
Alamofire.request("example.com/file.json").responseJSON{ response in
if let JSON = response.result.value as? [String:AnyObject] {
self.mypickerview.delegate = self
self.mypickerview.dataSource = self
let result = JSON.values.flatMap({ String(describing: $0) })
self.pickerData.append(contentsOf: result)
self.pickerData.sort()
self.verbose.text = "Content saved!"
self.mypickerview.reloadAllComponents()
self.mypickerview.delegate = self;
self.verbose.text = "Finished Loading!"
}
}
The JSON file:
{"One":"Mac","Two":"Apple iPhone","Three":"Test"}
1.On viewDidLoad fire the webService.
2.On Webservice completion handler ,retreive the label values and assign to objects.
3.Reload tableview.
your JSON should look like this:
{
"titles": [
"mac",
"iphone",
"test"
]
}
and you will do something like cell.titleLabel.text = [[yourJSON valueForKey:#"titles"] objectAtIndex:indexPath.row];
(this is Obj-C version, but ofc it can be done also in swift)