I am using AFNetworking to download files from my server. It works fine. But I have one issue: My ProgressView updates wrong cell(UI, not data) when I scroll up or down. Here is my code:
My Cell:
AFHTTPRequestOperation *operation;
#property (weak, nonatomic) IBOutlet DACircularProgressView *daProgressView;
- (IBAction)pressDown:(id)sender {
AFAPIEngineer *apiEngineer = [[AFAPIEngineer alloc] initWithBaseURL:[NSURL URLWithString:AF_API_HOST]];
operation = [apiEngineer downloadFile:(CustomObject*)object withCompleteBlock:^(id result) {
} errorBlock:^(NSError *error) {
}];
__weak typeof(self) weakSelf = self;
apiEngineer.afProgressBlock = ^(double progress, double byteRead, double totalByToRead) {
[weakSelf.daProgressView setProgress:progress animated:YES];
};
}
- (void)setDataForCell:(id)object{
}
My table:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomCell *cell = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:NSStringFromClass([CustomCell class])];
cell.backgroundColor = [UIColor clearColor];
CustomObject *aObject = [listObject objectAtIndex:indexPath.row];
[cell setDataForCell: aObject];
return cell;
}
My downloadHelper:
- (AFDownloadRequestOperation*)downloadFile:(CustomObject*)aObject
withCompleteBlock:(AFResultCompleteBlock)completeBlock
errorBlock:(AFResultErrorBlock)errorBlock{
NSString *link = URL;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:link]];
NSString *path = [NSString databasePathWithPathComponent:[NSString stringWithFormat:#"%#.zip", #"noname"]];
AFDownloadRequestOperation *operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:path shouldResume:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completeBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
errorBlock(error);
}];
[operation setProgressiveDownloadProgressBlock:^(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
float progressF = (float)totalBytesReadForFile / totalBytesExpectedToReadForFile;
self.afProgressBlock(progressF, totalBytesReadForFile, totalBytesExpectedToReadForFile);
}];
[operation start];
return operation;
}
When I press button 'Download' in first cell:
When I scroll down then scroll up, It is second cell:
So, my question is: How to update UIProgressView on UITableViewCell? What is wrong with my code?
Because cells are re-used, you simply can't just keep a weak reference to the cell and update the cell's progress view. At the very least, rather than using a weak reference to the table view cell, you would use the index path, look up the correct cell using [tableView cellForRowAtIndexPath:indexPath] (this is a UITableView method that identifies the cell associated with a particular NSIndexPath and should not to be confused with the similarly named UITableViewDataSource method in which we do our cell dequeueing/configuring logic), and update that cell's progress view (assuming that cell is even visible).
Frankly, even that is dangerous, as it assumes that it's impossible to add or remove cells while downloads are in progress. (And that is obviously not a reasonable assumption/constraint to place upon an app.) Quite frankly, every time one wants to update download progress, one really should go back to the model, lookup the correct NSIndexPath based for the model object in question, and then use the above cellForRowAtIndexPath pattern to find and then update the cell in question.
Note, that suggests that the initiation of the download request may not be initiated by the table view cell. Personally, I maintain a model object that consists of an array of files to download, and associate the download queue with that, rather than the cell (though the progress handler would obviously update the cell). In short, you want a loosely coupled relationship between the UI (e.g. the cell) and the model driving the downloads.
Note, many naive implementations consider all of the above points and decide to abandon cell reuse in order to simplify the problem. I won't belabor the point, but I think that's a fundamentally misguided approach. In my opinion, one should have a proper model for the downloads separate from the view objects.
I have a small project on github that basically does what you want with a different style progress indicator. I built it a while back and it uses exactly the approach Rob mentions: The table and its cells are backed by a "model" that tells cells at a given index what their state should be. I also make sure cells are reused. It may be of some use to you:
https://github.com/chefnobody/StreamingDownloadTest
Related
I have to reload the data in the collection view, in order to set a new cells sizes according to the new data source .
Than i have to scroll to the start, and i would like to do that with animation .
every row has 1 cell in it .
So, using this :
[self.collectionView reloadData];
[self.collectionView setContentOffset:CGPointZero animated:YES];
Whats happens is that it scrolls to the start, and reloaded, but when he comes to image 1 , than i see many images replaced fast, and it stopes on some image that is not belong to the first cell, but to other cell .
If i scroll to the start- without animation (animated:NO) its not happens .
I need that animation.
What could cause this problem ?
EDIT:
I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.
I think i solve the problem, or improved things a lot .
Well when you start downloading from the net, give your block a tag :
- (void)downloadImageWithURL:(NSURL *)url AndTag:(int)Gtag completionBlock:(void (^)(BOOL succeeded, NSData *data,int tag))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
int thetag=Gtag;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (!error)
{
completionBlock(YES, data,thetag);
}
else
{
completionBlock(NO, nil,thetag);
}
}];
}
Than, when the download complete, we will check if the cell is on screen right now , if its not, we will not load its image to the cell :
[self downloadImageWithURL:url AndTag:(int)cell.tag completionBlock:^(BOOL succeeded, NSData *data,int tag)
{
BOOL isvisible=0;
for (iPhoneCostumCell *tcell in [self.collectionView visibleCells])
{
if(tcell.tag==tag)
{
isvisible=1;
break;
}
}
I think this is a good solution , and it makes things more stable . It also makes the processor work less, because if we dont find the image, we dont continue to the NSData conversion-made in another thread .
The problem is that cells are reused,
So let's see this example
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
cell.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *image = [self imageForIndexPathRow:#(indexPath.row)];
dispatch_async(dispatch_get_main_queue(), ^{
cell.image = image;
});
});
return cell;
}
As the operations are asynchronous, scrolling fast will cause to dispatch 2 operations for 1 cell.
For example lets name some cell pointer "ImageCell-1"
Now lets see, what will be, if you start fast scrolling, and 2 operations are dispatched.
First operation loads "a.png", and download is active, you scroll fast and "ImageCell-1" is now being loaded "b.png" (but "a.png" is still loading).
So "a.png" completes download and is set to "ImageCell-1", then completes "b.png" and is set to "ImageCell-1".
(this is the issue that you see => "I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.")
But also can be situation (when you use "high level libraries" like AFNetworking, when queues are managed by them, and concurrency downloading is available) "b.png" completes download and sets it to "ImageCell-1", after that completes "a.png" download and set to "ImageCell-1". This will cause to see is "a.png" instead of "b.png".
The solution is to cancel download operation, before starting new one, if you are using AFNetworking, keep NSOperations, and call [operation cancelOperation]; before starting new download.
EDIT:
If you're using NSURLConnection, you need to switch to modern way of using NSURLSession (to support request canceling).
Here I have wrote a small method, which resolves your problem
#interface MyCell ()
#property (strong, nonatomic) NSURLSessionDataTask *dataTask;
#end
- (void)loadImageFromPath:(NSString *)aPath
availableCache:(NSCache *)aCache
{
if (self.dataTask) {
[self.dataTask cancel];
}
NSData *cache = [aCache objectForKey:aPath];
if (cache) {
self.image = [UIImage imageWithData:cache];
return;
}
self.dataTask = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:aPath]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error)
{
if (data) {
self.image = [UIImage imageWithData:data];
self.dataTask = nil;
// Add to cache
[aCache setObject:data forKey:aPath];
}
}];
[self.dataTask resume];
}
Place this method in your Cell,
And call it from cellForRow, like this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
[cell loadImageFromPath:imagePath availableCache:self.cache];
return cell;
}
[UIView animateWithDuration:1.0 delay:0.0 options: UIViewAnimationOptionCurveEaseIn animations:^{
[self.collectionView setContentOffset:CGPointZero animated:NO];
} completion:nil];
}];
Try to make your own animation.
At this point I'm really fed up. It's been nearly a week now trying to solve this issue so I can move ahead. I've read multiple threads and done multiple searches in regards to my slow loading choppy UICollectionView.
I've tried to do this without any libraries as well as with SDWebImage and AFNetwork. It still doesn't fix things. Images loading isn't really a problem. The problem arrives when I scroll to cells that aren't currently showing on the screen.
As of now I've deleted all the code and all traces of any libraries and would like to get help in order to implement this properly. I've made about 2 posts on this already and this would be my third attempt coming from a different angle.
Information
My backend data is stored on Parse.com
I have access to currently loaded objects by calling [self objects]
My cellForItemAtIndex is a modified version that also returns the current object of an index.
From what I understand in my cellForItemAtIndex I need to check for an image, if there isn't one I need to download one on background thread and set it so it shows in the cell, then store a copy of it in cache so that if the associated cell goes off screen when I do scroll back to it I can use the cached image rather than downloading it again.
My custom parse collectionViewController gives me all the boiler plate code I need to get access to next set of objects, current loaded objects, pagination, pull to refresh etc. I really just need to get this collection view sorted. I never needed to do any of this with my tableview of a previous app which had much more images. It's really frustrating spending a whole day trying to solve an issue and getting no where.
This is my current collectionView cellForItemAtIndex:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
// check for image
// if there is a cached one use that
// if not then download one on background thread
// set my cells image view with that image
// cache image for re-use.
// PFFile *userImageFile = object[#"image"];
[[cell title] setText:[object valueForKey:#"title"]]; //title set
[[cell price] setText:[NSString stringWithFormat: #"£%#", [object valueForKey:#"price"]]]; //price set
return cell;
}
I am also using a custom collectionViewCell:
#interface VAGGarmentCell : UICollectionViewCell
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#property (weak, nonatomic) IBOutlet UITextView *title;
#property (weak, nonatomic) IBOutlet UILabel *price;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
If there's any more information you'd like please ask. I'd just like a clear example in code of how to do this correctly, if it still doesn't work for me then I guess there is something wrong some where within my code.
I'm going to continue reading through various threads and resources I've come across in the last few days. I can say one benefit in this experience is that I have a better understanding of threads and lazy loading but it is still very frustrated that I have made any progress with my actual app.
Incase you wondered here is my previous post: In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?
I'd either like to do this quick and manually or using the AFNetwork as that didn't cause any errors or need hacks like SDWebImage did.
Hope you can help
Kind regards.
You can make use of the internal cache used by NSURLConnection for this.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"VAGGarmentCell" forIndexPath:indexPath];
//Standard code for initialisation.
NSURL *url; //The image URL goes here.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0]; //timeout can be adjusted
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *image = [UIImage imageWithData:data];
//Add image as subview here.
}
}];
.
.
return cell;
}
This is for a table view, but same concept basically. I had the same issue you were having. I had to check for a cached image, if not, retrieve it from a server. The main thing to watch out for is when you retrieve the image back, you have to update it in the collection view on the main thread. You also want to check if the cell is still visible on the screen. Here is my code as an example. teamMember is a dictionary and #"avatar" is the key which contains the URL of the user's image. TeamCommitsCell is my custom cell.
// if user has an avatar
if (![teamMember[#"avatar"] isEqualToString:#""]) {
// check for cached image, use if it exists
UIImage *cachedImage = [self.imageCache objectForKey:teamMember[#"avatar"]];
if (cachedImage) {
cell.memberImage.image = cachedImage;
}
//else retrieve the image from server
else {
NSURL *imageURL = [NSURL URLWithString:teamMember[#"avatar"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
// if valid data, create UIImage
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
// if valid image, update in tableview asynch
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
// if valid cell, display image and add to cache
if (updateCell) {
updateCell.memberImage.image = image;
[self.imageCache setObject:image forKey:teamMember[#"avatar"]];
}
});
}
}
});
}
}
NSURLCache is iOS's solution to caching retrieved data, including images. In your AppDelegate, initialize the shared cache via:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:cache];
return YES;
}
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
Then use AFNetworking's UIImageView category to set the image using:
[imageView setImageWithURL:myImagesURL placeholderImage:nil];
This has proven to load images the second time around incredibly faster. If you are worried about loading images faster for the first time, you will have to create a way to determine when and how many images you want to load ahead of time. It is very common to load data using paging. If you are using paging and still are having trouble, consider using AFNetworking's:
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
This way you can create an array of UIImages and using this method to return the images for each cell before dequeuing the cell. So in this case you would have two parallel arrays; one holding your data and the other holding corresponding UIImages. Memory management will eventually get out of hand so keep that in mind. If someone scrolls quickly to the bottom of the available cells, there is honestly not much else you can do since the data depends on the network connection of the user.
After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.
I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.
I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.
So I've asked a couple of questions regarding the UICollectionView. Understanding how it works, I'm trying to implement lazy loading to load 15 images onto the view controller. I found many examples 1, 2, 3...first and third examples deal with only one operation, second example I don't think uses operations at all, only threads. My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again. Here's my code:
view controller:
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
self.images = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.images.delegate = self;
self.images.dataSource = self;
[self.images registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"cellIdentifier"];
[self.view addSubview:self.images];
self.operation = [[NSOperationQueue alloc]init];
[self.operation addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"getgallery.php?user=%#", userId] relativeToURL:[NSURL URLWithString:#"http://www.mywebsite.com/"]];
NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.images reloadData];
//reload collection view to place placeholders
}];
}];
- (void)viewDidAppear:(BOOL)animated{
//get the visible cells right away
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
self.processedImages = [[NSMutableDictionary alloc]initWithCapacity:visibleCellPaths.count];
}
#pragma mark - collection view
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
int i;
if (self.imagesGalleryPaths.count == 0)
i = 25;
else
i = self.imagesGalleryPaths.count;
return i;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
if ([visibleCellPaths containsObject:indexPath]) {
[self setUpDownloads:visibleCellPaths];
}
image.image = [UIImage imageNamed:#"default.png"];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}
return cell;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}
- (void)setUpDownloads:(NSArray *)visiblePaths{
//I want to pass the visiblePaths to the NSOperation class if visible cells changed
GalleryOps *gallery = [[GalleryOps alloc]init];
//I will use a dictionary to keep track of which indexPaths are being downloaded...
//...so there are no duplicate downloads
}
GalleryOps.m
#implementation GalleryOps
- (void)main{
//would I initialize all the operations here and perform them?
}
It's almost pointless to show the empty GalleryOps class because I have no idea how to initialize it with multiple operations. I know I have to override the main method, and once I get the image data from URL, I'll need to update the UI, for which I need a delegate method...another thing I don't yet know how to do but there are many examples to figure that out. My biggest question is how to pass the visible cells into this class and run multiple operations? When new visible cells come in, I'll run a check to see which to cancel, which to keep. Any advice here? Thanks in advance!
Looking at your proposed solution, it looks like you want to defer the question of making the operations cancelable. Furthermore, it looks like you want to defer the use of the cache (even though it's no more complicated than your NSMutableDictionary property).
So, setting that aside, your revised code sample has two "big picture" issues:
You can dramatically simplify the image retrieval process. The use of startOperationForVisibleCells and the two scroll delegates is unnecessarily complicated. There is a much simpler pattern in which you can retire those three methods (and achieve an even better UX).
Your cellForItemForIndexPath has a problem, that you're adding subviews. The issue is that cells are reused, so every time a cell is reused, you're adding more redundant subviews.
You really should subclass UICollectionViewCell (CustomCell in my example below), put the configuration of the cell, including the adding of subviews, there. It simplifies your cellForItemAtIndexPath and eliminates the problem of extra subviews being added.
In addition to these two major issues, there were a bunch of little issues:
You neglected to set maxConcurrentOperationCount for your operation queue. You really want to set that to 4 or 5 to avoid operation timeout errors.
You are keying your imageGalleryData with the NSIndexPath. The problem is that if you ever deleted a row, all of your subsequent indexes would be wrong. I suspect this isn't an issue right now (you're probably not anticipating deleting of items), but if you keyed it by something else, such as the URL, it's just as easy, but it is more future-proof.
I'd suggest renaming your operation queue from operation to queue. Likewise, I'd rename the UICollectionView from images (which might be incorrectly inferred to be an array of images) to something like collectionView. This is stylistic, and you don't have to do that if you don't want, but it's the convention I used below.
Rather than saving the NSData in your NSMutableDictionary called imageGalleryData, you might want to save the UIImage instead. This saves you from having to reconvert from NSData to UIImage (which should make the scrolling process smoother) as you scroll back to previously downloaded cells.
So, pulling that all together, you get something like:
static NSString * const kCellIdentifier = #"CustomCellIdentifier";
- (void)viewDidLoad
{
[super viewDidLoad];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
// renamed `images` collection view to `collectionView` to follow common conventions
self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
// you didn't show where you instantiated this in your examples, but I'll do it here
self.imageGalleryData = [NSMutableDictionary dictionary];
// register a custom class, not `UICollectionViewCell`
[self.collectionView registerClass:[CustomCell class] forCellWithReuseIdentifier:kCellIdentifier];
[self.view addSubview:self.collectionView];
// (a) change queue variable name;
// (b) set maxConcurrentOperationCount to prevent timeouts
self.queue = [[NSOperationQueue alloc]init];
self.queue.maxConcurrentOperationCount = 5;
[self.queue addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"getgallery.php?user=%#", userId] relativeToURL:[NSURL URLWithString:#"http://www.mywebsite.com/"]];
NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.collectionView reloadData];
}];
}];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagesGalleryPaths count]; // just use whatever is the right value here, don't make this unnecessarily smaller
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];
NSString *key = self.imagesGalleryPaths[indexPath.row]; // I don't know whether this was simply array, or some nested structure, so tweak this accordingly
UIImage *image = self.imageGalleryData[key];
if (image) {
cell.imageView.image = image; // if we have image already, just use it
} else {
cell.imageView.image = [UIImage imageNamed:#"profile_default.png"]; // otherwise set the placeholder ...
[self.queue addOperationWithBlock:^{ // ... and initiate the asynchronous retrieval
NSURL *url = [NSURL URLWithString:...]; // build your URL from the `key` as appropriate
NSData *responseData = [NSData dataWithContentsOfURL:url];
if (responseData != nil) {
UIImage *downloadedImage = [UIImage imageWithData:responseData];
if (downloadedImage) {
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageGalleryData[key] = downloadedImage;
CustomCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = downloadedImage;
}
}];
}
}
}];
}
return cell;
}
// don't forget to purge your gallery data if you run low in memory
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self.imageGalleryData removeAllObjects];
}
Now, clearly I don't have access to your server, so I couldn't check this (notably, I don't know if your JSON is returning a full URL or just a filename, or whether there was some nested array of dictionaries). But I don't want to you to get too lost in the details of my code, but rather look at the basic pattern: Eliminate your looping through visible cells and responding to scroll events, and let cellForItemAtIndexPath do all the work for you.
Now, the one thing that I introduced was the concept of CustomCell, which is a UICollectionViewCell subclass that might look like:
// CustomCell.h
#import <UIKit/UIKit.h>
#interface CustomCell : UICollectionViewCell
#property (nonatomic, weak) UIImageView *imageView;
#end
and then move cell configuration and adding of the subview here to the #implementation:
// CustomCell.m
#import "CustomCell.h"
#implementation CustomCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1;
self.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
[self addSubview:imageView];
_imageView = imageView;
}
return self;
}
#end
By the way, I still contend that if you want to do this properly, you should refer to my other answer. And if you don't want to get lost in those weeds of the proper implementation, then just use a third party UIImageView category that supports asynchronous image retrieval, caching, prioritizing network requests of visible cells, etc., such as SDWebImage.
You can use the queue and operations themselves to manage multiple operations. If I am reading your question correctly, you have one operation (get the list of image URLs from JSON) that you want to spawn child operations. You can do this by having the parent operation add the child operations to the queue, or by using dependent operations (child would have the parent as a dependancy).
For what you're trying to do you do not need to subclass NSOperation, NSBlockOperation should meet your needs. Subclassing NSOperation is trickier than it looks because of the KVO dependancies (it's very easy to get wrong).
But to the specifics of your question:
My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again
If you're initializing them again they're new objects (or at least, they should be). NSOperations can't be re-run because they have internal state - the tricky KVO bits I mention above. Once they go to "finished", that instance can't be returned to a clean state.
Operations should be fairly lightweight objects and there should not be any significant value in reusing them, and plenty of potential trouble. Creating new operations should be the way to go.
The Apple sample code "LazyTableImages" may give you some hints as how to accomplish what you're trying to do.
The constituent elements of an NSOperation-based lazy loading of images might include:
Create a dedicated NSOperationQueue that will be used for the download operations. Generally this is configured with a maxConcurrentOperationCount of 4 or 5 so that you enjoy concurrency, but so that you won't exceed the maximum number of concurrent network operations.
If you don't use this maxConcurrentOperationCount, with slow network connections (e.g. cellular), you risk having network requests time out.
Have a model object (e.g. an array) that backs your collection view or table view. This would generally only have some identifier for the image (e.g. the URL) not the image itself.
Implement a cache mechanism to store the downloaded images, to prevent the need to re-download images that have already been downloaded. Some implementations only do memory based cache (via NSCache). Others (e.g. SDWebImage) will do two tiers of cache, both memory (NSCache, for optimal performance) and a secondary persistent storage cache (so that when memory pressure forces you to purge the NSCache, you still have a rendition saved on the device so you don't have to re-retrieve it from the network). Others (e.g. AFNetworking) rely upon NSURLCache to cache the responses from the server into persistent storage.
Write a NSOperation subclass for downloading a single image. You want to make this cancelable operation. That implies two different design considerations
First, regarding the operation itself, you probably want to make a concurrent operation (see the Configuring Operations for Concurrent Execution section in the Concurrency Programming Guide).
Second, regarding the network request, you want a cancelable network request. If using NSURLConnection, this means using the delegate-based rendition (not any of the convenience methods). And if using NSURLConnection, the trick is that you have to schedule it in a run loop that persists. There are a number of tricks to accomplish this, but the easiest is to schedule it (with scheduleInRunLoop:forMode:) in the main run loop (though there are more elegant approaches), even though you will be running this from an operation in an NSOperationQueue. Personally I launch a new dedicated thread (like AFNetworking does) for this purpose, but the main run loop is easier and is fine for this sort of process.
If using NSURLSession, this process is conceivably a tad easier, because you can get away with using the completion block rendition of dataTaskWithRequest and not get into the delegate-based implementation if you don't want to. But this is iOS 7+ only (and if you need to do anything fancy like handle authentication challenge requests, you'll end up going the delegate-based approach anyway).
And combining those two prior points, the custom NSOperation subclass would detect when the operation is canceled and then cancel the network request and complete the operation.
By the way, instances of operations are never reused. You create a new operation instance for each image you are downloading.
By the way, if the images you've downloaded are large (e.g. they have dimensions greater than the number of pixels that the image view needs), you may want to resize the images before using them. When JPG or PNG images are downloaded, they are compressed, but when you use them in an image view they are uncompressed, usually require 4 bytes per pixel (e.g. a 1000x1000 image will require 4mb, even though the JPG is much smaller than that).
There are lots of image resizing algorithms out there, but here is one: https://stackoverflow.com/a/10859625/1271826
You will want a cellForItemAtIndexPath that then pulls the above pieces together, namely:
Check to see if the image is already in the cache, if so, use it.
If not, you will start a network request to retrieve the image. You might want to see if this cell (which may be a reused cell from your table view) already has an image operation already in progress, and if so, just cancel it.
Anyway, you can then instantiate a new NSOperation subclass for the downloading of the image and have the completion block update the cache and then also cell's image view.
By the way, when you asynchronously update the cell's image view, make sure the cell is still visible and that the cell hasn't been reused in the interim. You can do this my calling [collectionView cellForItemAtIndexPath:indexPath] (which should not be confused with the similarly named UICollectionViewDataSource method that you're writing here).
Those are the constituent parts of the process, and I'd suggest you tackle them one at a time. There's a lot involved in writing an elegant implementation of lazy loading.
The easiest solution is to consider using an existing UIImageView category (such as provided with SDWebImage) which does all of this for you. Even if you don't use that library, you'll might be able to learn quite a bit by reviewing the source code.
I figure my collection view is not something the user will come back to over and over again, just once in a while. So no reason to cache all the images.
The viewDidAppear is still the same, I get the visible cells right away. The reason why initially I put 25 cells into numberOfItems... is just to get the visible cells right away. So now my cellForItemAtIndexPath is this:
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
image.image = [UIImage imageNamed:#"profile_default.png"];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}
return cell;
In the viewDidLoad I added this:
if (self.imagesGalleryPaths.count != 0) {
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.images reloadData];
[self startOperationForVisibleCells];
}];
}
This is my startOperationForVisibleCells:
[self.operation addOperationWithBlock:^{
int i=0;
while (i < visibleCellPaths.count) {
NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
if (![self.imageGalleryData.allKeys containsObject:indexPath]) {
NSURL *url = [#"myurl"];
NSData *responseData = [NSData dataWithContentsOfURL:url];
if (responseData != nil) {
[self.imageGalleryData setObject:responseData forKey:indexPath];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
UIImageView *image = [UIImageView new];
image.image = [UIImage imageWithData:responseData];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}];
}
}
i++;
}
}];
And that's how I update the cells one by one. Also when the user scrolls away:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
for (int i=0; i<visibleCellPaths.count; i++) {
NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
if ([self.imageGalleryData.allKeys containsObject:indexPath]) {
UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
UIImageView *image = [UIImageView new];
image.image = [UIImage imageWithData:[self.imageGalleryData objectForKey:indexPath]];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}else{
[self startOperationForVisibleCells];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//same functions as previous
}
I am sure this is a very bad way of doing it but for now, it works. The images are loaded one by one and they stop loading when the user scrolls away.
Can anybody explain to me how MVC works when it comes to UITableView especially when getting data from the internet.
I would exactly like to know what is the model, view and controller when it comes to a UItableview
I have written the following ViewController code which sources data from the internet and displays it on a table using AFNetworking framework.
Could you please tell me how to change this and separate it into model, view and controller.
I have also written a refresh class, which i am guessing is a part of the model. Could you tell me how exactly do i make changes and make it a part of the model.
EDIT : The below answers help me understand the concept theoritically, Could someone please help me in changing the code accordingly( By writing a new class on how to call the array to this class and populate the table because i am using a json parser). I would like to implent it. And not just understand it theoritically.
#import "ViewController.h"
#import "AFNetworking.h"
#implementation ViewController
#synthesize tableView = _tableView, activityIndicatorView = _activityIndicatorView, movies = _movies;
- (void)viewDidLoad {
[super viewDidLoad];
// Setting Up Table View
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, self.view.bounds.size.height) style:UITableViewStylePlain];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.hidden = YES;
[self.view addSubview:self.tableView];
// Setting Up Activity Indicator View
self.activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicatorView.hidesWhenStopped = YES;
self.activityIndicatorView.center = self.view.center;
[self.view addSubview:self.activityIndicatorView];
[self.activityIndicatorView startAnimating];
// Initializing Data Source
self.movies = [[NSArray alloc] init];
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rocky&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
[refreshControl endRefreshing];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
}
- (void)refresh:(UIRefreshControl *)sender
{
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rambo&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
[sender endRefreshing];
}
- (void)viewDidUnload {
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
// Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.movies && self.movies.count) {
return self.movies.count;
} else {
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = #"Cell Identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellID];
}
NSDictionary *movie = [self.movies objectAtIndex:indexPath.row];
cell.textLabel.text = [movie objectForKey:#"trackName"];
cell.detailTextLabel.text = [movie objectForKey:#"artistName"];
NSURL *url = [[NSURL alloc] initWithString:[movie objectForKey:#"artworkUrl100"]];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"placeholder"]];
return cell;
}
#end
It's a pretty big question you are asking. But let me answer by making it as simple as possible.
Model - your data source; ultimately it's your web service data
Controller should be the thing that owns the table view and mediates setting properties on your view and reacting to events in the view and making changes , as needed, to the model
View(s) -- a combination of your table view and table view cells
There are a lot of approaches to coordinating between your web data and your table view but one I might suggest would be to refactor your web service calls into a separate store class - say iTunesStore - have that class be responsible for making the calls to the service and setting an internal array with the results, it should also be able to return a row count as well as a specific item for a given row index.
You then have this class respond to calls for the required table view delegate methods. Other things to consider, make this other class a singleton, have it conform to UITableviewDatasource protocol itself and assign it as the table views' data source.
Like I said, a big question with a lot of options for you, but I've given you some things to consider in terms of where to go next.
UPDATE
I'm adding some code examples to help clarify. At the outset, I want to make clear that I am not going to provide the total solution because doing so would require me to assume too much in terms of the necessary actual solution -- and because there are a few different ways to work with AFNetworking, web services, etc....and I don't want to get side tracked going down that rabbit hole. (Such as caching data on the client, background tasks & GCD, etc...) Just showing you how to wire up the basics -- but you will definitely want to learn how to use AFNetworking on a background task, look into Core Data or NSCoding for caching, and a few other topics to do this sort of thing correctly.
Suffice it to say that in a proper solution:
- You don't want to be calling your web service synchronously
- You also don't want to be re-requesting the same data every time - ie don't re-download the same record from the service unless its changed
- I am not showing how to do those things here because its way beyond the scope; look a the book recommendation below as well as this link to get an idea about these topics Ray Wenderlich - sync Core Data with a web service
For your data services code, I would create a 'store' class. (do yourself a favor and get the Big Nerd Ranch iOS book if you don't already have it.
iOS Programming 4th Edition
Take the following the code with a grain of salt - for reasons I can't go into I am not able to do this from my Mac (on a Win machine) and I also am not able to copy or even email myself the code ... so I am doing all in the StackOverflow editor...
My iTunesStore contract (header file) would look something like:
// iTunesStore.h
#import <Foundation/Foundation.h>
#interface iTunesStore : NSObject
- (NSUInteger)recordCount;
- (NSDictionary*)recordAtIndex:(NSUInteger)index; // could be a more specialized record class
+ (instancetype)sharedStore; // singleton
#end
...and the implementation would look something like:
// iTunesStore.m
#import "iTunesStore.h"
// class extension
#interface iTunesStore()
#property (nonatomic, strong) NSArray* records;
#end
#implementation iTunesStore
-(id)init
{
self = [super init];
if(self) {
// DO NOT DO IT THIS WAY IN PRODUCTION
// ONLY FOR DIDACTIC PURPOSES - Read my other comments above
[self loadRecords];
}
return self;
}
- (NSUInteger)recordCount
{
return [self.records count];
}
- (NSDictionary*)recordAtIndex:(NSUInteger)index
{
NSDictionary* record = self.records[index];
}
-(void)loadRecords
{
// simulate loading records from service synchronously (ouch!)
// in production this should use GCD or NSOperationQue to
// load records asynchrononusly
NSInteger recordCount = 10;
NSMutableArray* tempRecords = [NSMutableArray arrayWithCapacity:recordCount];
// load some dummy records
for(NSInteger index = 0; index < recordCount; index++) {
NSDictionary* record = #{#"id": #(index), #"title":[NSString stringWithFormat:#"record %d", index]};
[tempRecords addObject:record];
}
self.records = [tempRecords copy];
}
// create singleton instance
+ (instancetype)sharedStore
{
static dispatch_once_t onceToken;
static id _instance;
dispatch_once(&onceToken, ^{
_instance = [[[self class] alloc] init];
});
return _instance;
}
#end
I now have a 'store' object singleton I can use to get records, return a given record and also tell me a record count. Now I can move a lot of the logic doing this from the viewcontroller.
Now I don't need to do this in your VC viewDidLoad method. Ideally, you would have an async method in your store object to get records and a block to call you back once records are loaded. Inside the block you reload records. The signature for something like that 'might' look like:
[[iTunesStore sharedStore] loadRecordsWithCompletion:^(NSError* error){
... if no error assume load records succeeded
... ensure we are on the correct thread
[self.tableView reloadData]; // will cause table to reload cells
}];
Your view controller data source methods now look like:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection(NSInteger)section {
[[iTunesStore sharedStore] recordCount];
}
Inside cellForRowAtIndexPath - I also call my store object to get the correct record
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ... get cell
// ... get record
NSDictionary* record = [[iTunesStore sharedStore] recordAtIndex:indexPath.row];
// ... configure cell]
return cell;
}
That's the gist of it. Other things to do, as noted above would be:
Have ITunesStore implement UITableViewDataSource and then just directly handle the tableview datasource methods - if you do this you don't want to make iTunesStore a singleton. And you would set an instance of iTunesStore as the tableview's delegate, rather than the viewcontroller. There are pros and cons to such an approach.
I haven't shown any real async behavior or caching which this app is crying out for
This does show how to pull off some of your model responsibilities and separate some of the tableview data source concerns.
Hopefully this will help to give you some ideas about different directions you might explore.
Happy coding!
In terms of UITableViewController, typically all the roles Model, View and Controller (MVC) is played by your UITableViewController itself. That is the case with your code as well.
As Model - It supplies data to your table view.
As Controller - It controls the look and feel of the table like number of rows, sections, height and width of them etc., supplies data from model to table view
As View - Its view property holds the UITableView
Now, to adopt a different approach you could have Model separated out from your controller class. For that have a subclass from NSObject and have it set its state which could be used by Controller.
Hope this makes sense to you.
As the title implies, my UICollectionView doesn't update and display the cells immediately after calling reloadData. Instead, it seems to eventually update my collection view after 30-60 seconds. My setup is as follows:
UICollectionView added to view controller in Storyboard with both delegate and dataSource setup for the view controller and standard outlet setup
numberOfSectionsInRow & cellForItemAtIndexPath are both implemented and reference the prototyped cell and the imageView inside of it
Here is the code that goes to Twitter, get's a timeline, assigns it to a variable, reloads a table view with the tweets and then goes through the tweets to find photos and reloads the collection view with those items.
Even if I comment out the code to display the image, it still doesn't change anything.
SLRequest *timelineRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:timelineURL parameters:timelineParams];
[timelineRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if(responseData) {
JSONDecoder *decoder = [[JSONDecoder alloc] init];
NSArray *timeline = [decoder objectWithData:responseData];
[self setTwitterTableData:timeline];
for(NSDictionary *tweet in [self twitterTableData]) {
if(![tweet valueForKeyPath:#"entities.media"]) { continue; }
for(NSDictionary *photo in [[tweet objectForKey:#"entities"] objectForKey:#"media"]) {
[[self photoStreamArray] addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[photo objectForKey:#"media_url"], #"url",
[NSValue valueWithCGSize:CGSizeMake([[photo valueForKeyPath:#"sizes.large.w"] floatValue], [[photo valueForKeyPath:#"sizes.large.h"] floatValue])], #"size"
, nil]];
}
}
[[self photoStreamCollectionView] reloadData];
}
}];
This is a classic symptom of calling UIKit methods from a background thread. If you view the -[SLRequest performRequestWithHandler:] documentation, it says the handler makes no guarantee of which thread it will be run on.
Wrap your call to reloadData in a block and pass this to dispatch_async(); also pass dispatch_get_main_queue() as the queue argument.
You need to dispatch the update to the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self.photoStreamCollectionView reloadData];
});
or in Swift:
dispatch_async(dispatch_get_main_queue(), {
self.photoStreamCollectionView.reloadData()
})
Apple say:You should not call this method in the middle of animation blocks where items are being inserted or deleted. Insertions and deletions automatically cause the table’s data to be updated appropriately.
In face: You should not call this method in the middle of any animation (include UICollectionView in the scrolling).
so, you can:
[self.collectionView setContentOffset:CGPointZero animated:NO];
[self.collectionView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
or mark sure not any animation, and then call reloadData;
or
[self.collectionView performBatchUpdates:^{
//insert, delete, reload, or move operations
} completion:nil];