3

Apple says

There should typically be little need to subclass NSMutableDictionary. If you do need to customize behavior, it is often better to consider composition rather than subclassing.

(See https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/)

They should probably make this a little stronger and say pursue this at your own risk.

However, there are situations where it can be important to subclass NSMutableDictionary. In my case, notationally, it really was relevant to my code. There are quite a few hurdles to overcome. There are other web and SO entries on this, but I encountered some seemingly new issues on my travels through this, so wanted to write this up for my memory and help others. So, I'll post my answer to this. Feel free to contribute your own additional findings.

Chris Prince
  • 7,288
  • 2
  • 48
  • 66
  • 1
    This does not seem to actually be a question. – quellish Oct 09 '15 at 03:07
  • While this is great and allowed it is really a blog post. But the answer seems to be more of a question of how do I make this work properly: `NSCoding` and Swift subscripting. Further this may all break for Swift in an update, Swift is still changing. – zaph Oct 09 '15 at 12:05

1 Answers1

1

1) There are no proxy objects. At the outset, for some reason, Apple seems to have made NSMutableDictionary different in some unusual ways than NSMutableSet. My underlying need to subclass NSMutableDictionary really stems from a need to know about mutation changes to an NSMutableDictionary instance. NSMutableSets, for example, make this some what easier. NSMutableSets give you access to a "proxy" object: mutableSetValueForKey. This gives you a mechanism to know when the set contents mutate. See https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/ for some details. What you'd expect to see would be something like mutableDictValueForKey but that seems to not exist.

2) Implement init in your subclass methods! Apple tells you you need to override methods:

In a subclass, you must override both of its primitive methods:

setObject:forKey:

removeObjectForKey:

You must also override the primitive methods of the NSDictionary class.

and the NSDictionary primitive methods are:

initWithObjects:forKeys:count:

@property count

objectForKey:

keyEnumerator:

BUT, you must also override the init method!

3) Doing this in Swift doesn't work yet! At least as of the date I was trying this (about 10/8/15, and Xcode 7), you must do make your NSMutableDictionary subclass in Objective-C, not Swift. See Cannot override initializer of NSDictionary in Swift

4) NSCoding doesn't work with NSMutableDictionary subclasses! In my NSMutableDictionary subclass, I tried implementing the NSCoding protocol, but couldn't get it work in the context of keyed archivers. The keyed archiver would generate an empty NSMutableDictionary (when decoded), not my own subclass, and I don't know why. Some special NSMutableDictionary magic?

5) subscript in Swift may not cut it. I tried only implementing the subscript method for Swift (see https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html), but notationally this left much to be desired. I really wanted a type that was fully interoperable with NSDictionary/NSMutableDictionary, which seems to require a subclass.

6) Don't just implement the methods; you need your own data! If you just try to override the methods as above, and invoke "super" your code will not work. You need to use "composition" to internally implement an NSMutableDictionary property. Or whatever other mechanism you want for implementing your dictionary. Again, some class cluster magic going on. See my dict property in the .m file below.

Here's what I have to date in terms of my Objective-C code:

//
//  SMMutableDictionary.h
//  Dictionary
//
//  Created by Christopher Prince on 10/6/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

/* I subclassed NSMutableDictionary because:
    1) because I needed a way to know when a key was set or removed. With other mutable objects you can use proxy objects (e.g., see https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/), but a proxy object doesn't seem to be provided by Apple for NSMutableDictionary's.
    2) for notational convenience in some other code that I was writing.
*/

// QUESTION: Can I set up an observer to detect any changes to the value of the key's within the dictionary? We'd have to remove this KVO observer if the object was removed. Presumably, with this interface, the way that the object would be removed would be (a) setting with nil, and (b) deallocation of this SMMutableDictionary itself.

#import <Foundation/Foundation.h>

@class SMMutableDictionary;

@protocol SMMutableDictionaryDelegate <NSObject>

@required

// Reports on the assignment to a keyed value for this dictionary and the removal of a key: setObject:forKey: and removeObjectForKey:
- (void) dictionaryWasChanged: (SMMutableDictionary * _Nonnull) dict;

@end

@interface SMMutableDictionary : NSMutableDictionary

// For some reason (more of the ugliness associated with having an NSMutableDictionary subclass), when you unarchive a keyed archive of an SMMutableDictionary, it doesn't give you back the SMMutableDictionary, it gives you an NSMutableDictionary. So, this method is for your convenience. AND, almost even better, when you use a keyed archiver to archive, it uses our encoder method, but doesn't actually generate an archive containing our dictionary!! SO, don't use keyed archiver methods directly, use the following two methods:
- (NSData * _Nullable) archive;
+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;

// Optional delegate
@property (nonatomic, weak, nullable) id<SMMutableDictionaryDelegate> delegate;

@end

Here's the .m file:

//
//  SMMutableDictionary.m
//  Dictionary
//
//  Created by Christopher Prince on 10/6/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

