UITableView with complex cells is slow and laggy - ios

I've almost finished my app and everything seems to work but the main view.
It's an UIViewController with an embedded UITableView.
I'm using Parse as the backend, and I get an array of the objects I need in my viewDidLoad method.
Each cell contains some data that I'm fetching in the tableView:cellForRowAtIndexPath and I'm afraid that this is the reason why my table view is so laggy, but I don't know how to fetch the data I need for each object in my array without having the indexPath.row number.
I've already made each cell element "opaque" as suggested in other answers.
This is my code, any help would be greatly appreciated:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"cellHT";
CellHT *cell = (CellHT *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[CellHT alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// self.hH is an NSArray containing all the objects
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
cell.lblTitle.text = [self.hH[indexPath.row] objectForKey:#"title"];
cell.lblVenueName.text = [self.hH[indexPath.row] objectForKey:#"venueName"];
cell.lblDistance.text = NSLocalizedString(#"Distance from you", nil);
self.geo = [self.hH[indexPath.row] objectForKey:#"coordinates"];
// the formatters are initialized in the viewDidLoad: method
self.formatData = [NSDateFormatter dateFormatFromTemplate:#"dd/MM" options:0 locale:[NSLocale currentLocale]];
[self.formatterData setDateFormat:self.formatData];
self.formatOra = [NSDateFormatter dateFormatFromTemplate:#"j:mm" options:0 locale:[NSLocale currentLocale]];
[self.formatterOra setDateFormat:self.formatOra];
self.dal = NSLocalizedString(#"from", nil);
self.ore = NSLocalizedString(#"at", nil);
CLLocation *vLoc = [[CLLocation alloc] initWithLatitude:self.geo.latitude longitude:self.geo.longitude];
CLLocation *user = [[CLLocation alloc] initWithLatitude:self.userGeo.latitude longitude:self.userGeo.longitude];
CLLocationDistance distance = [user distanceFromLocation:venueLoc];
if ([[prefs objectForKey:#"unit"] isEqualToString:#"km"]) {
cell.lblDist.text = [NSString stringWithFormat:#"%.1f Km", distance /1000];
} else {
cell.lblDist.text = [NSString stringWithFormat:#"%.1f Miles", distance /1609];
}
// compare the object's starting date with the current date to set some images in the cell
NSComparisonResult startCompare = [[self.hH[indexPath.row] objectForKey:#"startDate"] compare: [NSDate date]];
if (startCompare == NSOrderedDescending) {
cell.quad.image = [UIImage imageNamed:#"no_HT"];
cell.lblStartTime.textColor = [UIColor redColor];
} else {
cell.quad.image = [UIImage imageNamed:#"yes_HT"];
cell.lblStartTime.textColor = [UIColor colorWithRed:104.0/255.0 green:166.0/255.0 blue:66.0/255.0 alpha:1.0];
}
NSString *dataInizio = [NSString stringWithFormat:#"%# %# %# %#", self.dal, [self.formatterData stringFromDate:[self.hH[indexPath.row] objectForKey:#"startDate"]], self.ore, [self.formatterOra stringFromDate:[self.hH[indexPath.row] objectForKey:#"endDate"]]];
cell.lblStartTime.text = dataInizio;
PFObject *cat = [self.hH[indexPath.row] objectForKey:#"catParent"];
NSString *languageCode = [[NSLocale preferredLanguages] objectAtIndex:0];
if ([languageCode isEqualToString:#"it"]) {
cell.lblCategory.text = [cat objectForKey:#"nome_it"];
} else if ([languageCode isEqualToString:#"es"]) {
cell.lblCategory.text = [cat objectForKey:#"nome_es"];
} else {
cell.lblCategory.text = [cat objectForKey:#"nome_en"];
}
//getting the image data from the Parse PFFile
PFFile *theImage = [self.hH[indexPath.row] objectForKey:#"photo"];
[theImage getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
cell.cellImageView.image = [UIImage imageWithData:data];
}
}];
//getting the cell object's owner and his profile
PFUser *usr = [self.hH[indexPath.row] objectForKey:#"parent"];
PFQuery *prof = [PFQuery queryWithClassName:#"Profile"];
prof.cachePolicy = kPFCachePolicyCacheThenNetwork;
[prof whereKey:#"parent" equalTo:usr];
[prof getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
//getting the object's rating and the number of votes
PFQuery *rateQuery = [PFQuery queryWithClassName:#"Rating"];
[rateQuery whereKey:#"parent" equalTo:object];
[rateQuery getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
float vote = [[object objectForKey:#"rate"] floatValue];
float temp = ((vote * 2) + 0.5);
int tempvote = (int)temp;
float roundedVote = (float)tempvote / 2;
// drawing the stars number, depending on the rating obtained
UIImage *starsImage = [UIImage imageNamed:#"stars"];
UIGraphicsBeginImageContextWithOptions(cell.imgVoto.frame.size, NO, 0);
CGPoint starPoint = (CGPoint) {
.y = (cell.imgVoto.frame.size.height * (2 * roundedVote + 1)) - (starsImage.size.height)
};
[starsImage drawAtPoint:starPoint];
cell.imgVoto.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
cell.lblVoto.text = [NSString stringWithFormat:#"(%d)", [[object objectForKey:#"voters"] intValue]];
}
}];
}
}];
return cell;
}
EDIT: this is the cell code:
+ (void)initialize {
if (self != [HH class]) {
return;
}
}
-(id)initWithCoder:(NSCoder *)aDecoder {
if ( !(self = [super initWithCoder:aDecoder]) ) return nil;
self.cellImageView.image = [UIImage imageNamed:#"icona_foto"];
self.cellImageView.contentMode = UIViewContentModeScaleToFill;
self.formatterData = [[NSDateFormatter alloc] init];
self.formatData = [[NSString alloc] init];
self.formatterOra = [[NSDateFormatter alloc] init];
self.formatOra = [[NSString alloc] init];
self.formatData = [NSDateFormatter dateFormatFromTemplate:#"dd/MM" options:0 locale:[NSLocale currentLocale]];
[self.formatterData setDateFormat:self.formatData];
self.formatOra = [NSDateFormatter dateFormatFromTemplate:#"j:mm" options:0 locale:[NSLocale currentLocale]];
[self.formatterOra setDateFormat:self.formatOra];
self.lblVoto.text = #"(0)";
return self;
}
SECOND EDIT: this is the code in the viewDidLoad method:
PFQuery *hours = [PFQuery queryWithClassName:#"HH"];
hours.cachePolicy = kPFCachePolicyCacheThenNetwork;
// here I'm making lots of query constraints that I'll not include
[hours findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.objectsNumber = objects.count;
self.hH = [[NSArray alloc] initWithArray:objects];
}
}];
[self.tableView reloadData];
}

I would move as much of the logic out of cellForRowAtIndexPath: as you can, it needs to be very light-weight to get good scrolling performance. You're doing a lot of work on the main thread, and I would do a lot more of this work when you get your model objects back from Parse (if you could post viewDidLoad I can give you more specific help) and update the table view when these calls are done:
[UIImage imageWithData:data]
anything to do with NSDateFormatter
CLLocation's initWithLatitude:longitude:
creating the rating stars image
None of these depend on the state of the table view, so they can be effectively precomputed and cached in a model object. If you simply scroll up and down the table, you're doing allo f the same work over and over, killing your performance.
Updated for the questioner's newest code:
I won't include all of your functionality here but this should give you an idea:
// create a single shared formatter instead of one per object
NSDateFormatter *dateFormatter = [NSDateFormatter dateFormatFromTemplate:#"dd/MM" options:0 locale:[NSLocale currentLocale]];
NSDateFormatter *timeFormatter = [NSDateFormatter dateFormatFromTemplate:#"j:mm" options:0 locale:[NSLocale currentLocale]];
[hours findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.objectsNumber = objects.count;
for (SomeObject *modelObj in objects) {
// if you can add properties to your model object directly, do that
// otherwise write a category on the Parse object to add the ones you need
modelObj.dateString = [NSString stringWithFormat:#"%# %# %# %#", modelObj.dal, [self.dateFormatter stringFromDate:[modelObj objectForKey:#"startDate"]], modelObj.ore, [self.timeFormatter stringFromDate:[modelObj objectForKey:#"endDate"]]];
// create your locations, images, etc in here too
}
self.hH = [[NSArray alloc] initWithArray:objects];
}
}];]
Then in cellForRowAtIndexPath:, take the precomputed properties and simply assign them to the appropriate labels, image views, etc.
It would be even better to do most of this processing off the main thread via GCD, but that is most likely out of scope for this question. See Using GCD and Blocks Effectively for more information. Just remember do only interact with UIKit from the main thread!

have a try by removing
CLLocation *vLoc = [[CLLocation alloc] initWithLatitude:self.geo.latitude longitude:self.geo.longitude];
CLLocation *user = [[CLLocation alloc] initWithLatitude:self.userGeo.latitude long itude:self.userGeo.longitude];
CLLocationDistance distance = [user distanceFromLocation:venueLoc];
This was at first sight , then I see your all your code and I realize a lot of image are used

Because UITableView takes some time to layout cells.
Solution:
step1. Set section number and row number to 0.
step2. Reload tableView in viewDidAppear.
Then your view controller cloud response quickly and then show cells.

Related

Difficulty getting media to replace placeholder following download using JSQMessage

I'm using JSQMessage and am having a little difficulty with showing the placeholder for media until I have it correctly downloading, and then replacing with the media. I have everything working correctly as far as adding the messages and media to server, I just can't get it to replace the placeholders.
Currently, I have a function that queries my database and pulls an array of objects for messages and then loops through and calls this function for each object to output and add it to my message thread. I'm struggling to figure out why the section with "messageToAdd.isMediaMessage" is not replacing the placeholders with the actual media following it's download from the server. Does anyone know how I should be handling this to make sure it adds the message with a placeholder, and then replaces once the media is downloaded correctly?
- (void)addMessage:(PFObject *)object
{
id<JSQMessageMediaData> messageMedia = nil;
PFObject *user = object[#"messageSender"];
[users addObject:user];
NSString *name = #"";
if(user[#"profileFName"] && user[#"profileLName"])
name= [NSString stringWithFormat:#"%# %#",user[#"profileFName"],user[#"profileLName"]];
else
name= [NSString stringWithFormat:#"%# %#",user[#"consultantFName"],user[#"consultantLName"]];
if([object[#"messageFileType"] isEqual: #"video"]){
JSQVideoMediaItem *messageMedia = [[JSQVideoMediaItem alloc] init];
messageMedia.fileURL = nil;
messageMedia.isReadyToPlay = NO;
messageToAdd = [JSQMessage messageWithSenderId:user.objectId displayName:name media:messageMedia];
} else if ([object[#"messageFileType"] isEqual: #"image"]){
JSQPhotoMediaItem *messageMedia = [[JSQPhotoMediaItem alloc] init];
messageMedia.image = nil;
messageToAdd = [JSQMessage messageWithSenderId:user.objectId displayName:name media:messageMedia];
} else{
messageToAdd= [[JSQMessage alloc] initWithSenderId:user.objectId senderDisplayName:name date:object[#"sendDate"] text:object[#"messageContent"]];
}
if(isLoadMore)
[messages insertObject:messageToAdd atIndex:0];
else
[messages addObject:messageToAdd];
// NOT TRIGGERING THESE AFTER MEDIA DOWNLOADED
if (messageToAdd.isMediaMessage) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
if ([object[#"messageFileType"] isEqual: #"image"]){
[object[#"messageMedia"] getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
JSQPhotoMediaItem *photoItem = [[JSQPhotoMediaItem alloc] initWithImage:[UIImage imageWithData:imageData]];
((JSQPhotoMediaItem *)messageMedia).image = [UIImage imageWithCGImage:photoItem.image.CGImage];
[self.collectionView reloadData];
}
}];
}
else if([object[#"messageFileType"] isEqual: #"video"]){
PFFile *videoFile = object[#"messageMedia"];
NSURL *videoURL = [NSURL URLWithString:videoFile.url];
((JSQVideoMediaItem *)messageMedia).fileURL = videoURL;
((JSQVideoMediaItem *)messageMedia).isReadyToPlay = YES;
[self.collectionView reloadData];
}
else {
NSLog(#"%s error: unrecognized media item", __PRETTY_FUNCTION__);
}
});
}
}
For others who come along with the same issue/question, I resolved how it was working by looking at the project NotificationChat here:https://github.com/relatedcode/NotificationChat/blob/master/NotificationChat/Classes/Chat/ChatView.m. It gives a really good overview of using the JSQMessage platform.
Here's my modified function so you can see the finished product.
- (void)addMessage:(PFObject *)object
{
PFObject *user = object[#"messageSender"];
[users addObject:user];
PFFile *mediaMessage = object[#"messageMedia"];
NSString *name = #"";
if(user[#"profileFName"] && user[#"profileLName"])
name= [NSString stringWithFormat:#"%# %#",user[#"profileFName"],user[#"profileLName"]];
else
name= [NSString stringWithFormat:#"%# %#",user[#"consultantFName"],user[#"consultantLName"]];
if([object[#"messageFileType"] isEqual: #"video"]){
JSQVideoMediaItem *mediaItem = [[JSQVideoMediaItem alloc] initWithFileURL:[NSURL URLWithString:mediaMessage.url] isReadyToPlay:YES];
mediaItem.appliesMediaViewMaskAsOutgoing = [user.objectId isEqualToString:self.senderId];
messageToAdd = [[JSQMessage alloc] initWithSenderId:user.objectId senderDisplayName:name date:object.createdAt media:mediaItem];
} else if ([object[#"messageFileType"] isEqual: #"image"]){
JSQPhotoMediaItem *mediaItem = [[JSQPhotoMediaItem alloc] initWithImage:nil];
mediaItem.appliesMediaViewMaskAsOutgoing = [user.objectId isEqualToString:self.senderId];
messageToAdd = [[JSQMessage alloc] initWithSenderId:user.objectId senderDisplayName:name date:object.createdAt media:mediaItem];
[mediaMessage getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error)
{
if (error == nil)
{
mediaItem.image = [UIImage imageWithData:imageData];
[self.collectionView reloadData];
}
}];
} else{
messageToAdd= [[JSQMessage alloc] initWithSenderId:user.objectId senderDisplayName:name date:object[#"sendDate"] text:object[#"messageContent"]];
}
if(isLoadMore)
[messages insertObject:messageToAdd atIndex:0];
else
[messages addObject:messageToAdd];
}
Based on the code I think one possible reason is you need reloadData on main(UI) thread after download data successfully and asynchronously on background thread

Adding mapview annotations within parse query returns null randomly

I am creating an iOS app using Parse database(asynchronously) to store information that will be used when populating a mapview. I have been trying to figure out what is wrong for a long time and have done plenty of research without any luck. I have, however, found the source of the issue.
In my code, I am querying the parse database in hopes of getting the information I want and then storing the information in a custom pointAnnotation class, which is of type MkPointAnnotation. Each item is stored in an array of pointAnnotations, and once all items in the database have been stored in the array, the annotations are added to MyMapView. --I have tried adding the annotations as they are created, which does not change anything.
The issue I have been having is that randomly, the query will iterate under the for(PFObject *vendor in Vendors) and reach an error, calling NSLog(#"%#", error.debugDescription); which shows (null) in the output log. The amount of objects that return null seems to change each time I run the application, and occasionally it will work as expected. After adding a do while(pointArray.count < query.countObjects), the function will iterate roughly 20-30 times and then will add the correct number of annotations, however, it is extremely inefficient.
Is this an inefficiency within Parse or is there a better way to achieve the expected results?
PFQuery *query = [PFQuery queryWithClassName:#"Vendors"];
[query orderByDescending:#"updatedAt"];
[query findObjectsInBackgroundWithBlock:^(NSArray *vendors, NSError *error){
NSMutableArray *pointArray = [[NSMutableArray alloc] init];
if (!error) {
// The find succeeded.
// Do something with the found objects
do {
pointArray = [[NSMutableArray alloc] init];
for (PFObject *vendor in vendors) {
NSDate *lastUpdated = vendor.updatedAt;
NSDate *today = [NSDate date];
NSDate *newDate = [lastUpdated dateByAddingTimeInterval:86400];
if (today <= newDate) {
PFGeoPoint *point = vendor[#"Location"];
NSString *vendor_ID = vendor[#"Vendor_ID"];
NSMutableArray *FruitList = vendor[#"Fruits"];
NSMutableArray *VeggieList = vendor[#"Veggies"];
NSMutableArray *addressArray = vendor[#"Address"];
NSString *startHr = vendor[#"Start_Time"];
NSString *endHr = vendor[#"End_Time"];
Boolean more = false;
NSString *moreString = vendor[#"And_More"];
if ([moreString isEqual: #"true"]) {
more = true;
}
CLLocationCoordinate2D location;
location.latitude = point.latitude;
location.longitude = point.longitude;
pointAnnotation *newAnnotation = [[pointAnnotation alloc] init];
if ([[[NSUserDefaults standardUserDefaults] objectForKey:#"language"] isEqual:#"ENGLISH"]){
FindCartsLabel.text = #"Find Carts within:";
MilesTextField.text = #"Show All";
milesArray=[[NSArray alloc]initWithObjects:#"Show All", #"1 Mile", #"5 Miles", #"10 Miles", #"20 Miles", nil];
AddressBar.placeholder = ENGLISH_Address;
newAnnotation.title = #"Good. To. Go. Vendor";
newAnnotation.fruits = FruitList;
newAnnotation.veggies = VeggieList;
}else if ([[[NSUserDefaults standardUserDefaults] objectForKey:#"language"] isEqual:#"SPANISH"]){
FindCartsLabel.text = #"Encuentra Carros Dentro:";
newAnnotation.title = #"Good. To. Go. Vendedor";
AddressBar.placeholder = SPANISH_Address;
NSMutableArray *spanishFruitList = [[NSMutableArray alloc]init];
for (NSString *current in FruitList) {
MilesTextField.text = #"Mostrar Todo";
milesArray=[[NSArray alloc]initWithObjects:#"Mostrar Todo", #"1 Milla", #"5 Millas", #"10 Millas", #"20 Millas", nil];
if ([current isEqual:#"Apples"]) {
[spanishFruitList addObject:SPANISH_Apples];
}
if ([current isEqual:#"Bananas"]) {
[spanishFruitList addObject:SPANISH_Bananas];
}
if ([current isEqual:#"Citrus"]) {
[spanishFruitList addObject:SPANISH_Citrus];
}
if ([current isEqual:#"Mangos"]) {
[spanishFruitList addObject:SPANISH_Mangos];
}
if ([current isEqual:#"Strawberries"]) {
[spanishFruitList addObject:SPANISH_Strawberries];
}
if ([current isEqual:#"And More"]) {
[spanishFruitList addObject:SPANISH_More];
}
}
NSMutableArray *spanishVeggieList = [[NSMutableArray alloc]init];
for (NSString *current in VeggieList) {
if ([current isEqual:#"Avocados"]) {
[spanishVeggieList addObject:SPANISH_Avocados];
}
if ([current isEqual:#"Broccoli"]) {
[spanishVeggieList addObject:SPANISH_Broccoli];
}
if ([current isEqual:#"Carrots"]) {
[spanishVeggieList addObject:SPANISH_Carrots];
}
if ([current isEqual:#"Squash"]) {
[spanishVeggieList addObject:SPANISH_Squash];
}
if ([current isEqual:#"Onions"]) {
[spanishVeggieList addObject:SPANISH_Onions];
}
if ([current isEqual:#"Tomatoes"]) {
[spanishVeggieList addObject:SPANISH_Tomatoes];
}
if ([current isEqual:#"And More"]) {
[spanishVeggieList addObject:SPANISH_More];
}
}
newAnnotation.fruits = spanishFruitList;
newAnnotation.veggies = spanishVeggieList;
}
newAnnotation.coordinate = location;
newAnnotation.vendorID = vendor_ID;
newAnnotation.startHour = startHr;
newAnnotation.endHour = endHr;
newAnnotation.loc = point;
newAnnotation.isCustomAddress = false;
//newAnnotation.subtitle = address;
__block NSString *address = [NSString stringWithFormat:#"%# %#, %#, %#, %#",
addressArray[0], addressArray[1],
addressArray[2], addressArray[3],
addressArray[4]];
__block NSString *currAddress = [NSString stringWithFormat:#"%# %#\n"
"%#, %#, %#\n"
"%#\n",
addressArray[0], addressArray[1],
addressArray[2], addressArray[3],
addressArray[4], addressArray[5]];
newAnnotation.subtitle = address;
newAnnotation.addressFormatted = currAddress;
static NSString *identifier = #"MyLocation";
MKPinAnnotationView *currentView = [[MKPinAnnotationView alloc] initWithAnnotation:newAnnotation reuseIdentifier:identifier];
[pointArray addObject:currentView];
} else {
//[self viewDidLoad];
NSLog(#"%#", error.debugDescription);
}
//} ];
}
} while (pointArray.count < query.countObjects);
}
if (pointArray.count == query.countObjects) {
for (MKPinAnnotationView *currentPoint in pointArray) {
[self.MyMapView addAnnotation:currentPoint.annotation];
}
}
}];
Thanks in advance for the help. I do not really understand why this code would not complete after only one iteration.
The NSLog(#"%#", error.debugDescription); doesn't look like it's in the right place. It's in an else block that is associated with the if (today <= newDate) which is inside a block of code that is only executed if error is null which is why it says null in the log (when what it really means is "today > newDate"). – Anna

Received Memory Warning in ARC

I am making a call to syncWithCalendar and after events are successfully added, I get low memory warning and app terminates with "Received Low Memory" warning. The events generated and saved in calendar are more than 50. I tried using instruments but I am not able to find the code where memory leak occurs and also through live bytes that show in instruments I am not able to track the code that is causing the leak. Can anyone please help me solve this issue.
- (void)syncWithCalendar
{
#autoreleasepool {
[self deleteEventsIfExist];
NSMutableDictionary *dictionary = [util readPListData];
NSMutableArray *courses = [util getCourses];
__block NSMutableArray *lessons;
__block NSMutableDictionary *lesson;
NSString *studentID = [util getProgramDetails].studentId;
NSString *programName = [util getProgramDetails].programName;
double offset[] = {0, 0, -300, -900, -1800, -3600, -7200, -86400, -172800};
__block NSString *startDateString = #"", *endDateString = #"";
NSTimeInterval relativeOffsetValue = 0;
int index = [[dictionary objectForKey:#"event-alert-option"] intValue];
relativeOffsetValue = offset[index];
NSDateFormatter *formatter;
formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"MM/dd/yyyy HH:mm:ss"];
[formatter setDateFormat:#"MM/dd/yyyy"];
NSString *currentDateString = [NSString stringWithFormat:#"%# 09:00:00", [formatter stringFromDate:[NSDate date]]];
[formatter setDateFormat:#"MM/dd/yyyy HH:mm:ss"];
NSDate *currentDate = [formatter dateFromString:currentDateString];
EKEventStore *eventStore = [[EKEventStore alloc] init];
if([eventStore respondsToSelector:#selector(requestAccessToEntityType:completion:)]) {
// iOS 6 and later
[eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error) {
if (granted){
//---- codes here when user allow your app to access theirs' calendar.
dispatch_async(dispatch_get_main_queue(), ^{
// Event creation code here.
for (int i=0; i<[courses count]; i++)
{
#autoreleasepool {
lessons = [[courses objectAtIndex:i] objectForKey:#"lessons"];
for (int j=0; j<[lessons count]; j++)
{
#autoreleasepool {
lesson = [lessons objectAtIndex:j];
NSString *title = nil;
title = [NSString stringWithFormat:#"%# %#-Complete %# lesson",studentID,programName,[lesson objectForKey:#"lesson-name"]];
if ([[lesson objectForKey:#"actual-exam-date"] isEqualToString:#"00/00/0000"])
{
startDateString = [NSString stringWithFormat:#"%# %#", [lesson objectForKey:#"plan-exam-date"], #"09:00:00"];
endDateString = [NSString stringWithFormat:#"%# %#", [lesson objectForKey:#"plan-exam-date"], #"18:00:00"];
}
else
{
if ([[lesson objectForKey:#"retake-actual-date"] isEqualToString:#"00/00/0000"])
{
startDateString = [NSString stringWithFormat:#"%# %#", [lesson objectForKey:#"retake-plan-date"], #"09:00:00"];
endDateString = [NSString stringWithFormat:#"%# %#", [lesson objectForKey:#"retake-plan-date"], #"18:00:00"];
}
}
if (!([startDateString isEqualToString:#""] && [endDateString isEqualToString:#""]))
{
EKEvent *event = [EKEvent eventWithEventStore:eventStore];
event.title=title;
event.startDate = [formatter dateFromString:startDateString];
event.endDate = [formatter dateFromString:endDateString];
event.allDay = NO;
if (index != 0)
{
event.alarms = [NSArray arrayWithObjects:[EKAlarm alarmWithRelativeOffset:relativeOffsetValue], nil];
}
[event setCalendar:[eventStore defaultCalendarForNewEvents]];
// Compare current date to event start date, if start date has been passed then preventing to sync with calendar
NSComparisonResult result = [event.startDate compare:currentDate];
if (result != NSOrderedAscending)
{
NSError *err = nil;
[eventStore saveEvent:event span:EKSpanThisEvent commit:YES error:&err];
if (err) {
NSLog(#"event not saved .. error = %#",err);
} else {
NSLog(#"event added successfully");
}
}
}
} // autoreleasepool
} // lessons for loop
} // autoreleasepool
} // courses for loop
[self hideModal];
});
}else
{
//----- codes here when user NOT allow your app to access the calendar.
// [self performSelectorOnMainThread:#selector(hideModal) withObject:nil waitUntilDone:NO];
}
}];
} else {
// sync calendar for <iOS6
}
} // autoreleasepool
}
- (void)deleteEventsIfExist
{
#autoreleasepool {
NSMutableArray *courses = [util getCourses];
__block NSMutableArray *lessons;
__block NSMutableDictionary *lesson;
NSString *studentID = [util getProgramDetails].studentId;
NSString *programName = [util getProgramDetails].programName;
EKEventStore* store = [[EKEventStore alloc] init];
dispatch_async(dispatch_get_main_queue(), ^{
// Event creation code here.
NSDate* endDate = [NSDate dateWithTimeIntervalSinceNow:[[NSDate distantFuture] timeIntervalSinceReferenceDate]];
NSPredicate *fetchCalendarEvents = [store predicateForEventsWithStartDate:[NSDate date] endDate:endDate calendars:store.calendars];
NSArray *allEvents = [store eventsMatchingPredicate:fetchCalendarEvents];
for (int i=0; i<[courses count]; i++)
{
#autoreleasepool {
lessons = [[courses objectAtIndex:i] objectForKey:#"lessons"];
for (int j=0; j<[lessons count]; j++)
{
#autoreleasepool {
lesson = [lessons objectAtIndex:j];
NSString *oldEventSubtitle = [NSString stringWithFormat:#"%# %#-Complete %# lesson",studentID,programName,[lesson objectForKey:#"lesson-name"]];
for (EKEvent *e in allEvents)
{
if ( [oldEventSubtitle isEqualToString:e.title])
{
NSError* error = nil;
[store removeEvent:e span:EKSpanThisEvent commit:YES error:&error];
NSLog(#"deleting events");
}
}
} // autoreleasepool
} // lessons
} // autoreleasepool
} // courses
});
} // autoreleasepool
}
It's a rough guess, but it seems the asynchronous invocations may lead to troubles.
In order to test this, just use dispatch_sync instead of dispatch_async and examine the memory consumption. If this leads to an improvement, then a solution is in sight, which involves to re-factor your current asynchronous "parallel" approach and turn it into an appropriate asynchronous "serial" approach or an complete synchronous approach.
This may also require to "serialize" all invocations of this asynchronous method:
[eventStore requestAccessToEntityType:EKEntityTypeEvent
completion:^(BOOL granted, NSError *error) {
...
}]
This is how I made a call to syncWithCalendar function
if([eventStore respondsToSelector:#selector(requestAccessToEntityType:completion:)]) {
// iOS 6 and later
[eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted,
NSError *error) {
if (granted){
dispatch_async(dispatch_get_main_queue(), ^{
[self syncWithCalendar];
});
} else {
// calendar access not granted
}
}];
}
And in syncWithCalendar function everything remains same except the line of code that
was creating the crash/memory issue. Below is the incorrect line of code that I was
using earlier
// wrong
[self.eventstore saveEvent:event span:EKSpanThisEvent commit:YES error:&err];
The correct way to save event: (Note: I didn't require event identifier in my case)
// correct
[self.eventstore saveEvent:event span:EKSpanThisEvent commit:NO error:&err];
and then use [self.eventstore commit:NULL] after all the events are saved. This stopped the crash in my case. Hope this post will
help other get the solution. Thanks !!!!
You need to clear the cache when you are receiving memory warning, use this method it will help you.
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}

How do I randomly display images from Parse.com

I'd like to randomly display profile pictures in a UITableViewCell.
My table is set up as a way to filter. I have one table setup on Parse for users which also contains a category column and picture column. I want to query by the category (each cell row is a category as exampled below), and randomly display image(s) in the UITableViewCell.
So the table would be set up such as:
Category A
Category B
Category C
Each cell will have UIImageViews in them, and I'd like to randomly display images in each cell.
Here's what I have tried, but no luck.
// in ViewDidLoad
self.theArray = [[NSArray alloc] initWithObjects:#"A", #"B", #"C", #"D", nil];
// in cellForRowAtIndexPath
cell.label.text = [self.theArray objectAtIndex:indexPath.row];
PFQuery * query = [PFUser query];
[query whereKey:#"category" equalTo:cell.label.text];
[query findObjectsInBackgroundWithBlock:(NSArray *objects, NSError error) {
if (error)
{
NSLog(#"Error: %# %#", error, [error userInfo]);
}
else
{
for (int i = 0; i < objects.count; i++)
{
self.profilePicObject = [objects objectAtIndex:i];
int imgRandom = random() % [objects count];
self.randomProfilePicObject = [objects objectAtIndex:imgRandom];
self.imageFile = [self.randomProfilePicObject objectForKey:#"imageFile"];
NSURL * imageFileURL = [[NSURL alloc] initWithString:self.imageFile.url];
NSData * imageData = [NSData dataWithContentsOfURL:imageFileURL];
UIImage * aImage = [[UIImage alloc] initWithData:imageData];
cell.profilePicOne.image = aImage;
}
}
}];
EDIT:
I have modified the code above, and it is working now. What would be the most efficient way to modify this to fill in more than one UIImageView in a cell?
try this:
for (int i = 0; i < objects.count; i++)
{
//self.profilePicObject = [objects objectAtIndex:i];
int randomImgNumber = arc4random_uniform(5); // You could several many times the same picture !!!
self.profilePicObject = [objects objectAtIndex:randomImgNumber];
self.imageFile = [self.profilePicObject objectForKey:#"imageFile"];
NSURL * imageFileURL = [[NSURL alloc] initWithString:self.imageFile.url];
NSData * imageData = [NSData dataWithContentsOfURL:imageFileURL];
UIImage * aImage = [[UIImage alloc] initWithData:imageData];
// aImage = [objects objectAtIndex:randomImgNumber];
cell.profilePicOne.image = aImage;
}

UITableViewCell's Data is being mixed up when using GCD

I'm using GCD to load my UITableView data on the background thread, however doing so mixes up the data in my custom UITableViewCell. The titleLabel and imageView on the cell are fine, but the textLabel (the subtitle) is wrong on every cell. This doesn't happen when the data is loaded on the main thread, and the data doesn't come from multiple arrays, so I can only guess it's because of my use of GCD, which I am new to.
Firstly, I set up the NSOperationQueue like so...
- (void)setUpTableForAlbums
{
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
[self setUpTableForAlbumsFD];
dispatch_async(dispatch_get_main_queue(), ^
{
[albumTable reloadData];
});
});
}
The setUpTableForAlbumsFD selector is as so...
- (void)setUpTableForAlbumsFD
{
// __block CLProgressIndeterminateView *clP = [[CLProgressIndeterminateView alloc] initWithFrame:CGRectMake(325, tableScrollView.frame.size.height/2, 310, 20)];
// [tableScrollView addSubview:clP];
// [clP startAnimating];
type = #"Albums";
queryAlbums = [MPMediaQuery albumsQuery];
[queryAlbums setGroupingType:MPMediaGroupingAlbum];
mainArrayAlbum = [[NSMutableArray alloc] init];
otherArrayAlbum = [[NSMutableArray alloc] init];
theOtherArrayAlbum = [[NSMutableArray alloc] init];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSArray *fullArray = [queryAlbums collections];
for (MPMediaItemCollection *collection in fullArray)
{
item = [collection representativeItem];
NSString *albumName = [item valueForProperty:MPMediaItemPropertyAlbumTitle];
NSString *albumArtist = [item valueForProperty:MPMediaItemPropertyArtist];
NSString *filePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.png", albumName]];
Album *album = [[Album alloc] init];
album.albumTitle = albumName;
album.albumArtwork = [UIImage imageImmediateLoadWithContentsOfFile:filePath];
if (album.albumTitle.length > 4)
{
if ([album.albumTitle hasPrefix:#"The "])
{
album.albumOrderTitle = [album.albumTitle substringFromIndex:4];
}
else
{
album.albumOrderTitle = album.albumTitle;
}
}
else
{
album.albumOrderTitle = album.albumTitle;
}
album.albumArtist = albumArtist;
if (![mainArrayAlbum containsObject:album])
{
[mainArrayAlbum addObject:album];
}
}
}
The Album custom class is just a container for the data.
The cellForRowAtIndex path method is as so...
MasterCellAlbum *albumCell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!albumCell)
{
albumCell = [[MasterCellAlbum alloc] initWithStyle:nil reuseIdentifier:#"Cell"];
}
alphabet = [self alphabet:#"album" withIndex:YES];
[albumCell setSelectionStyle:UITableViewCellEditingStyleNone];
NSString *alpha = [alphabet objectAtIndex:indexPath.section];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.albumOrderTitle beginswith[c] %#", alpha];
NSArray *predict = [mainArrayAlbum filteredArrayUsingPredicate:predicate];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
Album *album1 = [predict objectAtIndex:indexPath.row];
albumCell.titleLabel.text = album1.albumTitle;
albumCell.textLabel.text = album1.albumArtist;
albumCell.avatarImageView.image = album1.albumArtwork;
longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(albumLittleMenu:)];
[albumCell addGestureRecognizer:longPress];
return albumCell;
Am I using GCD correctly, or is there another way I should be doing it?
Yikes. There are lots of things that are, shall we say, interesting about this code. Let's start with the first method:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSInvocationOperation *operation = [NSInvocationOperation alloc];
operation = [operation initWithTarget:self selector:#selector(setUpTableForAlbumsFD) object:nil];
[operation setCompletionBlock:^
{
[albumTable reloadData];
}];
[operationQueue addOperation:operation];
operation = nil;
What I think you're tying to do is execute the -setUpTableForAlbumsFD method in the background, and then when it's done, reload the tableView.
First, the completionBlock doesn't execute on the main thread (which is where you MUST call -reloadData from). The docs say:
The exact execution context for your completion block is not guaranteed but is typically a secondary thread. Therefore, you should not use this block to do any work that requires a very specific execution context.
The simpler way to do this method would be:
dispatch_async(dispatch_get_global_queue(0,0), ^{
[self setUpTableForAlbumsFD];
dispatch_async(dispatch_get_main_queue(), ^{
[albumTable reloadData];
}
});
Now for the setUpTableForAlbumsFD method...
- (void)setUpTableForAlbumsFD {
type = #"Albums";
queryAlbums = [MPMediaQuery albumsQuery];
[queryAlbums setGroupingType:MPMediaGroupingAlbum];
mainArrayAlbum = [[NSMutableArray alloc] init];
NSArray *fullArray = [queryAlbums collections];
for (MPMediaItemCollection *collection in fullArray) {
item = [collection representativeItem];
NSString *albumName = [item valueForProperty:MPMediaItemPropertyAlbumTitle];
NSString *albumArtist = [item valueForProperty:MPMediaItemPropertyArtist];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
You should do these two lines of finding the NSDocumentDirectory outside of the for loop, for efficiency.
NSString *filePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.png", albumName]];
UIImage *artwork = [UIImage imageImmediateLoadWithContentsOfFile:filePath];
I'm assuming this is a UIImage category method?
Album *album = [[Album alloc] init];
album.albumTitle = albumName;
if (album.albumTitle.length > 4) {
if ([[NSString stringWithFormat:#"%c%c%c%c", [album.albumTitle characterAtIndex:0], [album.albumTitle characterAtIndex:1], [album.albumTitle characterAtIndex:2], [album.albumTitle characterAtIndex:3]] isEqual: #"The "]) {
Yikes! Just do: if ([album.albumTitle hasPrefix:#"The "]) {
album.albumOrderTitle = [album.albumTitle substringWithRange:NSMakeRange(4, album.albumTitle.length-4)];
And here do: album.albumOrderTitle = [album.albumTitle substringFromIndex:4];
} else {
album.albumOrderTitle = album.albumTitle;
}
} else {
album.albumOrderTitle = album.albumTitle;
When you see multiple lines that are doing the same thing like this, it's a sign you can pull it out and do it differently. For example, you could always set the album.albumOrderTitle to the albumTitle, and then only do something different if the albumTitle length is more than 4 and it has a prefix of #"The ".
}
album.albumArtist = albumArtist;
album.albumArtwork = artwork;
if (![mainArrayAlbum containsObject:album]) {
[mainArrayAlbum addObject:album];
}
}
}
Your cellForRowAtIndexPath: is similarly convoluted:
MasterCellAlbum *albumCell = [[MasterCellAlbum alloc] init];
You should be using UITableView's cell-reuse mechanism.
alphabet = [self alphabet:#"album" withIndex:YES];
[albumCell setSelectionStyle:UITableViewCellEditingStyleNone];
NSString *alpha = [alphabet objectAtIndex:indexPath.section];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.albumOrderTitle beginswith[c] %#", alpha];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
NSArray *predict = [mainArrayAlbum filteredArrayUsingPredicate:predicate];
Why are you re-filtering the mainArrayAlbum every time you need a cell? It looks like you're always going to be grabbing the same alphabet, which means you're always going to be defining the same predicate, which means you're always going to be ending up with the same predict array.
Album *album1 = [predict objectAtIndex:indexPath.row];
albumCell.titleLabel.text = album1.albumTitle;
albumCell.textLabel.text = album1.albumArtist;
if (album1.albumArtwork) {
albumCell.avatarImageView.image = album1.albumArtwork;
} else {
albumCell.avatarImageView.image = [UIImage imageNamed:#"albumArtInvertedLight1.png"];
}
longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(albumLittleMenu:)];
[albumCell addGestureRecognizer:longPress];
return albumCell;
So, there are some obvious places where your code can use some improvement. Honestly, I think the answer to the problem you're having is because you're trying to reload the tableview on a background thread, which is a Bad Idea™.

Resources