Populate UICollectionView Images From Document Directory AND Update Custom NSMutableArray object? - objective-c

In my data controller I have book objects and each object contains these properties (among others):
novel.title = #"Book One";
novel.imageArray = [[NSMutableArray alloc] initWithObjects: [UIImage imageNamed: #"book1image1"], nil];
In the app, users can add a book image using UIImagePickerController like this:
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(nonnull NSDictionary<NSString *,id> *)info {
[picker dismissViewControllerAnimated:YES completion:nil];
_addedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
_addedImage = [self scaleImage:_addedImage toSize:CGSizeMake(120, 168)];
[_book.imageArray addObject:_addedImage];
NSString *stringPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:_book.imageArrayID];//folder name
NSError *error = nil;
if (![[NSFileManager defaultManager] fileExistsAtPath:stringPath])
[[NSFileManager defaultManager] createDirectoryAtPath:stringPath withIntermediateDirectories:NO attributes:nil error:&error];
int i = 0;
for(_addedImage in _book.imageArray)
{
_book.bookAddedToArray = YES;
NSString *fileName = [stringPath stringByAppendingFormat:#"/image%i.jpg", i++];//image name
NSData *data = UIImageJPEGRepresentation(_addedImage, 1.0);
[data writeToFile:fileName atomically:YES];
}
[self.collectionView reloadData];
}
This works perfectly fine. All images are saved in the right place as expected.
As long as the app stays open, the new images can be viewed in the collectionView. You can navigate anywhere in the app, come back, and view the collectionView some more. When the app is completely exited out of and then reopened, the collectionView is reset and only shows the initial image that was set in the dataController (no matter what code I've implemented so far. It just always resets). All the user generated images are still in their respective folders in the documents directory but I cannot seem to update the cellForItemAtIndexPath: with the stored images presumably because the images are not being saved to the _book.imageArray. Right now I have the cell being populated by the default imagearray so, of course, that is what will show up. How do I update the array and pull from documents directory to show user images?
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
if(_book.bookAddedToArray)
{
//*** HOW TO UPDATE _BOOK.IMAGEARRAY ????
cell.bookImageView.image = [_book.imageArray objectAtIndex:indexPath.row];
}
else
{
cell.bookImageView.image = [_book.imageArray objectAtIndex:indexPath.row];
}
return cell;
}
I've read that you can store an array of directory paths in nsuserdefaults and retrieve it to populate an imagearray but I have yet to find a solution that works for me. None of the answers seem to address loading into a collectionview or tableview. Can anyone point me in the right direction? Please? Anyone? Lol. Let me know if you need to see more code. All suggestions are much appreciated!
Eventually the app will need to be rewritten as it has grown beyond the scope of my original code, but at this time, this is what I'm working with. I'll probably eventually use CoreData but I've even seen where people on S.O. recommended against storing images in CoreData and to use the documents directory. That still leaves me in this same situation then.

Yes! I got it figured out and it works like a charm. It seems that I could not actually update my custom NSMutableArray. I could make it read another array but could not actually change the array itself. (I'm referring to the _book.imageArray.)
Everything stayed the same in the didFinishPickingMediaWithInfo.
I didn't need to add anything to the cellForItemAtIndexPath. It is just simply:
if(_book.bookAddedToArray)
{
cell.bookImageView.image = [_book.imageArray objectAtIndex: indexPath.row];
}
I created a property for my new array:
#property (nonatomic, strong) NSMutableArray *imagePathArray;
All the magic happens in ViewDidLoad.
- (void)viewDidLoad
{
[super viewDidLoad];
//initialize new array here
_imagePathArray = [[NSMutableArray alloc] init];
if (_book.bookAddedToArray)
{
int i = 0;
//this is the path to the folder.
NSString *stringPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:_book.imageArrayID];
int z;
NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:stringPath error:NULL];
for (z = 0; z < (int)[directoryContent count]; z++)
{
//this is the path to each image file
NSString *fileName = [stringPath stringByAppendingFormat:#"/image%i.jpg", i++];
NSLog(#"imagefile = %#", fileName);
UIImage *fileNameImage = [UIImage imageWithContentsOfFile:fileName];
//add each image to a new array
[_imagePathArray addObject:fileNameImage];
}
//set my array to the new array
_book.imageArray = _imagePathArray;
}
}
I can thank this post for helping to read the images within each folder:
go to link.
//these are all properties in my Data Class.
_book.imageArrayID
_book.bookAddedToArray
_book.imageArray
I hope this helps someone out!

Related

Objective-C: full list of file paths to images and videos in the device

I need to retrive the full list (as comma-separated NSString) of file paths to images and videos stored in an iPhone.
I wrote the following code, currently it gets only images. Can you show me how to improve that code to get also video? Thank you
Please also check if there is something wrong: I have the impression to get less photo then the ones actually stored in the device.
#import <Photos/Photos.h>
-(NSString*)listItems{
PHFetchResult *result = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil];
// NSLog(#"Total: %d",(int)result.count);
NSMutableArray *images = [NSMutableArray arrayWithCapacity:[result count]];
for (PHAsset *asset in result) {
[asset requestContentEditingInputWithOptions:[PHContentEditingInputRequestOptions new] completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
NSURL *imageURL = contentEditingInput.fullSizeImageURL;
[images addObject : imageURL.absoluteString];
}];
}
NSMutableString * listFiles = [[NSMutableString alloc] init];
for (NSObject * obj in images)
{
[listFiles appendString:[obj description]];
[listFiles appendString:#","];
}
return listFiles;
}
#end

How to prevent textfields from getting empty when adding/removing UITableView rows?

I'm creating a UITableView in which product information can be added. In each row, the user can add information about a product, and, obviously, the user can set the number of rows himself.
the user can add or remove one row a time by tapping either the "add row" or "remove row" button in the NavigationBar. this is how it works:
- (void)viewDidLoad
{
[super viewDidLoad];
tableRows = [NSNumber numberWithInt:12];
}
-(void) addRow
{
NSNumber *addRow =[NSNumber numberWithInt:1];
tableRows= [NSNumber numberWithInt:(tableRows.intValue + addRow.intValue)];
[self.tableView reloadData];
NSLog(#"%#", tableRows);
}
-(void) removeRow
{
NSNumber *addRow =[NSNumber numberWithInt:1];
tableRows= [NSNumber numberWithInt:(tableRows.intValue - addRow.intValue)];
[self.tableView reloadData];
NSLog(#"%#", tableRows);
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return tableRows.intValue;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CustomCellIdentifier = #"CustomCellIdentifier ";
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"CustomCell"
owner:self options:nil];
for (id oneObject in nib) if ([oneObject isKindOfClass:[CustomCell class]])
cell = (CustomCell *)oneObject;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
NSUInteger *row = [indexPath row];
return cell;
}
The editing works perfect but when I add or remove a row, the text I inserted in the textfields of my tableview disappears.
does anybody know how to prevent this?
A couple things: The table view doesn't have responsibility to remember what's in each of the cells. It throws away cells as the scroll away and asks the datasource to initialize them again via cellForRowAtIndexPath. Reloaddata - which you use in your add/remove methods - will cause the table to refresh all of the visible cells. Don't expect anything to appear in your table that isn't setup in cellForRowAtIndexPath.
Next, your "model" for this table is an NSNumber "tableRows" indicating the number of rows. This is an insufficient model for a table view. Replace it with an NSMutableArray. At the very least, this array should contain strings representing the state of each text field. (and it might need even more elaborate objects, but start with strings).
With that, your view controller class will look more like this...
// this is your table's model
#property (nonatomic, strong) NSMutableArray *rows;
// in init for the class
_rows = [NSMutableArray array];
// somewhere else, put some data in it
[self.rows addObject:#"Foo"];
[self.rows addObject:#"Bar"];
Now your datasource methods:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return self.rows.count;
}
Then, in cellForRowAtIndexPath:
NSUInteger *row = [indexPath row]; // as you have it
NSString *rowText = self.rows[row]; // this is new syntax, the old way is [self.rows objectAtIndex:row];
// your CustomCell needs to provide a way to get at the textField it contains
// it might do this with an outlet or viewWithTag...
cell.myTextField.text = rowText;
return cell;
Finally, text fields in the cells pose a particular challenge. How to save their current state when the view isn't scrolling. This problem has been asked and answered multiply in SO (here, for example). In a nutshell, the most common solution is to make the view controller the delegate of the text fields in the cells. Then, on textFieldDidEndEditing, save the value of the textField in your model like this...
- (void)textFieldDidEndEditing:(UITextField *)textField {
NSIndexPath *indexPath = [self indexPathOfCellWithSubview:textField];
self.rows[indexPath.row] = textField.text;
}
// I think this is the best way to get the index path of a cell, given some subview it contains
- (NSIndexPath *)indexPathOfCellWithSubview:(UIView *)view {
while (view && ![view isKindOfClass:[UITableViewCell self]]) {
view = view.superview;
}
return [self.tableView indexPathForCell:(UITableViewCell *)view];
}
EDIT Say there's more to the model than just a single string. This is where you would apply a custom subclass of NSObject.
// MyModel.h
#interface MyModel : NSObject
#property (strong, nonatomic) NSString *itemName;
#property (assign, nonatomic) CGFloat price;
#property (strong, nonatomic) NSString *imageFileName;
#property (strong, nonatomic) UIImage *image;
- (id)initWithItemName:(NSString *)itemName price:(CGFloat)price imageFileName:(NSString *)imageFileName;
- (NSString *)stringPrice;
- (void)setStringPrice:(NSString *)stringPrice;
#end
// MyModel.m
#implementation MyModel
- (id)initWithItemName:(NSString *)itemName price:(CGFloat)price imageFileName:(NSString *)imageFileName {
self = [self init];
if (self) {
_itemName = itemName;
_price = price;
_imageFileName = imageFileName;
}
return self;
}
// override the image getter to "lazily" create and cache the image
// if the images are on the web, this will require a slighly more elaborate method
// employing NSURLConnection.
- (UIImage *)image {
if (!_image) {
_image = [UIImage imageNamed:self.imageFileName];
}
return _image;
}
// added these to show you how you can conveniently encapsulate other
// behavior, like type conversion or validation, though, real ones of these
// would probably use NSNumberFormatter
- (NSString *)stringPrice {
return [NSString stringWithFormat: #"%.2f", self.price];
}
- (void)setStringPrice:(NSString *)stringPrice {
self.price = [stringPrice floatValue];
}
Now you can create one like this and add it to your table. (Be sure to #import "MyModel.h")
[self.rows addObject:[[MyModel alloc] initWithItemName:#"Toaster" price:39.95 imageFileName:#"toaster.png"]];
The view controller containing the table stays more or less the same (when you change one class a lot and change a closely related class very little, it tells you that your OO design is probably pretty good). For the fancy model replacing the string, we need to change cellForRowAtIndexPath...
NSUInteger *row = [indexPath row];
MyModel *myModel = self.rows[row];
cell.itemNameTextField.text = myModel.itemName;
cell.priceTextField.text = [myModel stringPrice];
cell.imageView.image = myModel.image;
// additional OO idea: teach your cell how to configure itself and move the foregoing code there
// [cell configureWithModel:myModel];
return cell;
ANOTHER EDIT: We can teach this model how to post itself to a remote web service as follows:
- (void)post {
NSString *hostStr = #"http://myhost/create_product.php";
NSURL *url = [NSURL URLWithString:hostStr];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = #"POST";
NSString *post =[NSString stringWithFormat:#"item_name=%#&price=%#",self.itemName, [self stringPrice];
NSString *postEscaped = [post stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSData *postData = [postEscaped dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
[request setHTTPBody:postData];
[request setValue:#"application/x-www-form-urlencoded charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"response %#", string);
} else {
NSLog(#"error %#", error);
}
}];
}
Declare this method in the .h, add other fields to the post as you see fit (e.g. the image file name, etc.)
In your view controller, pick out the action that means the user wants to commit the new row (maybe it's when the text field is finished editing?), and add this...
// text field finished editing
MyModel *myModel = self.rows[indexPath.row];
myModel.itemName = textField.text;
[myModel post];
Since the image will probably come from your remote service, you'll want to change the lazy loading image getter I added earlier. The right way to load this image is asynchronously, but doing so complicates the interaction with the table view too much to discuss here. Refer to apple docs or this SO post to learn more about that. In the meantime, here's the quick -- but basically wrong -- way to get the image synchronously...
- (UIImage *)image {
if (!_image) {
// note - now the file name must be of the form #"http://host/path/filename.png"
NSURL *url = [NSURL URLWithString:self.imageFileName
_image = [UIImage imageWithContentsOfURL:url];
}
return _image;
}
It would be helpful to see your code for cellForRowAtIndexPath, we need to know more about the model you intend to store data in.
When you delete a row from the table, that cell is thrown out, and the tableview will not remember the contents automatically. You must save the changes in a model object as they occur, and then use that to populate the cell's contents when returning a cell from cellForRowAtIndexPath.

UIImageView shows image on simulator but not on iPhone

I've been getting a very weird bug with one of my UIImageViews. I have a UITableView that lists items, and each cell is an item. Each item cell has a thumbnail image, a title, and a subtitle. I am able to show the thumbnail in every cell perfectly fine on the simulator and on the iPhone.
I also have a detail VC that, when a certain cell gets tapped, it segue's to that VC and shows the large image, along with some more info about the item. I've been able to load the image up in the simulator, but on the iPhone, all I get is a blank UIImageView.
This is how it looks in the simulator.
And my detail VC:
The blue view at the bottom is where I will add labels to show more details about the item, and the orange is a UIImageView that will show a custom image just saying 'item details'. One can tap on that, or my big UImageView and it will animate up (and down to dismiss).
Now, the problem is - when I segue from my table VC to my detail VC, the UIImageView is just blank on the iPhone. It works perfectly fine on the simulator, though, so I'm passing the right image and such.
Here is the code to segue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"showItemDetails"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
ItemDetailViewController *itemDetailVC = segue.destinationViewController;
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
if(item)
{
itemDetailVC.pictureImage = [UIImage imageWithContentsOfFile:item.photoURL];
}
}
}
My items gets saved using Core Data. photoURL is the URL of the photo in the documents directory of my app, as I do not save the entire image to Core Data, just a reference to it for performance purposes.
Here is part of the code that gets data from another VC in an "unwind" segue.
- (IBAction)saveItem:(UIStoryboardSegue *)segue
{
ManageItemViewController *manageItemVC = (ManageItemViewController *)segue.sourceViewController;
NSMutableDictionary *itemInfo = [[NSMutableDictionary alloc] init];
...
// write the original photo to a file and keep an URL to it that we pass to Core Data
NSData *imageData = [NSData dataWithData:UIImagePNGRepresentation(manageItemVC.pictureImage)];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0]; //Get the docs directory
NSString *fileName = [NSString stringWithFormat:#"%lf.png", [[NSDate date] timeIntervalSince1970]];
NSString *filePath = [documentsPath stringByAppendingPathComponent:fileName]; //Add the file name
[imageData writeToFile:filePath atomically:YES]; //Write the file
[itemInfo setObject:filePath forKey:CD_ITEM_PHOTOURL_PROPERTY];
...
[self saveItemToCoreData:[itemInfo copy]];
}
In my detail VC, this is all I do to load the UIImage I send to the big UIImageView.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(self.pictureImage) {
[self.pictureImageView setImage:self.pictureImage];
}
}
And when the user delete an item (delete row), I also remove the file from my documents directory, as such (this code is in my table VC):
- (void)deleteItemImageFromDevice:(NSString *)filePath
{
if(filePath)
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
if([fileManager removeItemAtPath:filePath error:&error]) {
NSLog(#"Image deleted from documents directory!");
} else {
NSLog(#"%#", error);
}
}
}
I logged this and I do not get any errors in the simulator.
Anyone know what the issue is here? Thanks.
I've managed to fix this issue.
For future reference, here are the fixes I made to my code.
Instead of a time interval, I now get a guaranteed unique string (no need for long float conversions) using NSProcessInfo, as so:
NSString *fileName = [NSString stringWithFormat:#"%#.png", [[NSProcessInfo processInfo] globallyUniqueString]];
Then, in my prepareForSegue: method, I get an UIImage from data that exists in our documents folder (a file). The item.photoURL is our file path.
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
if(item.photoURL)
{
itemDetailVC.pictureImage = [UIImage imageWithData:[NSData dataWithContentsOfFile:item.photoURL]];
}
Lastly, in our detail VC header file, our UIImage (not UIImageView!) needs to be strong reference, not a weak one, as we need to access it even before the VC is on screen:
#property (nonatomic, strong) UIImage *pictureImage;
The UIImageView we assign this UIImage to can be weak because it only needs to be shown when the view is on screen, and that holds a strong pointer to all of its subviews. For an excellent analogy of strong vs weak, check this.

How Can I Save This Array of Images?

I'm very new to programming, and I jumped right into a project (I know thats not the smartest thing to do, but I'm learning as I go). The app that I'm writing has 10 UIImageViews that display a picture from the users camera roll. The code I'm using needs each of the UIImageViews to have tags. I'm currently using NSData to save the array images, and it works great, but I can't use this method anymore because NSData doesn't support the use of tags. I also can't use NSUserDefaults, because I can't save images to a plist. Here is how I'm attempting to do this (using the NSData method, which works but I have to edit this so that my tags work.)
This is my current code:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)img editingInfo:(NSDictionary *)editInfo {
if (imageView.image == nil) {
imageView.image = img;
[self.array addObject:imageView.image];
[picker dismissModalViewControllerAnimated:YES];
[self.popover dismissPopoverAnimated:YES];
return;
}
if (imageView2.image == nil) {
imageView2.image = img;
NSLog(#"The image is a %#", imageView);
[self.array addObject:imageView2.image];
[picker dismissModalViewControllerAnimated:YES];
[self.popover dismissPopoverAnimated:YES];
return;
}
...
- (void)applicationDidEnterBackground:(UIApplication*)application {
NSLog(#"Image on didenterbackground: %#", imageView);
[self.array addObject:imageView.image];
[self.array addObject:imageView2.image];
[self.user setObject:self.array forKey:#"images"];
[user synchronize];
}
- (void)viewDidLoad
{
self.user = [NSUserDefaults standardUserDefaults];
NSLog(#"It is %#", self.user);
self.array = [[self.user objectForKey:#"images"]mutableCopy];
imageView.image = [[self.array objectAtIndex:0] copy];
imageView2.image = [[self.array objectAtIndex:1] copy];
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:app];
[super viewDidLoad];
}
Any help or suggestions on how to edit this code so that I can save the images, while using tags is much appreciated, thanks!
EDIT: Here is my updated code:
-(IBAction)saveButtonPressed:(id)sender {
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0];
for (UIImageView *imageView in self.array) {
NSInteger tag = self.imageView.tag;
UIImage *image = self.imageView.image;
NSString *imageName = [NSString stringWithFormat:#"Image%i.png",tag];
NSString *imagePath = [docsDir stringByAppendingPathComponent:imageName];
[UIImagePNGRepresentation(image) writeToFile:imagePath atomically:YES];
}
NSLog(#"Saved Button Pressed");
}
- (void)applicationDidEnterBackground:(UIApplication*)application {
}
-(void)viewDidLoad {
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];
NSArray *docFiles = [[NSFileManager defaultManager]contentsOfDirectoryAtPath:docsDir error:NULL];
for (NSString *fileName in docFiles) {
if ([fileName hasSuffix:#".png"]) {
NSString *fullPath = [docsDir stringByAppendingPathComponent:fileName];
UIImage *loadedImage = [UIImage imageWithContentsOfFile:fullPath];
if (!imageView.image) {
imageView.image = loadedImage;
} else {
imageView2.image = loadedImage;
}
}
}
}
You need to use "Fast Enumeration" to parse the array's objects, and write each object to disk sequentially. First, you're going to need to add the UIImageView objects to the array instead of the UIImage property of the UIImageView, so you can recover the tag. So instead of writing
[self.array addObject:imageView.image];
It will be
[self.array addObject:imageView];
Try to follow along with my code. I inserted comments on each line to help.
-(void)applicationDidEnterBackground:(UIApplication *)application {
//Obtain the documents directory
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainmask,YES) objectAtIndex:0];
//begin fast enumeration
//this is special to ObjC: it will iterate over any array one object at a time
//it's easier than using for (i=0;i<array.count;i++)
for (UIImageView *imageView in self.array) {
//get the imageView's tag to append to the filename
NSInteger tag = imageView.tag;
//get the image from the imageView;
UIImage *image = imageView.image;
//create a filename, in this case "ImageTAGNUM.png"
NSString *imageName = [NSString stringWithFormat:#"Image%i.png",tag];
//concatenate the docsDirectory and the filename
NSString *imagePath = [docsDir stringByAppendingPathComponent:imageName];
[UIImagePNGRepresentation(image) writeToFile:imagePath atomically:YES];
}
}
To load the images from disk, you'll have to look at your viewDidLoad method
-(void)viewDidLoad {
//get the contents of the docs directory
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainmask,YES) objectAtIndex:0];
//Get the list of files from the file manager
NSArray *docFiles = [[NSFileManager defaultManager]contentsOfDirectoryAtPath:docsDir error:NULL]);
//use fast enumeration to iterate the list of files searching for .png extensions and load those
for (NSString *fileName in docFiles) {
//check to see if the file is a .png file
if ([fileName hasSuffix:#".png"]) {
NSString *fullPath = [docsDir stringByAppendingPathComponent:fileName];
UIImage *loadedImage = [UIImage imageWithContentsOfFile:fullPath];
//you'll have to sort out how to put these images in their proper place
if (!imageView1.image) {
imageView1.image = loadedImage;
} else {
imageView2.image = loadedImage;
}
}
}
}
Hope this helps
One thing you need to be aware of is that when an app enters the background it has about 5 seconds to clean up its act before it's suspended. The UIPNGRepresentation() function takes a significant amount of time and is not instantaneous. You should be aware of this. It would probably be better to write some of this code in other places and do it earlier than at app backgrounding. FWIW
You can use the [NSbundle Mainbundel] to store that images.
To get path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
First, there's still a problem in your for loop.
for (UIImageView *imageView in self.array) {
NSInteger tag = self.imageView.tag;
UIImage *image = self.imageView.image;
// ...
}
Before you make any other changes, you must understand why. imageView is your for loop control variable, which changes on each iteration through the loop. self.imageView is a different thing. It is the first of the 10 imageViews attached to your viewController. Every time this loop cycles, it looks at the first imageView, and only the first.
As for why saving doesn't work, it's probably because the arrays elsewhere aren't working. Add some logging to make sure there's something in the array, and that it has as many elements as you expect.
-(IBAction)saveButtonPressed:(id)sender {
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0];
// Log to make sure the views expected have previously been stored.
// If the array is empty, or shorter than expected, the problem is elsewhere.
NSLog(#"Image view array before saving = %#", self.array);
for (UIImageView *imageViewToSave in self.array) {
NSInteger tag = imageViewToSave.tag;
UIImage *image = imageViewToSave.image;
NSString *imageName = [NSString stringWithFormat:#"Image%i.png",tag];
NSString *imagePath = [docsDir stringByAppendingPathComponent:imageName];
// Log the image and path being saved. If either of these are nil, nothing will be written.
NSLog(#"Saving %# to %#", image, imagePath);
[UIImagePNGRepresentation(image) writeToFile:imagePath atomically:NO];
}
NSLog(#"Save Button Pressed");
}

Populate UITableView in ViewController class from separate DataController class that is using grand central dispatch

I have a UITableView in a ViewController class. The ViewController class uses a custom dataController (specified in the AppDelegate). In the dataController class I'm fetching some JSON from the web, parsing it to an NSMutableArray, then using that data to populate the UITableView in the ViewController.
This all works great, except there is a noticeable lag when the app starts up since it takes time to get the JSON and work with it. I'd like to show an empty UITableView with an activity indicator while this data is loading. Unfortunately whenever I put the code in the dataController class into a dispatch queue, the UITableView is never populated with data (the data is loaded according to the log). All I see is a blank table.
I guess my main issue is I don't know how to set up a queue in the dataController class and then update the UI with the data in that queue but in another class.
Relevant code:
from dataController class:
- (void)initializeDefaultDataList {
NSMutableArray *dataList = [[NSMutableArray alloc] init];
self.masterDataList = dataList;
dispatch_queue_t myQueue = dispatch_queue_create("name.queue.my", NULL);
dispatch_async(myQueue, ^{
NSString *jsonString = [JSONHelper JSONpostString:#"http://webservice/getData"];
NSError *jsonError = nil;
//convert string to dictionary using NSJSONSerialization
NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData: [jsonString dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &jsonError];
if (jsonError) NSLog(#"[%# %#] JSON error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), jsonError.localizedDescription);
NSArray *dataArray = [jsonResults objectForKey:#"d"];
for (NSString *dataItem in dataArray) {
[self addDataWithItem:dataItem];
}
});
}
from AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
MyMasterViewController *firstViewController = (MyMasterViewController *)[[navigationController viewControllers] objectAtIndex:0];
MyDataController *aDataController = [[MyDataController alloc] init];
firstViewController.dataController = aDataController;
return YES;
}
from ViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//would this go here?
dispatch_async(dispatch_get_main_queue(), ^{
MyObject *objectAtIndex = [self.dataController objectInListAtIndex:indexPath.row];
[[cell textLabel] setText:objectAtIndex.name];
});
return cell;
}
In case you couldn't tell I'm really new to iOS and Objective C. Any help or hints you can give would be greatly appreciated. I'm not even sure if I'm expressing my question properly - it just seems that what I want to do shouldn't be this difficult. Thanks!
EDIT
Ok, so maybe this is a life cycle issue. Just realized that anything I set within the async block is nil outside the block, at least it is until it's too late to make a difference. That's why cellForRowAtIndexPath is never called - because the masterDataList being passed to the UITableView is empty. Tested this by initializing
__block NSString *s = [[NSString alloc] init];
outside the block, then setting a value inside the block:
s = #"Testing...";
and finally NSLogging the value of s after the block has supposedly run. But obviously the block hadn't run yet because s was nil.
It looks like you're doing the right thing to get back on the main thread after your work is done, but you haven't told the table view it needs to show the new data. [self.tableView reloadData] ought to help.
As I discovered in posts such as this one, data set within the async dispatch cannot be used outside the queue. As I understand it, the whole idea of GCD is that it determines when it's best to run and dispose of data.
As a result, I ended up splitting up my code so I was only using the DataController class to, well, control data (I know, revolutionary) and moved all the GCD parts to my ViewController. Amended code:
DataController class:
- (void)initializeDefaultDataList {
NSMutableArray *dataList = [[NSMutableArray alloc] init];
self.masterDataList = dataList;
}
ViewController class:
#interface ObjectMasterViewController () {
__block NSString *jsonString;
}
#end
...
- (void)getJSONString
{
jsonString = [JSONHelper JSONpostString:#"http://webservice/getData"];
}
...
- (void)initData {
NSError *jsonError = nil;
//convert string to dictionary using NSJSONSerialization
NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData: [jsonString dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &jsonError];
if (jsonError) NSLog(#"[%# %#] JSON error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), jsonError.localizedDescription);
NSArray *dataArray = [jsonResults objectForKey:#"d"];
//loop through array and add items to list
for (NSString *dataItem in dataArray) {
[self addDataWithItem:dataItem];
}
}
...
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t myQueue = dispatch_queue_create("name.queue.my", NULL);
dispatch_async(myQueue, ^{
//initalize service url string
[self getJSONString];
dispatch_async(dispatch_get_main_queue(), ^{
//retrieve data
[self initData];
//reload tableView with new data
[self.tableView reloadData];
});
});
}
Hope this can help someone who might be in the same boat I was in.