In my app, there is component for sending one message to multiple users at the same time.
I would like to implement it similar to facebook message composing. By using uitextfield with local notifications, I'm able to filter my UITableView like this;
self.searchTextFieldObserver = [[NSNotificationCenter defaultCenter]
addObserverForName:UITextFieldTextDidChangeNotification
object:self.searchTextView
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
if ([self.displayArray count]) {
NSArray *tempUsersArray = nil;
NSArray *tempNonUsersArray = nil;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"fullName CONTAINS[cd] %#", self.searchTextView.text];
self.displayArray = [MyAddressBook sortedContacts:self.contacts];
if ([self.searchTextView.text length] > 0) {
tempUsersArray = [self.displayArray[0] filteredArrayUsingPredicate:predicate];
self.displayArray = #[tempUsersArray];
}
}
[self.tableView reloadData];
}];
This works as expected... As I'm providing input for textfield, my table gets filtered. What I want to implement is this approach:
When I select one user by searching the table view, his nickname should be inserted in my textfield and predicate should be reseted to empty string, so I can start filtering again. Back button should select already inserted name and on second backspace tap, it should delete it.
I was thinking about this for some time, and all I could come up with, was using multiple element on top of each other. UITextView would change it's frame as users are added up (saved in array, which is source for UILabel's text, that partly replaces frame of textview), but I hitted wall, when I had to have possibility of removing already picked users.
If you guys know about another approach, or about any library that could help me, I would be very glad.
Sorry for making this an answer instead of just a comment, but I don't have enough reputation yet to comment.
Anyway, I think I would probably have an array of the selected user names that have been added to the textField. Then you could use UIControlEventEditingChanged to check for the text changing, and in that method that gets called take the textField's current text, strip out any strings that match those in the selected user names array as well as commas and such, then set your filter string to what's left over.
As an example of how you could use UIControlEventEditingChanged:
[_textField addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
In the case of removing users from the textField, you could use that same textFieldDidChange method to compare the array of added users to the textField, and if one exists in the array that doesn't exist in the textField anymore remove it before re-filtering.
-Stephen
Related
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];
}
Here is how things are looking:
My root controller is a UITabBarController. Number 13 in the image is my default view that I've set to show a list of people when the app is first opened up.
Number 4 shows a page I'm taken to by clicking a settings link on page 3. On page 4 I can manage the people on the main view by confirming pending people or taking confirmed people offline. Basically I can set whether they show up or not or delete if I wish.
Changes are always shown in 5 and 6 because I remove rows after after deletions, confirmations or status changes. When making edits in the detail views 7 the changes are updated easily because I update the Person object that has been passed over during segue. So when I go back to the table view in 5 I just "self.tableView reloadData" in my viewWillLoad method.
In table 6 and action sheet pops up when I tap on a row and gives me the option to take any currently confirmed person back offline. All I do is remove the row and that's it. ViewDidLoad is run when ever I go back to controller view 4 then click "pending" or "confirmed".
Now the issue
Unlike my controller in number 4 my main table in number 13 is shown when my first UITableViewController tab is tapped or when the app is first loaded up. My code below is loaded in the viewDidLoad method and therefore only loaded once.
This is how my data is shown on main view:
- (void)populatePeopleArrayWithCloudData {
// Grab data and store in Person instances and then store each person in array
people = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"People"];
[query whereKey:#"active" equalTo:#1];
[query orderByDescending:#"createdAt"];
[query setLimit:10];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
Person *person = [[Person alloc] init];
[person setName:[object objectForKey:#"name"]];
[person setNotes:[object objectForKey:#"notes"]];
[person setAge:[[object objectForKey:#"age"] intValue]];
[person setSince:[object objectForKey:#"since"]];
[person setFrom:[object objectForKey:#"from"]];
[person setReferenceNumber:[object objectForKey:#"referenceNumber"]];
PFFile *userImageFile = object[#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:imageData];
[person setImage:image];
}
}];
[person setActive:[[object objectForKey:#"active"] intValue]];
[person setObjectId:[object objectId]];
[people addObject:person];
}
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
NSLog(#"Calling reloadData on %# in viewDidLoad", self.tableView);
[self.tableView reloadData];
}];
}
The code grabs my data from parse, runs a for loop storing each person in a Person instance and adding it to an NSMutableArray instance "people". This people instance is accessed by my tableview datasource methods.
When I tap another tab and then tap my main tab again, viewDidLoad is not loaded again. Ok, that's understandable. Now what if I want my main table(13) to reflect changes made in tables 5 and 6? I thought I could just go to viewWillAppear and run "self.tableView reloadData". I did this and nothing happened. Well that's what I thought. After some messing around I realised that my "people" array wasn't updated with the latest data from my database.
I then decided to try and call my populatePeopleArrayWithCloudData method from viewWillAppear but this just causes duplicate entries. My people array is used to populate table 13 twice. I said ok let me take the populatePeopleArrayWithCloudData method out of viewDidLoad. This works and changes are shown but then I start having issues with images being loaded, showing up late , last row issues etc. Calling populatePeopleArrayWithCloudData method in viewDidLoad solves all those issues.
I like having the code in viewDidLoad because it means less calls to the database. Like when a user clicks on a row and it takes them to the detail views 14 and 15 and return back to 13 no calls are made to the database like they are when code is in viewWillAppear.
So I'm wondering if the best way to solve this issue I'm having to, is to some how update the "people" instance from tableView 6 (as soon as "take offline is click") and in tableview 5 (as soon as "publish" is clicked to confirm a person). Publishing a user sets active status to 1. Objects with a status of 1 are included in the query result above and therefore shown in the main tableview list. Taking a person offline sets their active status to 0 meaning they don't show up in the main list.
1. Would it be good/right to access the "people" instance from my table views 5 and 6 then update that people array appropriately? Maybe after the update have a method similar to populatePeopleArrayWithCloudData that repopulates people. Then in viewWillAppear I just reload the table again hoping it detects the new people instance and uses that.
2. If so how do I do this? Can you show a clear example if possible...
3. If not what way would you suggest I do this and can you show a clear example.
Thanks for your time
Kind regards.
There are several approaches you can take for this but (assuming I understand your problem) I think that the best is to have a shared instance of your array of people. What you do is create an NSObject called ManyPeople and add the following:
ManyPeople.h:
#property (nonatomic, strong) NSMutableArray *manyPeople;
+(ManyPeople *) sharedInstance;
ManyPeople.m
+ (ManyPeople *) sharedInstance
{
if (!_sharedInstance)
{
_sharedInstance = [[ManyPeople alloc] init];
}
return _sharedInstance;
}
Now all your UIViewControllers can call ManyPeople *people = [ManyPeople sharedInstance]; and will get the same array. When make a call to the database you populate the shared instance. When you make changes to the settings you also make changes to the shared instance. The beauty is that when you return to your view controller 13 all you do is reloadData and you don't need to make a call to the database. The data source is already points to the updated array.
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
I currently have an array of buttons being generated which works great. Outside of this on touch up inside each button calls on a -(void)onTouch function where some math is done to determine an action. This all works great except I would like to store a history of the pressed buttons. I've tried many different ways to create an NSMutableArray and store the values of the pressed buttons, but because I can only declare the array within the -onTouch action, every time a button is pressed the array is reset so it never remembers more than one move. If I try to declare the array in my header and synthesize it outside I either get the error that nsmutable array is not a compile time thinger or it doesn't store anything (log output is "(null)". Can someone paste in some code on how to declare an array that can store and append the uibutton tags outside of where the uibutton press event happens? I'll post code later tonight if this isn't clear.
Cheers
You need to not only declare the array, but also initialise it. If you don't initialise, you won't necessarily get a warning, but you will get lots of nil data.
You only want to initialise the array once (as you have noticed) so viewDidLoad is a good place to do it. Another good place is in a custom accessor...
- (NSMutableArray*)historyArray
{
if (!_historyArray) {
_historyArray = [[NSMutableArray alloc] init];
}
return _historyArray;
}
Now the first time you try to [self.historyArray addObject:sender], the accessor will note the absence of a historyArray, create one and return it. Next time round it won't be recreated as it already exits.
#property(nonatomic,retain)NSMutableArray *tapCollection;
-(void)viewDidLoad{
self.tapCollection = [[NSMutableArray alloc] init];
}
-(void)onTouch:(id)sender{
UIButton *btnTapped = (UIButton *)sender;
[self.tapCollection addObject:[NSNumber numberWithInt:btn.tag]];
}
Apologies in advance if my question is unclear as I'm new to iOS programming (and programming in general).
Right now, I have a UITableView that displays an alphabetical NSMutableArray of the names of a user's friends, something like:
friends: { #"Andy",#"Anand",#"Bob",#"Cat" };
I have another NSMutableArray that holds the matching ages of the user's friends, something that the user never sees but is passed on to the previous view controller after the user picks the name of a friend. This ages array is something like:
friendsAges: {#"25",#"23",#"30",#"22"};
My question is, if I added a searchBar to the UITableview of the friends' names, how do I make sure that the corresponding age object is sent back to the previous view controller if the user picks a name from the filtered array, searchResultsTableView?
For example, if there wasn't a searchBar, picking "Bob" would also send "30" to the previous controller. But if the user types in "bo" and only Bob is shown in the filtered searchResultsTableView, then age "25" will be sent instead to the previous controller b/c it's the first index.
Here's my incorrect code:
if (tableView == self.searchDisplayController.searchResultsTableView) {
NSString *theFriend = [_filteredFriendArray objectAtIndex:indexPath.row];
// not sure what I should use instead for the _filteredFriendArray below
NSString *theFriendAge = [_filteredFriendArray objectAtIndex:indexPath.row];
[self.delegate friendPickerViewController:self
didSelectFriend:theFriend
didSelectFriendAge:theFriendAge];
} else { //if not using search bar and seeing filtered search results
NSString *theFriend = [friends objectAtIndex:indexPath.row];
NSString *theFriendAge = [friendsAge objectAtIndex:indexPath.row];
[self.delegate friendPickerViewController:self
didSelectFriend:theFriend
didSelectFriendAge:theFriendAge];
}
I feel like it might be easier to use an NSDictionary instead of trying to match the index of a filtered search view back to main array, but am not sure how to best do that.
Any help would be appreciated!
You are right, it is easier to use NSDictionary. Each person can be represented by an NSDictionary like #{#"name":#"Stephen Hawking", #"age":70}. Populate your friends table view with [personDict objectForKey:#"name"] and on selection get the value for [person objectForKey:#"age"] and pass it back to the previous view controller.