1

I'm using an NSTextView to display the result of a long search, where lines are added as they are found by a background thread using

[self performSelectorOnMainThread: @selector(addMatch:) 
      withObject:options waitUntilDone:TRUE];

As the update routine I have

-(void)addMatch:(NSDictionary*)options{
 ...
 NSTextStorage* store = [textView textStorage];
 [store beginEditing];
 [store appendAttributedString:text];
  ...
 [store endEditing];
}

This works fine, until the user scrolls through the matches as they are being updated, at which point there's an exception

-[NSLayoutManager _fillLayoutHoleForCharacterRange:desiredNumberOfLines:isSoft:] *** attempted layout while textStorage is editing. It is not valid to cause the layoutManager to do layout while the textStorage is editing (ie the textStorage has been sent a beginEditing message without a matching endEditing.)

within a layout call:

    0   CoreFoundation                      0x00007fff92ea364c __exceptionPreprocess + 172
    1   libobjc.A.dylib                     0x00007fff8acd16de objc_exception_throw + 43
    2   CoreFoundation                      0x00007fff92ea34fd +[NSException raise:format:] + 205
    3   UIFoundation                        0x00007fff8fe4fbc1 -[NSLayoutManager(NSPrivate) _fillLayoutHoleForCharacterRange:desiredNumberOfLines:isSoft:] + 641
    4   UIFoundation                        0x00007fff8fe5970c _NSFastFillAllLayoutHolesForGlyphRange + 1493
    5   UIFoundation                        0x00007fff8fda8821 -[NSLayoutManager lineFragmentRectForGlyphAtIndex:effectiveRange:] + 39
    6   AppKit                              0x00007fff8ef3cb02 -[NSTextView _extendedGlyphRangeForRange:maxGlyphIndex:drawingToScreen:] + 478
    7   AppKit                              0x00007fff8ef3ba97 -[NSTextView drawRect:] + 1832
    8   AppKit                              0x00007fff8eed9a09 -[NSView(NSInternal) _recursive:displayRectIgnoringOpacity:inGraphicsContext:CGContext:topView:shouldChangeFontReferenceColor:] + 1186
    9   AppKit                              0x00007fff8eed9458 __46-[NSView(NSLayerKitGlue) drawLayer:inContext:]_block_invoke + 218
    10  AppKit                              0x00007fff8eed91f1 -[NSView(NSLayerKitGlue) _drawViewBackingLayer:inContext:drawingHandler:] + 2407
    11  AppKit                              0x00007fff8eed8873 -[NSView(NSLayerKitGlue) drawLayer:inContext:] + 108
    12  AppKit                              0x00007fff8efaafd2 -[NSTextView drawLayer:inContext:] + 179
    13  AppKit                              0x00007fff8ef22f76 -[_NSBackingLayerContents drawLayer:inContext:] + 145
    14  QuartzCore                          0x00007fff9337c177 -[CALayer drawInContext:] + 119
    15  AppKit                              0x00007fff8ef22aae -[_NSTiledLayer drawTile:inContext:] + 625
    16  AppKit                              0x00007fff8ef227df -[_NSTiledLayerContents drawLayer:inContext:] + 169
    17  QuartzCore                          0x00007fff9337c177 -[CALayer drawInContext:] + 119
    18  AppKit                              0x00007fff8f6efd64 -[NSTileLayer drawInContext:] + 169
    19  QuartzCore                          0x00007fff9337b153 CABackingStoreUpdate_ + 3306
    20  QuartzCore                          0x00007fff9337a463 ___ZN2CA5Layer8display_Ev_block_invoke + 59
    21  QuartzCore                          0x00007fff9337a41f x_blame_allocations + 81
    22  QuartzCore                          0x00007fff93379f1c _ZN2CA5Layer8display_Ev + 1546
    23  AppKit                              0x00007fff8ef226ed -[NSTileLayer display] + 119
    24  AppKit                              0x00007fff8ef1ec34 -[_NSTiledLayerContents update:] + 5688
    25  AppKit                              0x00007fff8ef1d337 -[_NSTiledLayer display] + 375
    26  QuartzCore                          0x00007fff93379641 _ZN2CA5Layer17display_if_neededEPNS_11TransactionE + 603
    27  QuartzCore                          0x00007fff93378d7d _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 35
    28  QuartzCore                          0x00007fff9337850e _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 242
    29  QuartzCore                          0x00007fff93378164 _ZN2CA11Transaction6commitEv + 390
    30  QuartzCore                          0x00007fff93388f55 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 71
    31  CoreFoundation                      0x00007fff92dc0d87 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    32  CoreFoundation                      0x00007fff92dc0ce0 __CFRunLoopDoObservers + 368
    33  CoreFoundation                      0x00007fff92db2f1a __CFRunLoopRun + 1178
    34  CoreFoundation                      0x00007fff92db2838 CFRunLoopRunSpecific + 296
    35  UIFoundation                        0x00007fff8fdfe744 -[NSHTMLReader _loadUsingWebKit] + 2097
    36  UIFoundation                        0x00007fff8fdffb55 -[NSHTMLReader attributedString] + 22
    37  UIFoundation                        0x00007fff8fe12cca _NSReadAttributedStringFromURLOrData + 10543
    38  UIFoundation                        0x00007fff8fe10306 -[NSAttributedString(NSAttributedStringUIFoundationAdditions) initWithData:options:documentAttributes:error:] + 115

