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()
}
}
}
Related
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") }
}
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.
What I'm trying to accomplish is when user selects an element for UITableView this element gets append to servicioSeleccionadoarray. But I'm stuck in this because if user decides to deselect the cell I want to remove that item from the array. I've try anyarray.remove(at:)but I can figure the way to tapp into that index.
This is my code so far.
class ServicioHogarViewController: UIViewController{
let serviciosHogar = [String](arrayLiteral: "Alfombras", "Muebles Madera", "Sillones", "Marmol", "Aplicación Teflón","Vestiduras", "Salas", "Colchones", "Sillas Oficinas")
#IBOutlet weak var servicioHogarTB1: UITableView!
var selectedIndex : Int? = nil
var servicioSeleccionado : [String] = []
#IBAction func doneButton(_ sender: UIButton) {
performSegue(withIdentifier: "datePick2", sender: self)
}
override func viewDidLoad() {
super.viewDidLoad()
servicioHogarTB1.delegate = self
servicioHogarTB1.dataSource = self
servicioHogarTB1.register(UINib(nibName: "ServicioHogarCell", bundle: nil), forCellReuseIdentifier: "servicioCell1")
servicioHogarTB1.separatorStyle = .none
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
print(servicioSeleccionado)
}
}
// MARK : UITabeView Delegation
extension ServicioHogarViewController : UITableViewDelegate, UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return serviciosHogar.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.cellForRow(at: indexPath)?.accessoryType == .checkmark {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
} else {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
if tableView.cellForRow(at: indexPath)?.accessoryType == .checkmark {
servicioSeleccionado.append(serviciosHogar[indexPath.row])
print(servicioSeleccionado)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "servicioCell1", for: indexPath) as! ServicioHogarCell
let servicio = serviciosHogar[indexPath.row]
cell.servicioLabel.text = servicio
return cell
}
}
If you have an array :
var cast = ["Vivien", "Marlon", "Kim", "Karl"]
and you want to remove "Marlon" from it, you can find the index of Marlon using the func firstIndex(of: Element) -> Int? method for an array and then remove it like so :
if let index = cast.firstIndex(of: "Marlon"){
cast.remove(at: index)
print(cast)
}
That said, on didSelectRowAt run this function for your array and you'll accomplish what you're looking to.
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'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()