Error unarchiving NSAttributedString on iOS13 with NSKeyedUnarchiver when NSTextAttachment is used

267 Views Asked by At

I archive an NSAttributedString, which contains an image with NSTextAttachment on iOS14 and noticed, that unarchiving it on iOS13 fail. On iOS14 the unarchive is successful.

The error logged is this:

Error decoding string object. error=Error Domain=NSCocoaErrorDomain Code=4864 
"value for key 'NS.objects' was of unexpected class 'NSTextAttachment'. Allowed classes are '{(
    NSGlyphInfo,
    UIColor,
    NSURL,
    UIFont,
    NSParagraphStyle,
    NSString,
    NSAttributedString,
    NSArray,
    NSNumber,
    NSDictionary
)}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'NSTextAttachment'. Allowed classes are '{(
    NSGlyphInfo,
    UIColor,
    NSURL,
    UIFont,
    NSParagraphStyle,
    NSString,
    NSAttributedString,
    NSArray,
    NSNumber,
    NSDictionary
)}'.}

Is there an option to make that work, or am I doomed here?

This is how the image is inserted into an NSMutableAttributedString using NSTextAttachment:

// Add an image:
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
textAttachment.image = image;
NSMutableAttributedString *strWithImage = [[NSAttributedString attributedStringWithAttachment:textAttachment] mutableCopy];

[s appendAttributedString:strWithImage];

This is the archiving line:

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:str requiringSecureCoding:YES error:&error];

This is the unarchiving line, which returns an NSError instance:

NSError *error = nil;
NSAttributedString *str = [NSKeyedUnarchiver unarchivedObjectOfClass:NSAttributedString.class fromData:data error:&error];

I create the NSData instance on iOS14, store it to a file and read it in iOS13. This is when it fails.

2

There are 2 best solutions below

2
On

The error clearly says that NSTextAttachment can not be decoded becuase it is not in the list of classes those are supported.

You have to find a workaround to make it work on iOS 13 or lower.

POSSIBLE SOLUTION

  1. Remove all attachments from the NSMutableAttributedString instance.
  2. archive it the same way that you are doing today.
  3. Encode and store the [TextAttachmentInfo] JSON data paired with above data.
  4. unarchive it the same way that you are doing today.
  5. Decode the paired JSON data as [TextAttachmentInfo].
  6. Insert NSTextAttachments into the unarchived NSMutableAttributedString instance

Here's some helper code that needs to be tuned and tested further according to your own use case.

extension NSMutableAttributedString {
    
    struct TextAttachmentInfo: Codable {
        let location: Int
        let imageData: Data
    }
    
    func removeAllTextAttachments() -> [TextAttachmentInfo] {
        guard self.length > 0 else { return [] }
        
        var textAttachmentInfos: [TextAttachmentInfo] = []
        
        for location in (0..<self.length).reversed() {
            var effectiveRange = NSRange()
            let attributes = self.attributes(at: location, effectiveRange: &effectiveRange)
            
            for (_, value) in attributes {
                if let textAttachment = value as? NSTextAttachment,
                   let data = textAttachment.image?.pngData() {
                    self.replaceCharacters(in: effectiveRange, with: "")
                    textAttachmentInfos.append(.init(location: effectiveRange.location, imageData: data))
                }
            }
        }
        
        return textAttachmentInfos.reversed()
    }
    
    func insertTextAttachmentInfos(_ infos: [TextAttachmentInfo]) {
        for info in infos {
            let attachment = NSTextAttachment()
            attachment.image = UIImage(data: info.imageData)
            self.insert(NSAttributedString(attachment: attachment), at: info.location)
        }
    }
    
}
0
On

A solution for me was to provide a list of acceptable classes in the initWithCoder: method of the class which contains the NSAttributedString. (Credits go to a solution from Markus Spoettl in the cocoa-dev mailing list from a couple of years ago.)

-(instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        NSAttributedString *str = nil;
        // This fails on iOS13:
        // str = [coder decodeObjectOfClass:NSAttributedString.class forKey:@"attrStr"];

        // Provide a set of classes to make NSTextAttachment accepted:
        str = [coder decodeObjectOfClasses:[NSSet setWithObjects:
            NSAttributedString.class, 
            NSTextAttachment.class, nil] forKey:@"attrStr"];
        self.attrStr = str;
    }
    return self;
}

This seems to be an issue with other classes used with NSAttributedString too like NSTextTab etc. so depending on the use case, one can extend the list of acceptable classes.