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.
Related
So I am currently in the process of making a todo-list app: incorporating timers and also other things. While I was adding some finishing touches to the application, I wanted to make sure that I could set a due date to the task that the person sets. The problem however, arises when the application switches from the "ToDoList" table-view controller to a separate view controller with a date picker and a UITextField for adding the task to the "REALM" database with a given category, name, due date and date created.
Everything was set up and the whole system was working until I clicked save task and no task was added to the realm directory. After hours of testing with print statements, I discovered that the error arose RIGHT after a function was called in the other view controller which imported the data to the ToDoList view controller for saving. As can be seen in the code snippet below, in order for the task to be added the optional "categorySelected" needs to have a non-nil value. And when I change view controllers, this value turns to nil:
"AddTaskViewController" :
if task != "" {
let mainVC = storyboard?.instantiateViewController(withIdentifier: "tasksTable") as! TodoListViewController
dateFormatter.dateFormat = "dd-MM-yyyy"
date = dateFormatter.string(from: datePicker.date)
mainVC.dataRecieved(data1: task, data2: date)
performSegue(withIdentifier: "unwindToTable", sender: self)
}
"dataRecieved Function on the other VC + Point of error" :
func dataRecieved (data1: String, data2 : String) {
let taskName = data1
let taskDueDate = data2
if taskName != "" {
if let currentCateogry = selectedCategory {
// This is the point of error
do {
try realm.write{
let newItem = Item()
newItem.title = taskName
newItem.dateDue = taskDueDate
newItem.dateCreated = Date()
currentCateogry.items.append(newItem)
print("finished appending")
}
} catch {
print("Error aappending new items into realm: \(error)")
}
tableView.reloadData()
}
} else {
tableView.reloadData()
}
}
The variable which turns to nil is the "selectedCategory". In order to show you how this variable works, let me introduce you to a small wire-frame layout of the application.
categoryViewController is the ROOT-View controller
Creating a category just includes a UIAlertView where you put the name
Selecting the category takes you to the ToDoList view controller.
A task has a parent category of "categorySelected" and is therefore appended in that portion of realm.
The selectedCategory variable is defined as such:
var selectedCategory : Category? {
didSet{
loadItems()
}
}
The loadItems() method:
func loadItems() {
todoItems = selectedCategory?.items.sorted(byKeyPath: "title", ascending: true)
tableView.reloadData()
}
Selected Category in the CategoryViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToItems" {
let destinationVC = segue.destination as! TodoListViewController
if let indexPath = tableView.indexPathForSelectedRow{
destinationVC.selectedCategory = categories?[indexPath.row]
}
}
**Sorry for the missing } at the end.
So basically, when the person using the application clicks on a category cell, that value is transported through the view controllers before the segue is made. For that reason, the value stays inside the variable as long as the tableView controller is on the fore-ground.
When I actually go to the new view controller, this value is erased and so... despite everything working... when I press the "saveTaskButton", since the "selectedCategory" is nil... the appending never happens, and there is no task to be saved.
I am requesting for one of 2 solutions:
If I could somehow use a UIAlertView for a date input... so therefore I do not have to even change view controllers for the add task method.
Some way to restore the category value (which is of type category due to class defining systems)
The solutions I have tried thus far and the problems:
ViewDidAppear --> The function is called in the other view, when the value for the category is nil, therefore the viewDidAppear really serves no purpose
Somehow storing the value of the "selectedCategory" type category item --> Since I use the "didSet" method... If I force this value to be preserved to the next view controller I get the error selectedCategory is a "get" only dataType
I do not think that this is a very distinct problem, but I have only been learning swift for about 2-3 months and this is only the first time I have encountered such an issue. I hope you guys can help me.
Thanks!
(Also, please just comment if you need to see any other part of the code)
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.
I have a view controller that will show images from Flickr. I put Flickr API request methods in a separate class "FlickrAPIRequests", and now I need to update the image in the view controller when I get the data.
I choose to go with that using a protocol to eliminate the coupling of the two classes. How would I achieve that?
You could define a protocol and set up your FlickrAPIRequests class to take a delegate. I suggest another approach.
Set up your FlickrAPIRequests to have a method that takes a completion handler. It might look like this:
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure)
The FlickrAPIRequests function would take a URL to the file to download, as well as a block of code to execute once the file has downloaded.
You might use that function like this (In this example the class is called DownloadManager)
DownloadManager.downloadManager.downloadFileAtURL(
url,
//This is the code to execute when the data is available
//(or the network request fails)
completion: {
[weak self] //Create a capture group for self to avoid a retain cycle.
data, error in
//If self is not nil, unwrap it as "strongSelf". If self IS nil, bail out.
guard let strongSelf = self else {
return
}
if let error = error {
print("download failed. message = \(error.localizedDescription)")
strongSelf.downloadingInProgress = false
return
}
guard let data = data else {
print("Data is nil!")
strongSelf.downloadingInProgress = false
return
}
guard let image = UIImage(data: data) else {
print("Unable to load image from data")
strongSelf.downloadingInProgress = false
return
}
//Install the newly downloaded image into the image view.
strongSelf.imageView.image = image
}
)
I have a sample project on Github called Asyc_demo (link) that has uses a simple Download Manager as I've outlined above.
Using a completion handler lets you put the code that handles the completed download right in the call to start the download, and that code has access to the current scope so it can know where to put the image data once it's been downloaded.
With the delegation pattern you have to set up state in your view controller so that it remembers the downloads that it has in progress and knows what to do with them once they are complete.
Write a protocol like this one, or similar:
protocol FlickrImageDelegate: class {
func displayDownloadedImage(flickrImage: UIImage)
}
Your view controller should conform to that protocol, aka use protocol method(s) (there can be optional methods) like this:
class ViewController:UIViewController, FlickrImageDelegate {
displayDownloadedImage(flickrImage: UIImage) {
//handle image
}
}
Then in FlickrAPIRequests, you need to have a delegate property like this:
weak var flickrDelegate: FlickrImageDelegate? = nil
This is used in view controller when instantiating FlickrAPIRequests, set its instance flickrDelegate property to view controller, and in image downloading method,when you download the image, you call this:
self.flickrDelegate.displayDownloadedImage(flickrImage: downloadedImage)
You might consider using callback blocks (closures) in FlickrAPIRequests, and after you chew that up, look into FRP, promises etc :)
Hope this helps
I followed silentBob answer, and it was great except that it didn't work for me. I needed to set FlickrAPIRequests class as a singleton, so here is what I did:
protocol FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage) }
class FlickrAPIRequests{
private static let _instance = FlickrAPIRequests()
static var Instance: FlickrAPIRequests{
return _instance
}
var flickrDelegate: FlickrAPIRequestDelegate? = nil
// Make the Flickr API call
func flickrAPIRequest(_ params: [String: AnyObject], page: Int){
Then in the view controller when I press the search button:
// Set flickrDelegate protocol to self
FlickrAPIRequests.Instance.flickrDelegate = self
// Call the method for api requests
FlickrAPIRequests.Instance.flickrAPIRequest(paramsDictionary as [String : AnyObject], page: 0)
And Here is the view controller's extension to conform to the protocol:
extension FlickrViewController: FlickrAPIRequestDelegate{
func showFlickrPhoto(photo: UIImage){
self.imgView.image = photo
self.prepFiltersAndViews()
self.dismissAlert()
}
}
When the api method returns a photo I call the protocol method in the main thread:
// Perform this code in the main UI
DispatchQueue.main.async { [unowned self] in
let img = UIImage(data: photoData as Data)
self.flickrDelegate?.showFlickrPhoto(photo: img!)
self.flickrDelegate?.setPhotoTitle(photoTitle: photoTitle)
}
I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: 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
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link
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.