Storing NSAttributedString with UIImage in Core Data


I have recently worked on solving a bug in a simple note-taking app. It allowed the user to save notes that could contain text together with images. The app was built using Core Data and there was a bug where notes that contained images were not saved properly. When the notes were later retrieved, the images were missing and only text was present.


Converting NSAttributedString to NSData and vice versa

To display the note content the app was using a UITextView and its attributedText property which is an NSAttributedString. Since Core Data doesn’t support storing an NSAttributedString, the content had to be converted to NSData before saving:

extension NSData {
    func toAttributedString() -> NSAttributedString? {
        let data = Data(referencing: self)
        let options : [NSAttributedString.DocumentReadingOptionKey: Any] = [
            .documentType: NSAttributedString.DocumentType.rtfd,
            .characterEncoding: String.Encoding.utf8
        ]

        return try? NSAttributedString(data: data,
                                       options: options,
                                       documentAttributes: nil)
    }
}

Later, that value would be converted back from NSData to NSAttributedString:

extension NSAttributedString {
    func toNSData() -> NSData? {
        let options : [NSAttributedString.DocumentAttributeKey: Any] = [
            .documentType: NSAttributedString.DocumentType.rtfd,
            .characterEncoding: String.Encoding.utf8
        ]

        let range = NSRange(location: 0, length: length)
        guard let data = try? data(from: range, documentAttributes: options) else {
            return nil
        }

        return NSData(data: data)
    }
}

Note that during conversion the documentType has to be rtfd. It is a rich text format document that supports attachments which in our case are images.


Adding images

We will use a UIImagePickerController to pick an image from photo library and add the image to the attributed text using NSTextAttachment. When the user picks an image a delegate method in UIImagePickerControllerDelegate will get called:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)

    guard let image = info[.originalImage] as? UIImage,

        // scale the image to fit the textView frame
        let scaledImage = image.resized(toWidth: self.textView.frame.size.width),

        // encode the image in base64
        let encodedImageString = scaledImage.pngData()?.base64EncodedString(),

        // create an attributed string by embedding the encoded image in HTML
        let attributedString = NSAttributedString(base64EndodedImageString: encodedImageString) else {
          return
    }

    // append the image to the attributed text in the textView
    let attributedText = NSMutableAttributedString(attributedString: textView.attributedText)
    attributedText.append(attributedString)
    textView.attributedText = attributedText
}


Image resizing

We have to resize the image to fit the textView frame size using the custom resized(toWidth:) method on UIImage:

extension UIImage {
    func resized(toWidth width: CGFloat) -> UIImage? {
        let height = CGFloat(ceil(width / size.width * size.height))
        let canvasSize = CGSize(width: width, height: height)
        UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
        defer { UIGraphicsEndImageContext() }
        draw(in: CGRect(origin: .zero, size: canvasSize))
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}


Creating an NSAttributedString using an encoded image

The most straightforward way of adding a UIImage to NSAttributedString is by using the NSTextAttachment:

let attachment = NSTextAttachment()
attachment.image = image
let attributedString = NSAttributedString(attachment: attachment)

Unfortunately, this approach causes a bug when converting the attributed string into NSData where the image gets removed during the conversion process. So I had to find another way of doing it. Fortunately, using HTML to add the image to the attributed string doesn’t cause the aforementioned bug. We only need to add the Base64 encoded image to the <img> tag:

extension NSAttributedString {
    convenience init?(base64EndodedImageString encodedImageString: String) {
        var html = """
        <!DOCTYPE html>
        <html>
          <body>
            <img src="data:image/png;base64,\(encodedImageString)">
          </body>
        </html>
        """
        let data = Data(html.utf8)
        let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]
        try? self.init(data: data, options: options, documentAttributes: nil)
    }
}

It’s interesting to note that NSTextAttachment is used internally when the attributed string is created this way. This can be easily inspected by using the po command in the debugger.