How to update the model after tableView was edited - swift

I am learning Swift by writing a single table app view which lists the content of a Core Data table (entity) upon start-up. Then the user can reorder the rows in the table view.
I need to be able to save the newly ordered rows such that they replace the previous database table, so when the user starts the app again, the new order is shown.
The editing (re-ordering) feature is activated by a long press and calls
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
self.projectTableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
}
A second long press then inactivates the editing feature:
// Called when long press occurred
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .ended {
let touchPoint = gestureRecognizer.location(in: self.projectTableView)
if let indexPath = projectTableView.indexPathForRow(at: touchPoint) {
if self.projectTableView.isEditing == true {
self.projectTableView.isEditing = false
db.updateAll() //this is a stub
} else {
self.projectTableView.isEditing = true
}
}
}
}
The call to db.updateAll() in 'handleLongPress' above is just a blank, and I don't know how to update the database. Is there a way to read the content of the tableView in the new sequence into an array, then replace the table in the db? Feels a little "brute force" but can't see any other solution.

Ok you can achieve that in several ways :
1- Using NSFetchedResultsController , here you can automatically synchronizing changes made to your core data persistence store with a table view,
so quickly here are the steps :
Conform to NSFetchedResultsControllerDelegate
Declare an instance of NSFetchedResultsController with you core data model
Make an NSFetchRequest, call NSFetchedResultsController initializer with the request, then assign it to your instance declared before
call performFetch method on your instance
set the viewController to be the delegate
And now you can implement the delegates, here you want didChange , so something like that :
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
switch type {
/*
....
*/
case .move:
if let deleteIndexPath = indexPath {
self.tableView.deleteRows(at: [deleteIndexPath], with: .fade)
}
if let insertIndexPath = newIndexPath {
self.tableView.insertRows(at: [insertIndexPath], with: .fade)
}
}
}
2- Second option which personally i prefer it over the NSFetchedResultscontroller
You can add a property in your model (core data model). That can be an Int for example "orderNum".
So when you fetch request you can order the result using this prperty.
So if your table view cell re-arranged, after implementing moveItem method you can update this property for all your objects(loop over them) and they will be as they are displayed.
try to save your managed object context now,
Next time when you want to fetch request you can use a sort descriptor to sort on the "orderNum".

