@QuinceyMorris is right on the money here in that NSAttributedString
and NSMutableAttributedString
are class clusters β from the NSAttributedString
docs overview:
The clusterβs two public classes, NSAttributedString
and NSMutableAttributedString
, declare the programmatic interface for read-only attributed strings and modifiable attributed strings, respectively.
Traditionally, this means that although it might appear that you're working with an NSAttributedString
or NSMutableAttributedString
object, you're really operating on an object of some private subclass that offers concrete behavior.
- In the case of
NSAttributedString
, you're likely working with an NSConcreteAttributedString
instance
- In the case of
NSMutableAttributedString
, you're likely working with an NSConcreteMutableAttributedString
You can more clearly see this in Objective-C, where the allocation phase of an object is separate from its initialization phase:
NSAttributedString *str = [NSAttributedString alloc];
NSLog(@"%p (%@)", str, [str class]); // => 0x600000a1b5a0 (NSConcreteAttributedString)
In class clusters, the "public" classes typically override +alloc
to return an instance of an object of a different class, and that class may offer the concrete behavior β or even override init
methods to return yet another private class depending on how they are initialized. For example, with one of the new Markdown methods on NSAttributedString
:
NSAttributedString *str = [NSAttributedString alloc];
NSLog(@"%p (%@)", str, [str class]); // => 0x60000267f4c0 (NSConcreteAttributedString)
str = [str initWithMarkdownString:@"**Hello, world!**" options:nil baseURL:nil error:nil];
NSLog(@"%p (%@)", str, [str class]); // => 0x600002678080 (NSConcreteMutableAttributedString)
For the most part, this is entirely hidden from API consumers, but it does come into play when you'd like to participate in the class cluster.
The specific issue here isn't unique to Swift, actually, and happens just the same in Objective-C (and in fact, happens without even overriding any methods):
#import <Foundation/Foundation.h>
@interface MyString: NSMutableAttributedString
@end
@implementation MyString
@end
int main(int argc, char *argv[]) {
@autoreleasepool {
[[MyString alloc] initWithString:@"Str"]; // *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyString initWithString:]: unrecognized selector sent to instance 0x6000003ac040'
}
}
The reason for this becomes a little bit more apparent once you start splitting allocation and initialization apart:
MyString *str = [MyString alloc];
NSLog(@"%@", [str class]); // => MyString
str = [str initWithString:@"Hello, world!"]; // => -[MyString initWithString:]: unrecognized selector sent to instance 0x6000004901a0
NSLog(@"%@", str);
Here, +[MyString alloc]
actually produces a MyString
object, on which we call -initWithString:
. Having not overridden -initWithString:
from the superclass, this forwards to -[NSMutableAttributedString initWithString:]
... which NSMutableAttributedString
doesn't actually implement!
NSMutableAttributedString
is an abstract class which is never meant to be allocated directly β its +alloc
returns NSConcreteMutableAttributedString
, which does implement the initialization and other backing methods. We can see this a little bit more clearly if we override +alloc
ourselves inside of the subclass:
@implementation MyString
+ (instancetype)alloc {
return [NSMutableAttributedString alloc];
}
// ...
@end
// ...
MyString *str = [MyString alloc];
NSLog(@"%@", [str class]); // => NSConcreteMutableAttributedString
// This now dispatches just fine:
str = [str initWithString:@"Hello, world!"];
NSLog(@"%@", str); // => Hello, world!{
}
Of course, this doesn't help us because we're not actually working with a MyString
object, but an NSConcreteMutableAttributedString
, so any methods we might expect to have called through our class won't actually dispatch.
The NSAttributedString
class cluster is a really tricky one to participate in. Unlike, say, NSArray
, which offers subclassing notes telling you which methods must be overridden in order to correctly create a subclass which will work, NSAttributedString
does not β so it's not clear which functionality you'd need to implement manually, except via trial and error.
In this case, you'd need to, for instance, actually provide an implementation of -initWithString:
that does not call the superclass, because it's not implemented there.
What are you interested in specifically subclassing from NSMutableAttributedString
? With some more info, we might be able to offer help/alternatives.