How do I transfer data from one table view cell to another? - ios

I am trying to create a favorites page. How can I let a user click on an image in one table view and then the data from the table view they clicked on is transferred to another tableview in another page?
#IBAction func favoritesSelected(sender: AnyObject)
{
if toggleState == 1
{
sender.setImage(UIImage(named:"Star Filled-32.png"),forState:UIControlState.Normal)
isFav = true
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext
var newFave = NSEntityDescription.insertNewObjectForEntityForName("Favorites", inManagedObjectContext: context) as NSManagedObject
newFave.setValue("" + nameLabel.text!, forKey: "favorite")
do
{
try context.save()
}
catch _
{
print("error")
}
//print("\(newFave)")
print("Object saved")
toggleState = 2
}
From the code above, you can see what happens when a user clicks on the favorites button. The image changes and it uploads the name to the core data.
I'm trying to get it to go to another table view cell class so that when it gets to the favorites page, the names that were favorited will already be there.
I will show what I have in that class but I'm sure it's wrong.
if (result == 2)
{
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext
var request = NSFetchRequest(entityName: "Favorites")
request.returnsObjectsAsFaults = false
do
{
var results:NSArray = try context.executeFetchRequest(request)
if (results.count <= 0)
{
print("Either all object deleted or error")
}
}
catch _
{
print("error")
}
}
else
{
print("no show")
}

Option 1: NSNotification triggers tableView reload:
Register the UITableView tableView with the notification:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
func contextDidSave(sender: NSNotification) {
tableView.reloadData()
}
After the user clicks on the star in the first example and the context was saved properly, the contextDidSave callback will be executed and the tableView will load with the latest state in the DB
Option 2: Setup UITableView with NSFetchedResultsController
With this option, once the user clicks on the star and the context saves, iOS will trigger the update to selected cells automatically. See this article for reference:
https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/

In my applications with a favourite function I have done the following:
1) On my model objects have a BOOL property called favourite, with a default value of false.
2) In the table view which lists all objects, when a user taps the favourite button, set the favourite property on the corresponding model object to true and save your managed object context.
3) In the table view in which you wish to display the favourited objects, query core data for all of your model objects with a favourite property that is true and display those results. As mentioned in Christopher Harris' answer, this is trivial if you are a using an NSFetchedResultController.

Related

Saving data in UITextView

I'm writing notes app for iOS and I want all data which user enter in notes will be automatically saved when user typing automatically. I'm using Core Data and now I save data on viewWillDisappear, but I want the data also be saved if user terminate the app or the app will be automatically terminated in the background.
I use this code:
import UIKit
import CoreData
class AddEditNotes: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
var note: Note!
var notebook: Notebook?
var userIsEditing = true
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
context = appDelegate.persistentContainer.viewContext
if (userIsEditing == true) {
textView.text = note.text!
title = "Edit Note"
}
else {
textView.text = ""
}
}
override func viewWillDisappear(_ animated: Bool) {
if (userIsEditing == true) {
note.text = textView.text!
}
else {
self.note = Note(context: context)
note.setValue(Date(), forKey: "dateAdded")
note.text = textView.text!
note.notebook = self.notebook
}
do {
try context.save()
print("Note Saved!")
}
catch {
print("Error saving note in Edit Note screen")
}
}
}
I understand what I can use applicationWillTerminate for this, but how I can pass there the data user entered? This functionality is in default notes app from Apple. But how it can be released?
There are two subtasks to saving the data: updating the Core Data entity with the contents of the text view and saving the Core Data context.
To update the contents of the Core Data entity, add a function to the AddEditNotes class that saves the text view contents.
func saveTextViewContents() {
note.text = textView.text
// Add any other code you need to store the note.
}
Call this function either when the text view ends editing or the text changes. If you call this function when the text changes, the Core Data entity will always be up to date. You won't have to pass the data to the app delegate because the app delegate has the Core Data managed object context.
To save the Core Data context, add a second function to the AddEditNotes class that saves the context.
func save() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.saveContext()
}
}
This function assumes you selected the Use Core Data checkbox when you created the project. If you did, the app delegate has a saveContext function that performs the Core Data save.
You can now replace the code you wrote in viewWillDisappear with the calls to the two functions to save the text view contents and save the context.
The last code to write is to go to your app delegate file and add the following line of code to the applicationDidEnterBackground and applicationWillTerminate functions:
self.saveContext()
By adding this code your data will save when someone quits your app.