What is wrong, given that everything is between beginEditing and endEditing?

wordy
  • 539
  • 5
  • 15
  • BeginUpdate and EndUpdate aren't locks. They're just used for optimizing multiple changes. This may be relevant: [NSTextStorage limitation on size and frequency of updates][1]. [1]: http://stackoverflow.com/questions/9780032/nstextstorage-limitation-on-size-and-frequency-of-updates – geowar Jan 20 '15 at 15:27
  • Thanks, though already dispatching on the main queue I think. Btw, this code used to work reliably when it was first written (in Lion); now on Yosemite it reliably crashes. – wordy Jan 27 '15 at 08:51

2 Answers2

7

From the stack trace (which is not complete), it looks like a run loop source is firing at an inopportune moment.

NSAttributedString uses WebKit to parse HTML. WebKit sometimes runs the run loop. For the general case, it may need to fetch resources from the network to render properly. Since that takes time, it runs the run loop to wait for the result and process other things at the same time.

One of the other run loop sources seems to be a Core Animation source to do the next step in some animation (scrolling the text view, presumably).

You didn't show all of the code between beginEditing and endEditing. I suspect you have constructed an NSAttributedString from HTML or data fetched from a URL in between those two places. That allows the Core Animation run loop source to fire. That asks the text view to draw, which asks its layout manager to lay out the text. This is occurring after beginEditing but before endEditing, which is the cause for the exception.

So, try reordering your code to construct all NSAttributedStrings before beginEditing.

And file a bug with Apple. In my opinion, when NSAttributeString uses WebKit to render HTML, it needs to make WebKit use a private run loop mode so no other sources can fire. They may prefer a different solution, but the bug is real.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • correct, I was generating part of the string with initWithHTML (though no URL). Good solution! – wordy Feb 12 '15 at 23:48
0

As far as I can tell, there's no fix for this. An alternative that works is storing the matches as attributed strings in an array, and using an NSTableView to show the matches by setting the textField.attributedStringValue (calling reloadData every time you add a new match); something like this (where matchContent is an NSMutableArray):

-(void)addMatch:(NSDictionary*)options{
 ...
 [matchContent addObject:text];
 [resultTableView reloadData];
}

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return matchContent.count;
}

- (NSView *)tableView:(NSTableView *)tableView
   viewForTableColumn:(NSTableColumn *)tableColumn
                  row:(NSInteger)row {

    NSTableCellView *result = [tableView makeViewWithIdentifier:@"MyView" owner:self];
    result.textField.attributedStringValue  = [matchContent objectAtIndex:row];
    return result;
}

If the result is multi-line, you may also need to check the autoresizing mask of the cell/text field, and return the row height for the table view using the attributed string's boundingRectWithSize method.

wordy
  • 539
  • 5
  • 15