PageRenderTime 20ms CodeModel.GetById 12ms app.highlight 3ms RepoModel.GetById 2ms app.codeStats 0ms

/AppKit/CPObjectController.j

http://github.com/cacaodev/cappuccino
Unknown | 827 lines | 665 code | 162 blank | 0 comment | 0 complexity | c0b4ae4898122115c8245b8890c8475a MD5 | raw file
  1/*
  2 * CPObjectController.j
  3 * AppKit
  4 *
  5 * Created by Ross Boucher.
  6 * Copyright 2009, 280 North, Inc.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import <Foundation/CPDictionary.j>
 24@import <Foundation/CPCountedSet.j>
 25@import <Foundation/_CPCollectionKVCOperators.j>
 26
 27@import "CPController.j"
 28@import "CPKeyValueBinding.j"
 29
 30/*!
 31    @class
 32
 33    CPObjectController is a bindings-compatible controller class.
 34    Properties of the content object of an object of this class can be bound to user interface elements to change and access their values.
 35
 36    The content of an CPObjectController instance is an CPMutableDictionary object by default.
 37    This allows a single CPObjectController instance to be used to manage several properties accessed by key value paths.
 38    The default content object class can be changed by calling setObjectClass:, which a subclass must override.
 39*/
 40@implementation CPObjectController : CPController
 41{
 42    id              _contentObject;
 43    id              _selection;
 44
 45    Class           _objectClass;
 46    CPString        _objectClassName;
 47
 48    BOOL            _isEditable;
 49    BOOL            _automaticallyPreparesContent;
 50
 51    CPCountedSet    _observedKeys;
 52}
 53
 54+ (void)initialize
 55{
 56    if (self !== [CPObjectController class])
 57        return;
 58
 59    [self exposeBinding:@"editable"];
 60    [self exposeBinding:@"contentObject"];
 61}
 62
 63+ (CPSet)keyPathsForValuesAffectingContentObject
 64{
 65    return [CPSet setWithObjects:"content"];
 66}
 67
 68+ (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey
 69{
 70    if (aKey === @"contentObject")
 71        return NO;
 72
 73    return YES;
 74}
 75
 76+ (CPSet)keyPathsForValuesAffectingCanAdd
 77{
 78    return [CPSet setWithObject:"editable"];
 79}
 80
 81+ (CPSet)keyPathsForValuesAffectingCanInsert
 82{
 83    return [CPSet setWithObject:"editable"];
 84}
 85
 86+ (CPSet)keyPathsForValuesAffectingCanRemove
 87{
 88    return [CPSet setWithObjects:"editable", "selection"];
 89}
 90
 91/*!
 92    @ignore
 93*/
 94- (id)init
 95{
 96    return [self initWithContent:nil];
 97}
 98
 99/*!
100    Inits and returns a CPObjectController object with the given content.
101
102    @param id aContent - The object the controller will use.
103    @return id the CPObjectConroller instance.
104*/
105- (id)initWithContent:(id)aContent
106{
107    if (self = [super init])
108    {
109        [self setEditable:YES];
110        [self setObjectClass:[CPMutableDictionary class]];
111
112        _observedKeys = [[CPCountedSet alloc] init];
113        _selection = [[CPControllerSelectionProxy alloc] initWithController:self];
114
115        [self setContent:aContent];
116    }
117
118    return self;
119}
120
121/*!
122    Returns the controller's content object.
123    @return id - The content object of the controller.
124*/
125- (id)content
126{
127    return _contentObject;
128}
129
130/*!
131    Sets the content object for the controller.
132    @param id aContent - The new content object for the controller.
133*/
134- (void)setContent:(id)aContent
135{
136    [self willChangeValueForKey:@"contentObject"];
137    [self _selectionWillChange];
138
139    _contentObject = aContent;
140
141    [self _selectionDidChange];
142    [self didChangeValueForKey:@"contentObject"];
143}
144
145/*!
146    @ignore
147*/
148- (void)_setContentObject:(id)aContent
149{
150    [self setContent:aContent];
151}
152
153/*!
154    @ignore
155*/
156- (id)_contentObject
157{
158    return [self content];
159}
160
161/*!
162    Sets whether the controller automatically creates and inserts new content objects automatically when loading from a cib file.
163    If you pass YES and the controller uses prepareContent to create the content object.
164    The default is NO.
165
166    @param BOOL shouldAutomaticallyPrepareContent - YES if the content should be prepared, otherwise NO.
167*/
168- (void)setAutomaticallyPreparesContent:(BOOL)shouldAutomaticallyPrepareContent
169{
170    _automaticallyPreparesContent = shouldAutomaticallyPrepareContent;
171}
172
173/*!
174    Returns if the controller prepares the content automatically.
175    @return BOOL - YES if the content is prepared, otherwise NO.
176*/
177- (BOOL)automaticallyPreparesContent
178{
179    return _automaticallyPreparesContent;
180}
181
182/*!
183    Overridden by a subclass that require control over the creation of new objects.
184*/
185- (void)prepareContent
186{
187    [self setContent:[self newObject]];
188}
189
190/*!
191    Sets the object class when creating new objects.
192    @param Class - the class of new objects that will be created.
193*/
194- (void)setObjectClass:(Class)aClass
195{
196    _objectClass = aClass;
197}
198
199/*!
200    Returns the class of what new objects will be when they are created.
201
202    @return Class - The class of new objects.
203*/
204- (Class)objectClass
205{
206    return _objectClass;
207}
208
209/*!
210    @ignore
211*/
212- (id)_defaultNewObject
213{
214    return [[[self objectClass] alloc] init];
215}
216
217/*!
218    Creates and returns a new object of the appropriate class.
219    @return id - The object created.
220*/
221- (id)newObject
222{
223    return [self _defaultNewObject];
224}
225
226/*!
227    Sets the controller's content object.
228    @param id anObject - The object to set for the controller.
229*/
230- (void)addObject:(id)anObject
231{
232    [self setContent:anObject];
233
234    var binderClass = [[self class] _binderClassForBinding:@"contentObject"];
235    [[binderClass getBinding:@"contentObject" forObject:self] reverseSetValueFor:@"contentObject"];
236}
237
238/*!
239    Removes a given object from the controller.
240    @param id anObject - The object to remove from the receiver.
241*/
242- (void)removeObject:(id)anObject
243{
244    if ([self content] === anObject)
245        [self setContent:nil];
246
247    var binderClass = [[self class] _binderClassForBinding:@"contentObject"];
248    [[binderClass getBinding:@"contentObject" forObject:self] reverseSetValueFor:@"contentObject"];
249}
250
251/*!
252    Creates and adds a sets the object as the controller's content.
253    @param id aSender - The sender of the message.
254*/
255- (void)add:(id)aSender
256{
257    // FIXME: This should happen on the next run loop?
258    [self addObject:[self newObject]];
259}
260
261/*!
262    @return BOOL - YES if you can added to the controller using add:
263*/
264- (BOOL)canAdd
265{
266    return [self isEditable];
267}
268
269/*!
270    Removes the content object from the controller.
271    @param id aSender - The sender of the message.
272*/
273- (void)remove:(id)aSender
274{
275    // FIXME: This should happen on the next run loop?
276    [self removeObject:[self content]];
277}
278
279/*!
280    @return BOOL - Returns YES if you can remove the controller's content using remove:
281*/
282- (BOOL)canRemove
283{
284    return [self isEditable] && [[self selectedObjects] count];
285}
286
287/*!
288    Sets whether the controller allows for the editing of the content.
289    @param BOOL shouldBeEditable - YES if the content should be editable, otherwise NO.
290*/
291- (void)setEditable:(BOOL)shouldBeEditable
292{
293    _isEditable = shouldBeEditable;
294}
295
296/*!
297    @return BOOL - Returns YES if the content of the controller is editable, otherwise NO.
298*/
299- (BOOL)isEditable
300{
301    return _isEditable;
302}
303
304/*!
305    @return CPArray - Returns an array of all objects to be affected by editing.
306*/
307- (CPArray)selectedObjects
308{
309    return [[_CPObservableArray alloc] initWithArray:[_contentObject]];
310}
311
312/*!
313    Returns a proxy object representing the controller's selection.
314*/
315- (id)selection
316{
317    return _selection;
318}
319
320/*!
321    @ignore
322*/
323- (void)_selectionWillChange
324{
325    [_selection controllerWillChange];
326    [self willChangeValueForKey:@"selection"];
327}
328
329/*!
330    @ignore
331*/
332- (void)_selectionDidChange
333{
334    if (_selection === undefined || _selection === nil)
335        _selection = [[CPControllerSelectionProxy alloc] initWithController:self];
336
337    [_selection controllerDidChange];
338    [self didChangeValueForKey:@"selection"];
339}
340
341/*!
342    @return id - Returns the keys which are being observed.
343*/
344- (id)observedKeys
345{
346    return _observedKeys;
347}
348
349- (void)addObserver:(id)anObserver forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
350{
351   [_observedKeys addObject:aKeyPath];
352   [super addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
353}
354
355- (void)removeObserver:(id)anObserver forKeyPath:(CPString)aKeyPath
356{
357   [_observedKeys removeObject:aKeyPath];
358   [super removeObserver:anObserver forKeyPath:aKeyPath];
359}
360
361@end
362
363var CPObjectControllerContentKey                        = @"CPObjectControllerContentKey",
364    CPObjectControllerObjectClassNameKey                = @"CPObjectControllerObjectClassNameKey",
365    CPObjectControllerIsEditableKey                     = @"CPObjectControllerIsEditableKey",
366    CPObjectControllerAutomaticallyPreparesContentKey   = @"CPObjectControllerAutomaticallyPreparesContentKey";
367
368@implementation CPObjectController (CPCoding)
369
370- (id)initWithCoder:(CPCoder)aCoder
371{
372    self = [super init];
373
374    if (self)
375    {
376        var objectClassName = [aCoder decodeObjectForKey:CPObjectControllerObjectClassNameKey],
377            objectClass = CPClassFromString(objectClassName);
378
379        [self setObjectClass:objectClass || [CPMutableDictionary class]];
380        [self setEditable:[aCoder decodeBoolForKey:CPObjectControllerIsEditableKey]];
381        [self setAutomaticallyPreparesContent:[aCoder decodeBoolForKey:CPObjectControllerAutomaticallyPreparesContentKey]];
382        [self setContent:[aCoder decodeObjectForKey:CPObjectControllerContentKey]];
383
384        _observedKeys = [[CPCountedSet alloc] init];
385    }
386
387    return self;
388}
389
390- (void)encodeWithCoder:(CPCoder)aCoder
391{
392    [aCoder encodeObject:[self content] forKey:CPObjectControllerContentKey];
393
394    if (_objectClass)
395        [aCoder encodeObject:CPStringFromClass(_objectClass) forKey:CPObjectControllerObjectClassNameKey];
396    else if (_objectClassName)
397        [aCoder encodeObject:_objectClassName forKey:CPObjectControllerObjectClassNameKey];
398
399    [aCoder encodeBool:[self isEditable] forKey:CPObjectControllerIsEditableKey];
400    [aCoder encodeBool:[self automaticallyPreparesContent] forKey:CPObjectControllerAutomaticallyPreparesContentKey];
401}
402
403- (void)awakeFromCib
404{
405    if (![self content] && [self automaticallyPreparesContent])
406        [self prepareContent];
407}
408
409@end
410
411@implementation _CPObservationProxy : CPObject
412{
413    id      _keyPath;
414    id      _observer;
415    id      _object;
416
417    BOOL    _notifyObject;
418
419    id      _context;
420    int     _options;
421}
422
423- (id)initWithKeyPath:(id)aKeyPath observer:(id)anObserver object:(id)anObject
424{
425    if (self = [super init])
426    {
427        _keyPath  = aKeyPath;
428        _observer = anObserver;
429        _object   = anObject;
430    }
431
432    return self;
433}
434
435- (id)observer
436{
437    return _observer;
438}
439
440- (id)keyPath
441{
442    return _keyPath;
443}
444
445- (id)context
446{
447   return _context;
448}
449
450- (int)options
451{
452   return _options;
453}
454
455- (void)setNotifyObject:(BOOL)notify
456{
457   _notifyObject = notify;
458}
459
460- (BOOL)isEqual:(id)anObject
461{
462    if (self === anObject)
463        return YES;
464
465    if (!anObject || [anObject class] !== [self class] || anObject._observer !== _observer || anObject._keyPath !== _keyPath || anObject._object !== _object)
466            return NO;
467
468    return YES;
469}
470
471- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)change context:(id)context
472{
473    if (_notifyObject)
474        [_object observeValueForKeyPath:aKeyPath ofObject:_object change:change context:context];
475
476    [_observer observeValueForKeyPath:aKeyPath ofObject:_object change:change context:context];
477}
478
479- (CPString)description
480{
481    return [super description] + [CPString stringWithFormat:@"observation proxy for %@ on key path %@", _observer, _keyPath];
482}
483
484@end
485
486// FIXME: This should subclass CPMutableArray not _CPJavaScriptArray
487@implementation _CPObservableArray : _CPJavaScriptArray
488{
489    CPArray     _observationProxies;
490}
491
492+ (id)alloc
493{
494    var a = [];
495    a.isa = self;
496
497    var ivars = class_copyIvarList(self),
498        count = ivars.length;
499
500    while (count--)
501        a[ivar_getName(ivars[count])] = nil;
502
503    return a;
504}
505
506- (CPString)description
507{
508    return "<_CPObservableArray: " + [super description] + " >";
509}
510
511- (id)initWithArray:(CPArray)anArray
512{
513    self = [super initWithArray:anArray];
514
515    self.isa = [_CPObservableArray class];
516    self._observationProxies = [];
517
518    return self;
519}
520
521- (void)addObserver:(id)anObserver forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
522{
523    if (aKeyPath.charAt(0) === "@")
524    {
525        // Simple collection operators are scalar and can't be proxied
526        if ([_CPCollectionKVCOperator isSimpleCollectionOperator:aKeyPath])
527            return;
528
529        var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObserver object:self];
530
531        proxy._options = options;
532        proxy._context = context;
533
534        [_observationProxies addObject:proxy];
535
536        var dotIndex = aKeyPath.indexOf("."),
537            remaining = aKeyPath.substring(dotIndex + 1),
538            indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
539
540        [self addObserver:proxy toObjectsAtIndexes:indexes forKeyPath:remaining options:options context:context];
541    }
542    else
543    {
544        var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
545        [self addObserver:anObserver toObjectsAtIndexes:indexes forKeyPath:aKeyPath options:options context:context];
546    }
547}
548
549- (void)removeObserver:(id)anObserver forKeyPath:(CPString)aKeyPath
550{
551    if (aKeyPath.charAt(0) === "@")
552    {
553        // Simple collection operators are scalar and can't be proxied
554        if ([_CPCollectionKVCOperator isSimpleCollectionOperator:aKeyPath])
555            return;
556
557        var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObserver object:self],
558            index = [_observationProxies indexOfObject:proxy];
559
560        proxy = [_observationProxies objectAtIndex:index];
561
562        var dotIndex = aKeyPath.indexOf("."),
563            remaining = aKeyPath.substring(dotIndex + 1),
564            indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
565
566        [self removeObserver:proxy fromObjectsAtIndexes:indexes forKeyPath:remaining];
567    }
568    else
569    {
570        var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
571        [self removeObserver:anObserver fromObjectsAtIndexes:indexes forKeyPath:aKeyPath];
572    }
573}
574
575- (void)insertObject:(id)anObject atIndex:(CPUInteger)anIndex
576{
577    for (var i = 0, count = [_observationProxies count]; i < count; i++)
578    {
579        var proxy = [_observationProxies objectAtIndex:i],
580            keyPath = [proxy keyPath],
581            operator = keyPath.charAt(0) === ".";
582
583        if (operator)
584            [self willChangeValueForKey:keyPath];
585
586        [anObject addObserver:proxy forKeyPath:keyPath options:[proxy options] context:[proxy context]];
587
588        if (operator)
589            [self didChangeValueForKey:keyPath];
590    }
591
592    [super insertObject:anObject atIndex:anIndex];
593}
594
595- (void)removeObjectAtIndex:(CPUInteger)anIndex
596{
597    var currentObject = [self objectAtIndex:anIndex];
598
599    for (var i = 0, count = [_observationProxies count]; i < count; i++)
600    {
601        var proxy = [_observationProxies objectAtIndex:i],
602            keyPath = [proxy keyPath],
603            operator = keyPath.charAt(0) === ".";
604
605        if (operator)
606            [self willChangeValueForKey:keyPath];
607
608        [currentObject removeObserver:proxy forKeyPath:keyPath];
609
610        if (operator)
611            [self didChangeValueForKey:keyPath];
612    }
613
614    [super removeObjectAtIndex:anIndex];
615}
616
617- (CPArray)objectsAtIndexes:(CPIndexSet)theIndexes
618{
619    return [_CPObservableArray arrayWithArray:[super objectsAtIndexes:theIndexes]];
620}
621
622- (void)addObject:(id)anObject
623{
624   [self insertObject:anObject atIndex:[self count]];
625}
626
627- (void)removeLastObject
628{
629   [self removeObjectAtIndex:[self count]];
630}
631
632- (void)replaceObjectAtIndex:(CPUInteger)anIndex withObject:(id)anObject
633{
634    var currentObject = [self objectAtIndex:anIndex];
635
636    for (var i = 0, count = [_observationProxies count]; i < count; i++)
637    {
638        var proxy = [_observationProxies objectAtIndex:i],
639            keyPath = [proxy keyPath],
640            operator = keyPath.charAt(0) === ".";
641
642        if (operator)
643            [self willChangeValueForKey:keyPath];
644
645        [currentObject removeObserver:proxy forKeyPath:keyPath];
646        [anObject addObserver:proxy forKeyPath:keyPath options:[proxy options] context:[proxy context]];
647
648        if (operator)
649            [self didChangeValueForKey:keyPath];
650    }
651
652    [super replaceObjectAtIndex:anIndex withObject:anObject];
653}
654
655@end
656
657@implementation CPControllerSelectionProxy : CPObject
658{
659    id                  _controller;
660    id                  _keys;
661
662    CPDictionary        _cachedValues;
663    CPArray             _observationProxies;
664
665    Object              _observedObjectsByKeyPath;
666}
667
668- (id)initWithController:(id)aController
669{
670    if (self = [super init])
671    {
672        _cachedValues = @{};
673        _observationProxies = [CPArray array];
674        _controller = aController;
675        _observedObjectsByKeyPath = {};
676    }
677
678    return self;
679}
680
681- (id)_controllerMarkerForValues:(CPArray)theValues
682{
683    var count = [theValues count],
684        value;
685
686    if (!count)
687        value = CPNoSelectionMarker;
688    else if (count === 1)
689        value = [theValues objectAtIndex:0];
690    else
691    {
692        if ([_controller alwaysUsesMultipleValuesMarker])
693            value = CPMultipleValuesMarker;
694        else
695        {
696            value = [theValues objectAtIndex:0];
697
698            for (var i = 0, count = [theValues count]; i < count && value != CPMultipleValuesMarker; i++)
699            {
700                if (![value isEqual:[theValues objectAtIndex:i]])
701                    value = CPMultipleValuesMarker;
702            }
703        }
704    }
705
706    if (value === nil || value.isa && [value isEqual:[CPNull null]])
707        value = CPNullMarker;
708
709    return value;
710}
711
712- (id)valueForKeyPath:(CPString)theKeyPath
713{
714    var values = [[_controller selectedObjects] valueForKeyPath:theKeyPath];
715
716    // Simple collection operators like @count return a scalar value, not an array or set
717    if ([values isKindOfClass:CPArray] || [values isKindOfClass:CPSet])
718    {
719        var value = [self _controllerMarkerForValues:values];
720        [_cachedValues setObject:value forKey:theKeyPath];
721
722        return value;
723    }
724    else
725        return values;
726}
727
728- (id)valueForKey:(CPString)theKeyPath
729{
730    return [self valueForKeyPath:theKeyPath];
731}
732
733- (void)setValue:(id)theValue forKeyPath:(CPString)theKeyPath
734{
735    [[_controller selectedObjects] setValue:theValue forKeyPath:theKeyPath];
736    [_cachedValues removeObjectForKey:theKeyPath];
737
738    // Allow handlesContentAsCompoundValue to work, based on observation of Cocoa's
739    // NSArrayController - when handlesContentAsCompoundValue and setValue:forKey:@"selection.X"
740    // is called, the array controller causes the compound value to be rewritten if
741    // handlesContentAsCompoundValue == YES. Note that
742    // A) this doesn't use observation (observe: X is not visible in backtraces)
743    // B) it only happens through the selection proxy and not on arrangedObject.X, content.X
744    // or even selectedObjects.X.
745    // FIXME The main code for this should somehow be in CPArrayController and also work
746    // for table based row edits.
747    [[CPBinder getBinding:@"contentArray" forObject:_controller] _contentArrayDidChange];
748}
749
750- (void)setValue:(id)theValue forKey:(CPString)theKeyPath
751{
752    [self setValue:theValue forKeyPath:theKeyPath];
753}
754
755- (unsigned)count
756{
757    return [_cachedValues count];
758}
759
760- (id)keyEnumerator
761{
762    return [_cachedValues keyEnumerator];
763}
764
765- (void)controllerWillChange
766{
767    _keys = [_cachedValues allKeys];
768
769    if (!_keys)
770        return;
771
772    for (var i = 0, count = _keys.length; i < count; i++)
773        [self willChangeValueForKey:_keys[i]];
774
775    [_cachedValues removeAllObjects];
776}
777
778- (void)controllerDidChange
779{
780    [_cachedValues removeAllObjects];
781
782    if (!_keys)
783        return;
784
785    for (var i = 0, count = _keys.length; i < count; i++)
786        [self didChangeValueForKey:_keys[i]];
787
788   _keys = nil;
789}
790
791- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)change context:(id)context
792{
793    [_cachedValues removeObjectForKey:aKeyPath];
794}
795
796- (void)addObserver:(id)anObject forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
797{
798    var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObject object:self];
799
800    [proxy setNotifyObject:YES];
801    [_observationProxies addObject:proxy];
802
803    // We keep a reference to the observed objects because removeObserver: will be called after the selection changes.
804    var observedObjects = [_controller selectedObjects];
805    _observedObjectsByKeyPath[aKeyPath] = observedObjects;
806    [observedObjects addObserver:proxy forKeyPath:aKeyPath options:options context:context];
807}
808
809- (void)removeObserver:(id)anObject forKeyPath:(CPString)aKeyPath
810{
811    [_observationProxies enumerateObjectsUsingBlock:function(aProxy, idx, stop)
812    {
813        if (aProxy._object === self && aProxy._keyPath == aKeyPath && aProxy._observer === anObject)
814        {
815            var observedObjects = _observedObjectsByKeyPath[aKeyPath];
816
817            [observedObjects removeObserver:aProxy forKeyPath:aKeyPath];
818            [_observationProxies removeObjectAtIndex:idx];
819
820            _observedObjectsByKeyPath[aKeyPath] = nil;
821
822            stop(YES);
823        }
824    }];
825}
826
827@end