// I wanted to make this a Swift NSMutableDictionary subclass, but run into issues...
// See https://stackoverflow.com/questions/28636598/cannot-override-initializer-of-nsdictionary-in-swift
// http://www.cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html
// See also https://stackoverflow.com/questions/10799444/nsdictionary-method-only-defined-for-abstract-class-my-app-crashed
// I tried only implementing the subscript method for Swift (see https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html), but notationally this left much to be desired. I really wanted a type that was fully interoperable with NSDictionary/NSMutableDictionary, which seems to require a subclass.

// See also http://www.smackie.org/notes/2007/07/11/subclassing-nsmutabledictionary/

#import "SMMutableDictionary.h"

@interface SMMutableDictionary()
@property (nonatomic, strong) NSMutableDictionary *dict;
@end

// See this for methods you have to implement to subclass: https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/index.html
// HOWEVER, while they didn't say you have to subclass the init method, it did't work for me without doing that. i.e., I needed to have [1] below.

@implementation SMMutableDictionary

- (instancetype) initWithObjects:(const id  _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying>  _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt;
{
    self = [super init];
    if (self) {
        self.dict = [[NSMutableDictionary alloc] initWithObjects:objects forKeys:keys count:cnt];
    }
    return self;
}

// [1].
- (instancetype) init;
{
    self = [super init];
    if (self) {
        self.dict = [NSMutableDictionary new];
    }
    return self;
}

// Both of these are useless. See the keyed archiver/unarchiver methods on the .h interface.
/*
- (void)encodeWithCoder:(NSCoder *)aCoder;
{
    //[aCoder encodeObject:self.dict];
    [aCoder encodeObject:self.dict forKey:@"dict"];
}
 */

/*
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        //self.dict = [aDecoder decodeObject];
        self.dict = [aDecoder decodeObjectForKey:@"dict"];
    }
    return self;
}
*/

- (NSData * _Nullable) archive;
{
    return [NSKeyedArchiver archivedDataWithRootObject:self.dict];
}

+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;
{
    NSMutableDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:keyedArchiverData];
    if (nil == dict) return nil;

    return [[SMMutableDictionary alloc] initWithDictionary:dict];
}

- (NSUInteger) count;
{
    return self.dict.count;
}

- (id) objectForKey:(id)aKey;
{
    return [self.dict objectForKey:aKey];
}

- (NSEnumerator *)keyEnumerator;
{
    return [self.dict keyEnumerator];
}

- (void) setObject:(id)anObject forKey:(id<NSCopying>)aKey;
{
    [self.dict setObject:anObject forKey:aKey];
    if (self.delegate) {
        [self.delegate dictionaryWasChanged:self];
    }
}

- (void) removeObjectForKey:(id)aKey;
{
    [self.dict removeObjectForKey:aKey];
    if (self.delegate) {
        [self.delegate dictionaryWasChanged:self];
    }
}

@end

Update on 10/9/15

To clarify what I meant by "mutation changes" (responding to @quelish below), here's a KVO example with an NSMutableDictionary. Note that the output of this does not reflect Test 1 below. I.e., a change to a key is not indicated by KVO. This example is adapted from https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-XID_5

If you do know all of the keys to your dictionary, you may be able to use KVO. See Observing NSMutableDictionary changes

//
//  ViewController.swift
//  Dictionary2
//
//  Created by Christopher Prince on 10/9/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

import UIKit

private var myContext = 0

class ViewController: UIViewController {
    var obj = MyObserver()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        print("Test 1")
        obj.objectToObserve.myDict["key1"] = "value1"

        print("Test 2")
        obj.objectToObserve.myDict = NSMutableDictionary()
    }
}

class MyObjectToObserve: NSObject {
    dynamic var myDict = NSMutableDictionary()
    override var description : String {
        return "\(myDict)"
    }
}

class MyObserver: NSObject {
    var objectToObserve = MyObjectToObserve()

    override init() {
        super.init()
        objectToObserve.addObserver(self, forKeyPath: "myDict", options: NSKeyValueObservingOptions(rawValue: 0), context: &myContext)
    }

    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if context == &myContext {
            //let newValue = change?[NSKeyValueChangeNewKey]
            print("change: \(change)")
            print("object: \(object)")
        } else {
            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
        }
    }

    deinit {
        objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
    }
}
Community
  • 1
  • 1
Chris Prince
  • 7,288
  • 2
  • 48
  • 66
  • 1
    "stems from a need to know about mutation changes". Use Key Value Observing. – quellish Oct 09 '15 at 03:11
  • KVO is good for telling you that the value of a property has changed, but less good for telling you that a mutable collection contents have changed. This is what I meant by "mutation changes". I'll add a demonstration below my answer above. – Chris Prince Oct 10 '15 at 02:58
  • Not true at all. Collections send key value notifications when their contents change. This is true for ordered, unordered, and key value collections. – quellish Oct 10 '15 at 03:01