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.
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
Remove all attachments from the NSMutableAttributedString instance.
archive it the same way that you are doing today.
Encode and store the [TextAttachmentInfo] JSON data paired with above data.
unarchive it the same way that you are doing today.
Decode the paired JSON data as [TextAttachmentInfo].
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)
}
}
}
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.
I am working in project in which I store string of image data to database by using this library https://github.com/bborbe/base64-ios/tree/master/Base64.
So when I get that image field I decode that data and shows image
My code to encode Image data
imageData = UIImageJPEGRepresentation(self.img_user_profile.image, 0.4);;
NSString *strEncodeImg = [Base64 encode:imageData];
My code to decode image data
NSData *str = [Base64 decode:[[arr_user_info valueForKey:#"image"] objectAtIndex:0]];
_img_user.image =[UIImage imageWithData:str];
My problem is When I get data string from database and load image in imageview, Memory increases with every image.
Please help me with it
Instead of declaring the attribute as NSString you can declared that as NSData. Now you can directly store the imageData into core data. When fetching you can use imageData directly to load the image
I'm having difficult with converting an image to base64 and then posting it to a server, where I will receive a number in return. I am using objective c.
Any ideas? I've tried a couple of things but I always get a thread error when trying to set certain NSDictionary parameters.
Convert UIImage in base64
NSData *imageData = UIImageJPEGRepresentation(uploadImage, 1.0);
NSString *base64String = [imageData base64EncodedStringWithOptions:kNilOptions];
NSString *encodedString2 = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes( NULL, (CFStringRef)base64String, NULL, CFSTR("!*'();:#&=+$,/?%#[]\" "), kCFStringEncodingUTF8));
send this string using normal way and post on server. Also need to minor changes on your server to getting this image.
Convert UIImage to base64 string For Objective c
NSData *imageData = UIImageJPEGRepresentation(_profileImgObj.image, 1.0);
NSString *base64Img = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
For Swift
let Imagedata = UIImageJPEGRepresentation(_profileImgObj.image, 0.5)
let strBase64Image = Imagedata!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions())
Heres how I do it with Swift:
let data = UIImageJPEGRepresentation(image, 0.5)
let imageString = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.allZeros)
I'm trying to display an image from my Database.
This part works nicely.
I save the image in the DB like this :
UIImage *resizedImg = [Generics scaleImage:self.photo.image toResolution:self.photo.frame.size.width and:self.photo.frame.size.height];
NSData *imgData = UIImageJPEGRepresentation(resizedImg, 0.9);
NSString *stringDataImage = [NSString stringWithFormat:#"%#",imgData];
[dict setObject:stringDataImage for key#"image"];
// POST Request to save the image in DB ...
Now when I try to load the image and set it into my UIImageView I do this way :
NSData *data = [[[MyUser sharedUser] picture] dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:NO];
self.imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithData:data]];
Or
NSData *data = [[[MyUser sharedUser] picture] dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
self.imageView.image = [UIImage withData:data];
But this doesn't work.
data is not equal to imgData, I think it's the encoding but I can find the good one.
And [UIImage withData:data] return nil;
Can you help me?
EDIT :
Convert and save
UIImage *resizedImg = [Generics scaleImage:self.photo.image toResolution:self.photo.frame.size.width and:self.photo.frame.size.height];
NSData *imgData = UIImageJPEGRepresentation(resizedImg, 0.9);
[dict setObject:[imgData base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength] forKey:#"image"];
Load the image
NSData *data = [[NSData alloc] initWithBase64EncodedData:[[MyUser sharedUser] picture] options:kNilOptions];
NSLog(#"%#", data);
self.image.image = [UIImage imageWithData:data];
You are converting the NSData to a string and saving that to your database. Two issues:
Your choice of using the stringWithFormat construct is an inefficient string representation of your data (resulting in string representation that is roughly twice the size). You probably want to use base64 (for which the string representation is only 33% larger).
You are saving your string representation, but never converting it back to binary format after retrieving it. You could write a routine to do this, but it's better to just use established base64 formats.
If you want to save the image as a string in your database, you should use base64. Historically we would have directed you to one of the many third party libraries (see How do I do base64 encoding on iphone-sdk?) for converting from binary data to base64 string (and back), or, iOS 7 now has native base 64 encoding (and exposes the private iOS 4 method, in case you need to support earlier versions of iOS).
Thus to convert NSData to NSString base64 representation:
NSString *string;
if ([data respondsToSelector:#selector(base64EncodedStringWithOptions:)]) {
string = [data base64EncodedStringWithOptions:kNilOptions]; // iOS 7+
} else {
string = [data base64Encoding]; // pre iOS7
}
And to convert base64 string back to NSData:
NSData *data;
if ([NSData instancesRespondToSelector:#selector(initWithBase64EncodedString:options:)]) {
data = [[NSData alloc] initWithBase64EncodedString:string options:kNilOptions]; // iOS 7+
} else {
data = [[NSData alloc] initWithBase64Encoding:string]; // pre iOS7
}
If you really want to turn binary data into a string you should be using base64 encoding. Luckily for you, NSData now supports Base64 natively
So you could get your string from data with:
NSString *stringDataImage = [imgData base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength];
And you could turn this back into NSData with:
NSData *imgData = [[NSData alloc] initWithBase64EncodedString:stringDataImage options:kNilOptions];
Is it possible to save images into NSUserDefaults as an object and then retrieve for further use?
To save an image in NSUserDefaults:
[[NSUserDefaults standardUserDefaults] setObject:UIImagePNGRepresentation(image) forKey:key];
To retrieve an image from NSUserDefaults:
NSData* imageData = [[NSUserDefaults standardUserDefaults] objectForKey:key];
UIImage* image = [UIImage imageWithData:imageData];
ATTENTION! IF YOU'RE WORKING UNDER iOS8/XCODE6 SEE MY UPDATE BELOW
For those who still looking for answer here is code of "advisable" way to save image in NSUserDefaults. You SHOULD NOT save image data directly into NSUserDefaults!
Write data:
// Get image data. Here you can use UIImagePNGRepresentation if you need transparency
NSData *imageData = UIImageJPEGRepresentation(image, 1);
// Get image path in user's folder and store file with name image_CurrentTimestamp.jpg (see documentsPathForFileName below)
NSString *imagePath = [self documentsPathForFileName:[NSString stringWithFormat:#"image_%f.jpg", [NSDate timeIntervalSinceReferenceDate]]];
// Write image data to user's folder
[imageData writeToFile:imagePath atomically:YES];
// Store path in NSUserDefaults
[[NSUserDefaults standardUserDefaults] setObject:imagePath forKey:kPLDefaultsAvatarUrl];
// Sync user defaults
[[NSUserDefaults standardUserDefaults] synchronize];
Read data:
NSString *imagePath = [[NSUserDefaults standardUserDefaults] objectForKey:kPLDefaultsAvatarUrl];
if (imagePath) {
self.avatarImageView.image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imagePath]];
}
documentsPathForFileName:
- (NSString *)documentsPathForFileName:(NSString *)name {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
return [documentsPath stringByAppendingPathComponent:name];
}
For iOS8/XCODE6
As tmr and DevC mentioned in comments below there is a problem with xcode6/ios8. The difference between xcode5 and xcode 6 installation process is that xcode6 changes apps UUID after each run in xcode (see hightlighted part in path: /var/mobile/Containers/Data/Application/B0D49CF5-8FBE-4F14-87AE-FA8C16A678B1/Documents/image.jpg).
So there are 2 workarounds:
Skip that problem, as once app installed on real device it's never changes UUID (in fact it does, but it is new app)
Save relative path to required folder (in our case to app's root)
Here is swift version of code as a bonus (with 2nd approach):
Write data:
let imageData = UIImageJPEGRepresentation(image, 1)
let relativePath = "image_\(NSDate.timeIntervalSinceReferenceDate()).jpg"
let path = self.documentsPathForFileName(relativePath)
imageData.writeToFile(path, atomically: true)
NSUserDefaults.standardUserDefaults().setObject(relativePath, forKey: "path")
NSUserDefaults.standardUserDefaults().synchronize()
Read data:
let possibleOldImagePath = NSUserDefaults.standardUserDefaults().objectForKey("path") as String?
if let oldImagePath = possibleOldImagePath {
let oldFullPath = self.documentsPathForFileName(oldImagePath)
let oldImageData = NSData(contentsOfFile: oldFullPath)
// here is your saved image:
let oldImage = UIImage(data: oldImageData)
}
documentsPathForFileName:
func documentsPathForFileName(name: String) -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true);
let path = paths[0] as String;
let fullPath = path.stringByAppendingPathComponent(name)
return fullPath
}
While it is possible to save a UIImage to NSUserDefaults, it is often not recommended as it is not the most efficient way to save images; a more efficient way is to save your image in the application's Documents Directory.
For the purpose of this question, I have attached the answer to your question, along with the more efficient way of saving a UIImage.
NSUserDefaults (Not Recommended)
Saving to NSUserDefaults
This method allows you to save any UIImage to NSUserDefaults.
-(void)saveImageToUserDefaults:(UIImage *)image ofType:(NSString *)extension forKey:(NSString *)key {
NSData * data;
if ([[extension lowercaseString] isEqualToString:#"png"]) {
data = UIImagePNGRepresentation(image);
} else if ([[extension lowercaseString] isEqualToString:#"jpg"]) {
data = UIImageJPEGRepresentation(image, 1.0);
}
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:data forKey:key];
[userDefaults synchronize];
}
This is how you call it:
[self saveImageToUserDefaults:image ofType:#"jpg" forKey:#"myImage"];
[[NSUserDefaults standardUserDefaults] synchronize];
Loading From NSUserDefaults
This method allows you to load any UIImage from NSUserDefaults.
-(UIImage *)loadImageFromUserDefaultsForKey:(NSString *)key {
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
return [UIImage imageWithData:[userDefaults objectForKey:key]];
}
This is how you call it:
UIImage * image = [self loadImageFromUserDefaultsForKey:#"myImage"];
A Better Alternative
Saving to Documents Directory
This method allows you to save any UIImage to the Documents Directory within the app.
-(void)saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
if ([[extension lowercaseString] isEqualToString:#"png"]) {
[UIImagePNGRepresentation(image) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"png"]] options:NSAtomicWrite error:nil];
} else if ([[extension lowercaseString] isEqualToString:#"jpg"] || [[extension lowercaseString] isEqualToString:#"jpeg"]) {
[UIImageJPEGRepresentation(image, 1.0) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"jpg"]] options:NSAtomicWrite error:nil];
} else {
NSLog(#"Image Save Failed\nExtension: (%#) is not recognized, use (PNG/JPG)", extension);
}
}
This is how you call it:
NSString * documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
[self saveImage:image withFileName:#"Ball" ofType:#"jpg" inDirectory:documentsDirectory];
Loading From Documents Directory
This method allows you to load any UIImage from the application's Documents Directory.
-(UIImage *)loadImageWithFileName:(NSString *)fileName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
UIImage * result = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/%#.%#", directoryPath, fileName, [extension lowercaseString]]];
return result;
}
This is how you call it:
NSString * documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
UIImage * image = [self loadImageWithFileName:#"Ball" ofType:#"jpg" inDirectory:documentsDirectory];
A Different Alternative
Saving UIImage to Photo Library
This method allows you to save any UIImage to the device's Photo Library, and is called as follows:
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
Saving multiple UIImages to Photo Library
This method allows you to save multiple UIImages to the device's Photo Library.
-(void)saveImagesToPhotoAlbums:(NSArray *)images {
for (int x = 0; x < [images count]; x++) {
UIImage * image = [images objectAtIndex:x];
if (image != nil) UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}
}
This is how you call it:
[self saveImagesToPhotoAlbums:images];
Where images is your NSArray composed of UIImages.
For Swift 4
I almost tried everything in this question but no one is worked for me. and I found my solution.
first I created an extension for UserDefaults like below, then just called get and set methods.
extension UserDefaults {
func imageForKey(key: String) -> UIImage? {
var image: UIImage?
if let imageData = data(forKey: key) {
image = NSKeyedUnarchiver.unarchiveObject(with: imageData) as? UIImage
}
return image
}
func setImage(image: UIImage?, forKey key: String) {
var imageData: NSData?
if let image = image {
imageData = NSKeyedArchiver.archivedData(withRootObject: image) as NSData?
}
set(imageData, forKey: key)
}
}
to set image as background in settingsVC I used code below.
let croppedImage = cropImage(selectedImage, toRect: rect, viewWidth: self.view.bounds.size.width, viewHeight: self.view.bounds.size.width)
imageDefaults.setImage(image: croppedImage, forKey: "imageDefaults")
in mainVC :
let bgImage = imageDefaults.imageForKey(key: "imageDefaults")!
For swift 2.2
To store:
NSUserDefaults.standardUserDefaults().setObject(UIImagePNGRepresentation(chosenImage), forKey: kKeyImage)
To retrieve:
if let imageData = NSUserDefaults.standardUserDefaults().objectForKey(kKeyImage),
let image = UIImage(data: imageData as! NSData){
// use your image here...
}
Yes , technically possible as in
[[NSUserDefaults standardUserDefaults] setObject:UIImagePNGRepresentation(image) forKey:#"foo"];
But not advisable because plists are not appropriate places for large blobs of binary data especially User Prefs. It would be better to save image to user docs folder and store the reference to that object as a URL or path.
For Swift 3 and JPG format
Register Default Image :
UserDefaults.standard.register(defaults: ["key":UIImageJPEGRepresentation(image, 100)!])
Save Image :
UserDefaults.standard.set(UIImageJPEGRepresentation(image, 100), forKey: "key")
Load Image :
let imageData = UserDefaults.standard.value(forKey: "key") as! Data
let imageFromData = UIImage(data: imageData)!
It's technically possible, but it's not advisable. Save the image to disk instead. NSUserDefaults is meant for small settings, not big binary data files.
From apple documentation,
The NSUserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Booleans, and URLs. A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
You can save image like this:-
[[NSUserDefaults standardUserDefaults] setObject:UIImagePNGRepresentation([UIImage imageNamed:#"yourimage.gif"])forKey:#"key_for_your_image"];
And read like this:-
NSData* imageData = [[NSUserDefaults standardUserDefaults]objectForKey:#"key_for_your_image"];
UIImage* image = [UIImage imageWithData:imageData];
Save image to NSUserDefault:
NSData *imageData;
// create NSData-object from image
imageData = UIImagePNGRepresentation([dic objectForKey:[NSString stringWithFormat:#"%d",i]]);
// save NSData-object to UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:[NSString stringWithFormat:#"%d",i]];
Load Image from NSUserDefault:
NSData *imageData;
// Load NSData-object from NSUserDefault
imageData = [[NSUserDefaults standardUserDefaults] valueForKey:[NSString stringWithFormat:#"%d",i]];
// get Image from NSData
[image setObject:[UIImage imageWithData:imageData] forKey:[NSString stringWithFormat:#"%d",i]];
Yes, you can use. But since it is for storage of preferences, you can better save images to document folder.
And you can have the path in the NSUserDefaults, if required.
Since this question has a high google search index - here's #NikitaTook's answer in today's day and age i.e. Swift 3 and 4 (with exception handling).
Note: This class is solely written to read and write images of JPG format to the filesystem. The Userdefaults stuff should be handled outside of it.
writeFile takes in the file name of your jpg image (with .jpg extension) and the UIImage itself and returns true if it is able to save or else returns false if it is unable to write the image, at which point you can store the image in Userdefaults which would be your backup plan or simply retry one more time. The readFile function takes in the image file name and returns a UIImage, if the image name passed to this function is found then it returns that image else it just returns a default placeholder image from the app's asset folder (this way you can avoid nasty crashes or other weird behaviors).
import Foundation
import UIKit
class ReadWriteFileFS{
func writeFile(_ image: UIImage, _ imgName: String) -> Bool{
let imageData = UIImageJPEGRepresentation(image, 1)
let relativePath = imgName
let path = self.documentsPathForFileName(name: relativePath)
do {
try imageData?.write(to: path, options: .atomic)
} catch {
return false
}
return true
}
func readFile(_ name: String) -> UIImage{
let fullPath = self.documentsPathForFileName(name: name)
var image = UIImage()
if FileManager.default.fileExists(atPath: fullPath.path){
image = UIImage(contentsOfFile: fullPath.path)!
}else{
image = UIImage(named: "user")! //a default place holder image from apps asset folder
}
return image
}
}
extension ReadWriteFileFS{
func documentsPathForFileName(name: String) -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let path = paths[0]
let fullPath = path.appendingPathComponent(name)
return fullPath
}
}
Swift 4.x
Xcode 11.x
func saveImageInUserDefault(img:UIImage, key:String) {
UserDefaults.standard.set(img.pngData(), forKey: key)
}
func getImageFromUserDefault(key:String) -> UIImage? {
let imageData = UserDefaults.standard.object(forKey: key) as? Data
var image: UIImage? = nil
if let imageData = imageData {
image = UIImage(data: imageData)
}
return image
}