HOWTO: Sort TableView per column using NSSortDescriptor when column names are unknown? - objective-c

I'm loading a CSV file that I know nothing about except that the first row are column headers, which could be anything. The number of columns is unknown too. I tried to add a sortDescriptorPrototype to each column and when I do I get up/down arrows visible in the TableView.
for(NSTableColumn * col in self.tableView.tableColumns){
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: col.title ascending: YES];
col.sortDescriptorPrototype = sortDescriptor;
}
Each time I click on the column sortDescriptorsDidChange is called:
- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray *)oldDescriptors
{
[_data sortUsingDescriptors: [tableView sortDescriptors]];
[tableView reloadData];
}
_data is a NSMutableArray where data is stored and first row are the header names. Each row is another NSArray Unfortunately I get this error:
2017-04-17 12:35:09.979142+0200 Table Tool[20954:2577488] [General] [<__NSCFString 0x610000474840> valueForUndefinedKey:]: this class is not key value coding-compliant for the key beds.
2017-04-17 12:35:09.982161+0200 Table Tool[20954:2577488] [General] (
0 CoreFoundation 0x00007fffb31a837b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fffc7f9c48d objc_exception_throw + 48
2 CoreFoundation 0x00007fffb31a82c9 -[NSException raise] + 9
3 Foundation 0x00007fffb4c83e5e -[NSObject(NSKeyValueCoding) valueForUndefinedKey:] + 226
4 Foundation 0x00007fffb4b55d9c -[NSObject(NSKeyValueCoding) valueForKey:] + 283
5 Foundation 0x00007fffb4b59aac -[NSArray(NSKeyValueCoding) valueForKey:] + 467
6 Foundation 0x00007fffb4b55b79 -[NSArray(NSKeyValueCoding) valueForKeyPath:] + 448
7 Foundation 0x00007fffb4b7cc9f _sortedObjectsUsingDescriptors + 371
8 Foundation 0x00007fffb4be94f7 -[NSMutableArray(NSKeyValueSorting) sortUsingDescriptors:] + 468
9 Table Tool 0x00000001000051d8 -[Document tableView:sortDescriptorsDidChange:] + 184
10 AppKit 0x00007fffb0f05425 -[NSTableView setSortDescriptors:] + 260
11 AppKit 0x00007fffb13c9fbc -[NSTableView _changeSortDescriptorsForClickOnColumn:] + 480
Obviously I have read a lot on the topic, but all examples assume that you know column names at design-time. I'm interested in a solution with NSSortDescriptor and sortUsingDescriptors in Obj-C or Swift, some explanation on sortDescriptorPrototype and why you need it in the first place, since sortDescriptor changes when I click on the column. Is there anything in KVC I can use for the sorting key, like "self" (there is ...valueForKeyPath:#"#sum.self"...)?
Some useful links:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/SortingTableViews/SortingTableViews.html
https://www.raywenderlich.com/143828/macos-nstableview-tutorial

Here is a completely moronic way of solving it that partly works:
First I extract the column index with this method:
-(NSUInteger)getColumnIndexFromTableViewSortDescriptor {
for(NSUInteger i = 0; i < _maxColumnNumber; i++){
if(_tableView.tableColumns[i].title == _tableView.sortDescriptors[0].key){
return i;
}
}
assert(false && "Missing column name");
return 0;
}
Then I sort the array in-place with sortUsingComparator. Surely this has to be the "wrong". It's completely idiotic to use sortDescriptors if they are not used, other than to figuring out which column was clicked. I guess the click event could figure that out too?
-(void)tableView:(NSTableView *)tableView sortDescriptorsDidChange: (NSArray *)oldDescriptors
{
NSUInteger idx = self.getColumnIndexFromTableViewSortDescriptor;
if(tableView.sortDescriptors[0].ascending){
[_data sortUsingComparator: ^(id obj1, id obj2) {
return [[obj2 objectAtIndex:idx] compare:[obj1 objectAtIndex:idx]];
}];
} else {
[_data sortUsingComparator: ^(id obj1, id obj2) {
return [[obj1 objectAtIndex:idx] compare:[obj2 objectAtIndex:idx]];
}];
}
[tableView reloadData];
}
Now the CSV data I'm importing the data in the columns is not uniform, so it could be a NSNumber, followed by three NSDecimalNumber and a few NSString. Partly that's a problem with the data and/or the CSV parser I'm using (ripped from the web, but I will rewrite the sucker later).
So you could end up comparing NSDecimalNumber with NSString which will generate errors like:
unrecognized selector sent to instance
So I continued my stupidity with:
#interface NSDecimalNumber (CompareString)
-(NSComparisonResult)compare:(NSString *)string;
#end
#implementation NSDecimalNumber (CompareString)
-(NSComparisonResult)compare:(NSString *)string{
NSString *decimalString = self.stringValue;
return[decimalString compare:string options:NSCaseInsensitiveSearch];
}
Now if you compare two NSDecimalNumber the above will be called and the parameter "string" NSDecimalNumber, even though "string" declared NSString, resulting in "unrecognized selector sent to instance" once again.

Related

UITableView Alphabetizing

I have a UITableView that is successfully populated from a database, put into a NSArray and every cell is clickable and performs the correct action when selected, however the list is out of alphabetical order. In order to sort them into alphabetical order, I use the following code:
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"Name" ascending:YES];
sortedArray = [buildings sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
The alphabetizing works correctly, however when some of the cells (in this example it was the first cell in the list) are clicked the app terminates with an uncaught exception:
'NSRangeException' '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
I'd be happy to post more code excerpts, I am just unsure of what relevant information would be needed to help answer this question.
Edit:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger r = [indexPath row];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if(infoInst != nil) {
infoInst = nil;
}
infoInst = [[DirInfoListing alloc] initWithNibName:#"DirInfoListing" bundle:nil building:[self.listData objectAtIndex:r] map:mapUI];
NSLog(#"infoInst building: %#", [self.listData objectAtIndex:r]);
NSString *deviceType = [UIDevice currentDevice].model;
if([deviceType isEqualToString:#"iPad"] || [deviceType isEqualToString:#"iPad Simulator"]){
[self presentModalViewController:infoInst animated:YES];
} else {
[self.navigationController pushViewController:infoInst animated:YES];
}
}
Edit: Here is the stack trace immediately before the error is thrown
2013-07-03 16:56:56.579 thestanforddaily[32907:1a303] Stack trace : (
0 thestanforddaily 0x002190d9 -[Directory tableView:didSelectRowAtIndexPath:] + 457
1 UIKit 0x01ae871d -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1164
2 UIKit 0x01ae8952 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
3 Foundation 0x0237086d __NSFireDelayedPerform + 389
4 CoreFoundation 0x02acd966 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
5 CoreFoundation 0x02acd407 __CFRunLoopDoTimer + 551
6 CoreFoundation 0x02a307c0 __CFRunLoopRun + 1888
7 CoreFoundation 0x02a2fdb4 CFRunLoopRunSpecific + 212
8 CoreFoundation 0x02a2fccb CFRunLoopRunInMode + 123
9 GraphicsServices 0x0327b879 GSEventRunModal + 207
10 GraphicsServices 0x0327b93e GSEventRun + 114
11 UIKit 0x01a58a9b UIApplicationMain + 1175
12 thestanforddaily 0x00002f4d main + 141
13 thestanforddaily 0x00002e75 start + 53
)
2013-07-03 16:57:13.740 thestanforddaily[32907:1a303] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:
(0x2af9052 0x28e8d0a 0x2ae5db8 0x21cb0e 0x1b1e64e 0x1b1e941 0x1b3047d 0x1b3066f 0x1b3093b 0x1b313df 0x1b31986 0x1b315a4 0x219250 0x1ae871d 0x1ae8952 0x237086d 0x2acd966 0x2acd407 0x2a307c0 0x2a2fdb4 0x2a2fccb 0x327b879 0x327b93e 0x1a58a9b 0x2f4d 0x2e75)
Thank you!
The only array access I see in didSelectRowAtIndexPath is [self.listData objectAtIndex:r]. So it would seem your listData array isn't in sync with the table's data model. From the code you posted, the array sortedArray should correspond to the data model, so make sure listData and sortedArray are kept in sync or just have one array. If you still can't figure it out, please post your UITableViewDataSource methods as well as the methods where you set up your various array variables.
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"Name" ascending:YES];
sortedArray = [buildings count] <= 1 ? buildings : [buildings sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
Unless you require sortedArray to be a different instance than buildings. Then you should change the statement accordingly.

Core Plot: Exception 'Number of x and y values do not match'

I am unable to find a whole lot of information about this and I'm having zero luck in any way that I try to narrow down where this problem is coming from so I'm hoping somebody here can give me a bit more information and potential leads on why I might be getting this error.
I'm plotting a data set in real time as the data set is constantly increasing through me constantly receiving new data. When I receive new data I take the time i receive it as the x and the value itself as y, this is how I generate my points.
Full Error: Terminating app due to uncaught exception 'CPTException', reason: 'Number of x and y values do not match'
I have looked at my data set before the crash, I have made sure my point creation was never failing neither had anything wrong with them. I'm guessing at this point that is has something to do with my version of Scatter Plot, probably in the numberOfRecordsForPlot function. It doesn't seem to crash anywhere in that function however. The crash doesn't happen until usually 10+ seconds in, but again its not consistent, and before it crashes and am getting perfectly working plotting.
Any light people can shed on this is very much appreciated.
PS: If people want to see code, let me know what, anything non standard I have verified to be functioning to the best of my ability, and anything to do with Scatter Plot is fairly standard.
-(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
{
//Translation: The array with key of the top of the first selected parameter
NSInteger curCount = [self.graphParams count];
NSLog(#"Current Count: %d", curCount);
if ([plot.identifier isEqual:#"Sel_1"] && (curCount >= 1) ) {
if ([self.graphParams objectAtIndex: 0] != nil) {
//NSString *myText = ((UITableViewCell *)[self.graphParams objectAtIndex: 0]).textLabel.text;
//NSInteger myNum = [[self.graphData objectForKey: myText] count];
//return [[self.graphData objectForKey: myText] count];
//return [[self.graphData objectForKey: ((UITableViewCell *)[self.graphParams objectAtIndex: 0]).textLabel.text] count];
return [[self.graphData objectForKey: [self.graphParams objectAtIndex: 0]] count];
}
else
return 0;
}
else if ([plot.identifier isEqual:#"Sel_2"] && (curCount >= 2) ) {
if ([self.graphParams objectAtIndex: 1] != nil)
//return [[self.graphData objectForKey: ((UITableViewCell *)[self.graphParams objectAtIndex: 1]).textLabel.text] count];
return [[self.graphData objectForKey: [self.graphParams objectAtIndex: 1]] count];
else
return 0;
}
else if ([plot.identifier isEqual:#"Sel_3"] && (curCount >= 3) ) {
if ([self.graphParams objectAtIndex: 2] != nil)
//return [[self.graphData objectForKey: ((UITableViewCell *)[self.graphParams objectAtIndex: 2]).textLabel.text] count];
return [[self.graphData objectForKey: [self.graphParams objectAtIndex: 2]] count];
else
return 0;
}
return 0;
}
-(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index
{
//Translation: The array with key of the top of the first selected parameter
NSValue *value = nil;
if ( [plot.identifier isEqual:#"Sel_1"] ) {
value = [[self.graphData objectForKey: [self.graphParams objectAtIndex:0]] objectAtIndex:index];
}
else if ( [plot.identifier isEqual:#"Sel_2"] ) {
value = [[self.graphData objectForKey: [self.graphParams objectAtIndex: 1]] objectAtIndex:index];
}
else if ( [plot.identifier isEqual:#"Sel_3"] ) {
value = [[self.graphData objectForKey: [self.graphParams objectAtIndex: 2]] objectAtIndex:index];
}
if (value != nil)
{
CGPoint point = [value CGPointValue];
if ( fieldEnum == CPTScatterPlotFieldX )
return [NSNumber numberWithFloat:point.x];
else if ( fieldEnum == CPTScatterPlotFieldY )
return [NSNumber numberWithFloat:point.y];
}
return [NSNumber numberWithFloat:0];
}
EDIT: Posted some scatter plot code where I think error may be coming from, but I dont know how useful this is to any of you. As always comment for additional requests and I'll be happy to provide anything that makes sense.
Make sure all access to the graphData dictionary and its contents are thread-safe. Core Plot does all of its datasource access and drawing on the main thread.
Always call -reloadData, -insertDataAtIndex:numberOfRecords:, and other Core Plot methods from the main thread.
I have also seen this. The exception happens when drawing the graph, not calling any of the data source functions.
Number of x and y values do not match
(
0 CoreFoundation 0x00007fff88f4df56 __exceptionPreprocess + 198
1 libobjc.A.dylib 0x00007fff851cfd5e objc_exception_throw + 43
2 CoreFoundation 0x00007fff88f4dd8a +[NSException raise:format:arguments:] + 106
3 CoreFoundation 0x00007fff88f4dd14 +[NSException raise:format:] + 116
4 CorePlot 0x0000000100027c31 -[CPTScatterPlot renderAsVectorInContext:] + 561
5 CorePlot 0x000000010004dd50 -[CPTLayer drawInContext:] + 96
6 CorePlot 0x000000010001d023 -[CPTPlot drawInContext:] + 99
7 QuartzCore 0x00007fff8a673701 CABackingStoreUpdate_ + 3221
8 QuartzCore 0x00007fff8a672616 _ZN2CA5Layer8display_Ev + 1086
9 QuartzCore 0x00007fff8a66a4e6 _ZN2CA5Layer17display_if_neededEPNS_11TransactionE + 560
10 QuartzCore 0x00007fff8a66949b _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 319
11 QuartzCore 0x00007fff8a669218 _ZN2CA11Transaction6commitEv + 274
12 QuartzCore 0x00007fff8a668819 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 63
13 CoreFoundation 0x00007fff88f0d8e7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
14 CoreFoundation 0x00007fff88f0d846 __CFRunLoopDoObservers + 374
15 CoreFoundation 0x00007fff88ee24a2 CFRunLoopRunSpecific + 258
16 HIToolbox 0x00007fff8d59c4d3 RunCurrentEventLoopInMode + 277
17 HIToolbox 0x00007fff8d5a3781 ReceiveNextEventCommon + 355
18 HIToolbox 0x00007fff8d5a360e BlockUntilNextEventMatchingListInMode + 62
19 AppKit 0x00007fff87df4e31 _DPSNextEvent + 659
20 AppKit 0x00007fff87df4735 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 135
21 AppKit 0x00007fff87df1071 -[NSApplication run] + 470
22 AppKit 0x00007fff8806d244 NSApplicationMain + 867
23 0x0000000100001a62 main + 34
24 0x0000000100001a34 start + 52
)
My app is also MultiThreaded, but I have made sure that [thePlot setDataNeedsReloading] is called in the main thread, and I have ensured that all my data source callbacks are being called in the main thread.
I'm sure that somehow, CorePlot is making two calls to get my data, and I'm adding more data in between those 2 calls, and CorePlot is confused.

Autorelease methods leaking

Hi everyone,
I'm stuck these days on some memory leaks. The app I'm making is working like that :
1 - Loads a file into memory
2 - Create a screen according to some values read on that file
3 - Display the view
Far from now everything is normal when I start the app and get the first screen. There is no leaks.
But when I want to load an other screen from the current view I got plenty of leaks from autoreleased objects. And I don't understand because when I load a new view from the current one the process is similar :
1 - Desctruction of the current view
2 - Loads a file into memory
3 - Create a screen according to some values read on that file
4 - Display the view
Here are some concrete example of what is leaking :
-(NSString*)pathForApplicationName:(NSString*)appName
withImage:(NSString*)imagePath {
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [searchPaths lastObject];
return [NSString stringWithFormat:#"%#/%#/assets/www/%#",documentPath,[self convertSpacesInDashes:appName],imagePath];
}
The stringWithFormat:.. is leaking. An other example :
-(UIColor*)convertHexColorToUIColor:(NSString*)hexColor {
if ([hexColor length] == 7) {
unsigned c = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexColor];
[scanner setScanLocation:1];
[scanner scanHexInt:&c];
return [UIColor colorWithRed:((c>>16)&0xFF)/255.0
green:((c>>8)&0xFF)/255.0
blue:(©&0xFF)/255.0
alpha:1.];
}
else {
return nil;
}
}
Same, colorWithRed:.. is leaking.
I've read apple's documentation regarding autoreleased objects. And I've even tried to recreate a new pool like that, without any success :
-(UIView)myFonctionWhoGenerateScreens:
for () {
NSAutoreleasePool *subPool = [[NSAutoreleasePool alloc] init];
// There are all of the autoreleased method calls that are leaking...
[subPool drain];
}
}
I think I am missing something. Does anyone has an idea?
Leak backtrace
Thanks a lot.
Edit :
Here is how the return of the leaking function is handled in applyPropertyName:withPropertyType:onComponent:withValue:forStyling method :
else if ([propertyType isEqualToString:#"AB_IMAGE"]) {
UIImage *image = [UIImage imageWithContentsOfFile:[self pathForApplicationName:applicationName
withImage:value]];
#try {
[component setValue:image
forKey:propertyName];
}
#catch (NSException *exception) {
#if DEBUG_CONTROLLER
NSLog(#" %# Not key-value compliant for <%#,%#>",component,propertyName,image);
#endif
}
}
Edit 2 :
Here is the complete back trace http://ganzolo.free.fr/leak/%20Leak.zip
Looking at your Leak document, and picking the [NSString stringWithFormat] object at address 0xdec95c0 as an example, it shows balanced retain count operations for the Foundation, ImageIO, and CoreGraphics use of the object but ABTwoImageItemImageLeftComponent is still holding an unreleased reference.
0 0xdec95c0 CFString (immutable) Malloc 1 00:39.994.343 144 Foundation +[NSString stringWithFormat:]
1 0xdec95c0 CFString (immutable) Autorelease <null> 00:39.994.376 0 Foundation +[NSString stringWithFormat:]
2 0xdec95c0 CFString (immutable) CFRetain 2 00:39.994.397 0 iOS Preview App -[ABTwoImageItemImageLeftComponent setSrcProperty:]
3 0xdec95c0 CFString (immutable) CFRetain 3 00:39.996.231 0 ImageIO CGImageReadCreateWithFile
4 0xdec95c0 CFString (immutable) CFRetain 4 00:39.998.012 0 CoreGraphics CGPropertiesSetProperty
5 0xdec95c0 CFString (immutable) CFRelease 3 00:40.362.865 0 Foundation -[NSAutoreleasePool release]
6 0xdec95c0 CFString (immutable) CFRelease 2 01:14.892.330 0 CoreGraphics CGPropertiesRelease
7 0xdec95c0 CFString (immutable) CFRelease 1 01:14.892.921 0 ImageIO releaseInfoJPEG

Objective-C enumerateObjectsUsingBlock vs fast enumeration?

What are the advantages and disadvantages of the following two approaches:
enumerate using block
NSArray *myArray = [NSArray new];
[myArray enumerateObjectsUsingBlock:^(id anObject, NSUInteger idx, BOOL *stop) {
if (anObject == someOtherObject) {
[anObject doSomething:idx];
*stop = YES;
}
}];
fast enumeration
NSArray *myArray = [NSArray new];
int idx = 0
for (id anObject in myArray) {
if (anObject == someOtherObject) {
[anObject doSomething:idx];
break;
}
++idx;
}
This blog post covers the major differences. In summary:
Fast enumeration is available on OS X 10.5+, blocks are available on 10.6+
For simple enumeration, fast enumeration is a bit faster than block-based enumeration
It's easier (and more performant) to do concurrent or reverse enumeration with block-based enumeration than with fast enumeration
When enumerating over an NSDictionary you can get key and value in one hit with a block-based enumerator, whereas with fast enumeration you have to use the key to retrieve the value in a separate message send.
Regarding the last point (NSDictionary enumeration), compare this:
for (id key in dictionary)
{
id obj = [dictionary objectForKey: key];
// do something with key and obj
}
to this:
[dictionary enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) {
// do something with key and obj
}];
In addition, both methods protect mutable collections from mutation inside the enumeration loop. Interestingly, if you try to mutate the collection inside a block-based enumeration, you get an exception thrown by CoreFoundation's __NSFastEnumerationMutationHandler, suggesting that there's some common code under the hood.
NSMutableArray *myArray = [NSMutableArray arrayWithObjects:#"a", #"b", nil];
[myArray enumerateObjectsUsingBlock:^(id anObject, NSUInteger idx, BOOL *stop) {
// Attempt to mutate the array during enumeration
[myArray addObject:#"c"];
}];
Output:
2011-12-14 22:37:53.716 Untitled[5809:707] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x109614190> was mutated while being enumerated.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff8cca7286 __exceptionPreprocess + 198
1 libobjc.A.dylib 0x00007fff8319ad5e objc_exception_throw + 43
2 CoreFoundation 0x00007fff8cd311dc __NSFastEnumerationMutationHandler + 172
3 CoreFoundation 0x00007fff8cc9efb4 __NSArrayEnumerate + 612
4 Untitled 0x00000001094efcea main + 250
5 Untitled 0x00000001094efbe4 start + 52
6 ??? 0x0000000000000001 0x0 + 1
)
terminate called throwing an exceptionRun Command: line 1: 5809 Abort trap: 6 ./"$2"
First thoughts that come to my mind
Blocks are available in iOS 4 and later so if you need to support older versions then you can not use the block syntax.
They are pretty equivalent in terms of what they do apart from you can't accidentally mess up the counter in the block version.
One other potential difference is that you can define the block elsewhere and pass in different blocks dependant on your state.
Hopefully this was just a very rough example as the code snippet is pretty poor and there are more efficient way of doing this ;)

How to extend NSTextStorage?

According to documentation, I try to subclass NSTextStorage and use it in text view:
/*
NSTextStorage is a semi-abstract subclass of NSMutableAttributedString. It
implements change management (beginEditing/endEditing), verification of
attributes, delegate handling, and layout management notification. The one
aspect it does not implement is the actual attributed string storage --- this is
left up to the subclassers, which need to override the two
NSMutableAttributedString primitives:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;
*/
So I try to use delegate, to handle all needed events with same functionality:
#implementation MyTextStorage
- (id) init {
self = [super init];
if (self != nil) {
storage = [[NSMutableAttributedString alloc] init];
}
return self;
}
- (void) dealloc {
[storage release];
[super dealloc];
}
- (NSString *) string {
return [storage string];
}
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)str {
[storage replaceCharactersInRange:range withString:str];
}
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range {
[storage setAttributes:attrs range:range];
}
#end
It is no matter what I'm use as delegate: NSTextStorage or NSMutableAttributedString, The result is the same:
An uncaught exception was raised
* NSRunStorage, _NSBlockNumberForIndex(): index (18446744073709551615) beyond array
bounds (0)
* Terminating app due to uncaught exception 'NSRangeException', reason:
'*** NSRunStorage,
_NSBlockNumberForIndex(): index (18446744073709551615) beyond array
bounds (0)'
Stack trace:
0 CoreFoundation 0x00007fff840cd7b4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff885390f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff840cd5d7 +[NSException raise:format:arguments:] + 103
3 CoreFoundation 0x00007fff840cd564 +[NSException raise:format:] + 148
4 AppKit 0x00007fff86cc6288 _NSBlockNumberForIndex + 86
5 AppKit 0x00007fff86cc71d5 -[NSLayoutManager textContainerForGlyphAtIndex:effectiveRange:] + 364
6 AppKit 0x00007fff86d1f121 -[NSTextView(NSSharing) didChangeText] + 340
7 AppKit 0x00007fff86d44b68 -[NSTextView insertText:replacementRange:] + 2763
8 CocoaCalculator 0x0000000100002312 -[CalcTextView insertText:] + 65
9 CocoaCalculator 0x00000001000027ac -[CalcTextContainer initWithFrame:] + 1176
10 AppKit 0x00007fff86c23d44 -[NSCustomView nibInstantiate] + 646
11 AppKit 0x00007fff86b7be17 -[NSIBObjectData instantiateObject:] + 259
12 AppKit 0x00007fff86b7b202 -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:] + 336
13 AppKit 0x00007fff86b7988d loadNib + 226
14 AppKit 0x00007fff86b78d9a +[NSBundle(NSNibLoading) _loadNibFile:nameTable:withZone:ownerBundle:]
Termination start when I try to call
[textView insertText:#"123"];
But in standard NSTextStorage version all works properly.
What I need to do to extend this class ?
In addition to the NSAttributedString primitive methods that need implemented, I believe you also need to make a call to edited:range:changeInLength: inside your overrides of replaceCharactersInRange:withString: and setAttributes:range: respectively.
Something like:
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)str {
[storage replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:[str length] - range.length];
}
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range {
[storage setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}
The NSTextStorage documentation is a little bit vague in this area. The comment you quote in your question, about only needing to override 2 of the NSMutableAttributedString primitives only means that you don't have to override the other NSMutableAttributedString primitives.
You still have to override the primitives for the NSAttributedString superclass. Which means you need to implement -attributesAtIndex:effectiveRange:, -length and -string. I notice you did include an implementation of -string, so the two remaining ought to do it.
If you add:
- (NSUInteger)length
{
return [storage length];
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
return [storage attributesAtIndex:location effectiveRange:range];
}
... then hopefully it will fix this problem.