How to update user interface on Core Data? - swift

I have an app with UITableView, Core Data and NSFetchedResultsController as well. I have passed data to the DetailViewController. And I can delete them from the DetailViewController! In the Apple's iOS Notes app, you can see such as functions as I wanted! When you delete a notes from the DetailViewController ( for example ), object deleted and Notes app automaticlly shows the next or previos notes! I want to create such as function. How update user interface after deleted current object? Here's my codes! Thanks `
import UIKit
import CoreData
class DetailViewController: UIViewController {
#IBOutlet weak var containerLabel: UILabel!
var retrieveData:NSManagedObject!
var managedObjectContext:NSManagedObjectContext!
var manager:Manager!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.containerLabel.userInteractionEnabled = false
self.containerLabel.textColor = UIColor.redColor()
self.containerLabel.alpha = 0
UIView.animateWithDuration(2.5) { () -> Void in
self.containerLabel.alpha = 1
}
if let demo = self.retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func backToMain(sender: AnyObject) {
// Back to the MainTableViewController
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func trashButton(sender: AnyObject) {
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
} catch {
}
self.dismissViewControllerAnimated(true, completion: nil)
}
`
If I have 5 items on the list like so:
When I select fourth item from the list ( for example ). And detailVC shows me selected item like this:
And I want to delete them. When I delete "Four" and then my containerLabel.text shows previous objects from the list. They're after "Four" is deleted, "Three","Two" and "One" as well. After "One" is deleted my containerLabel.text shows strings
But I have left single object called as "Five"
My problem is "Five"! I can't delete it. Example: In iOS Notes App, if you have five objects on the list like my demo app. When you select fourth object from the list ( for example ). And begin deleting them, after "Four" is delete iOS Notes App shows "Five". And "Five" ( last object on the list ) is deleted and then iOS Notes App shows "Three", "Two" and "One". Maybe problem line is here:
if index != 0 {
self.retrieveData = fetchedObject[index! - 1]
} else {
self.retrieveData == fetchedObject[0]
}

Let's take the easy (but not so elegant) route here. You'll have to pass over all the fetched objects to the detail VC like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegueIdentifier"{
if let destinationVC = segue.destinationViewController as? DetailViewController{
destinationVC.managedObjectContext = yourContext
destinationVC.retrieveData = yourManagedObject
destinationVC.arrayOfFetchedObjects = yourFetchedResultsController.fetchedObjects
//pass over other data...
}
}
}
Then, in your detailVC, write a method that will be executed when you press the delete button. Something like this:
#IBAction func trashButton(sender: AnyObject) {
//make sure you have an array with YourObjects
guard let fetchedObjects = arrayOfFetchedObjects as? [YourObjectType] else {return}
//get index of the shown object in the array of fetched objects
let indexOfObject = fetchedObjects.indexOf(retrieveData)
//delete the object from the context
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
//delete the object from the fetchedObjects array
fetchedObjects.removeAtIndex(indexOfObject)
} catch {
}
//get the object that should be shown after the delete
if indexOfObject != 0{
//we want the object that represents the 'older' note
retrieveData = fetchedObjects[indexOfObject - 1]
updateUserInterface(true)
}
else{
//the index was 0, so the deleted object was the oldest. The object that is the oldest after the delete now takes index 0, so just use this index. Also check for an empty array.
if fetchedObjects.isEmpty{
updateUserInterface(false)
}
else{
retrieveData = fetchedObjects[0]
updateUserInterface(true)
}
}
}
func updateUserInterface(note: Bool){
switch note{
case true:
//update the user interface
if let demo = retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
case false:
self.containerLabel.text = "no more notes"
}
}

You either need to pass the details view controller
A list of all managed objects and an index for where in the list to start
A current managed object and a callback to get the next object
In order for it to have enough information to do what you want. The callback approach is nicest and is a simple form of delegate, where your master view controller is the delegate supplying the extra data.

Related

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

Download single Object of Firestore and save it into an struct/class object

