I am trying to implement ForEach.onMove using Core Data. I have two entities, Parent with a To-Many relationship to Child, with the relationship marked as Ordered. The view is something like this:
struct ParentView : View {
#ObservedObject var parent : Parent
var body : some View {
List {
ForEach($parent.children! as! [Child]) { child in
ChildView(child)
}.onMove {
parent.managedObjectContext!.performAndWait { indices, to in
(parent.children! as! NSMutableOrderedSet).moveObjects(at: indices, to: to)
try! parent.managedObjectContext!.save()
parent.objectWillChange.send()
}
}
}
}
}
Results are:
No errors of any kind.
During debug of the onMove function, I can see that the items are re-ordered as required
The managedObjectContext.updatedObjects is empty at the same debug step, before the call to save()
When reloading the app, the re-ordering is obviously not saved (apparently because the updatedObjects set was empty at #3.)
What am I doing wrong? How can I make the MOC realize the re-ordering change?
I fixed this, apparently with a simple solution. I am posting this for future generations of google searchers. :D
The problem is with this line:
(parent.children! as! NSMutableOrderedSet).moveObjects(at: indices, to: to)
Apparently, taking the immutable version and making it mutable works, but doesn't update the managed object. This works:
parent.mutableOrderedSetValue(forKey: "children").moveObjects(at: indices, to: to)
Related
My end goal is to create a SwiftUI List with children. I have sections (parent) that contain projects (children), and the sections will expand and collapse their list of projects.
I have this built with an NSOutlineView using Realm, but now I'm trying to build it with SwiftUI and Core Data. *GULP* : )
I'm not very experienced with Core Data, but I think I have my data set up right. I have a Section entity and a Project entity, and the Section has a projects attribute that has a to-many relationship.
So, I presume (incorrectly as shown below) section.projects is a collection of Project objects.
For simplicity, I'm just trying to nest to the two inside a List (I'll worry about the DisclosureGroup stuff later):
List{
ForEach(sections, id: \.recordName){ section in
Text(section.name)
ForEach(section.projects, id: \.recordName){ project in //<-- ERROR
Text(project.name)
}
}
}
The line marked above has two errors that seems to suggest I don't have an array of objects to work with in section.projects:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'NSObject' conform to 'RandomAccessCollection'
Value of optional type 'NSOrderedSet?' must be unwrapped to a value of type 'NSOrderedSet'
If section.projects is an NSOrderedSet, why can't I iterate over it?
I figured this out with the help of this article: https://www.hackingwithswift.com/books/ios-swiftui/one-to-many-relationships-with-core-data-swiftui-and-fetchrequest
I had to change my Core Data entities to have their Codegen setting be Manual/None (in the .xcdatamodeld editor in Xcode). I then select my entities and go to Editor > Create NSManagedObject Subclass... which created a bunch of files that reflect my Core Data models.
Then inside the Section+CoreDataProperties.swift file, I added a computed property to give me access to my projects (sorted by name):
public var projectArray: [Project] {
let set = projects as? Set<Project> ?? []
return set.sorted {
$0.wrappedName < $1.wrappedName
}
}
And then, for convenience, inside Project+CoreDataProperties.swift I added this:
public var wrappedName: String{
name ?? "Unknown Name"
}
...which allows my computed property to sort my projects by name (since Core Data treats strings as optionals by default).
As a result, I was able to iterate over my nested data like this:
ForEach(sections, id: \.self) { section in
Text(section.wrappedName)
ForEach(section.projectArray, id: \.self) { project in
Text(project.wrappedName)
}
}
I have a view using core data and xcdatamodeld file that contains the definition for an Item struct. If I use Xcode to generate the files for Item and manually manage them, preview works fine. However, when I use Codegen in either of the other formations, I get errors saying that the entire struct is undefined. This prevents the previews from working.
Code:
struct ArchiveView: View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
#FetchRequest(entity: Item.entity(), sortDescriptors: []) var fetchedResults: FetchedResults<Item>
var body: some View {
return NavigationView {
List(fetchedResults, id: \.self ) { (fetchedResult: Item) in
return PurchaseView(name: fetchedResults.name price: fetchedResults.price, purchaseDate: fetchedResults.date)
}
}.navigationBarTitle("Order")
}
}
Both the Item+CoreDataClass and Item+CoreDataProperties are missing since they are automatically generated by xcode.
I am now using manual Codegen to be able to see the previews, but am curious whether I could use the other options. How can I use Class Defintion Codegen for the core data files and still be able to use SwiftUI previews?
After some testing, something appears to be wrong with the xcode code generator itself. Whenever I change the code generator to anything other than Class Definition at least once, this breaks the linking with previews.
The fix was to create a new project and copy over the old files.
I'm trying to build an example BBQ app to learn SwiftUI (XCode 11 beta 5), and have been unable to figure out how to navigate to an object's detail view from a list view of objects coming from Core Data. Xcode is unhelpful, mostly popping unrelated errors depending on what I try.
I've tried applying state, bindings, observable objects etc to the best of my logical ability but haven't been able to crack it.
Here is my list view, which builds just fine, and allows me to navigate to destination (which is just a view containing the id property as a string):
struct CookListView: View {
#ObservedObject var cookListVM: CookListViewModel
#State var cookCreatorVM = CookCreatorViewModel()
init() {
cookListVM = CookListViewModel()
}
var body: some View {
NavigationView {
List {
ForEach(cookListVM.cooks, id: \.id) { cook in
NavigationLink(destination: Text("\(cook.id)")) {
VStack {
Text(cook.protein)
}
}
}
}.navigationBarTitle("BBQ")
}
}
}
But if I change my NavigationLink destination like so:
NavigationView {
List {
ForEach(cookListVM.cooks, id: \.id) { cook in
NavigationLink(destination: CookDetailView(cook: cook)) {
VStack {
Text(cook.protein)
}
}
}
}
}
Xcode will no longer build the project. The errors it gives me seem somewhat unrelated, such as Type '_' has no member 'id' on the ForEach line, or if I remove , id: \.id from the ForEach (which I don't really need thanks to Identifiable), I'll get Type of expression is ambiguous without more context on the Text(cook.protein) line.
If I use a hard-coded array it builds and can navigate perfectly well. The issue only arises when I'm trying to use Core Data.
My CookDetailView looks like this:
struct CookDetailView: View {
var cook: Cook
var body: some View {
VStack {
Text("\(cook.protein!)")
}
}
}
And the model for the Cook object itself looks like this:
class CookViewModel: ObservableObject, Identifiable {
var protein: String = ""
var type: String = ""
var id: UUID = UUID()
init(cook: Cook){
self.protein = cook.protein!
self.type = cook.type!
self.id = UUID()
}
}
It's also been set up thru the .xcdatamodeld file.
I'm more than happy to add in any additional/omitted code, such as how I'm writing to/reading from core data, if that would be helpful.
I can totally identify with your frustration with Xcode's error messages. The actual error is often nowhere near the error message. [Sug: use source control and commit after each clean compile ;-) ] The error that you are getting in your List view is because you specified a wrong type in your detail view's argument list. Try the following:
struct CookDetailView: View {
var cook: CookViewModel
var body: some View {
VStack {
Text("\(cook.name)")
}
}
}
By the way, since you are fetching into an array, you will not observe changes in that array unless you manually publish them. I gave up on trying to get my NSManagedObject subclasses to publish their own changes. There are two ways to workaround that. You can call objectWillChange from NSFetchedResultsController's controllerDidChangeContent delegate or you can do the same while observing NSManagedObjectContextDidSave notifications when not using a fetched results controller.
This answer is applicable to Beta 5. Things will certainly change in future betas.
I get strange errors when I am trying to pass around NSManagedObject through several functions. (all are in the same VC).
Here are the two functions in question:
func syncLocal(item:NSManagedObject,completionHandler:(NSManagedObject!,SyncResponse)->Void) {
let savedValues = item.dictionaryWithValuesForKeys([
"score",
"progress",
"player"])
doUpload(savedParams) { //do a POST request using params with Alamofire
(success) in
if success {
completionHandler(item,.Success)
} else {
completionHandler(item,.Failure)
}
}
}
func getSavedScores() {
do {
debugPrint("TRYING TO FETCH LOCAL SCORES")
try frc.performFetch()
if let results = frc.sections?[0].objects as? [NSManagedObject] {
if results.count > 0 {
print("TOTAL SCORE COUNT: \(results.count)")
let incomplete = results.filter({$0.valueForKey("success") as! Bool == false })
print("INCOMPLETE COUNT: \(incomplete.count)")
let complete = results.filter({$0.valueForKey("success") as! Bool == true })
print("COMPLETE COUNT: \(complete.count)")
if incomplete.count > 0 {
for pendingItem in incomplete {
self.syncScoring(pendingItem) {
(returnItem,response) in
let footest = returnItem.valueForKey("player") //only works if stripping syncScoring blank
switch response { //response is an enum
case .Success:
print("SUCCESS")
case .Duplicate:
print("DUPLICATE")
case .Failure:
print("FAIL")
}
}
} //sorry for this pyramid of doom
}
}
}
} catch {
print("ERROR FETCHING RESULTS")
}
}
What I am trying to achieve:
1. Look for locally saved scores that could not submitted to the server.
2. If there are unsubmitted scores, start the POST call to the server.
3. If POST gets 200:ok mark item.key "success" with value "true"
For some odd reason I can not access returnItem at all in the code editor - only if I completely delete any code in syncLocal so it looks like
func syncLocal(item:NSManagedObject,completionHandler:(NSManagedObject!,SyncResponse)->Void) {
completionHandler(item,.Success)
}
If I do that I can access .syntax properties in the returning block down in the for loop.
Weirdly if I paste the stuff back in, in syncLocal the completion block keeps being functional, the app compiles and it will be executed properly.
Is this some kind of strange XCode7 Bug? Intended NSManagedObject behaviour?
line 1 was written with stripped, line 2 pasted rest call back in
There is thread confinement in Core Data managed object contexts. That means that you can use a particular managed object and its context only in one and the same thread.
In your code, you seem to be using controller-wide variables, such as item. I am assuming the item is a NSManagedObject or subclass thereof, and that its context is just one single context you are using in your app. The FRC context must be the main thread context (a NSManagedObjectContext with concurrency type NSMainThreadConcurrencyType).
Obviously, the callback from the server request will be on a background thread. So you cannot use your managed objects.
You have two solutions. Either you create a child context, do the updates you need to do, save, and then save the main context. This is a bit more involved and you can look for numerous examples and tutorials out there to get started. This is the standard and most robust solution.
Alternatively, inside your background callback, you simply make sure the context updates occur on the main thread.
dispatch_async(dispatch_get_main_queue()) {
// update your managed objects & save
}
I have the code below but I'm getting ConcurrentModificationException, how should I avoid this issue? (I have to use WeakHashMap for some reason)
WeakHashMap<String, Object> data = new WeakHashMap<String, Object>();
// some initialization code for data
for (String key : data.keySet()) {
if (data.get(key) != null && data.get(key).equals(value)) {
//do something to modify the key
}
}
The Javadoc for WeakHashMap class explains why this would happen:
Map invariants do not hold for this class. Because the garbage
collector may discard keys at any time, a WeakHashMap may behave as
though an unknown thread is silently removing entries
Furthermore, the iterator generated under the hood by the enhanced for-loop you're using is of fail-fast type as per quoted explanation in that javadoc.
The iterators returned by the iterator method of the collections
returned by all of this class's "collection view methods" are
fail-fast: if the map is structurally modified at any time after the
iterator is created, in any way except through the iterator's own
remove method, the iterator will throw a
ConcurrentModificationException. Thus, in the face of concurrent
modification, the iterator fails quickly and cleanly, rather than
risking arbitrary, non-deterministic behavior at an undetermined time
in the future.
Therefore your loop can throw this exception for these reasons:
Garbage collector has removed an object in the keyset.
Something outside the code added an object to that map.
A modification occurred inside the loop.
As your intent appears to be processing the objects that are not GC'd yet, I would suggest using an iterator as follows:
Iterator<String> it = data.keySet().iterator();
int count = 0;
int maxTries = 3;
while(true) {
try {
while (it.hasNext()) {
String str = it.next();
// do something
}
break;
} catch (ConcurrentModificationException e) {
it = data.keySet().iterator(); // get a new iterator
if (++count == maxTries) throw e;
}
}
You can clone the key set first, but note that you hold the strong reference after that:
Set<KeyType> keys;
while(true) {
try {
keys = new HashSet<>(weakHashMap.keySet());
break;
} catch (ConcurrentModificationException ignore) {
}
}
for (KeyType key : keys) {
// ...
}
WeakHashMap's entries are automatically removed when no ordinary use of the key is realized anymore, this may happens in a different thread. While cloning the keySet() into a different Set a concurrent Thread may remove entries meanwhile, in this case a ConcurrentModificationException will 100% be thrown! You must synchronize the cloning.
Example:
Collections.synchronizedMap(data);
Please understand that
Collections.synchronizedSet(data.keySet());
Can not be used because data.keySet() rely on data's instance who is not synchronized here! More detail: synchronize(keySet) prevents the execution of methods on the keySet but keySet's remove-method is never called but WeakHashMap's remove-method is called so you have to synchronize over WeakHashMap!
Probably because your // do something in the iteration is actually modifying the underlying collection.
From ConcurrentModificationException:
For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.
And from (Weak)HashMap's keySet():
Returns a Set view of the keys contained in this map. The set is backed by the map, so changes to the map are reflected in the set, and vice-versa. If the map is modified while an iteration over the set is in progress (except through the iterator's own remove operation), the results of the iteration are undefined.