NStableView Cocoa - macos

I populated my NStableView with tableView Controller and it's working fine. I only want to know why every time I am getting the data (Presented in table Cell) whenever the user hovers on a particular cell in a tableview, it starts displaying the data in console.
I found that this - (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row calls every time and I stack traced using instruments and this method is taking a lot of memory.
Is there any way to stop this method drawing the data every time.

Add the delegate -viewWillMoveToWindow to the view subclass contain the table. here i have used the BOOL named reloadTable. NSTrackingArea is the answer for your problem
- (void) viewWillMoveToWindow:(NSWindow *)newWindow
{
// Setup a new tracking area when the view is added to the window.
NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:[yourTable frame]
options: (NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways|NSTrackingEnabledDuringMouseDrag) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void) mouseEntered:(NSEvent*)theEvent {
reloadTable=YES;
NSLog(#"enter %#",theEvent);
}
- (void) mouseExited:(NSEvent*)theEvent {
reloadTable=YES;
}
then use it in your NSTableViewDataSource methods

Related

NSCollectionView memory leaks

I have an NSCollectionView specified as both my DataSource and my Delegate.
I have two issues:
Rather than doing the registerClass method, attempting to instead use the 3 lines of commented code with the (non-nil) protoNib means of registering with an NSCollectionView causes theItem to always be nil.
Using the class registry option, all works mostly fine. But if I remove the willDisplayItem and didEndDisplayingItem stubs, the system eats up gobs of memory on its first call to itemForRepresentedObjectAtIndexPath (with thousands of internal calls to these two stubs) and eventually crashes. Instruments shows thousands of 4k #autoreleasepool content items being created by AppKit.
Any idea why this might be happening?
-(void)awakeFromNib {
[self registerClass:[MECollectionViewItem class] forItemWithIdentifier:#"EntityItem"];
// NSString *nibName = NSStringFromClass([MECollectionViewItem class]);
// NSNib *protoNib = [[NSNib alloc] initWithNibNamed:nibName bundle:nil];
// [self registerNib:protoNib forItemWithIdentifier:#"EntityItem"];
__weak typeof(self) weakSelf = self;
[self setDelegate:weakSelf];
[self setDataSource:weakSelf];
...
}
- (MECollectionViewItem *)collectionView:(NSCollectionView *)collectionView
itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath;
{
MECollectionViewItem *theItem = [self makeItemWithIdentifier:#"EntityItem"
forIndexPath:indexPath];
return theItem;
}
-(void)collectionView:(NSCollectionView *)collectionView
willDisplayItem:(NSCollectionViewItem *)item
forRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath
{
}
-(void)collectionView:(NSCollectionView *)collectionView
didEndDisplayingItem:(nonnull NSCollectionViewItem *)item
forRepresentedObjectAtIndexPath:(nonnull NSIndexPath *)indexPath
{
}
The Appkit classes are not designed to be their own delegate. NSCollectionView implements several NSCollectionViewDelegate methods and calls the delegate. I don't know why it's implemented like this and it doesn't feel right but it is what it is. If the collection view is its own delegate and a delegate method isn't implemented in the subclass then the call causes an infinite loop. Solution: don't set delegate to self.

How to write to the pasteboard with NSFilePromiseProvider

I'm trying to support multi-item dragging with NSTableView and NSCollectionView using the new NSPasteboardWriting APIs. In my real app, I have dragging working for my table view, but not for my collection view (the NSFilePromiseProviderDelegate methods never get called). When I tried building a demo app from the ground up, I was able to reproduce this with an NSTableView.
I've set breakpoints inside both methods of DragDelegate, and neither gets called. -tableView:pasteboardWriterForRow: does get called, though. When I drag outside the app, I see the row's image attached to the cursor, but as far as Finder is concerned, there are no files on the pasteboard. There's no option to drop onto the Dock or a Finder window.
An instance of CollectionController is set as my table view's dataSource. It has a single column, whose text label is bound to the represented object (since it's just an NSString). I'm running Xcode 10.0 on Mojave 10.14.0. Here are the classes I have:
CollectionController
#interface CollectionController : NSObject <NSTableViewDataSource>
#property (strong) id<NSFilePromiseProviderDelegate> dragDelegate;
#end
#implementation CollectionController
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return 1;
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row
{
return #"Test string";
}
- (id<NSPasteboardWriting>)tableView:(NSTableView *)tableView pasteboardWriterForRow:(NSInteger)row {
self.dragDelegate = [[DragDelegate alloc] init];
return [[NSFilePromiseProvider alloc] initWithFileType:#"public.text"
delegate:self.dragDelegate];
return prov;
}
#end
DragDelegate
#interface DragDelegate: NSObject <NSFilePromiseProviderDelegate>
#end
#implementation DragDelegate
- (NSString *)filePromiseProvider:(NSFilePromiseProvider *)filePromiseProvider
fileNameForType:(NSString *)fileType
{
return #"file.txt";
}
- (void)filePromiseProvider:(NSFilePromiseProvider *)filePromiseProvider
writePromiseToURL:(NSURL *)url
completionHandler:(void (^)(NSError * _Nullable))completionHandler
{
NSData *data = [#"test file contents" dataUsingEncoding:NSUTF8StringEncoding];
[data writeToURL:url atomically:YES];
completionHandler(nil);
}
#end
Set the default dragging operation with
- (void)setDraggingSourceOperationMask:(NSDragOperation)mask forLocal:(BOOL)isLocal;

What does OSX do when I customize an NSTableView cell?

I am trying to customize an NSImageCell for NSTableView using NSArrayController and bindings to change the background of the cell which is selected. So, I created two NSImage images and retain them as normalImage and activeImage in the cell instance, which means I should release these two images when the cell calls its dealloc method. And I override
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
and
- (void) setObjectValue:(id) inObject
But I find that when I click any cell in the tableview, the cell's dealloc method is called.
So I put NSLog(#"%#", self); in the dealloc method and - (void)drawInteriorWithFrame:inView: and I find that these two instance are not same.
Can anyone tell me why dealloc is called every time I click any cell? Why are these two instances not the same? What does OS X do when I customize the cell in NSTableView?
BTW: I found that the -init is called only once. Why?
EDIT:
My cell code
#implementation SETableCell {
NSImage *_bgNormal;
NSImage *_bgActive;
NSString *_currentString;
}
- (id)init {
if (self = [super init]) {
NSLog(#"setup: %#", self);
_bgNormal = [[NSImage imageNamed:#"bg_normal"] retain];
_bgActive = [[NSImage imageNamed:#"bg_active"] retain];
}
return self;
}
- (void)dealloc {
// [_bgActive release]; _bgActive = nil;
// [_bgNormal release]; _bgNormal = nil;
// [_currentString release]; _currentString = nil;
NSLog(#"dealloc: %#", self);
[super dealloc];
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSLog(#"draw: %#", self);
NSPoint point = cellFrame.origin;
NSImage *bgImg = self.isHighlighted ? _bgActive : _bgNormal;
[bgImg drawAtPoint:p fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSPoint strPoint = cellFrame.origin;
strPoint.x += 30;
strPoint.y += 30;
[_currentString drawAtPoint:strPoint withAttributes:nil];
}
- (void) setObjectValue:(id) inObject {
if (inObject != nil && ![inObject isEqualTo:_currentString]) {
[self setCurrentInfo:inObject];
}
}
- (void)setCurrentInfo:(NSString *)info {
if (_currentString != info) {
[_currentString release];
_currentString = [info copy];
}
}
#end
As a normal recommendation, you should move to ARC as it takes cares of most of the memory management tasks that you do manually, like retain, releases. My answers will assume that you are using manual memory management:
Can anyone tell me why dealloc is called every time I click any cell ?
The only way for this to happen, is if you are releasing or auto-releasing your cell. If you are re-using cells, they shouldn't be deallocated.
Why these tow instance are not same ?
If you are re-using them, the cell that you clicked, and the cell that has been deallocated, they should be different. Pay close attention to both your questions, in one you assume that you are releasing the same cell when you click on it, on the second you are seeing that they are different.
What does Apple do when I custom the cell in NSTableView ?
Apple as a company? Or Apple as in the native frameworks you are using? I am assuming you are going for the second one: a custom cell is just a subclass of something that the NSTableView is expecting, it should behave the same as a normal one plus your custom implementation.
BTW: I found that the init is called only once, and why ?
Based on this, you are probably re-using cells, and only in the beginning they are actually being initialised.
It would be very useful to see some parts of your code:
Your Cell's code
Your NSTableView cell's creation code.

How to have checkboxes and textfields in a single tableview column using NSTableViewDataSource Protocol?

How can one have checkboxes and textfield (for section headings) in a single tableview column using NSTableViewDataSource Protocol?
My requirement is to use a Cell Based TableView.
I answered your other question without any code and i think you had trouble understanding it.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
array = [[NSMutableArray alloc]initWithObjects:#0,#1,#2, nil];//instead this you can add your class object
[self.myTableView reloadData];
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [array count];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSButtonCell * cell =[[NSButtonCell alloc]init];
[cell setButtonType:NSSwitchButton];
if([array objectAtIndex:row] == [NSNumber numberWithInt:0])
{
[tableColumn setDataCell:cell];
[[tableColumn dataCell]setTitle:#"Are you single?"];// instead this you can access title from your class object or from any other storage
}
else if ([array objectAtIndex:row] == [NSNumber numberWithInt:1])
{
[tableColumn setDataCell:[[NSTextFieldCell alloc]init]];
}
else if ([array objectAtIndex:row] == [NSNumber numberWithInt:2])
{
[tableColumn setDataCell:cell];
[[tableColumn dataCell]setTitle:#"Are you happy?"];
}
return [array objectAtIndex:row];
}
So thought this would help:) Cheers.
Here are the steps to make a single column tableview where the column can have row(s) that are section headings (NSTextFieldCells) followed by rows that are checkboxes (NSButtonCells) having descriptive titles. Similar to a listbox in MS MFC. To be compatible with older versions of OS X it needs to be a Cell based tableview:
Using IB drag a tableView control into the Application Window. In the Attributes inspector set Content Mode as "Cell Based", and Columns to 1.
Using IB drag a "Check Box Cell" control from the Object Library into the Application Window's column. (note: this step probably can be omitted since in the example code shown below, the cell type is being set explicitly to be either a NSButtonCell (checkbox) or NSTextFieldCell). If one needs to expand this example to use multiple columns, then probably want to set the Identifier for the NSTableColumn(s) in IB's Identity Inspector, in order that in the code one can filter by column/row instead of only by row (i.e. inside of the method objectValueForTableColumn).
Set the TableView's datasource and delegate to be the auto generated App Delegate object (in this case ApplicationAppDelegate.h). Do this by opening IB, and using the "Connection Inspector" click and drag from the datasource circle connection icon to the "App Delegate" object icon in the IB panel that shows objects that are loaded from the NIB such as controls, controllers etc.(icon for App Delegate is a blue cube). Do the same click and drag operation with the delegate circle icon.
Open the Assistant Editor, with the App Delegate's .h file showing in the left vertical pane and the IB view of the Table View in the right vertical pane. Select on the TableView control and create an IB outlet named "tableView" by holding the control key and dragging from the TableView Control to the section of the .h file where properties are listed.
Declare a NSMutableArray variable in the .h file. It should look like the following (Note: there has been added NSTableViewDataSource as a supported protocol of ApplicationAppDelegate):
#import <Cocoa/Cocoa.h>
#interface ApplicationAppDelegate : NSObject <NSApplicationDelegate,NSTableViewDataSource>
{
NSMutableArray *state;
}
#property (assign) IBOutlet NSWindow *window;
#property (weak) IBOutlet NSTableView *tableView;
#end
6 . Add the following functions to the App Delegate implementation file (.m):
#import "ApplicationAppDelegate.h"
#implementation ApplicationAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
state = [[NSMutableArray alloc]initWithObjects:#"Section Heading:",#0,#1, nil];//Note: values passed to NSButtonCells should be 0 or 1 or YES or NO, and the state passed to NSTextFieldCell is a NSString
[self.tableView reloadData];
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [state count];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSButtonCell * cell =[[NSButtonCell alloc]init];
[cell setButtonType:NSSwitchButton];
if (row == 0)
{
[tableColumn setDataCell:[[NSTextFieldCell alloc]init]];
}
else if (row == 1)
{
[tableColumn setDataCell:cell];
[[tableColumn dataCell]setTitle:#"title row1"];
}
else if (row == 2)
{
[tableColumn setDataCell:cell];
[[tableColumn dataCell]setTitle:#"title row2"];
}
return [state objectAtIndex:row];
}
- (void)tableView:(NSTableView *)tableView setObjectValue:(id)value forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
[state replaceObjectAtIndex:row withObject:value];
[tableView reloadData];
}
#end

NSOutlineView not refreshing when objects added to managed object context from NSOperations

Background
Cocoa app using core data Two
processes - daemon and a main UI
Daemon constantly writing to a data store
UI process reads from same data
store
Columns in NSOutlineView in UI bound to
an NSTreeController
NSTreeControllers managedObjectContext is bound to
Application with key path of
delegate.interpretedMOC
NSTreeControllers entity is set to TrainingGroup (NSManagedObject subclass is called JGTrainingGroup)
What I want
When the UI is activated, the outline view should update with the latest data inserted by the daemon.
The Problem
Main Thread Approach
I fetch all the entities I'm interested in, then iterate over them, doing refreshObject:mergeChanges:YES. This works OK - the items get refreshed correctly. However, this is all running on the main thread, so the UI locks up for 10-20 seconds whilst it refreshes. Fine, so let's move these refreshes to NSOperations that run in the background instead.
NSOperation Multithreaded Approach
As soon as I move the refreshObject:mergeChanges: call into an NSOperation, the refresh no longer works. When I add logging messages, it's clear that the new objects are loaded in by the NSOperation subclass and refreshed. It seems that no matter what I do, the NSOutlineView won't refresh.
What I've tried
I've messed around with this for 2 days solid and tried everything I can think of.
Passing objectIDs to the NSOperation to refresh instead of an entity name.
Resetting the interpretedMOC at various points - after the data refresh and before the outline view reload.
I'd subclassed NSOutlineView. I discarded my subclass and set the view back to being an instance of NSOutlineView, just in case there was any funny goings on here.
Added a rearrangeObjects call to the NSTreeController before reloading the NSOutlineView data.
Made sure I had set the staleness interval to 0 on all managed object contexts I was using.
I've got a feeling this problem is somehow related to caching core data objects in memory. But I've totally exhausted all my ideas on how I get this to work.
I'd be eternally grateful to anyone who can shed any light as to why this might not be working.
Code
Main Thread Approach
// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
// Delay to allow time for the daemon to save
[self performSelector:#selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}
-(void)refreshTrainingEntriesAndGroups {
NSSet *allTrainingGroups = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
[interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];
NSError *saveError = nil;
[interpretedMOC save:&saveError];
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// In window controller class
-(void)refreshTrainingView {
[trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
[trainingView reloadData];
}
NSOperation Multithreaded Approach
// In App Delegate (just the changed method)
-(void)refreshTrainingEntriesAndGroups {
JGRefreshEntityOperation *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
NSOperationQueue *refreshQueue = [[NSOperationQueue alloc] init];
[refreshQueue setMaxConcurrentOperationCount:1];
[refreshQueue addOperation:trainingGroupRefresh];
while ([[refreshQueue operations] count] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// JGRefreshEntityOperation.m
#implementation JGRefreshEntityOperation
#synthesize started;
#synthesize executing;
#synthesize paused;
#synthesize finished;
-(void)main {
[self startOperation];
NSSet *allEntities = [imoc fetchAllObjectsForEntityName:entityName];
for(id thisEntity in allEntities)
[imoc refreshObject:thisEntity mergeChanges:YES];
[self finishOperation];
}
-(void)startOperation {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isStarted"];
[self setStarted:YES];
[self setExecuting:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isStarted"];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setStalenessInterval:0];
[imoc setUndoManager:nil];
[imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
}
-(void)finishOperation {
saveError = nil;
[imoc save:&saveError];
if (saveError) {
NSLog(#"Error saving. %#", saveError);
}
imoc = nil;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
[self setExecuting:NO];
[self setFinished:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(id)initWithEntityName:(NSString *)entityName_ {
[super init];
[self setStarted:false];
[self setExecuting:false];
[self setPaused:false];
[self setFinished:false];
[NSThread setThreadPriority:0.0];
entityName = entityName_;
return self;
}
#end
// JGRefreshEntityOperation.h
#interface JGRefreshEntityOperation : NSOperation {
NSString *entityName;
NSManagedObjectContext *imoc;
NSError *saveError;
BOOL started;
BOOL executing;
BOOL paused;
BOOL finished;
}
#property(readwrite, getter=isStarted) BOOL started;
#property(readwrite, getter=isPaused) BOOL paused;
#property(readwrite, getter=isExecuting) BOOL executing;
#property(readwrite, getter=isFinished) BOOL finished;
-(void)startOperation;
-(void)finishOperation;
-(id)initWithEntityName:(NSString *)entityName_;
-(void)mergeChanges:(NSNotification *)notification;
#end
UPDATE 1
I just found this question. I can't understand how I missed it before I posted mine, but the summary is: Core Data wasn't designed to do what I'm doing. Only one process should be using a data store.
NSManagedObjectContext and NSArrayController reset/refresh problem
However, in a different area of my application I have two processes sharing a data store with one having read only access and this seemed to work fine. Plus none of the answers to my last question on this topic mentioned that this wasn't supported in Core Data.
I'm going to re-architect my app so that only one process writes to the data store at any one time. I'm still skeptical that this will solve my problem though. It looks to me more like an NSOutlineView refreshing problem - the objects are created in the context, it's just the outline view doesn't pick them up.
I ended up re-architecting my app. I'm only importing items from one process or the other at once. And it works perfectly. Hurrah!

Resources