I am coding since January 2019 and this is my first post here.
I am using Swift and Firestore. In my App is a tableView where I display events loaded out of a single Document with an array of events inside as [String: [String:Any]]. If the user wants to get more infos about an event he taps on it. In the background the TableViewController will open a new "DetailEventViewController" with a segue and give it the value of the eventID in the tapped cell.
When the user is on the DetailViewController Screen the app will download a new Document with the EventID as key for the document.
I wanna save this Data out of Firestore in a Struct called Event. For this example just with Event(eventName: String).
When I get all the data I can print it directly out but I can't save it in a variable and print it out later. I really don't know why. If I print the struct INSIDE the brackets where I get the data its working but if I save it into a variable and try to use this variable it says its nil.
So how can I fetch data out of Firestore and save in just a Single ValueObject (var currentEvent = Event? -> currentEvent = Event.event(for: data as [String:Any]) )
I search in google, firebaseDoc and stackoverflow but didn't find anything about it so I tried to save all the singe infos of the data inside a singe value.
// Struct
struct Event {
var eventName: String!
static func event(for eventData: [String:Any]) -> Event? {
guard let _eventName = eventData["eventName"] as? String
else {
print("error")
return nil
}
return Event(eventName: _eventName)
}
// TableView VC this should work
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowEventDetailSegue" {
if let ShowEvent = segue.destination as? DetailEventViewController, let event = eventForSegue{
ShowEvent.currentEventId = event.eventID
}
}
}
// DetailViewController
var currentEvent = Event()
var currentEventId: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let _eventID = currentEventId else {
print("error in EventID")
return}
setupEvent(eventID: _eventID) /* currentEvent should be set here */
setupView(event: currentEvent) /* currentEvent has after "setupEvent" the value of nil */
}
func setupEvent(eventID: String) {
let FirestoreRef = Firestore.firestore().collection("events").document(eventID)
FirestoreRef.getDocument { (document, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
SVProgressHUD.showError(withStatus: "Error in Download")
}else {
if let document = document, document.exists {
guard let data = document.data() else {return}
let eventData = Event.event(for: data as [String:Any])
print(eventData)
//here all infos are printed out - so I get them
self.currentEvent = eventData!
//Here is the error.. I can't save the fetched Data in my single current Event
} else {
SVProgressHUD.showError(withStatus: "Error")
}
}
}
}
func setupView(event: Event) {
self.titleLabel.text = event.eventName
}
I expect that the function setupEvents will give the currentEvent in the DetailViewController a SINGLEvalue cause its a SINGLE document not an array. So I can use this single Eventvalue for further actions. Like starting a new segue for a new ViewController and just push the Event there not

How to do an undo function within a tableviewcell?

I am having troubles getting my undo button working. I'm trying to get it to where if you press the delete button on a tableview cell, the undo can re-enter the cell.
My undo:
#IBAction func undoBtnWasPressed(_ sender: Any) {
undoItem()
undoView.isHidden = true
}
func undoItem() {
undoManager?.registerUndo(withTarget: GoalCell.self, selector: #selector(removeGoal(atIndexPath:)), object: nil)
undoManager?.undo()
}
My remove:
#objc func removeGoal(atIndexPath indexPath: IndexPath) {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
managedContext.delete(goals[indexPath.row])
undoView.isHidden = false
do {
try managedContext.save()
print("Successfully removed goal.")
} catch {
debugPrint("Could not save: \(error.localizedDescription)")
}
}
You can just save the data from the datasource of the deleted cell in a property or an array and if the undo button is pressed, you re-add the data into the datasource and reload the row or the full data of the tableview.
EDIT
So for instance, in your example you had the delete function delete data from core data based on goals[indexPath.row]
Before deleting, save the content from goals[indexPath.row] into a separate array.
When undo is tapped, just grab the value from the separate array and add it back to goals and add it back into core data.
Then just do a tableview.reloadData()
EDIT 2:
let dataSource: [CustomObject] = [Object1, Object2, Object3]
var undoSource: [CustomObject] = []
func removeRow(indexPath) {
let object = dataSource[indexPath.row]
undoSource.append(object)
dataSource.remove(object)
}
func undo() {
for object in undoSource {
dataSource.append(object)
}
tableView.reloadData()
}

Adding a new page into PageViewController

