I have a UICollectionView and it works fine, but I want to add a few UICollectionViewCells items programmatically into the collection view.
So how can I achieve this?
To further clarify: when I say programmatically I mean inserting a cell during runtime, when an action is fired, not when the app is loaded (using the viewDidLoad method). I know when the model is updated and the call is made to UICollectionView in the insertItemsAtIndexPaths: method. It should create a new cells, but it's not doing that, it's throwing an error.
...By referring to UICollectionView documentation
You can accomplish:
Inserting, Deleting, and Moving Sections and Items To insert, delete,
or move a single section or item, follow these steps:
Update the data in your data source object.
Call the appropriate method of the collection view to insert or delete the section or item.
It is critical that you update your data source before notifying the
collection view of any changes. The collection view methods assume
that your data source contains the currently correct data. If it does
not, the collection view might receive the wrong set of items from
your data source or ask for items that are not there and crash your
app. When you add, delete, or move a single item programmatically, the
collection view’s methods automatically create animations to reflect
the changes. If you want to animate multiple changes together, though,
you must perform all insert, delete, or move calls inside a block and
pass that block to the performBatchUpdates:completion: method. The
batch update process then animates all of your changes at the same
time and you can freely mix calls to insert, delete, or move items
within the same block.
From your Question: you can for example register A gesture Recognizer, and Insert a NEW cell by
doing the following:
in
// in .h
#property (nonatomic, strong) NSMutableArray *data;
// in .m
#synthesize data
//
- (void)ViewDidLoad{
//....
myCollectonView.dataSource = self;
myCollectionView.delegate = self;
data = [[NSMutableArray alloc] initWithObjects:#"0",#"1", #"2" #"3", #"4",
#"5",#"6", #"7", #"8", #"9",
#"10", #"11", #"12", #"13",
#"14", #"15", nil];
UISwipeGestureRecognizer *swipeDown =
[[UISwipeGestureRecognizer alloc]
initWithTarget:self action:#selector(addNewCell:)];
swipeDown.direction = UISwipeGestureRecognizerDirectionDown;
[self.view addGestureRecognizer:swipeDown];
//..
}
-(void)addNewCell:(UISwipeGestureRecognizer *)downGesture {
NSArray *newData = [[NSArray alloc] initWithObjects:#"otherData", nil];
[self.myCollectionView performBatchUpdates:^{
int resultsSize = [self.data count]; //data is the previous array of data
[self.data addObjectsFromArray:newData];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (int i = resultsSize; i < resultsSize + newData.count; i++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i
inSection:0]];
}
[self.myCollectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
} completion:nil];
}
If you are inserting multiple items into UICollectionView, you can use performBatchUpdates:
[self.collectionView performBatchUpdates:^{
// Insert the cut/copy items into data source as well as collection view
for (id item in self.selectedItems) {
// update your data source array
[self.images insertObject:item atIndex:indexPath.row];
[self.collectionView insertItemsAtIndexPaths:
[NSArray arrayWithObject:indexPath]];
}
} completion:nil];
– insertItemsAtIndexPaths: does the job
Here is how to insert an item in Swift 3 :
let indexPath = IndexPath(row:index, section: 0) //at some index
self.collectionView.insertItems(at: [indexPath])
You must first update your data.
Related
I'm using a UICollectionView to display some data received from a remote server and I'm making two reloadData calls in a short time, first one to show a spinner (which is actually a UIActivityIndicatorView in a UICollectionViewCell) and the second one after the data was retrieved from server. I store the models based on which the collection view cells are created in a self.models NSArray and in cellForItemAtIndexPath I dequeue cells based on this model. My issue is that when I call reloadData after the request was completed (the second time) the collection view seems to be still in the process of creating the cells for the first reloadData call and it shows just the first two cells and a big blank space in place where the rest of the cells should appear.
I wrote some logs to get some insights of what's happening and the current workflow is:
I'm populating self.models with the model for the spinner cell and then I call reloadData on the UICollectionView.
numberOfItemsInSection is called, which is just returning [self.models count]. The returned value is 2, which is fine (one cell which acts like a header + the second cell which is the one with the UIActivityIndicator inside).
I'm making the request to the server, I get the response and I populate self.models with the new models received from the remote server (removing the model for the spinner cell but keeping the header cell). I call reloadData on the UICollectionView instance.
numberOfItemsInSection is called, which is now returning the number of items retrieved from the remote server + the model for the header cell (let's say that the returned value is 21).
It's just now when cellForItemAtIndexPath is called but just twice (which is the value returned by numberOfItemsInSection when it was called first).
It seems like the collection view is busy with the first reloadData when I call this method for the second time. How should I tell the collection view to stop loading cells for the first reloadData call? I tried with [self.collectionView.collectionViewLayout invalidateLayout] which seemed to work but the issue reappeared, so it didn't did the trick.
Some snippets from my code:
- (void)viewDidLoad {
[super viewDidLoad];
[ ...... ]
self.models = #[];
self.newsModels = [NSMutableArray array];
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:YES]; // to show the loading indicator
[self.collectionView reloadData];
[self updateNewsWithBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:NO];
[self.collectionView reloadData];
});
}];
}
- (void) buildModel:(BOOL)showSpinnerCell {
NSMutableArray *newModels = [NSMutableArray array];
[newModels addObject:self.showModel]; // self.showModel is the model for the cell which acts as a header
if ([self.newsModels count] != 0) {
// self.newsModels is populated in [self updateNewsWithBlock], see below
[newModels addObjectsFromArray:self.newsModels];
} else if (showSpinnerCell) {
[newModels addObject:[SpinnerCellModel new]];
}
self.models = [NSArray arrayWithArray:newModels];
}
- (void) updateNewsWithBlock:(void (^)())block {
// Here I'm performing a GET request using `AFHTTPSessionManager` to retrieve
// some XML data from a backend, then I'm processing it and
// I'm instantiating some NewsCellModel objects which represents the models.
[ ..... ]
for (NSDictionary *item in (NSArray*)responseObject) {
NewsCellModel *model = [[NewsCellModel alloc] init];
model.itemId = [item[#"id"] integerValue];
model.title = item[#"title"];
model.headline = item[#"short_text"];
model.content = item[#"text"];
[self.newsModels addObject:model];
}
block();
}
I have a ViewController that has a TableView generated with Custom Cells. Each Custom Cell has a NSDictionary that generates few arrays.
I want to find the way to replace one array in Custom Cell using [myArray replaceObjectAtIndex:... withObject:...] pointing towards a specific cell in a TableView, provided that I already know the indexPath of that cell that contains that array.
Another words I have to somehow indicate that Custom Cell indexPath, get there and refresh array.
Is there a way to accomplish that mission in Objective-C?
You can get a cell reference using cellForRowAtIndexPath call if you know the indexPath of the cell that you want to change if each of your custom cell is different unique.
But I strongly recommend changing the data model by getting it using cell location as the reference. Because table views reuse cell you could end up with unexpected results. You should instead change your table model and then reload the table or perhaps reload just the specific cells that have changed.
Once you have the indexPath of your cell, you can do something like:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPathOfYourCell, nil] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
You got several option for reloading the UITableView taken inside the UIViewController
say your table view is such
UITableView *tableView
case 1: Reload the whole table view appledoc
[tableView reloadData];
case 2: Reload all the rows inside a section appledoc
[tableView reloadRowsAtIndexPaths:yourIndexPath withRowAnimation: UITableViewRowAnimationNone];
case 3: Reload multiple section at once appledoc
tableView reloadSections:yourIndexPathSet withRowAnimation:UITableViewRowAnimationNone];
NB :: must do any of these operation in your mainThread / gui thread.
Solution.
First, we need to make sure that every Custom Cell has a #property NSInteger cellIndex;. When tableView populates Custom Cells, make sure to add cell.cellIndex = indexPath.row;. That way every Custom Cell will have it equal to tableView row.
Second, when making changes in parent viewController, in the end of update we post notification [[NSNotificationCenter defaultCenter] postNotificationName:#"refreshCollectionView" object:self];.
Third, in Custom Cell we listen to that notification:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(refreshCollect) name:#"refreshCollectionView" object:nil];. That triggers method (also included in Custom Cell) with reference to cellIndex:
-(void) refreshCollect {
if (cellIndex == [[[NSUserDefaults standardUserDefaults] valueForKey:#"cellIndex"] intValue]) {
NSURL *newsImgURL = [NSURL URLWithString:[NSString stringWithFormat:#"http://www.website.com/core/functions/ios/page.news.image?news_id=%#", [[NSUserDefaults standardUserDefaults] valueForKey:#"newsID"]]];
NSData *newsImgData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:newsImgURL] returningResponse:nil error:nil];
newsImgDict = [NSJSONSerialization JSONObjectWithData:newsImgData options: NSJSONReadingMutableContainers error:nil];
newsImgID2 = [NSMutableArray arrayWithArray: [newsImgDict valueForKey:#"image_id"]];
newsImgTime = [NSMutableArray arrayWithArray: [newsImgDict valueForKey:#"timestamp"]];
newsIDImg = [NSMutableArray arrayWithArray: [newsImgDict valueForKey:#"news_id"]];
newsImgDescr = [NSMutableArray arrayWithArray: [newsImgDict valueForKey:#"description"]];
newsImgExt2 = [NSMutableArray arrayWithArray: [newsImgDict valueForKey:#"ext"]];
} }
In my case I have NSDictionary that rebuilds some arrays just for that particular Custom Cell.
I am trying to remove a cell from my collectionView, however. When I remove the object from my datasource and attempt a batchupdate, it says the cell doesn't exist anymore.:
'NSInternalInconsistencyException', reason: 'attempt to delete item 0 from section 0 which only contains 0 items before the update'
Before this code I remove the content from my core data, and the line [[usermanager getSelectedUser]loadCards]; actually reloads the datasource containing the content for the cells by getting them from the Core Data.
- (void)cardRemoved:(NSNotification *)note {
NSDictionary *args = [note userInfo];
Card *card = [args objectForKey:#"card"];
[[usermanager getSelectedUser]loadCards];
[self.collectionView performBatchUpdates:^{
NSIndexPath *indexPath =[NSIndexPath indexPathForRow:card.position.intValue inSection:0];
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
} completion:^(BOOL finished){
[self.collectionView setDelegate:self];
[self.collectionView setDataSource:self];
[self.collectionView reloadData];
}];
}
If I print out the amount of Cells before I call the loadCards line, I get the correct amount of rows(As expected).
EDIT
This is what loadCards calls:
-(NSMutableArray *)getCards{
UserModel *selectedUser = [self getSelectedUserFromDB];
NSMutableArray *cards = [[NSMutableArray alloc] init];
for(CardModel *cardModel in selectedUser.cards){
[cards addObject:[self modelToCard:cardModel]];
}
return cards;
}
I noticed, even if I don't call the loadCards method, It says there are no items in the view.
Can anyone help me out? Thank you
Remove the cells from the UICollectionView, then remove them from the model. The same thing applies to UITableView. You also don't need to reload the collection view after removing items.
If you prefer you can just remove items from the model and then reload the collection view and the items that aren't in the model will disappear from the collection view, but without the same animation that comes from removing items from a collection view.
I am new in objective-c developpment, I need to find a way to refresh my tableview.
I have 2 UIViewControllers, in the second one I insert data into my database and then I instantiate the first viewcontroller, it contains my tableview. I call a method that allows it to recover all of the data from the database, but when I use [tableview reloadData] nothing happens and cellforrowatindexpath isn't called.
econdviewcontroller:
//I insert data in database and I instanciate class where my tableview is and call refresh method
first = [[FirstviewController alloc]initWithNibName:#"FirstviewController" bundle:nil];
[first refreshList];
in Firstviewcontroller
-(void)refreshList{
self.tableview= [[[UITableView alloc] initWithFrame:self.view.bounds] autorelease];
tableview.dataSource = self;
tableview.delegate = self;
NSMutableArray *array = [[NSMutableArray alloc] init];
//I recover my data from data base
IPADAGRIONEActivityList *arrayActivities = [IPADAGRIONEActivity findAll];
if ([arrayActivities length] > 0)
{
for (IPADAGRIONEActivity * oneRec in arrayActivities)
{
[array addObject:oneRec];
}
}
//activities is NSMutablearray that contains all my data
self.activities = array;
//I build dictionnary
[self buildObjectsDictionnary:activities
NSLog(#"self.act%#",self.tableview);
[array release];
[self.tableview reloadData];
}
//numberofrowsinSection:
NSLog(#"rows%d",[[objects objectForKey:[objectsIndex objectAtIndex:section]] count]);
return [[objects objectForKey:[objectsIndex objectAtIndex:section]] ;
//numberOfSection:
NSLog(#"nbre of section%d",[objectsIndex count]);
return [objectsIndex count];}
//CellforRowatInddexPath: It dosen't access to this method
if (cell== nil) {
cell = [[MHCActivityListCell alloc]init];
}
IPADAGRIONEActivity *activite;
cell.activityCategory.text = [NSString stringWithFormat:#"%#", [activite EMAIL]];
You are instantiating a new tableview every time you call -refreshList. Remember that the tableView is a view, thus it needs to be added as a subview.
Once you understand that, you'll see that there is no reason to alloc+init a new instance of tableView every time you need it to refresh (and if, for any reason you need it, you need to add the tableView as a subview again). Basically, you need to make sure that the tableView that is being displayed is the same tableView that you're storing on your tableView property.
As mentioned earlier by Bruno, you are instantiating your tableview every time and this is not the right approach. You need to refresh the same tableview which had the original data.
in your refresh list do something like this
-(void) refreshList
{
// Get a reference to your viewcontroller's tableview
UITableview *tableview = [self tableview];
// Do your data manipulation
[tableview reloadData];
}
After using [UITableView deleteSections:withRowAnimation:] on a section which is out of view - the section header remains visible.
On this image, we see the visible part of the tableview
On the next image, we see the whole tableview - AISLE 2 is hidden until the user scrolls down, it contains only one row:
When I scroll down and delete the last row, AISLE 2 section header remains visible, even though I used deleteSections. if I delete a row from AISLE 1, the section header remains on the same place, and by scrolling down I can still see it.
Furthermore, when trying to scroll down so that AISLE 2 header is in the view, the UI acts as AISLE2 is NOT part of the tableview, and immediately scrolls me back up. Which means - this is a garbage view that is obviously not part of the table, since I removed it. for some reason, iOS doesn't remove this view, but de-associates it from the table.
Any ideas?
Try [tableview reload] with making numberofsections as 1.
Where is your data coming from and how do you know how many sections there are at the start? I think your problem can easily be resolved by explaining this.
It looks like you may have a multidimentional nsmutablearray where each index is an aisle and each object contains the products for that isle? Or you may have a different array for each aisle?
When you delete a cell, simply check how many cells are left, and call [self.tableView reloadData];
For (hypothetical) example;
if you have arrays of your Aisles:
NSMutableArray *aisleOne = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleTwo = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleThree = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleFour = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
and you add them to one array:
NSMutableArray *aisleArray = [[NSMutableArray alloc] initWithObjects:aisleOne, aisleTwo, aisleThree, aisleFour, nil];
then, call this code when you delete a cell. It will remove all empty Aisles from the aisleArray (which needs to be globally defined):
NSMutableArray *tempArray = [[NSMutableArray alloc] initWithArray:aisleArray];
for (int i=0; i<[tempArray count]; i++) {
if ([[tempArray objectAtIndex:i] count] == 0) {
[aisleArray removeObjectAtIndex:i];
}
}
[self.tableView reloadData];
For this to work, these two methods should be:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [aisleArray count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[aisleArray objectAtIndex:section] count];
}
(untested)
Just get the table haderview for that section and remove it from it's superview and tableView is not managing it.
UIView *mysteriousView = [tableView headerViewForSection:indexPath.section];
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationAutomatic];
[mysteriousView removeFromSuperview];
Hope this helps!
Few yers later I'm facing same issue... Deleting last section doesn't remove last section header from table view.
I noticed however that the remaining header has frame exactly just below normal table view content.
So the workaround (in swift but you can easily translate it to Objective C) that worked for me is something like this:
DispatchQueue.main.async {
let unwantedViews = self.tableView.subviews.filter{ $0.frame.minY >= self.tableView.contentSize.height}
unwantedViews.forEach{ $0.isHidden = true }
}