image and label in interface builder overlap my data in the TableView cell

I am a beginner in iOS development, and I want to make an instagram clone app, and I have a problem when making the news feed of the instagram clone app.
So I am using Firebase to store the image and the database. after posting the image (uploading the data to Firebase), I want to populate the table view using the uploaded data from my firebase.
But when I run the app, the dummy image and label from my storyboard overlaps the downloaded data that I put in the table view. the data that I download will eventually show after I scroll down.
Here is the gif when I run the app:
http://g.recordit.co/iGIybD9Pur.gif
There are 3 users that show in the .gif
username (the dummy from the storyboard)
JokowiRI
MegawatiRI
After asynchronously downloading the image from Firebase (after the loading indicator is dismissed), I expect MegawatiRI will show on the top of the table, but the dummy will show up first, but after I scroll down and back to the top, MegawatiRI will eventually shows up.
I believe that MegawatiRI is successfully downloaded, but I don't know why the dummy image seems overlaping the actual data. I don't want the dummy to show when my app running.
Here is the screenshot of the prototype cell:
And here is the simplified codes of the table view controller:
class NewsFeedTableViewController: UITableViewController {
var currentUser : User!
var media = [Media]()
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
// to set the dynamic height of table view
tableView.estimatedRowHeight = StoryBoard.mediaCellDefaultHeight
tableView.rowHeight = UITableViewAutomaticDimension
// to erase the separator in the table view
tableView.separatorColor = UIColor.clear
}
override func viewWillAppear(_ animated: Bool) {
// check wheter the user has already logged in or not
Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
RealTimeDatabaseReference.users(uid: user.uid).reference().observeSingleEvent(of: .value, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Any] {
self.currentUser = User(dictionary: userDict)
}
})
} else {
// user not logged in
self.performSegue(withIdentifier: StoryBoard.showWelcomeScreen, sender: nil)
}
}
tableView.reloadData()
fetchMedia()
}
func fetchMedia() {
SVProgressHUD.show()
Media.observeNewMedia { (mediaData) in
if !self.media.contains(mediaData) {
self.media.insert(mediaData, at: 0)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StoryBoard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
// to remove table view highlight style
cell.selectionStyle = .none
return cell
}
}
And here is the simplified code of the table view cell:
class MediaTableViewCell: UITableViewCell {
var currentUser: User!
var media: Media! {
didSet {
if currentUser != nil {
updateUI()
}
}
}
var cache = SAMCache.shared()
func updateUI () {
// check, if the image has already been downloaded and cached then just used the image, otherwise download from firebase storage
self.mediaImageView.image = nil
let cacheKey = "\(self.media.mediaUID))-postImage"
if let image = cache?.object(forKey: cacheKey) as? UIImage {
mediaImageView.image = image
} else {
media.downloadMediaImage { [weak self] (image, error) in
if error != nil {
print(error!)
}
if let image = image {
self?.mediaImageView.image = image
self?.cache?.setObject(image, forKey: cacheKey)
}
}
}
So what makes the dummy image overlaps my downloaded data?
Answer
The dummy images appear because your table view controller starts rendering cells before your current user is properly set on the tableViewController.
Thus, on the first call to cellForRowAtIndexPath, you probably have a nil currentUser in your controller, which gets passed to the cell. Hence the didSet property observer in your cell class does not call updateUI():
didSet {
if currentUser != nil {
updateUI()
}
}
Later, you reload the data and the current user has now been set, so things start to work as expected.
This line from your updateUI() should hide your dummy image. However, updateUI is not always being called as explained above:
self.mediaImageView.image = nil
I don't really see a reason why updateUI needs the current user to be not nil. So you could just eliminate the nil test in your didSet observer, and always call updateUI:
var media: Media! {
didSet {
updateUI()
}
Alternatively, you could rearrange your table view controller to actually wait for the current user to be set before loading the data source. The login-related code in your viewWillAppear has nested completion handers to set the current user. Those are likely executed asynchronously .. so you either have to wait for them to finish or deal with current user being nil.
Auth.auth etc {
// completes asynchronously, setting currentUser
}
// Unless you do something to wait, the rest starts IMMEDIATELY
// currentUser is not set yet
tableView.reloadData()
fetchMedia()
Other Notes
(1) I think it would be good form to reload the cell (using reloadRows) when the image downloads and has been inserted into your shared cache. You can refer to the answers in this question to see how an asynch task initiated from a cell can contact the tableViewController using NotificationCenter or delegation.
(2) I suspect that your image download tasks currently are running in the main thread, which is probably not what you intended. When you fix that, you will need to switch back to the main thread to either update the image (as you are doing now) or reload the row (as I recommend above).
Update your UI in main thread.
if let image = image {
DispatchQueue.main.async {
self?.mediaImageView.image = image
}
self?.cache?.setObject(image, forKey: cacheKey)
}

Using a delegate to save values to core data

Beginner here, this is my first shot at using a delegate and I'm pretty confused - I'm trying to pass data between two controllers, the first of which is a tableview displaying some products, and the other is a modal view which allows the user to enter a new product to be displayed on that tableview. When the user hits "Save" in the modal view, I want to save the new product into core data and have it be displayed on the tableview.
The user enters information in three text fields in AddProductController (the modal view) and then hits save which calls handleSave:
func handleSave() {
guard let newProductUrl = self.urlTextField.text else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
DispatchQueue.main.async {
self.productSaveDelegate?.save(name: newProductName, url: newProductUrl, image: newProductImage)
let companyController = CompanyController()
self.navigationController?.pushViewController(companyController, animated: true)
}
}
Which in turn calls save in ProductController (the tableview):
func save(name: String, url: String, image: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Product",
in: managedContext)!
let product = NSManagedObject(entity: entity,
insertInto: managedContext)
product.setValue(name, forKey: "name")
product.setValue(url, forKey: "url")
product.setValue(image, forKey: "image")
do {
try managedContext.save()
products.append(product)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
If I'm understanding correctly, I'm using the delegate as a sort of link between the two so that I can pass the user-entered values directly into my save function? Correct me if I'm wrong I'm pretty new. But I create the delegate outside the class scope at the top of ProductController (the tableview controller) like so:
protocol ProductSaveDelegate {
func save(name: String, url: String, image: String)
}
Then in AddProductController (the modal view where the user enters the new product information) I initialize the delegate near the top of the class:
var productSaveDelegate: ProductSaveDelegate?
And then use it to call the save function in handleSave() as seen above.
When I try to add ProductSaveDelegate to the class definition of AddProductController I get an error saying that AddProductController does not conform to the protocol.
What can I change here to make the user-entered product save to core data properly? Thanks in advance for any help!
Delegates are really cool and powerful, but sometimes, yeah, they can be confusing. You need to initialize the delegate inside of the ProductController, not the AddProductController.
This is a great image which shows how a delegate and protocol setup works:
Image from Andrew Bancroft's website
In this instance, your Delegate is your ProductController and your Delegator is your AddProductController
MAKE SURE YOU HAVE SUBSCRIBED THE DELEGATE
Do NOT forget -
classObject.delegate = self
I suppose you are already familiar with the delegates,
You're get this error because, either you have not implemented the delegate method from the AddProductController(which in your case is false, as i can see it implemented), or you forgot to subscribe the delegate, to do so you need to make sure -
Subscribing the delegate, i.e. before transitioning to the next controller set the delegate to self like -
// addProductController is the AddProductController Class's Object
addProductController.productSaveDelegate = self
self.navigationController?.present(addProductController, animated: true, completion: nil)
also in the same class you need to implement the method defined in the protocol, i.e func save(name: String, url: String, image: String) in your case(already implemented)
You can also check a small demo, i have impleted here for the protocols.

Core data object get saved, but doesn't show up in table until I pop to related object table and then come forward

I have three view controllers embedded in a navigation controller.
The first view controller is a list of tests, which use fetchedResultsController to populate the table.
The second view controller is a detail view controller, which allows adding a new test, editing it's detail, saving the new test or adding Questions to the test. It also includes a table of questions, which is also populated with a fetchedResultsController.
The problem I have is that when I have an existing Test which is saved, then when I go to add on questions, they correctly populate the table when I pop back to the TestDetailsVC, however, if I'm adding a new test, and then adding new questions to it, the test saves to core data, then in the QuestionDetailsVC questions also save to core data. However, when I pop back to the TestDetailsVC, the new questions don't populate the table. However, when I navigate back to the TestListVC and then go forward, the questions then populate the table correctly. I'm trying to get the questions to populate the table the first time I pop back to the table. Shouldn't the table re-fetche this data when the view loads, and the table work correctly when I navigate back since these objects are saved in core data? Why does it work correctly with a previously saved test object, but not work with a newly saved test object?
My code for saving the new test in the TestDetailsVC is:
//if we tap on a row, then we select that question to edit
//if a question is selected, we will pass that information over to
//the question editor view so it can be edited
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//make sure there is at least one question
//objs = means object selected
if let objs = controller.fetchedObjects, objs.count > 0 {
//if there is, keep track of the test which is selected
let question = objs[indexPath.row]
//pass along that test to the editor to be edited
//the sender is the selected test at that particular row
performSegue(withIdentifier: "EditQuestionSegue", sender: question)
}
}
//we need to get ready to do the segue before we call it
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "EditQuestionSegue":
//find the question needing to be edited
//pass this along to the question editing view controller
if let destination = segue.destination as? QuestionDetailsVC {
if let question = sender as? Question {
destination.questionToEdit = question
//pass along the test that the question is
//related to also
if let test = testToEdit {
destination.testToEdit = test
}
}
}
case "AddNewQuestionSegue":
//if no test to edit got passed in, it means
//we're now editing a new test
//once we type in the test title, then we have to
//make sure the test has been saved if it's new test
//before we go on to edit questions
if testToEdit == nil {
saveTestBeforeAddingQuestion()
//make sure we set up as if new
//before we start adding questions
//these are called in the view did load
}
if let destination = segue.destination as? QuestionDetailsVC {
if let test = testToEdit {
destination.testToEdit = test
}
}
default:
print("no segue this time")
}
}
}
func saveTestBeforeAddingQuestion() {
var test: Test!
//if there's not a passed in value into the testToEdit core data
//object test entity, then we're going to edit as if new
if testToEdit == nil {
//then instantiate a new test object ready to be written to
test = Test(context: context)
} else {
test = testToEdit
}
if let title = titleTextField.text {
test.title = title
}
if let abrevTitle = abrevTitleTextField.text {
test.abrevTitle = abrevTitle
}
if let author = authorTextField {
test.author = author.text
}
if let publisher = publisherTextField {
test.publisher = publisher.text
}
ad.saveContext()
//since we have now saved a new test
//let's put that test into our testToEdit variable
//so we can pass it along to the next view controller
//during our segue
if let newTestCreated = test {
testToEdit = newTestCreated
}
}
My code for saving the new question is:
#IBAction func savePressed(_ sender: UIButton) {
var question: Question!
//if there's not a passed in value into the questionToEdit core data
//object test entity, then we're going to edit as if new
if questionToEdit == nil {
//then instantiate a new test object ready to be written to
question = Question(context: context)
} else {
question = questionToEdit
}
//make sure to relate the question added to the testToEdit test
question.test = self.testToEdit
//if there is something in the sentence text field
//then assign that value to the question.sentence attribute
//of the sentence entity in our core data context
if let sentence = questionSentenceTextField.text {
question.sentence = sentence
}
if let identifier = questionIdentifierTextField.text {
question.identifier = identifier
}
if let displayOrder = questionDisplayOrderTextField.text {
question.displayOrder = Int(displayOrder) as NSNumber?
}
// save the context
ad.saveContext()
_ = navigationController?.popViewController(animated: true)
}
I spent days trying to get the question table to load, but finally discovered that it wasn't loading back up because when I pop back to the view controller it doesn't automatically reload the table with the fetchedResultsController. So I had to place a call to refetch the data in the viewWillAppear method and then reload the tableView:
(TestDetailsVC)
override func viewWillAppear(_ animated: Bool) {
guard testToEdit != nil else {return}
attempFetch()
tableView.reloadData()
}

