Still broken with Xcode 15.0 (15A240d) on macOS 14.0 (23A344).
Post
Replies
Boosts
Views
Activity
I'm pretty sure that's populated by UI tests which show the string in question and which record a screenshot. This way, you can run your tests in various locale settings and confirm the string doesn't overflow a field and get truncated.
Forgot to mention that I haven't seen the problem outside of XCTest. VoiceOver properly moves the cursor into either popover and can hit the Cancel button.
Looks normal enough to me. What exactly do you mean by "untangle"? What's the concern?
I've managed to improve performance somewhat. Modifications are in the most recent commits on the repo.
Added a property to my Core Data objects which computes the height of that row from only the object count in the relevant fields
Implemented NSOutlineViewDelegate.outlineView(_:heightOfRowByItem:) to get the height from the object
Set up my outline view delegate to observe NSManagedObjectContext, get the index of every row the outline view is showing, and tell the outline view to recompute those rows' heights
Disabled usesAutomaticRowHeights
Before the modifications, I got 172ms mean hitch duration, 168ms median. Afterwards, I get 87ms mean 88ms median.
Scrolling performance still isn't great, but that may be down to my development machine being on the older side.
The problem is your view's newdrug value always has the same UUID. It gets allocated one time when the view is instantiated. You then add the item with that UUID to the list several times.
In your add button, replace this:
viewModel.favs.insert(newdrug, at: 0)
with this:
viewModel.favs.insert(Drug(name: newdrug.name, amount: newdrug.amount, bag: newdrug.bag), at: 0)
That will cause a whole new Drug object to be created and inserted. It gets a new UUID when it's created, so it won't match the ID of any of the other objects in the array. SwiftUI will then be able to recognize it's a different object.
You can't use @FetchRequest in a function, but you absolutely can use a fetch request in a function. You just have to build an NSFetchRequest object, set its predicate and sort descriptors, then fetch it from a managed object context. Depending on exactly where that function is in your code, you may be able to access the managed object context without passing it explicitly into the function. It would go a bit like this:
func searchResults(searchingFor: String)
-> [Recipe] {
let recipeFetch = Recipe.fetchRequest()
recipeFetch.predicate = NSPredicate(format: "title CONTAINS[c] %@",searchingFor)
recipeFetch.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
let results = (try? self.managedObjectContext.fetch(recipeFetch) as [Recipe]) ?? []
return results
}
I haven't run this exact code, but it's very similar to code I'm using in several of my applications.
The problem is you have named your new entity "List". SwiftUI also has a construct named "List" which you are trying to use. The compiler is confused, and is trying to use the entity in your UI.
Change the name of your entity, clean your build folder, and rebuild. I would use something like RecipeCollection, as I mentioned in my edit in your other thread. Depending on how exactly you want it to work, "RecipeFolder" or "RecipeTag" might be better names.
I would get rid of the 'recipes' attribute on your List entity. You traverse an NSManagedObject's relationships just like how you traverse attributes, so you can get the set of all of the recipes linked to a list like "myCurrentList.recipeList".
I would also change the name of the relationship from List to Recipe to instead be "recipes", but that's more a personal preference than a technical recommendation.
Edit: Just realized after posting that you have called one of your entities "List". You are also trying to use a SwiftUI construct named "List". They are conflicting. Rename your entity to something like "RecipeCollection", clean your build folder, rebuild, and they should no longer conflict.
This part was in my original post. It's wrong, but I'm leaving it for future readers, as it's good practice when making changes in Core Data. As for your errors, after you added the new entity and the new relationships, did you clean your build folder (Shift-Command-K) and rebuild from scratch? When updating Core Data's managed object model, it sometimes falls out of sync with Xcode's UI, which can lead to really weird errors. A clean and rebuild often gets you back to rational errors.
Core Data has some value transformers already. Set the attribute to be Type: Transformable, and set the custom class to be [Int]. It should work.
Yes, though I would drive a collection view with an NSFetchedResultsController and bindings.
NSManagedObject subclasses are automatically observable. You don't need to do anything for ObservableObject conformance, nor do you need to annotate properties with @Published. In SwiftUI views, you can just add an @ObservedObject var for your managed object, and the view tracks as properties and relationships of that object update.
Exploring this further, I tried switching my relationships to be ordered. Since NSTreeController apparently can't use an ordered relationship directly, I added a computed property called rulesArray and told my NSTreeController to use that as the child key path. I also added a little code as above to see when observers were added. Finally, I added some code for the various keyPathsForValuesAffecting methods to try and cause the NSTreeController to also observe the real property.
I noticed the keyPathsForValuesAffecting methods were never called. From the example implementation in the KVO guide, I noticed the default case calls to the same method on super, so I tried adding a super.addObserver(...) at the end of the override, and it seems to be working!
The extensions to my Core Data classes look like this:
extension Layer {
@objc public override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
print("Layer.keyPathsForValuesAffectingValue(forKey \(key)) called")
switch key {
case "rulesArray" :
return Set(["rules"])
default :
return super.keyPathsForValuesAffectingValue(forKey: key)
}
}
@objc public dynamic var rulesArray: [PolicyItem] {
return self.rules?.array as? [PolicyItem] ?? []
}
public override func addObserver(
_ observer: NSObject,
forKeyPath keyPath: String,
options: NSKeyValueObservingOptions = [],
context: UnsafeMutableRawPointer?)
{
print("Adding observer \(String(describing: observer)) to object \(String(describing: self)).\(keyPath)")
super.addObserver(observer, forKeyPath: keyPath, options: options, context: context)
}
}
Each object is extended in a way that the rulesArray comes from the real relationship (or in the case of case of a Rule, comes from the Rule's passToLayer's real relationship, if it exists).
And now, when the ordered set is rearranged, the UI updates as expected! I don't actually need the addObserver(...) override, so I'm going to remove it. Still, thought this would be useful to anybody else who has a similar problem.
I've been learning more about KVO in an attempt to figure out what is going on. I've added an extension like this:
extension Rule {
public override func addObserver(
_ observer: NSObject,
forKeyPath keyPath: String,
options: NSKeyValueObservingOptions = [],
context: UnsafeMutableRawPointer?)
{
print("Adding observer \(String(describing: observer)) to object \(String(describing: self)).\(keyPath)")
}
}
for each of my entities. When I initialize the tree controller and start showing the data, I get entries like this in my log:
Adding observer <NSTreeControllerTreeNode: 0x600002901b00>, child nodes {} to object <Section: 0x600002da3930> (entity: Section; id: 0xbd8a359649691e4d <x-coredata://7DDED95F-5892-47B7-B51E-46C60FFDEA6A/Section/p20>; data: {
inRulesOf = "0xbd8a359e87a91e0d <x-coredata://7DDED95F-5892-47B7-B51E-46C60FFDEA6A/Layer/p9007>";
metaRulesPosition = 4;
name = "Some Section";
rules = "<relationship fault: 0x600000d69520 'rules'>";
}).rules
It definitely adds the observer to the childrenKeyPath for every expanded element. metaRulesPosition is my manually-maintained sorting criteria, and NSTreeController definitely does not observe it.
It looks to me like I need to find a way for a change to the metaRulesPosition in the children to trigger a change notification for the 'rules' property of the parent.
My first thought is to set up observation from the entity to each of its children, but that seems to require making my entities' Codegen manual/none so I can add a property to track the observation for later removal.
Any other ideas?
Well, I thought ordering would be easier to solve, but I can't seem to figure it out.
I couldn't get NSTreeController to work with an ordered relationship in Core Data, so I went with unordered with a manual ordering criteria which I maintain. I added a column to my outline to show the ordering criteria.
Let's say I have a section which has three rules already. The ordering criteria is 1-indexed, so the three existing rules have positions 1, 2, and 3. Now I drag a rule from another section into this one, between existing positions 1 and 2. The new rule gets position 2, but the old rule still shows position 2 in the UI, and the bottom rule still shows position 3. The new rule generally ends up below old position 2, which is incorrect.
The managed objects are getting updated correctly. I added a double-click handler like so:
override func viewWillAppear() {
...
ruleOutlineView.doubleAction = #selector(self.doubleClick)
}
@objc func doubleClick(_ sender:NSOutlineView) {
treeController.rearrangeObjects()
}
When I double-click in the outline view, the rows update to the correct ordering, and the correct rule numbers are shown.
In my object which downloads the data and updates my local Core Data store, I have tried this in the tail end of the ordering criteria update code:
let workingSection = ruleToAdd.inRulesOfSection
let workingLayer = workingSection?.inRulesOf ?? ruleToAdd.inRulesOf ?? nil
workingSection?.rules = workingSection?.rules
workingLayer?.rules = workingLayer?.rules
The intent was to mark the layer and section as dirty, to hopefully get Added a breakpoint ahead of the workingSection?.rules line. The code definitely gets called when the server responds to the drag attempt, and workingSection and workingLayer definitely contain the correct managed objects when it does. Tried updating other criteria like the section's name, but no change to behavior.
Any ideas?
Yes, there was an option I missed. NSTreeController seems to handle this really well, with a few small quirks.
I had to add a 'rules' property to my rule. Simple extension:
extension Rule {
@objc public var rules: NSSet? {
return self.passToLayer?.rules
}
}
Setting up the binding was a little weird, as I've never done that in code before. Wound up with roughly this:
@IBOutlet var ruleViewContainer:NSView!
var ruleOutlineView:NSOutlineView = NSOutlineView()
private var layerToShow:Layer
private var treeController:NSTreeController = NSTreeController()
...
override func viewWillAppear() {
...
let outlineContainer = NSScrollView(frame:ruleViewContainer.bounds)
outlineContainer.autoresizingMask = [.width, .height]
outlineContainer.documentView = ruleOutlineView
outlineContainer.hasVerticalScroller = true
outlineContainer.hasHorizontalScroller = true
ruleViewContainer.addSubview(outlineContainer)
ruleOutlineView.usesAlternatingRowBackgroundColors = true
ruleOutlineView.usesAutomaticRowHeights = true
ruleOutlineView.delegate = self
ruleOutlineView.dataSource = self
ruleOutlineView.doubleAction = #selector(self.doubleClick)
ruleOutlineView.target = self
treeController.childrenKeyPath = "rules"
treeController.bind(NSBindingName.content, to: layerToShow, withKeyPath: "rules", options: nil)
ruleOutlineView.bind(NSBindingName.content, to: treeController, withKeyPath: "arrangedObjects", options: nil)
}
Now all the items in the NSOutlineView are of type NSTreeNode. This broke my existing drag-and-drop and outlineView(_ outlineView:, viewFor tableColumn:, item:). Easy enough to fix. Just wrap all the references to item like this:
let representedItem = (item as! NSTreeNode).representedObject
Then use representedItem just like you used item before.
I handle the updates in my API interaction object, which then updates my Core Data store if the server confirms the attempted change was successful. The UI then updates.
I still have a few weird glitches. Moved objects are sometimes out of order. That should be much easier to figure out, though.