This is from my window controller class, which has a (optional) reference to my
document subclass’s model data. Through Interface Builder, that data is
connected to a NSObjectController, and a property of that data is connected to
a NSArrayController. The window controller has outlets to both data
controllers. The model data is Core Data-based, BTW.
I have to watch the data as part of my scheme to create a character map to use
for NSTextFinder. Instead of KVO-ing the model data, I do the two data
controllers instead since those are the “truth” I need to track (in case they
cache any editing changes).
> headerController.addObserver(self, forKeyPath:
> MessageWindowController.headerKeyPath, options: .initial, context:
> &MessageWindowController.headerObservingContext)
> messageController.addObserver(self, forKeyPath:
> MessageWindowController.bodyKeyPath, options: .initial, context:
> &MessageWindowController.bodyObservingContext)
The header observation points to the array controller’s “arrangedObjects”. The
message observation points to the object controller’s “selection”, then a
specific property of my model (of type “String?”). I try to test an incomplete
version of the KVO function:
> override func observeValue(forKeyPath keyPath: String?, of object: Any?,
> change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
> guard let keyPath2 = keyPath, let object2 = object, let change2 =
> change, let context2 = context else {
> return super.observeValue(forKeyPath: keyPath, of: object,
> change: change, context: context)
> }
>
> let changeKind = NSKeyValueChange(rawValue: change2[.kindKey] as!
> UInt)!
> switch context2 {
> case &MessageWindowController.headerObservingContext:
> precondition(keyPath2 == MessageWindowController.headerKeyPath)
> precondition(object2 as AnyObject === headerController)
> break
> case &MessageWindowController.bodyObservingContext:
> precondition(keyPath2 == MessageWindowController.bodyKeyPath)
> precondition(object2 as AnyObject === messageController)
> assert(changeKind == .setting)
>
> let newString = (messageController.selection as!
> NSObject).value(forKey: #keyPath(RawMessage.body)) as! String?
> //messageController.value(forKeyPath: keyPath2) as! String?
> let bodyTextRangeIndex = textRanges.index(before:
> textRanges.endIndex)
> let oldBodyTextRange = textRanges[bodyTextRangeIndex]
> let newLength = (newString as NSString?)?.length ?? 0
> if oldBodyTextRange.length != newLength {
> textRanges[bodyTextRangeIndex] =
> NSMakeRange(oldBodyTextRange.location, newLength)
> }
> break
> default:
> super.observeValue(forKeyPath: keyPath2, of: object2, change:
> change2, context: context2)
> }
> }
And there’s a crash at the “newString” definition. (The current and
commented-out versions get the same error. The current version was copied from
my menu-item validation function, where it actually works.):
> Could not cast value of type '_NSStateMarker' (0x7fffa3003cf8) to 'NSString'
> (0x7fffa397df38).
> 2017-03-27 16:36:33.978709 XNW[39160:4830169] Could not cast value of type
> '_NSStateMarker' (0x7fffa3003cf8) to 'NSString' (0x7fffa397df38).
What is this “NSStateMarker” class? And why does it prevent me from accessing
the data?
Do NSObjectController and/or NSArrayController do any caching of its editing
data? If not, and we can’t directly solve this problem, I could read the data
directly from the document’s model objects.
Pre-Send Update:
I took out the “.initial” from the observing setup call, and it works! Why is
that? Now I have to hope that my concept of the initial text-range array is
accurate, since I can’t confirm it with an initial pass (for now).
Note that when the window controller goes through “windowDidLoad”, the
reference to the model data is NIL. (The object and array controllers are still
bound to it by then, though.) It doesn’t get set to an actual data tree until
the document’s “makeWindowControllers” call.
Pre-Send Update 2:
I completed a first try for the other property:
> let changeKind = NSKeyValueChange(rawValue: change2[.kindKey] as!
> UInt)!
> let bodyTextRangeIndex = textRanges.index(before: textRanges.endIndex)
> switch context2 {
> case &MessageWindowController.headerObservingContext:
> precondition(keyPath2 == MessageWindowController.headerKeyPath)
> precondition(object2 as AnyObject === headerController)
>
> let newArray = headerController.mutableArrayValue(forKeyPath:
> keyPath2) as NSArray
> switch changeKind {
> case .insertion, .removal, .replacement:
> fallthrough
> case .setting:
>