Maybe updating your data source (by removing and re-inserting the item) when moveRowAt is called would be better?
So something like:
// assuming your data source is an array of names
var data = ["Jon", "Arya", "Tyrion", "Sansa", "Winterfell"]
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
self.projectTableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
let item = self.data.remove(at: sourceIndexPath.row)
if sourceIndexPath.row > destinationIndexPath.row {
// "Sansa" was moved to be between "Jon" and "Arya"
self.data.insert(item, at: destinationIndexPath.row
} else {
// if the new destination comes after previous location i.e. "Sansa"
// was moved to the end of the list
self.data.insert(item, at: destinationIndexPath.row - 1
}
}

Related

How to make sure a line of code is executed after another is completed?

I have a viewControl called PostViewController which has a UITableView of posts. I also have a class called PostCell which defines the UITableViewCell. I made a button function in PostCell called likeButtonClicked to favour a post similar to twitter.
#IBAction func likesButtonClicked(_ sender: Any) { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil, userInfo: ["cell":self, "likesButton":likesButton!]) }
This is to pass the cell indexPath and the button name to PostViewController. I need indexPath to increase the likes by 1 and the button name to change its image to pink when post is favoured.
I then subscribed to the notification in viewDidLoad of PostViewController.
NotificationCenter.default.addObserver(self, selector: #selector(postLiked), name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil)
I then wrote this function in the same viewController
#objc func postLiked(notification: Notification){
if let cell = notification.userInfo?["cell"] as? UITableViewCell{
let likesButton = notification.userInfo?["likesButton"] as? SpringButton
if let indexPath = postsTableView.indexPath(for: cell){
let post = posts[indexPath.row]
postId = post.id
PostAPI.getPostById(postId: postId) { post in
//Check if the same post were already favoured.
if !self.liked || self.oldPostId != self.postId{
self.newLikes = post.likes + 1
self.liked = true
self.oldPostId = self.postId
}else{
self.newLikes = self.newLikes - 1
self.liked = false
}
PostAPI.favourPost(postId: self.postId, likes: self.newLikes) {
PostAPI.getPostById(postId: self.postId) { postResponse in
let post = postResponse
self.posts[indexPath.row] = post
let cellNumber = IndexPath(row: indexPath.row, section: indexPath.section)
self.reloadRowData(cellNumber: cellNumber){
if !self.liked{
likesButton?.tintColor = .systemPink
}else{
likesButton?.tintColor = .darkGray
}
}
}
}
}
}
}
}
func reloadRowData(cellNumber: IndexPath, completion: #escaping () -> ()) {
self.postsTableView.reloadRows(at: [cellNumber], with: .none)
completion()
}
Please tell me why the last 4 lines of postLiked function is executed before reloadRowData function, which causes the button to change its color to pink then returns immediately to gray when it should stay pink.
Any help will be most appreciated.
Thank you.
I expect the specific problem is your call to reloadRows. As the docs note:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
So this is likely creating an entirely new cell, and then the later code modifies the old cell that is being removed from the table. (So you see the change, and then the cell is removed and replaced.)
I would start by getting rid of the entire reloadRowData call.
Generally, though, this code is fragile, and I'd redesign it. The cell should take care of setting the tint colors itself based on the data. You generally shouldn't be reaching into a cell and manipulating its subviews. This will cause you a problem when cells are recycled (for example, when this scrolls off screen). All the configuration should be done in cellForRow(at:), and the cell should observe its Post and update itself when there are changes.
Data should live in the Model. The View should observe the Model and react. The Model should not reach into the View and manipulate anything.
As a side: your reloadRowData looks async, but it's not. There's no reason for a completion handler. It could just call return.
A table view's .reloadRows(at:...) (and .reloadData()) functions are async processes.
So your reloadRowData() func is returning before the table view actually reloads the row(s).
This is a rather unusual approach - both in using NotificationCenter for your cells to communicate with the controller, and in trying to change the button's tint color by holding a reference to the button.
The tint color really should be set in cellForRowAt, based on your data source.
Edit
My description of table view data reloading being asynchronous was misleading.
The point I wanted to make was this...
If I change my data, call .reloadData(), and then change my data again:
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
the table view will not display "You Say Hello" even though we call .reloadData() when those are the values of myData.
Here's a complete, simple example to demonstrate:
class TestTableViewController: UITableViewController {
var myData: [String] = ["Some", "Song", "Lyrics"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = myData[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// call my func when a row is selected
self.myReload()
}
func myReload() {
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
// table view doesn't reload itself we exit this func
// so we'll get
// I
// Say
// Goodbye
}
}
So, among the other issues described by Rob Napier in his answer, your original code was trying to change the tint color of an object in a cell before the table reloaded its data.

Saving Data in UITableview

I have an app whereby a user can add books to a favourites list, by populating a tableview. Details on the book are displayed in a view and by tapping a 'favourites', a segue is performed inputting info on that book into a tableview cell.
At present only one book can appear in the table at a time adding a new book will remove the initial entry (so in effect only the first cell of the tableview is ever used)
Is there a way to save each entry in the tableview so in effect a list of favourites is created
saveButton
#IBAction func saveButton(_ sender: Any) {
let bookFormat = formatLabel.text
if (bookFormat!.isEmpty)
{
displayMyAlertMessage(userMessage: "Please Select a Book Format")
return
}
else{
self.performSegue(withIdentifier: "LibViewSegue", sender: self)
}
}
TableView
extension LibrarybookViewController: UITableViewDataSource, UITableViewDelegate{
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 115
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(#function, dataSource.count)
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(#function, "indexPath", indexPath)
guard let bookCell = tableView.dequeueReusableCell(withIdentifier: "libCell", for: indexPath) as? LibrarybookTableViewCell else {
return UITableViewCell()
}
let libbook = dataSource[indexPath.row]
bookCell.cellTitleLabel.text = libbook.title
bookCell.cellReleaseLabel.text = libbook.release
bookCell.cellFormatLabel.text = bookFormat
return bookCell
}
I have been reading about defaults and CoreData but I'm not sure whether this should be implemented within the segue button action or within the tableview functions?
I see you have a dataSource array which contains list of books. At the simplest you could just append data to your dataSource, then reload your UITableView. But if you want persistent storage, you could look into local db solutions like SQLite, CoreData, or Realm. Then it would just be the matter of storing -> get data -> display on UITableView.
The general idea would be on add button tap (or whatever event listener you want to attach the action to), save it to persistent storage, reload your tableView, since the dataSource is from persistent storage, it'll update automatically. Suppose you load the data from persistent storage.
Also, don't store your array in UserDefaults, it's for bite sized data for things like user session etc.
Edit: as #Leo Dabus point out, indeed you can insert row without reloading the tableView by using
let index = IndexPath(row: items.count - 1, section: 0) // replace row with position you want to insert to.
tableView.beginUpdates()
tableView.insertRows(at: [index], with: .automatic)
tableView.endUpdates()

Dynamically adding item to TableView resulted in repeated cells

I have a tableview that's loading data dynamically from Firebase Database. Also I have an add button that allows the user to add inside the Firebase Database after that the table view must be updated and reload the data again from firebase plus the new added item. However, I'm having an issue that the data in the table view are duplicated after adding an item. I have tried to clear the datasource array and then reloading the table view data but it is not working. This is my code:
override func viewDidLoad()
{
recipeTableView.delegate = self
recipeTableView.dataSource = self
// clear the array
myRecipes.removeAll()
// get recipes
getRecipes()
}
Table view extension functions
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return myRecipes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
recipe = self.myRecipes[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "MyRecipeCell")
as! UIMyRecipesCell
cell.setRecipe(recipe: recipe!)
}
method to get data from fire base
func getRecipes()
{
self.myRecipes.removeAll()
childRef.observe(.value, with: { snapshot in
for child in snapshot.children
{
//create the object and get the info from the fire base and finally store it MyRecipes array
self.myRecipes.append(recipe)
}
self.recipeTableView.reloadData()
})
You need to use observeSingleEvent instead of observe
childRef.observeSingleEvent(of: .value) { snapshot in
////
}
OR
childRef.observe(.childAdded) { snapshot in
////
}
OR
childRef.observe(.value, with: { snapshot in
self.myRecipes.removeAll ()
///
}
observe fetches all the data again every change unlike observeSingleEvent that gets all the data once

How to store tableview cell selected index data into Userdefault and use it into previous page tableview cell using Swift?

My scenario, I am having two viewcontroller VC1 and VC2. Inside VC1 I am having one tableview with two cell for From and To translation language UI.
Here From cell or To cell click to showing common VC2 language list in a tableview. Based on language selection I need to store the selection data to VC1 and need to assign VC1 tableview cell label (immediate updating).
Below code I am using, need to simplified also how to use stored values in VC1 within tableview cell.
Here, My Code
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if language_segment.selectedSegmentIndex == 0 { //from
selectedFromLanguage = filteredLanguages[indexPath.row]
fromLanguageID = selectedFromLanguage?.icon
fromLanguageName = selectedFromLanguage?.languageName
fromLanguageCode = selectedFromLanguage?.languageCode
} else { //to
selectedToLanguage = filteredLanguages[indexPath.row]
toLanguageName = selectedToLanguage?.icon
toLanguageName = selectedToLanguage?.languageName
toLanguageCode = selectedToLanguage?.languageCode
}
tableView.reloadData()
}
#IBAction func close_Click(_ sender: Any) {
// From Language Responses
UserDefaults.standard.set(fromLanguageID ?? "",, forKey: "icon")
UserDefaults.standard.set(fromLanguageName ?? "", forKey: "fromLanguageName")
}
You should use a delegate model to inform the first VC that the language has changed.
Here's an example. First define a delegate interface, e.g.:
protocol LanguageSelectionDelegate
{
fun languageSelector(didSetFromLanguage: Int)
fun languageSelector(didSetToLanguage: Int)
}
You can either define a single method that passes both (when OK is clicked on the second VC) or use two methods as here, that are called as soon as the user clicks an option (which you use could depend on whether there is a 'Cancel' option which closes the second VC without applying changes).
Make your first VC conform to this protocol, and add an implementation. Within that implementation you may only need to reload the table view data - without the code I don't know. E.g. (only showing the 'from' language change, but 'to' is identical almost):
class FirstVC: UITableViewController, LanguageSelectionDelegate
{
private var fromLanguageId = UserDefaults.standard.integer(forKey: "fromLanguage")
fun languageSelector(didSetFromLanguage languageId: Int)
{
UserDefaults.standard.set(languageId, forKey: "fromLanguage")
fromLanguageId = languageId
tableView.reloadData()
}
}
This assumes that your cellForRowAt implementation will populate the cell according to the languageId. You need to store a delegate reference in the second VC, e.g.:
public val delegate: LanguageSelectionDelegate!
And make sure you initialise it in your first VC prepareFor(segue) function, e.g.:
let vc2 = segue.destination as! SecondVC
vc2.delegate = self
Your didSelectRowAt for the second VC would then become:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
tableView.deselectRow(at: indexPath, animated: true)
if language_segment.selectedSegmentIndex == 0
{ //from
selectedFromLanguage = filteredLanguages[indexPath.row]
delegate.languageSelector(didSetFromLanguage: selectedFromLanguage.id)
}
else
{ //to
selectedToLanguage = filteredLanguages[indexPath.row]
delegate.languageSelector(didSetToLanguage: selectedToLanguage.id)
}
}
There's no need to call reloadData() from this function. And note that you only really need to deal in a unique ID for the language (I've assumed an int here), rather than saving all details about it.