I am a Swift noob and am making a simple weather app. I used the Page-Based Application template.
The problem I have is the following:
When the user adds a city I call addCity and successfully append the new city name to my cities array. When I print that array in that function, it shows the new city at the end.
However, the function viewControllerAtIndex that creates a new page seems to use the old version of that array, without the new city appended. When I print the cities array, it is missing the new city name. Therefore, when the users swipes there is won't be a new page for the new city rendered. The user has to restart the app in order for the new city to show up.
I created a screen capture video to illustrate the problem.
https://youtu.be/DbMqgJ0lONk
(the cities array should also show "London", I think I just didn't restart the app)
I would appreciate any help here!
import UIKit
class ModelController: NSObject, UIPageViewControllerDataSource {
var rootViewController = RootViewController()
var cities = [""]
let defaults = UserDefaults.standard
override init() {
super.init()
self.cities = self.defaults.stringArray(forKey: "SavedStringArray") ?? [String]()
if self.cities == [""] || self.cities.count == 0 {
self.cities = ["Current Location"]
}
}
func addCity(name:String) {
self.cities.append(name)
self.defaults.set(self.cities, forKey: "SavedStringArray")
print ("cities from addCity:")
print (self.cities)
}
func viewControllerAtIndex(_ index: Int, storyboard: UIStoryboard) -> DataViewController? {
// Return the data view controller for the given index.
if (self.cities.count == 0) || (index >= self.cities.count) {
return nil
}
// Create a new view controller and pass suitable data.
let dataViewController = storyboard.instantiateViewController(withIdentifier: "DataViewController") as! DataViewController
//get city name
dataViewController.dataObject = self.cities[index]
print ("cities in viewControllerAtIndex:")
print (self.cities)
return dataViewController
}
func indexOfViewController(_ viewController: DataViewController) -> Int {
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
return self.cities.index(of: viewController.dataObject) ?? NSNotFound
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if (index == 0) || (index == NSNotFound) {
return nil
}
index -= 1
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == self.cities.count {
return nil
}
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
}
The problem is, that you use two different instances of ModelController. One for the RootViewController and another in the TableViewController. They don't know each other.
A couple of options to address the problem:
1.) Hand over the same instance of ModelController to TableViewController when you segue into it.
E.g. by adding this prepare(for segue:) method toRootViewController`
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if(segue.identifier == "Locations") {
let destVC: TableViewController = segue.destination as! TableViewController;
destVC.modelViewController = self.modelController;
}
}
This will ensure that the same ModelController will be handed over.
Note: you have to add this identifier ("Locations") to the segue going from Edit-button to the TableViewController scene.
Note 2: this code is untested and doesn't probably even compile. I'm not having Xcode available right now.
2.) Ensure that there cannot be more than one instance of ModelController (Singleton)
One random web link: https://thatthinginswift.com/singletons/

Swift prepareForSegue initializes empty array

when I pass data from FirstViewController to SecondViewController (table view) via prepareForSegue I have an empty array in secondViewController to collect the data, but every time the view loads the array is init as empty.
Before getting into core data or nsuserdefaults, how can I add the data tho the second view controller?
There is a similar problem here but the method has not solved my problem.
Trouble passing Array through prepareForSegue
// View Controller
// a new dictionary object is created
myDictionary = ["apples": 3, "oranges": 4, "bananas": 5]
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "saveData" {
let dvc = segue.destionationViewController as! MyTableViewController
dvc.arrayOfDictionaries += [myDictionary]
}
resetAll()
}
// My Table View Controller
var arrayOfDictionaries: [[String: AnyObject]] = []
var dictionary = [String:AnyObject]()
in TableViewContrller I have a func
override func viewDidLoad() {
super.viewDidLoad()
loadData() //
}
func loadData() {
for dict in arrayOfDictionaries {
for (key, value) in dict {
// extract data
dictionary[key] = value
}
}
}
How can I get data to persist in arrayOfDictionaries?
Thanks
Why are you appending values to the array if it is empty?
To avoid future problems, you can just call
dvc.arrayOfDictionaries = [myDictionary]
So you'll never care about the arrayOfDictionaries state (if it is initialized, if it's empty or anything else)
And please, check your viewDidLoad and other view-lifecycle methods in the table view controller. Is possible that you're initializing again the array there?