The question i have is in regards to a Core Data one-to-many relationship as of right now i have my app being able to let the user input employee information and saving to core data, then updating the employee table view. The problem i face is the relationship between the employee and delivery. Im currently trying to display a specific employees deliveries when clicking on an employee in the employee table view. After selecting an employee from the employee tableView i want it to segue to another tableview and display the employees deliveries in another UITableview.
What I'm trying to Accomplish:
1) Display Specific Employee's Deliveries
2) Add deliveries to the NSSet
Here are my two managedObjects
extension Delievery {
#NSManaged var address: String?
#NSManaged var phoneNumber: String?
#NSManaged var subTotal: NSNumber?
#NSManaged var total: NSNumber?
#NSManaged var employee: Employee?
}
extension Employee {
#NSManaged var first: String?
#NSManaged var last: String?
#NSManaged var phoneNumber: String?
#NSManaged var wage: NSNumber?
#NSManaged var id: NSNumber?
#NSManaged var delievery: NSSet?
}
how i prepared for segue from employeeTableViewController to deliveryTableViewContorller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue == "DelieverySegue"{
let employeeDeliveries = segue.destinationViewController as! DelieveryTableViewController
let indexPath = self.tableView.indexPathForSelectedRow
let selectedEmployee = employees[indexPath!.row]
employeeDeliveries.employee = selectedEmployee
}
}
The variables of the deliveryTableViewController are
var employee: NSManagedObject!
var deliveries : [NSManagedObject]!
var managedObjectContext : NSManagedObjectContext!
In this photo it shows the rest of my deliveryTableViewController the problem i have is how do i return the amount of deliveries for a specific employee in the numberOfRowsInSection function and how do i fetch the deliveries of the employee.
deliveryTableViewController set up
In this last photo my question is how to i add this delivery entry to the employee selected? this is how I'm currently saving my delivery entries
how i save my delivery entries
If you made it this far i appreciate you looking through my problem. If anyone can help me the slightest with this issue i'd appreciate it if you feel i've left some information out that is needed please let me know.
UPDATE:
Here is the picture of the DelieveryTableViewController (yes i know i spelt delivery wrong)
also need to set the NSPredicate
this is home I'm preparing for segue in EmployeeTableViewController
these are my variables for EmployeeTableViewController
Setting the relationship
With one to many relationships, it is easiest to set the to-one relationship:
delivery.employee = employee
Put this line in your completeOrder method (you may need to pass the employee reference from a previous VC). CoreData will automatically set the inverse relationship for you, adding delivery to the deliveries set for the employee.
Showing the Deliveries in a Table View
Having set the relationship, the deliveries property of employee contains a set of Delivery objects to which it is related. To display these in a table view, create your deliveries array from this set (eg. in viewDidLoad):
deliveries = employee.delivery.allObjects()
Your numberOfRowsInSection can then just use deliveries.count and the cellForRowAtIndexPath can use deliveries[indexPath.row] to determine which Delivery to display in each cell.
(An alternative is to fetch the deliveries array in the normal way, but to use a predicate to restrict the results to those that are related to your employee:
fetch.predicate = NSPredicate(format:"employee == %#", employee)
Longer term, you should consider using NSFetchedResultsController which is designed for displaying CoreData objects in table view.)
Update
You don't need the thisEmployee variable. Just change the Employee variable to be Employee class:
var employee : Employee!
Then you should be able to set
deliveries = employee.deliveries?.allObjects as! [NSManagedObject]
And in your fetchDelivery() method, set the predicate with
fetchRequest.predicate = NSPredicate(format:"employee == %#", employee)
(after let fetchRequest = ....).
Update 2
It's difficult to see where the nil value is. To track it down, try printing the value of employee in the viewDidLoad method of the DelieveryTableViewController. If it's nil, there is a problem with passing the value in prepareForSegue. If not, print employee.deliveries, etc. Post your results.
Related
I thought I'd ask here first before I sent my laptop flying out of the window. I am very new to iOS development and have been toying around with Core Data trying to create an expense manager app. I have a single entity named "Account" which has two non-optional attributes (name and initBalance) as follows:
I have also created an NSManagedObject subclass as follows:
//Account.swift
class Account: NSManagedObject {
override func awakeFromInsert() {
super.awakeFromInsert()
initBalance = NSNumber(double: 0.0)
name = ""
}
}
//Account+CoreDataProperties.swift
extension Account {
#NSManaged var initBalance: NSNumber
#NSManaged var name: String
}
The Account entities are presented in the AccountsViewController (a subclass of UITableViewController conforming to the NSFetchedResultsControllerDelegate protocol) using an NSFetchedResultsController. To add a new Account I use a segue from AccountsViewController to direct me to the AccountInfoViewController (shown below) which has two textFields, one for the name and one for the balance of the account. When the latter is about to disappear a new Account is inserted in the context with the name and balance derived from the textFields. Once the parent controller appears (AccountsViewController) it tries to save the changes and updates the tableView.
Now, if I insert a new Account for which the balance is an integer number life is good; the context is able to save the changes, tableView updates its rows by showing the newly inserted account, and I am happy. When I try to insert an Account for which the balance is a decimal number, the app crashes at the point where the context tries to save the changes. I get no useful error at all as to why it happens. Here is the code for the controller managing the textFields:
class AccountInfoViewController: UIViewController {
#IBOutlet weak var nameField: UITextField!
#IBOutlet weak var balanceField: UITextField!
var store: DataStore! // set by the parent viewController everytime this controller appears
let numberFormatter = MyFormatter()
// insert the new account into the context before the controller disappears
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
store.insertAccount(nameField.text!, balance: numberFormatter.numberFromString(balanceField.text!)!)
}
}
The MyFormatter is this:
func MyFormatter() -> NSNumberFormatter {
let formatter = NSNumberFormatter()
formatter.numberStyle = .DecimalStyle
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
return formatter
}
and the DataStore shown above contains a simple Core Data Stack and is responsible for inserting into the context:
// DataStore.swift
class DataStore {
let coreDataStack = CoreDataStack(modelName: "SmartMoney")
// inserts a new account into the context
func insertAccount(name: String, balance: NSNumber) -> Account {
let account = NSEntityDescription.insertNewObjectForEntityForName("Account",
inManagedObjectContext: coreDataStack.mainQueueContext) as! Account
account.name = name
account.initBalance = balance
return account
}
}
Is there any reason why the context fails to save the changes when the balanceField contains a non-integer number? Thank you for reading and please let me know if you would like me to post any other parts of the code.
I was finally able to figure out what's going on. Changing my attribute name from initBalance to startingBalance makes everything work again. In fact, changing the attribute's name to anything that does not start with new or init works just fine. I got the idea from this post.
It seems that when using ARC, your property's name should not start with the word new. It turns out that initBalance (or newBalance for that matter) produces the same issue. Hope it helps the next poor soul running in a similar problem.
I am trying to play with relationships in xCode and I'm having some problems:
I want to create a simple User to favoriteThing relationship.
I created a model, so every user looks like this:
extension User {
#NSManaged var name: String?
#NSManaged var favoriteThings: NSSet?
}
extension FavoriteThing {
#NSManaged var thingName: String?
#NSManaged var user: User?
}
And I got a tableview conected (so it displays favoriteThing for each user).
The thing is, when I create a new favoriteThing how to add this thing to favoriteThings NSSet that is created in User class?
And what about when I delete this favoriteThing from my table view? (it is managed by fetchedResultsController) How to delete it also from mentioned favoriteThings NSSet?
Any help appreciated!
In the case of a one-to-many relationship, the easiest way to
add an object to the to-many relationship is to set the property
of the inverse to-one relationship:
theFavoriteThing.user = theUser
This automatically updates theUser.favoriteThings to include
theFavoriteThing.
And to remove it just set the inverse to-one relationship to nil:
theFavoriteThing.user = nil
I have a CoreDataentity called "Person" with 3 attributes
import Foundation
import CoreData
class Person: NSManagedObject {
#NSManaged var name: String
#NSManaged var image: NSData
#NSManaged var info: String
}
EDITED:
EXAMPLE: (I think my first "Table/chair" example caused more confusion then explanation.)
I have 7 UIButtons. When a UIButton is tapped, My TableView is modally pushed and TableView cells are populated with attribute values of all available Person via
var request = NSFetchRequest(entityName: "Person")
request.returnsObjectsAsFaults = false;
results = context.executeFetchRequest(request, error: nil)!
I want to be able to double-tap a cell, which will close TableView and return to mainViewController the Person selected, and attach it to the UIButton that called the TableView. This UIButton will now display SelectedPerson's image, name, and info (or have direct access to attribute values).
How should I go about making this possible?
Is there a NSFetchRequest Method that I could simply input something like...
NSFetchRequest(entityName: "Person", attributeName: "name", String == "John Doe")
If so I could just pass the "name" value from TableView to MainViewController, and store a Person reference in MainViewController via something like this...
var PassedNameString = "John Doe"
var Person1: Person = NSFetchRequest(entityName: "Person", attributeName: "name", String == PassedNameString) as! Person
This way I'll have a reference to what Person entity is filling a "seat" and have direct access to it's info to read or edit.
My CoreData understanding is very low.
I've only learned how to create/store a file. I do not have a understanding of what is needed to recall a CoreData file and Edit if needed. I also don't know what kind of demand is put on the device when Fetching Vs holding a Reference to CoreData entry.
In your view controller have an array (or a dictionary if it suits you better) for the chairs. Each slot in the array represents a chair, and the index of the array links to the buttons you have (also should be in an array). The chair array holds instances of Person and is initially empty.
The table view controller should use a fetched results controller, with a batch size on the fetch. This is used to populate the table as usual. When a table row is selected the associated Person should be passed back and stored in the chair array.
Doing a second fetch is pointless and leads to name duplication issues.
Think of CoreData as your database, that stores the data that is categorized by your model structures.
From your description, I would do a fetch request somewhere in your ViewController's loading sequence, say in ViewDidLoad, and store the results (Person objects) in an array of persons ... var persons = [Person](). This can be a class variable. Then I would access the Person objects by calling array[index].
Let me know if this helps you with your thinking, or where you are confused.
var persons = [Person]()
override func viewDidLoad() {
...do your fetch call...get back an array of objects as a result
for person:Person in result {
self.persons.append(person)
}
// now to access your Person objects anywhere in the class just call
// self.persons[index] to get back an Person object
}
How does one add an object to a relationship property in an NSManagedObject subclass in Swift?
In Objective-C, when you generate an NSManagedObject subclass in Xcode from the data model, there's an automatically generated class extension which contains declarations like:
#interface MyManagedObject (CoreDataGeneratedAccessors)
- (void)addMySubObject: (MyRelationshipObject *)value;
- (void)addMySubObjects: (NSSet *)values;
#end
However Xcode currently lacks this class generation capability for Swift classes.
If I try and call equivalent methods directly on the Swift object:
myObject.addSubObject(subObject)
...I get a compiler error on the method call, because these generated accessors are not visible.
I've declared the relationship property as #NSManaged, as described in the documentation.
Or do I have to revert to Objective-C objects for data models with relationships?
As of Xcode 7 and Swift 2.0 (see release note #17583057), you are able to just add the following definitions to the generated extension file:
extension PersonModel {
// This is what got generated by core data
#NSManaged var name: String?
#NSManaged var hairColor: NSNumber?
#NSManaged var parents: NSSet?
// This is what I manually added
#NSManaged func addParentsObject(value: ParentModel)
#NSManaged func removeParentsObject(value: ParentModel)
#NSManaged func addParents(value: Set<ParentModel>)
#NSManaged func removeParents(value: Set<ParentModel>)
}
This works because
The NSManaged attribute can be used with methods as well as
properties, for access to Core Data’s automatically generated
Key-Value-Coding-compliant to-many accessors.
Adding this definition will allow you to add items to your collections. Not sure why these aren't just generated automatically...
Yeah that's not going to work anymore, Swift cannot generate accessors at runtime in this way, it would break the type system.
What you have to do is use the key paths:
var manyRelation = myObject.valueForKeyPath("subObjects") as NSMutableSet
manyRelation.addObject(subObject)
/* (Not tested) */
Core Data in Objective C automatically creates setter methods (1):
By default, Core Data dynamically creates efficient public and primitive get and set accessor methods for modeled properties (attributes and relationships) of managed object classes. This includes the key-value coding mutable proxy methods such as addObject: and removes:, as detailed in the documentation for mutableSetValueForKey:—managed objects are effectively mutable proxies for all their to-many relationships.
As things currently stand with Swift in Xcode6-Beta2, you'd have to implement those accessors yourself. For example if you have an unordered to-many relationship, from Way to Node, you'd implement addNodesObject like this:
class Way : NSManagedObject {
#NSManaged var nodes : NSSet
func addNodesObject(value: Node) {
self.mutableSetValueForKey("nodes").addObject(value)
}
}
Key here is that you'd have to use mutableSetValueForKey / mutableOrderedSetValueForKey / mutableArrayValueForKey. On these sets / arrays, you can call addObject and they'll be stored on the next flush.
You can just use a typed Set instead which is far easier. Following the example provided by #Nycen and #lehn0058 in the previous answer, you can just write:
extension PersonModel {
#NSManaged var parents: Set<ParentModel>?
}
And then use the insert and remove methods of the Set.
Expanding on the solution above one to many relationships are NSMutableSet so this allows you to directly add or remove the Person NSManagedObject to the Roles in this case a Person has one Role and Roles have many Person(s)
I have tested this solution under Xcode Beta-3 and this works!
This code takes out the Department to simplify showing the one to one and one to many code required to access Roles from a Person and Persons from a Role.
import CoreData
#objc(Person) class Person: NSManagedObject {
#NSManaged var name: String
//One to One relationship in your Model
#NSManaged var roles: Roles
}
#objc(Roles) class Roles: NSManagedObject {
#NSManaged var role: String
//One to Many relationship in your Model
#NSManaged var persons: NSMutableSet
}
extension Roles {
func addPersonsObject(value: Person) {
self.persons.addObject(value)
}
func removePersonsObject(value: Person) {
self.persons.removeObject(value)
}
func addPersons(values: [Person]) {
self.persons.addObjectsFromArray(values)
}
func removePersons(values: [Person]) {
for person in values as [Person] {
self.removePersonsObject(person)
}
}
}
As of Xcode 8 and Swift 3.0, Xcode now generates accessors for relationships. For example, I have an NSManagedObject class Store, that has a one to many relationship with Items; I've called that relationship SellsItems. The generated class for Store now has the following extension to add and remove from SellsItems. Adding or removing items to the relationship is as simple as calling these functions.
// MARK: Generated accessors for sellsItems
extension Store {
#objc(addSellsItemsObject:)
#NSManaged public func addToSellsItems(_ value: Item)
#objc(removeSellsItemsObject:)
#NSManaged public func removeFromSellsItems(_ value: Item)
#objc(addSellsItems:)
#NSManaged public func addToSellsItems(_ values: NSSet)
#objc(removeSellsItems:)
#NSManaged public func removeFromSellsItems(_ values: NSSet)
}
As you only need to set one side of a relationship for both to be set nowadays, it's particularly simple if you have a 1<->many relationship, e.g. a Department object has multiple Person objects, then you can just use:
aPerson.department = aDepartment
If you check you'll find that aDepartment.people (assuming that is the reciprocal relationship you've set up) will now contain the 'aPerson' Person object.
If the relationship is many<->many then one of the more complex solutions above appears necessary.
Let's say you have the following entities:
Person
Role
Department
In your Person entity, they have a to-many relationship with Role and to-one with Department. Your managed object might look something like this:
class Person : NSManagedObject
{
#NSManaged var roles : Array<Role>
#NSManaged var department : Department
}
Relationships with inverses (all should have them) only require one side to be set for the link to be established.
For example, if you set a Person's department property to a Department object, the inverse Department.people property would now also have this Person object contained inside.
Let's say I have two entities, Employee and Department. A department has a to-many relationship with an employee, many employees can be in each department but each employee only belongs to one department. I want to display all of the employees in a table view sorted by data that is a property of the department they belong to using an NSFetchedResultsController. The problem is that I want my table to update when a department object receives changes just like it does if the regular properties of employee change, but the NSFetchedResultsController doesn't seem to track related objects. I've gotten passed this issue partially by doing the following:
for (Employee* employee in department.employees) {
[employee willChangeValueForKey:#"dept"];
}
/* Make Changes to department object */
for (Employee* employee in department.employees) {
[employee didChangeValueForKey:#"dept"];
}
This is obviously not ideal but it does cause the employee based FRC delegate method didChangeObject to get called. The real problem I have left now is in the sorting a FRC that is tracking employee objects:
NSEntityDescription *employee = [NSEntityDescription entityForName:#"Employee" inManagedObjectContext:self.managedObjectContext];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"department.someProperty" ascending:NO];
This works great and sorts the employees correctly the first time it's called, the problem is that when I make changes to some property of a department that should change the sorting of my employee table, nothing happens. Is there any nice way to have my employee FRC track changes in a relationship? Particularly I just need some way to have it update the sorting when the sort is based on a related property. I've looked through some similar questions but wasn't able to find a satisfactory solution.
The NSFetchedResultsController is only designed to watch one entity at a time. Your setup, while it makes sense, it a bit beyond what the NSFetchedResultsController is currently capable of watching on its own.
My recommendation would be to set up your own watcher. You can base it off the ZSContextWatcher I have set up on GitHub, or you can make it even more straightforward.
Basically, you want to watch for NSManagedObjectContextDidSaveNotification postings and then reload your table when one fire that contains your department entity.
I would also recommend filing a rdar with Apple and asking for the NSFetchedResultsController to be improved.
Swift
Because the NSFetchedResultsController is designed for one entity at a time, you have to listen to the NSManagedObjectContextObjectsDidChangeNotification in order to be notified about all entity relationship changes.
Here is an example:
//UITableViewController
//...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChangeHandler(notification:)), name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
#objc fileprivate func managedObjectsDidChangeHandler(notification: NSNotification) {
tableView.reloadData()
}
//...
This is a known limitation of NSFetchedResultsController: it only monitors the changes of you entity's properties, not of its relationships' properties. But your use case is totally valid, and here is how to get over it.
Working Principle
After navigating a lot of possible solutions, now I just create two NSFetchedResultsController: the initial one (in your case, Employee), and another one to monitor the entities in the said relationship (Department). Then, when a Department instance is updated in the way it should update your Employee FRC, I just fake a change of the instances of affiliated Employee using the NSFetchedResultsControllerDelegate protocol. Note that the monitored Department property must be part of the NSSortDescriptors of its NSFetchedResultsController for this to work.
Example code
In your example if would work this way:
In your view controller:
var employeesFetchedResultsController:NSFetchedResultsController!
var departmentsFetchedResultsController:NSFetchedResultsController!
Also make sure you declare conformance to NSFetchedResultsControllerDelegate in the class declaration.
In viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
// [...]
employeesFetchedResultsController = newEmployeesFetchedResultsController()
departmentsFetchedResultsController = newDepartmentsFetchedResultsController()
// [...]
}
In the departmentsFetchedResultsController creation:
func newDepartmentsFetchedResultsController() {
// [specify predicate, fetchRequest, etc. as usual ]
let monitoredPropertySortDescriptor:NSSortDescriptor = NSSortDescriptor(key: "monitored_property", ascending: true)
request.sortDescriptors = [monitoredPropertySortDescriptor]
// continue with performFetch, etc
}
In the NSFetchedResultsControllerDelegate methods:
That's where the magic operates:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if controller == departmentsFetchedResultsController {
switch(type){
case .insert, .delete, .update:
managedObjectContext.performAndWait {
let department = anObject as! Department
for employee in (department.employees ?? []) {
// we fake modifying each Employee so the FRC will refresh itself.
let employee = employee as! Employee // pure type casting
employee.department = department
}
}
break
default:
break
}
}
}
This fake update of the department of each impacted employee will trigger the proper update of employeesFetchedResultsController as expected.
SwiftUI
I haven't seen posts that directly addressed this issue in SwiftUI. After trying solutions outlined in many posts, and trying to avoid writing custom controllers, the single factor that made it work in SwiftUI—which was part of the previous post from harrouet (thank you!)—is:
Make use of a FetchRequest on Employee.
If you care about, say, the employee count per department, the fake relationship updates did not make a difference in SwiftUI. Neither did any willChangeValue or didChangeValue statements. Actually, willChangeValue caused crashes on my end.
Here's a setup that worked:
import CoreData
struct SomeView: View {
#FetchRequest var departments: FetchedResults<Department>
// The following is only used to capture department relationship changes
#FetchRequest var employees: FetchedResults<Employee>
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: employees.count)
}
}
//.id(employees.count) does not trigger view updates
}
}
struct DepartmentView: View {
var department: Department
// Not used, but necessary for the department view to be refreshed upon employee updates
var totalEmployeeCount: Int
var body: some View {
// The department's employee count will be refreshed when, say,
// a new employee is created and added to the department
Text("\(department) has \(department.employees.count) employee(s)")
}
}
I don't know if this fixes all the potential issues with CoreData relationships not propagating to views, and it may present efficiency issues if the number of employees is very large, but it worked for me.
An alternative that also worked for establishing the right employee count without grabbing all employees—which may address the efficiency issue of the above code snippet—is to create a view dependency on a NSFetchRequestResultType.countResultType type of FetchRequest:
// Somewhere in a DataManager:
import CoreData
final class DataManager {
static let shared = DataManager()
let persistenceController: PersistenceController
let context: NSManagedObjectContext!
init(persistenceController: PersistenceController = .shared) {
self.persistenceController = persistenceController
self.context = persistenceController.container.viewContext
}
func employeeCount() -> Int {
var count: Int = 0
context.performAndWait {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Employee")
fetchRequest.predicate = nil
fetchRequest.resultType = NSFetchRequestResultType.countResultType
do {
count = try context.count(for: fetchRequest)
} catch {
fatalError("error \(error)")
}
}
return count
}
}
And the main View becomes:
import CoreData
struct SomeView: View {
#FetchRequest var departments: FetchedResults<Department>
// No #FetchRequest for all employees
var dataManager = DataManager.shared
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: dataManager.employeeCount())
}
}
//.id(dataManager.employeeCount()) does not trigger view updates
}
}
// DepartmentView stays the same.
Again, this may not resolve all possible relationship dependencies, but it gives hope that view updates can be prompted by considering various types of FetchRequest dependencies within the SwiftUI views.
A note that DataManager needs NOT be an ObservableObject being observed in the View for this to work.