Move all views up after hiding one

In my Android app, I was able to create use a standard table with rows and have text boxes in it. Based on user preferences, some of these rows are shown/hidden. For example if the user doesn't want to enter in cash/credit sales, or track any discounts given to a customer, etc, these fields shouldn't appear.
I simply would hide the entire row, and just like a DIV in html, the rest of the rows would move up nicely.
So for example:
element1
element2
element3
I'd want to remove element 2, and 3 moves up and takes 2's place. I'll probably have 8 of these fields btw.
In iOS, however, is something like this possible? Some googling yielded results that more or less made this seem very complicated for showing/hiding form elements and having everything slide up nicely. Some solutions included setting up very complex autolayout scenarios dynamically, or not using autolayout at all, etc. Then again, these solutions were for older versions of ios.
Is something like this possible? How could I achieve this?
Thanks!
Use UITableView.
UITableView allows you to represent list of cells and manipulate them (add, remove, reorder) with animations.
In you case every option will be a UITableViewCell.
Create a subclass of UITableViewController
Set a dataSource of UITableView
Implement UITableViewDataSource
That is 2 functions
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell
Update TableView items
You can easily reload tableView by editing your objects and calling reloadData method on tableView
// Add, remove or replace your objects you want to display in table View
objects = ["1", "2", "3"]
// call tableView to reload it's data. TableView will call dataSource delegates methods and update itself
tableView.reloadData()
You can remove or add specific item in tableView
First update your object, by removing or adding it, than call update tableView and say what cell (at which position) it should add or remove. You can also say what type of animation it should use
var index = 2
// Update your object Model, here we remove 1 item at index 2
objects.removeAtIndex(index)
// saying tableView to remove 1 cell with Fade animation.
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade)
Here is full Code example
class MasterViewController: UITableViewController {
var objects = ["Option 1", "Option 2", "Option 3"]
func insert(obj: String, at: Int) {
objects.append(obj)
let indexPath = NSIndexPath(forRow: at, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
func deleteAt(index: Int) {
objects.removeAtIndex(index)
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func insertNewObject(sender: AnyObject) {
insert(NSDate.date().description, at: 0)
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let object = objects[indexPath.row]
cell.textLabel?.text = object
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
deleteAt(indexPath.row)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}