Strange behaviour on table view with core data - ios

step by step I am advancing creating my first iOS app. Now I am experiencing a strange behaviour on a table view controller which I am not able to solve and I have been searching for the reason for hours. THis is the scenario:
Simple iOS app using core data.
Table view controller to show one core data entity objects.
One view controller (AddViewController) to enter several attributes values.
One view controller (EditViewController) to update the object values (after row selection on the table view controller.
To make the question clear enough, I will only take into account three of the attributes, 'thingName' , 'urgent' and 'color', all string attributes. Every row shows a string (thingName), an image as icon for the color attribute and another image as icon for the urgent attribute.
The app process begin with a blank table, after pressing the Add button, the app shows the AddViewController view, that is a view with a text field (thingName), a switch to mark the object as urgent/not urgent and a group of colored buttons to assign a color dependency to the object. After introducing the thingName, selecting the object as urgent or not urgent, and selecting one of the colour buttons, the user taps on the save button and then to the back button to go back to the table view controller. Everything works as expected. A new row appears containing the thingName text as cell.text, the icon representing that the object was marked as urgent and a color pencil from the color selected by the user.
Then, if the user wants to change the thingName text / urgent or not urgent / colour, selecting the object row, the app shows the editViewController. If the user changes the text and after saving, the updated text is also shown on the tableview, which means that the app has stored the changes. If the user changes the urgent state, from not urgent to urgent, after saving and going back to the table view, the urgent icon appears as expected, but after changing from urgent to not urgent, saving and going back to the table view, the urgent icon that shouldn't appear, does appear. To check the issue I have included a text field (urgentTextField) on the editviewcontroller to show the content of the urgent attribute, and it changes fine in response from the switch state. That means, if the user sets the switch to no urgent, the urgentTextField shows 'Not urgent', if the user sets the switch to urgent, the urgentTextField shows 'Urgent'.
Sample case:
thingName = #"test";
urgentTextField.text=#"Not urgent";
In this case, the row shows the expected text 'test'. but the icon is not responding as expected, then the urgent icon is shown....
To make it easier, this is the code:
RootViewController (table view controller):
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
NSManagedObject *managedObject = [fetchedResultsController objectAtIndexPath:indexPath];
NSString *colorValue = [[managedObject valueForKey:#"color"] description];
NSString *isUrgent = [[managedObject valueForKey:#"urgent"]description];
[[cell textLabel] setText:[[managedObject valueForKey:#"thingName"] description]];
NSString *myString = [NSString stringWithFormat:#"%#",[[managedObject valueForKey:#"todoYear"] description]];
//color pencil
if ([hasColorValue isEqual:#"Si color"]){
UIButton *colorButton = [[UIButton alloc]initWithFrame:CGRectMake(308, 5, 10, 40)];
[colorButton setImage:[UIImage imageNamed:colorValue]forState:UIControlStateNormal];
[cell addSubview:colorButton];
}
//urgent
if ([isUrgent isEqual:#"Urgent"]){
UIButton *urgentButton = [[UIButton alloc]initWithFrame:CGRectMake(71, 27, 18, 18)];
[urgentButton setImage:[UIImage imageNamed:#"urgent-3"]forState:UIControlStateNormal];
[cell addSubview:urgentButton];
}
../..
And this is the code for EditViewController:
- (IBAction)SaveButtonAction:(id)sender {
AppDelegate* appDelegate = [AppDelegate sharedAppDelegate];
NSManagedObjectContext* context = appDelegate.managedObjectContext;
[selectedObject setValue:ToDoTextField.text forKey:#"thingName"];
NSString *valorUrgent = urgentTextField.text;
[selectedObject setValue:valorUrgent forKey:#"urgent"];
NSError *error;
if(! [context save:&error])
{
NSLog(#"Whoopw,couldn't save:%#", [error localizedDescription]);
}
}
- (void)viewDidLoad
{
colorImagen.image = [UIImage imageNamed:nil];
[ToDoTextField becomeFirstResponder];
//recogiendo datos de selectedobject;
ToDoTextField.text = [[selectedObject valueForKey:#"thingName"]description];
colorTextField.text = [[selectedObject valueForKey:#"color"]description];
NSString *imageName = colorTextField.text;
colorImagen.image = [UIImage imageNamed:imageName];
NSString *urgentValue = [[selectedObject valueForKey:#"urgent"]description];
urgentTextField.text = urgentValue;
if ([urgentValue isEqual:#"Urgent"]){
[urgentSwitch setOn:YES animated:YES];
urgentImage.image=[UIImage imageNamed:#"urgent-3"];
}
if ([urgentValue isEqual:#"Not urgent"]){
[urgentSwitch setOn:NO animated:YES];
urgentImage.image=[UIImage imageNamed:nil];
}
[ToDoTextField addTarget:self action:#selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
Please, tell me if you need further information or code to detect where is the problem that I am not able to find.
Thank you

Your problem appears to be in the configureCell: method. You are not taking cell reuse into account. The cell you are configuring could have need used before and could have had an urgent status previously.
Your code only deals with the situation where the cell is urgent, in which case it adds a subview. There are two problems with this approach:
A reused cell will have the subview added every time - so there could be many urgent views on top of each other. The cell should only ever add this once and keep it in a property
Code must be added to configure the cell when it is not urgent (usually, an else after the if). This would hide the urgent icon.

The first issue with code I noticed is you must add you code after
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
not before it. It will guarantee that your view is, at least, loaded.
Second, make from all your string literals const string like this and use it everywhere
static NSString * const kUrgentState = #"Urgent";
And one more common hint: add NSLog(...); or test the value of variable in debug to localise the problem, cause right now it's really to much code, but not still not enough to find a problem.
Find the moment when values of variable goes wrong and then fix your question

Related

Setting textfield text does not work

Beginner programmer trying to understand why I cannot accomplish this simple task. I know it is most likely a very simple solution but hoping that someone will explain the WHY.
I have a screen where users can input emails into a textfield. The idea is that if upon entering the e-mail, if it is not already in the stored emails, it will prompt the user to enter the new email/contact into the store. Since I check if the e-mail is valid BEFORE popping up the screen for contact creation, I'd like to take the text entered and put it directly into the "Email" field on the new contact creation page and not allow editing. I've tried numerous methods but CANNOT get the email to show up in the text field. Can someone explain why this is?
Code from initial VC where users enter their emails. If the email does not exist in the store, this code creating the contact creator page is fired:
//I created this custom initializer since setting the text field (as I did below) would not work
ContactCreationViewController *contactCreationVC = [[ContactCreationViewController alloc] initWithEmail:trimmedText];
contactCreationVC.delegate = self;
//initially I tried setting the text here but it did not work, I now understand why
//[contactCreationVC.emailField setText:#"asdsadasd"];
[contactCreationVC setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
[contactCreationVC setModalPresentationStyle:UIModalPresentationOverCurrentContext];
[self presentViewController:contactCreationVC animated:YES completion:nil];
This is the code for the actual ContactCreatorVC:
-(instancetype) initWithEmail:(NSString*)email{
self = [super init];
//tried setting email here which works as I check with breakpoints
[self setEmail:email];
//self.email = email here when I check
return self;
}
....
- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
switch (indexPath.row) {
....
case 3: {
ContactCreationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:contactCreationCellIdentifier];
cell.titleLabel.text = #"E-Mail";
cell.userEntryTextfield.tag = 3;
cell.userEntryTextfield.delegate = self;
cell.userEntryTextfield.keyboardType = UIKeyboardTypeEmailAddress;
//I try setting the email here but self.email = nil (don't understand why)
cell.userEntryTextfield.text = [NSString stringWithFormat:#"%#", self.email];
cell.userEntryTextfield.userInteractionEnabled = NO;
cell.userEntryTextfield.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.emailField = cell.userEntryTextfield;
return cell;
}
.....
}
I feel stupid for even asking such a simple question but I clearly am not understanding what is going on behind the scenes cause I've tried everything. Can anyone explain what is going on and suggest the ideal solution from a best practice standpoint?
EDIT: I guess my question could be more concise...it basically boils down to: why when I set self.email in the init does it not stick when I access self.email in the cellForRow method (it becomes nil)?
(the textfield is in a cell in a tableview)
The problem is this line:
[contactCreationVC.emailField setText:#"asdsadasd"];
You have only just created contactCreationVC. Its view has not loaded, so emailField is nil (easily demonstrated by some rudimentary logging). A message to nil does nothing.
The correct approach is: never touch another view controller's outlets. Set a property of the other view controller and let it deal with its own outlets. In this case, the other view controller would need to use the property to set the emailField text in its viewDidLoad.
As to why that approach doesn't work for you, there isn't enough info to answer it as far as I can tell. If you can prove that things go in this order:
A ContactCreatorVC init is called with an actual email value, and the property is set.
The property has a strong (retain) policy so the value is retained.
The very same ContactCreatorVC instance now has its cellForRowAtIndexPath called, and that moment the property is nil.
If you can prove all that, then the only logical conclusion is that meanwhile some other code has come along and set the property to nil.

Updating UILabel.Text inside of a for loop after a button is pressed

I have a UIViewController, and in that I have an array of questions that I pull from a sqlite3 query. I am then using a for loop to iterate through each question in the array to change the UILabel.text to display the question on the screen. This is actually working for the first question in the array!
I then have four buttons for answers. I want to make it so if one of the buttons is pressed, the answer is saved and the next question in the loop updates the UILabel.text.
The four answers never change as it is more of a survey than answers, so one of the answers is "I agree" or "disagree", so the button text never changes.
Is this possible?
I have been on here and Google to find a way to link the button pressed with completing each iteration of the loop without any luck.
Why are you iterating through questions and changing UILabel's text? Shouldn't be it changed only on tapping one of the survey buttons?
If I got you correctly, you should do following:
1) Declare three properties in your controller: NSArray *questions, NSMutabelArray *answers, NSInteger currentIndex;
2) Init/alloc them in viewDidLoad (except currentIndex, of course, set it to 0).
3) Fill up questions array with your question strings.
4) Set text to UILabel, label.text = questions[currentIndex];
5) create IBAction method and link it to all survey buttons.
6) in IBAction method, insert button's title to answers array and show next question.
- (void)viewDidLoad {
[super viewDidLoad];
self.questions = {your questions array};
self.answers = [[NSMutableArray alloc] init];
self.currentIndex = 0;
}
- (IBAction)btnClicked:(id)sender {
UIButton *btn = (UIButton *)sender;
NSString *title = btn.titleLabel.text;
[self.answers addObject:title];
currentIndex++;
label.text = questions[currentIndex];
}
I hope you will understand the code.
In short, yes this is possible.
You'll first want to keep track of the question that your user is currently on. You can do this by storing an index in an instance variable or, if you plan on allowing the user to open the app and start from where they left off, you can use NSUserDefaults, which writes to disk and will persist.
// In the interface of your .m file
int questionIndex;
// In viewDidLoad of your controller, however this will start for index 0, the beginning of your questions array
questionIndex = 0
By storing the index in NSUserDefaults, you can grab it in ViewDidLoad, and start from where the user last left off:
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:questionIndex] forKey:#"questionIndex"];
To store your answer, you could add a method for your buttons called answerTapped:,
- (void)answerTapped:(UIButton *)answerButton
{
// Grab the answer from the text within the label of the button
// NOTE: This assume your button text is the answer that you want saved
NSString *answer = answerButton.titleLabel.text;
// You can use your questionIndex, to store which question this answer was for and you can then take the answer and store it in sqlite or where you prefer...
}
You can add this method to your buttons like so
[answerButton addTarget:self action:#selector(answerTapped:) forControlEvents:UIControlEventTouchUpInside];
You could then write a method to increment questionIndex now that an answer button has been pressed.
- (void)incrementQuestionIndex
{
// Increment index
questionIndex += 1;
// Update and save value in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:questionIndex] forKey:#"questionIndex"];
}
You could then call a separate, final method to update the question label.
- (void)updateQuestionLabel
{
// Grab the question using the index (omit this line and go straight to the next if storing the index in an iVar)
questionIndex = [[[NSUserDefaults standardUserDefaults] objectForKey:#"questionIndex"] integerValue];
// Grab the question using the index. Assumes you have a questions array storing your questions from sqlite.
NSString *question = [questions objectAtIndex:questionIndex];
// Update the question UILabel
[questionLabel setText:question];
}

Memory management using ARC on iOS

Just have a quickly question (more of a curiosity thing) based on a problem I just solved (I will post the answer to my problem in the post, which can be found here: My former question
The thing is that I have this UITableView which contains custom cell objects. Every time you enter this view, I generate new cells for the UITableView like this:
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"UploadCellView" owner:self options:nil];
cell = customCell;
}
Which happens in the standard method:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Now the problem is that my custom cell objects listens for NSNotifications about upload objects happening in the background, so they can update its model data to their labels and progress bars etc. It happens like this (this is a method from the custom cell objects):
-(void) uploadProgress: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
UIProgressView *theProgressBar = (UIProgressView*)[self viewWithTag:progressBarTag];
[theProgressBar setProgress:(uploadActivity.percentageDone / 100) animated:YES];
UILabel *statusText = (UILabel*)[self viewWithTag:percentageTag];
[statusText setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", uploadActivity.percentageDone, uploadActivity.totalMBUploaded, uploadActivity.totalMBToUpload]];
}
}
When an upload finish they simply do this:
-(void) uploadFinished: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
[self setUploadComplete];
[[ApplicationActivities getSharedActivities] markUploadAsFinished:uploadActivity];
NSLog(#"BEGINNING RELOAD");
[parentTable reloadData];
NSLog(#"ENDING RELOAD");
}
}
Now the problem is when they call their owning tableview. When the view which the tableview is contained within dismisses, the old custom cell objects are still alive in the background getting NSNotfications. And when that upload is then done, the old custom cell objects from the former table views still tries to call that parentTable property which was set at that time, now resulting in calling random junk memory.
The way I solved this was to keep an array of all cell objects getting created in the table and then make them stop listening when the view is dismissed like this:
-(void) viewWillDisappear:(BOOL)animated
{
for (UploadCell *aCell in lol)
{
[aCell stopListening];
}
[self.navigationController popViewControllerAnimated:YES];
}
But this seems like a bit of a hack. How would I go about making sure that the custom cell objects are deleted when the view is dismissed? Because when the view is intialized again, new cells are simply made anyways, so I have no use for the old ones.
The custom view cells have a strong property pointer to the tableview they get associated with, but I thought the ARC would make sure that TableView pointer would not get invalidated then? Obviously it is somehow. Maybe because of the containing view being deleted when popped?
Sounds like the cells have a retain property pointing back to your UITableViewDataSource class.
They should instead have an assign property, then they will be released properly when the table view is released (which it currently cannot be if your cells are retaining it).
Also, the cells should shut down notifications when they are dropped out of the tableview, by overriding the cells didMoveToSuperview method:
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ( [self superview] == nil )
{
[self unsubscribeFromYourNotifications];
}
}
That is so if they scroll off screen they will not be wasting resources updating things.
Have you considered a separate update model that keeps a map between uploadIDs and cells that listens for the notification? That way, the cells aren't responsible for updating the table themselves, the update model would do it. When the table goes away, you can shut down the update model.

Using Core Data with Images or buttons (or anything besides a tableView)

I'm trying to build an application that uses something like the following data model. I can do this fine in a tableView but I would like to be more creative. For Example, Inside a navigation controller, I want to have a view that has 10 images (or buttons with an image as a background).
(three entities)
entity 1: House
attribute: houseName
relationship:person
entity 2:Person
attribute: personName
relationship:house
relationship:children
entity 3:Children
attribute:name
attribute:birthPlace
relationship: adult
relationships are one to many down from
attributes are all strings
person<---->>house, children<--->>adult
In the first view there are images and each image is assigned to entity House (houseName 1, houseName 2, etc.). When you select a house it pushes the next view with 5 images (that is linked to the Person Entity (personName 1, personName2, etc). When you select the PersonName it will push the next view populated with the Children Entity.
I've read quite a bit of info on core data and I'm comfortable doing this:
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [[managedObject valueForKey#"houseName"] description];
but very unsure where I should even start with using and image or button to do this
I was thinking of something like this:
-(id)initWithHouseViewController:(HouseViewController*)aHouseViewController house:(NSManagedObject *)aHouse{
if blah blah
self.houseViewController = aHouseViewController;
self.house = aHouse
}return self;
}
// for a selector
-(void) showPersonView{
PersonViewController *pvc = [[PersonViewController alloc] initWithHouseViewController:self house:house];
in the viewDidLoad
{
if (house != nil) {
UIImageView *house1 = [[ UIImageView alloc.. blah blah
some kind of.. action:#selector(showPersonView)
((for each houseName in house) instead of house1, house2,)
...
}
Any suggestions with the viewDidLoad where I wouldn't need to hard code in each image (or button) would be great to make this more portable. Also, I'm not rooted to doing it this way if there are suggestions as to a nicer/more creative way to do this.
Sorry if this is a bit messy. It's messy in my mind as well.
Thanks for taking your time to read this,
I wouldn't use FetchResultController in this case.
Instead You can try This:
Create a view controller that will show all the houses as buttons, you can dynamically create the buttons according to the "Houses" count.
It will look something like this:
NSArray * allHouses = [Houses allObjects];
//you can sort them if you need
[allHouses enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
//create the buttons as custom and you can add any image to the button
UIButton *button....
button
//store the house index as the button tag, so you can get it later
button.tag = idx;
//add a selector to the button
[button addTarget:self
action:#selector(getPersons:)
forControlEvents:UIControlEventTouchUpInside];
}];
Create a new PersonViewController that will hold the persons. the view controller should get the house as the parameter and not the person. After it gets the house you will get the persons with the relationShip.
Now add this method to get the :
-(void)getPersons(UIButton*)sender{
NSInteger *tag = sender.tag;
House * house = (House*)[allHouses objectAtIndex:tag];
PersonViewController *pvc = [[PersonViewController alloc] init];
pvc.house = house;
[self.navigationController pushViewController.....];
}
Inside the person view controller you can get all the persons or with fetch request or simply with :
NSArray *allPersons = [house.person all objects];
Again create the persons buttons the same as you did with the first view controller.
Reapit the same procedure with the Children view controller.
GoodLuck
Shani

Another tableview reload problem

I'm loading a TableView from Core Data and it works like a charm. The data contains two fields: Category and Distance. The initial load of the table uses an array with the objects sorted based on Distance. I have a button in the Navigation Bar that I want the user to use to toggle between a Distance-sorted view (the default) and a Category-sorted view. My code for the toggle is:
-(void)toggleView {
NSString *baseItem = #"Proximity View";
NSString *currTitle = self.title;
NSComparisonResult result;
result = [baseItem compare:currTitle];
if (result == 0) {
self.title = NSLocalizedString(#"Category View",#"Categories");
tpData = tpDataCat; //tpDataCat is an array sorted by Category
[self.tblView reloadData];
} else {
self.title = NSLocalizedString(#"Proximity View",#"Distances");
tpData = tpDataDist; //tpDataDist is an array sorted by Distance
[self.tblView reloadData];
}
[baseItem release];
[currTitle release];
}
When I click the toggle button and fire `toggleView, the app just crashes. Any help would be greatly appreciated!!
You shouldn't be releasing baseItem and currTitle.
I would recommend reading the Memory Management Programming Guide; it's an excellent document that can provide background on the appropriate ownership of objects and when releasing would be required.

Resources