How to keep from having repeated items on a tableView due to a function triggered several times?

My issue is that I am querying elements in viewDidLoad and adding to the tableView. On another view, the user can add elements to Parse, that I am querying again on viewDidAppear to display only the newly added elements without requiring the view to re-load.
These elements are class of user with name, age etc...
On the viewDidAppear, I am querying the elements from Parse, going through a messy filtering to find out the ones that are not already displayed on the tableView, and then I trigger my function to add it.
It appears that even though I have removed duplicate items from my array, the function to set up my user (adding his name etc) gets called several times and consequently I still end up with duplicated items on my tableView.
The corresponding codes as below:
override func viewWillAppear(animated: Bool) {
var query = PFUser.query()
query!.whereKey("username", equalTo: PFUser.currentUser()!.username!)
query!.findObjectsInBackgroundWithBlock { (objects, NSError) -> Void in
if let objects = objects as? [PFObject] {
for member in objects {
if member["Network"] != nil {
var acceptedMembers: [String] = member["Network"] as! [String]
self.usernames = acceptedMembers
////
var query2 = PFUser.query()
query2?.findObjectsInBackgroundWithBlock({ (objects2, error2) -> Void in
if let objects2 = objects2 as? [PFUser] {
for otherUser in objects2 {
if contains(self.usernames, otherUser.username!) {
var arrayName1 = [String]()
arrayName1.append(otherUser.username!)
var arrayName2 = [String]()
for n in self.arrayMem {
arrayName2.append(n.name)
dispatch_async(dispatch_get_main_queue(), {
for extra in arrayName1 {
if contains(arrayName2, extra) {
} else {
var arrayName3 = [String]()
arrayName3.append(extra)
let unique3 = NSSet(array: arrayName3).allObjects
self.plusOne = unique3.first as! String
self.nameMember = self.plusOne as String
self.setName()
self.tableView.reloadData()
}
}
})
}
}}}}) }}}}
}
PS: I have tried cleaner solution to remove duplicate, converting my class to Hashable, then Equatable and using a simple function, however it turns up that this solution, messier as it is works more efficiently.
Anyway, the question here touches the function "self.setName()" that gets called repeatedly.
Do you have any idea how could this be fixed ?
Thank you infinitely,
A different approach would be to add a new object to your array after it's added in the other view controller and then add a new row for it into your table view. I would suggest communicating the newly formed object via an NSNotification.
View Controller with Table
Fetch your objects from Parse here, do it once in viewDidLoad
func fetchParseObjects () {}
Add an observer for this class to the NSNotificationCenter, also in viewDidLoad
NSNotificationCenter.defaultCenter().addObserver(self, selector: "objectUpdated:", name: "objectUpdated", object: nil)
Remove the observer in a deinit method
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "objectUpdated", object: nil)
}
func objectUpdated(notification: NSNotification) {
if let yourKindOfObject = notification.object as? YOUR_TYPE_HERE {
// Add, update or remove an item from the array that holds the original fetched data here based on the object
// Update your tableView accordingly (add, remove or update)
}
Where I have done something similarly I have created a custom object to hold some properties of what object needs to be updated and how it should be updated.
View Controller that Updates
Do what you normally do, but send a notification with a descriptive object to use to update your original data accordingly
NSNotificationCenter.defaultCenter().postNotificationName("objectUpdated", object: YOUR_OBJECT_WITH_UPDATE_DATA)

Resources