I have a UITableView where each section contains a single row which each contain a UICollectionView. The UITableView is the data source and delegate to a Core Data database. Updates to the database call the NSFetchedResultsController's controller: didChangeObject: method which queues blocks to update the relevant collection as so:
switch (type) {
case NSFetchedResultsChangeInsert: {
[self.blockOperation addExecutionBlock:^{
[collectionView insertSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] ];
}];
break;
}...
I then want to execute the blocks here as a batch:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[collectionView performBatchUpdates:^{
[self.blockOperation start];
} completion:^(BOOL finished) {
// Do whatever
}];
}
This is using Ash Furrow's and Blake Watters' techniques for hooking up a core data database to a UICollectionView.
My question is, how do I get access to the correct collectionView object in -controllerdidChangeContent:? Each block within self.blockOperation has the relevant collectionView, but I don't know how to parse it out from the NSBlockOperation's executionBlocks property, or even if that's the best way to get at it. Ash and Blake's example only has one UICollectionView whereas I have many.
You don't want to use only a single block operation because you won't be able to separate the data afterwards. Instead, use one block operation per collection view (which is also per section). Add each to a dictionary with the associated index path as the key (check it one already exists and create / update as necessary). When you run the blocks, iterate the dictionary, get the collection view for the index path (key) and run the block operation (value).
Related
I am rebuilding an application where I need to show a list of items. This list is retrieved using an API or retrieved from Core Data and shown in a UITableViewController. The problem I am having is that there are already seven different lists where there are small differences. Mostly of them are just different items, but also a search bar included in one list and previously stated one list will not load using an API, but from Core Data.
In my Storyboard I have added a UITableViewController with the class ItemsTableViewController which has a designed UITableViewCell. I have added an identifier to this cell so I can reuse it inside this view controller. There is a segue to this view controller from the home screen.
The idea was to create one parent object (ItemsTableViewController) and add multiple child objects (SavedItemsTableViewController, LocalItemsTableViewController, etc.) which will all use the shared logic of the parent with only some small changes (API and some custom things).
What I currently have is working, but without the child objects:
- (void)offlineButtonPressed {
[self performSegueWithIdentifier:#"openItemsTableViewController" sender:#(ItemListOffline)];
//[[self navigationController] pushViewController:[[OfflineItemsTableViewController alloc] init] animated:YES];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"openItemsTableViewController"]) {
switch((ItemList)[sender unsignedIntegerValue]) {
case ItemListOffline: { [(ItemsTableViewController *)[segue destinationViewController] retrieveOfflineDocuments]; break; }
case ItemListSearch: {
[(ItemsTableViewController *)[segue destinationViewController] retrieveDocumentsWithQuery:#""];
[(ItemsTableViewController *)[segue destinationViewController] addSearchBar];
break;
}
// Loop through all the list...
default: { NSAssert(NO, #"Unhandled type of document list."); break; }
}
}
}
So the application is calling just a function of the parent object which handles the request. But as you probably can feel, the object will be less maintainable. In comments I pushed the child view controller, but since it doesn't include the storyboard view, I need to recreate the cell from scratch, which doesn't feel good. Plus since the reuse identifier, it is not working since it is unable to dequeue a cell with the identifier set in the tableView:cellForRowAtIndexPath: method.
Now I am wondering what would be the best practice for this? It seems I can't use the save UITableViewController from the storyboard for multiple child classes, can I? But creating seven (and probably a even more in the future) controllers in the storyboard where I need to copy the cells to each controller and just give them different classes doesn't seem the way to do it. And add a method to the parent where the list would be retrieved differently and changing some stuff like adding a search bar is working, but also not the nicest way it seems.
Update: The logic in the ItemsTableViewController is pretty simple. In the delegate and datasource I handle the documents almost the same. The method that does the retrieving per list type is something like:
- (void)retrieveOfflineItems {
[self startLoading];
[[APIManager instance] getOfflineItems:^(NSArray<ItemList *> *list, NSError *error) {
[self setDocuments:list];
[[self tableView] reloadData];
}];
}
But there are more things, like the search has to add a search bar (once the view is loaded). So it needs to call multiple methods when performing the segue.
You can have a single UITableViewController, which includes all of your possible table cells, and as part of the prepareForSegue call, you should set an type identifier for the controller and the data - no matter where the data came from.
Within the UITableViewController class, you can hide / display the features that you need for this data type - such as the search bar, and in the tableView methods, choose which data source you need.
This way, the one UITableViewController class that you need to maintain is a little more complex than one dedicated class, but a lot more maintainable than 7 or more!
Assuming you have set up a variable dataType to identify the type of data you need, you could have something like this for the numberOfRowsInSection, and then similar for the other tableView methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch self.dataType {
case dataType.MyFirstDataType:
return myFirstDataTypeArray.count
case dataType.MySecondDataType:
return mySecondDataTypeArray.count
case dataType.TheCoreDataType:
return myCoreDataArray.count
default:
break
}
}
Create a parent UITableViewController with all subviews and cells including searchController. Initialize it and add/remove views and cells based on conditions
I have a tabBarView which have two tableViews. each of these tableViews will represent some news from a remote server. I want to populate tableView's datasource when tableViewController's init method is called. so I have put the needed networking operation inside init method. My init method is this:
- (instancetype) init{
self = [super init];
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
}];
self.tabBarItem.title = #"my title";
return self;
}
newsList is an array holding news loaded from server.
But when I run my project the order of invocation for tableViewController's methods is like the following:
tableViewController's init method is called and finished (but the completion handler block is not called yet)
tableViewController's viewDidLoad method is called ( it is called when the tableViewController is added to tabBarView's viewControllers array)
tableViewController's delegate method tableView:numberOfRowsInSection is called
the network operation's completionHandler block is called and the newsList array is set to the retrieved news from server
So my problem is that before my newsList array is populated the method tableView:numberOfRowsInSection is called and so my tableView is not filled with any news. How should I solve this issue?
Thanks
you should reload table data after you get data from server. then only your table will show updated data.
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
[yourTableview reloadData];//add this line
}];
The added line does the job and makes the new data to be loaded in the tableView but there is a small point that I think you should consider
[tableView reloadData]
will be executed in a thread other than mainThread and this will cause a 5 to 10 seconds delay for the data to be loaded on the tableView.
to prevent this you should somehow tell it to run the reloadData method on the main thread. this is done with the dispatch_async. So you should call [tableView reloadData] like this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
I am using a CollectionView which displays an array of objects.
On clicking a button i fill this array with a new set of data.
I want to refresh the CollectionView with this data.
Is there any statement to update this instead of comparing each items and selectively deleting and adding? The reloadData usually ends up in the following error.
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
In Short, I am looking for the following steps...
1)Fill the datasource array, show the data.
2)Fill the datasource array with new data, refresh the CollectionView to show the data.
Thanks in Advance
Try - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion.
In your case, you want "an all new set of data", so to speak, so e.g:
[myCV performBatchUpdates:^{
// one of:
// a)
[myCV deleteSection:someIndexSetForTheEntireSection];
[myRealDataSource empty:someIndexSetForTheEntireSection];
//
// OR b)
[myCV deleteItemsAtIndexPaths:someSetOfIndexPaths];
[myRealDataSource removeIndexPaths:someSetOfIndexPaths];
// Either case:
NSArray *indexPaths = [myRealDataSource getNewDataAndReturnIndexPaths];
// if a)
[myCV insertSections:newIndexSetForNewSection];
// Either case:
[myCV insertItemsAtIndexPaths:newIndexSetForInsertions];
}
completion:^(BOOL finished) {
NSLog(#"Done.");
// Maybe signal something else if you want.
}];
performBatchUpdates:completion: will expect the deletions & insertions from the original data source check entering the function to add up to the correct data source size leaving the method. It will loudly complain otherwise.
If you only have one section (section 0), you can be much more general than specific index paths if you are always "removing everything" and "inserting a complete new set".
Another option to to use KVO to listen on insertions and removals from the data source and simply reloadData, reloadItemsAtIndexPaths: or reloadSections: as appropriate.
I prefer the reactive KVO version, as I tend to use collection views with Core Data, and not pseudo-static or in-memory NSArray's.
To figure out the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION issue, I'd setup a breakpoint on all exceptions, and try to discover what is really triggering the issue. Likely your datasource is gone and there's a bad access when you try to read/write from it.
Suppose you arrive on your view then you can add data to your array in viewDidLoad method like so:
-(void)viewDidLoad
{
[super viewDidLoad];
// If you have already data
self.arr_sample=(NSArray *)array;
/*
When you want to download data from server than you have to call reloadData
method of collection because all delegate method already called before view load.
So loading data will take time to load data than you have to call all delegate
method of collectionview by calling reloadData method.
*/
[self loadData];
// Do any additional setup after loading the view from its nib.
}
but first of all you have set the delegate of collectionview .
do you want to download data from server than you can call reloaddata method of collection view. such as
-(void)loadData
{
// Do your downloading code her and call to reload data method of collectionview
[collectionview reloadData];
}
now again do you want to refill your array with new data on your button click than you can do
-(void)refillData
{
// Download new data code here and follow
[array removeAllObjects];
array=(NSArray *)newarray;
[collectionview reloadData];
}
I have a UITableView that sometimes has rapid insertions of new rows. The insertion of the new rows is handled by a notification observer listening for the update notification fired whenever the underlying data changes. I use a #synchronized block around all the data model changes and the actual notification post itself... hoping that each incremental data change (and row insertion) will be handled separately. However, there are times when this still fails. The exception will tell me that it expects 10 rows (based on the count from the data model), it previously had 8 rows, but the update notification only told it to insert a single row (as this is the first of two rapidly fired notifications).
I'm trying to understand how other people tend to handle these types of situations. How do other developers mitigate the problems of having multi-threaded race conditions between two table view update operations? Should I have a more secure lock that controls the update notifications (and why isn't #synchronized doing what it's supposed to)?
Any advice is greatly appreciated! Thanks.
Some pseudo-code:
My model class has a method like this, which gets called by other threads to append new rows to the table view:
- (void)addRow:(NSString *)data
{
#synchronized(self.arrayOfData)
{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
}
}
My controller class has a method like this to accept the kDataUpdatedNotification notification and actually perform the row insertion:
- (void)onDataUpdatedNotification:(NSNotification *)notification
{
NSDictionary *changes = notification.userInfo;
[self.tableView insertRowsAtIndexPaths:changes[#"insert"] withRowAnimation:UITableViewRowAnimationBottom];
}
You're going to have this problem if you change your data model asynchronously with the main queue because your table view delegate methods are looking at the current state of the data model, which may be ahead of the inserts you've reported to the table view.
UPDATE
One solution is to queue your updates on a private queue and have that queue update your data model on the main queue synchronously (I have not tested this code):
#interface MyModelClass ()
#property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
#end
#implementation MyModelClass
- (dispatch_queue_t)myDispatchQueue
{
if (_myDispatchQueue == nil) {
_myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
}
return _myDispatchQueue;
}
- (void)addRow:(NSString *)data
{
dispatch_async(self.myDispatchQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
});
}
The reason you need the intermediate dispatch queue is as follows. In the original solution (below), you get a series of blocks on the main queue that look something like this:
Add row N
Add row N+1
Block posted by table view for row N animation
Block posted by table view for row N+1 animation
In step (3), the animation block is out-of-sync with the table view because (2) happened first, which results in an exception (assertion failure, I think). So, by posting the add row blocks to the main queue synchronously from a private dispatch queue, you get something like the following:
Add row N
Block posted by table view for row N animation
Add row N+1
Block posted by table view for row N+1 animation
without holding up your worker queues.
ORIGINAL Solution still has issues with overlapping animations.
I think you'll be fine if you update your data model on the main queue:
- (void)addRow:(NSString *)data
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
}
I am new in iphone using MKMapView to load a google's map in my application .But its throwing an exception "MKMapView must be initialized on the main thread.". So where should I initialise my MKMapView object. I am initializing in viewDidLoad().
thanks in advance.....
The reason it isn't being creating on the main thread is one of 2 options
The View that creates it lives on one of the background threads
You are are calling performSelectorInBackground: to create your view
To perform a function on the Main Thread call performSelectorOnMainThread:
Off-topic: If the function requires more then 1 parameter, change it to an NSDictionary and load all of your settings into the Dictionary and pass it in eg
NSDictionary *params = ...... //load your parameters into here
[myMapView performSelectorOnMainThread:#selector(initMap:)
withObject:params
waitUntilDone:YES];
Also read the following First and Second to get a better understanding of multithreading in iOS
With Swift 3, the following will ensure your function runs on the main thread.
OperationQueue.main.addOperation{"your segue or function call"}
I got this error:
MKMapView must be initialized on the main thread
When I tried to change the content of a customized UITableViewCell in this way:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[myIndexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
The UITableviewCell in the corresponding indexpath contained a MKMapView that I tried to change its region.
myTabelViewCell *cell = [self.tableView cellForRowAtIndexPath:myIndexPath];
[cell setContentWithLatitude:currentLatitude longitude:currentLongitude];
Use the above code instead of [self.tableView reloadData] to refresh just one cell.