I'm relatively new to iOS programming, so please bear with me.
I'm creating an app that calls recipes from a table view, then listing the measurements and ingredients as labels in the detail view. I catalogued my recipes in Google Sheets, downloaded it as a .csv file and populated a table using SQLiteStudio. I then exported the database, and using FMDB, plopped it in my app. Everything works fine there and I'm able to retrieve various fields.
What I'm trying to do is list out the measurements and ingredients so that it displays with line breaks:
0.5 oz.
1.0 oz.
1 jigger
And not as: 0.5 oz., 1.0 oz., 1 jigger which is how I wrote it into the Google doc.
I've tried using \n in the SQLite viewer but it outputs it as a String rather than encoding it as a new line. However, when I view the accompanying .sql file, TextMate2 reads it as new lines. I'm not sure if I have to apply some code within my tableviewcontroller implementation file where I call the FMDB or elsewhere.
I have been looking for a solution for awhile now but no luck. Any help in steering me in the right direction would be greatly appreciated. Thanks!
CocktailListTableViewController.m
- (NSMutableArray *)recipeCocktails {
recipeCocktails = [[NSMutableArray alloc] init];
NSString *databasePath = [(AppDelegate *) [[UIApplication sharedApplication] delegate] databasePath];
FMDatabase *fmDB = [FMDatabase databaseWithPath:databasePath];
if(![fmDB open])
{
NSLog(#"Could not open DB, try again");
return nil;
}
FMResultSet *results = nil;
results = [fmDB executeQuery:#"SELECT * FROM recipesCocktails"];
NSLog(#"result %# ",results);
if ([fmDB hadError]) {
NSLog(#"DB Error %d: %#", [fmDB lastErrorCode], [fmDB lastErrorMessage]);
}
while ([results next]) {
Cocktails *cocktails = [[Cocktails alloc] init];
cocktails.recipeName = [results stringForColumn:#"recipeName"];
cocktails.recipeMeasure = [results stringForColumn:#"recipeMeasure"];
cocktails.recipeIngred = [results stringForColumn:#"recipeIngred"];
cocktails.recipeGlass = [results stringForColumn:#"recipeGlass"];
cocktails.recipeShaker = [results stringForColumn:#"recipeShaker"];
cocktails.recipeDirections = [results stringForColumn:#"recipeDirections"];
[recipeCocktails addObject:cocktails];
}
[fmDB close];
return recipeCocktails;
CocktailsDetailTableViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = cocktails.recipeName;
self.recipeIngred.text = cocktails.recipeIngred;
self.recipeMeasure.text = cocktails.recipeMeasure;
I can see you're trying to store all the ingredients as one long, pre-built string, and I think that approach is not the best.
It's gonna work, but it's not future-friendly or simply... usual.
I strongly suggest having a new table that knows all the possible ingredients, with ID's and other informations of your will (color, average weight, picture* and whatnot)
( please note that it's not advised to store images in a SQL database and you'd rather store references to where you saved them as files or URL's)*
Once you have that table, you're golden. You simply reference an array of ingredients in your recipe table. For example [1,27,133], and with that you can do what you want. You have an array of ingredient objects, which you can treat how you see fit.
I would, for example, loop over it and create labels under each other and you'd have your lines.
I could also simply use an NSMutableString and fill it with the ingredients.name (again, using examples)
You could fill a UITextView with newlines between each ingredient (by using the NSMutableString and adding newlines between each object name).
And more importantly, you can modify everything without having to re-work your layouts or labels or re-write your whole database because you have static newlines ingredients for each recipe.
That is the most important point I wanted to make. With that approach you can remove any ingredient from any recipe without having to think really far or risk forgetting something. You could also permanently destroy any ingredient to every recipe by removing it from the ingredient table. Same for adding new ones. Or saying you want to change the name of ingredient X, you dont' have to do it in every recipe, and so on.
EDIT: Answering your question in comment
To load the recipes in the tableview and the ingredients on the detail page, you can do it like so :
You load all your recipes in an array and display that (I think you're successful already on that part).
Note that all the recipes have a property called "Ingredients" which is an array of ingredient keys/ids/something
On cell tap, you want to load the ingredients that you only know the ID of, you simply get them from the array of Id's that you stored in your recipe, and SELECT them from your db.
Globally, you have no idea what the ingredients are until the user taps : if you don't need to display them in the table view, then you don't need to laod them yet. But you already know their id (otherwise you wouldn't be able to find them!).
There are two approaches to this problem.
One approach is to store the list of ingredients in as a single text field, with new line characters inside the string. SQLite (and FMDB) handle this fine.
From the UI perspective, you would then show this text field in a multiline UITextField or a UILabel. If you use UILabel, make sure the number of lines is set to something other than 1 (i.e. either zero or the actual number of lines).
The trick here is now to get this newline string into the database. And it depends upon how you're doing this. But if you're using some SQLite tool, you cannot use \n sequence, as that's not acceptable SQL syntax. You can use x'0a' or char(10):
insert into foo (bar) values ('baz' || x'0a' || 'qux');
or:
insert into foo (bar) values ('baz' || char(10) || 'qux');
or, if using SQLite command line tool, you can just insert newline:
insert into foo (bar) values ('baz
qux');
But you cannot use \n sequence. You can do this from your Objective-C code, but not from within a third party SQLite tool.
The other approach is to store the list of recipes and the list of ingredients for each recipe in two different tables (e.g. recipes and ingredients). Then you could store each of the ingredients in a separate row in the ingredients table.
This strikes me as a more logical implementation. If you did this, you would then have the choice as to how you wanted to show the list of ingredients (separate UILabel controls, a UITableView, or, if you really wanted to, concatenate them together, separated with newline characters and show them in a single multiline control.
Related
We're trying to use an NSFetchedResultsController to return people names and populate a UITableView in sorted order, using localizedCompare:. We're also trying to provide a section index in the UI (the right column of first characters of each section). We provide the NSFetchedResultsController with a selector on our entity which provides the section each entity should belong to (specifically, the first character of the person's name, capitalized).
When dealing with people names which utilize Unicode code points we've run into an issue. NSFetchedResultsController complains the entities are not sorted by section.
Specifically:
reason=The fetched object at index 103 has an out of order section name 'Ø. Objects must be sorted by section name'}, {
reason = "The fetched object at index 103 has an out of order section name '\U00d8. Objects must be sorted by section name'";
The issue appears to be that the comparison value returned by localizedCompare: is different for the whole "word" versus the leading character.
The following tests pass though I would expect consistent comparison results between ("Ø" and "O") vs. ("Østerhus" and "Osypowicz").
- (void)testLocalizedSortOrder300
{
NSString *str1 = #"Osowski";
NSString *str2 = #"Østerhus";
NSString *str3 = #"Osypowicz";
NSString *letter1 = #"O";
NSString *letter2 = #"Ø";
//localizedCompare:
//"Osowski" < "Østerhus"
NSComparisonResult res = [str1 localizedCompare:str2];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", str1, str2, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
//"Østerhus" < "Osypowicz"
res = [str2 localizedCompare:str3];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", str2, str3, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
//"O" < "Ø"
res = [letter1 localizedCompare:letter2];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", letter1, letter2, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
}
So, the question ultimately is, given a person name (or any other string) which utilize Unicode code points, how do we properly (in a localized manner) return a section name which will correspond with the sort order as dictated by localizedCompare:?
Additionally, what's going on with the localizedCompare: apparently treating "Ø" and "O" as NSOrderedSame when followed by additional characters?
I expect localizedCompare: is using a specific combination of NSStringCompareOptions flags that are causing this behavior.
https://developer.apple.com/documentation/foundation/nsstringcompareoptions?preferredLanguage=occ
You might get the outcome you want by using compare:options: and turning on NSDiacriticInsensitiveSearch.
For generating the section index, it might be best to strip the value of all extended characters first, and then take the first letter. Something like:
[[str1 stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch] substringToIndex:1]
That way a name starting with an accented letter such as "Édward" will get converted to "Edward" before you take the first letter for the section.
Yeah, been there. The only solution I found was to create a second field for search that simplifies the characters (don't remember off hand the method) and store it as a second field which is used for search. Not super elegant but it worked.
Ultimately the approach which solved this was to store normalized section names in the database.
#MartinR suggested SO post lead me to https://stackoverflow.com/a/13292767/397210 which talks about this approach and was the key "ah ha" moment to solve it.
While this does not explain the goofy behavior of localizedCompare: apparently treating "Ø" and "O" as NSOrderedSame when followed by additional characters it is, IMHO, a more robust and complete solution which works for all Unicode code points, in our testing.
Specifically, the approach is:
Create (or utilize an existing) field on your entity to receive a normalized section name for the entity (let's call it sectionName).
Populate this field (sectionName) with the normalized section name*, initially, and as needed (when the person name changes, for instance).
Use this section name field (sectionName) for the sectionNameKeyPath of NSFetchedResultsController -initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:
For the sort descriptors used by the fetch request passed to the NSFetchedResultsController be sure to sort first by section name then by how to sort the contents of the section (person name, for instance), paying attention to the use of the localized version of the comparison selectors. e.g.:
[NSSortDescriptor sortDescriptorWithKey:#"sectionName" ascending:YES selector:#selector(localizedStandardCompare:)],
[NSSortDescriptor sortDescriptorWithKey:#"personName" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]
Test.
*Normalized Section Name
We need to be careful about assuming what the first "character" is when dealing with unicode. "Characters" may be composed of more than one character.
See https://www.objc.io/issues/9-strings/unicode/
and also Compare arabic strings with special characters ios
This is the direction I used to generate a normalized section name:
NSString *decomposedString = name.decomposedStringWithCanonicalMapping;
NSRange firstCharRange = [decomposedString rangeOfComposedCharacterSequenceAtIndex:0];
NSString *firstChar = [decomposedString substringWithRange:firstCharRange];
retVal = [firstChar localizedUppercaseString];
Hopefully this approach is clear and useful to others, and thanks for the assist, all.
I'm trying to recreate the buggy CNContactPickerViewController made by Apple so I have an array of data [CNContact] which i need to display neatly in a UITableView. It all works great until i try to add sections based on the first letter of the contacts' last names to that table. The only solution i found is to iterate through the [CNContact] array and manually group every contact into a dictionary, based on their initials resulting in a [String:[CNContact]]. Is there a better way to do this?
The end result can be viewed in the screenshot below.
This will sort your contacts by last name. Might be overkill, as you want them grouped only by the first letter of the last name, whereas this will sort using the whole name.
var contacts :[CNContact] = [CNContact]();
// fill your contacts here
contacts.sortInPlace { (contact1, contact2) -> Bool in
contact1.familyName.compare(contact2.familyName) == .OrderedAscending
}
Not sure if you need the original ordering. If you want the original array, do create copy of the old one, and do sort in place for the copy.
I have a table in my database. I want to add values to a specific column in the table. Since there are like thousands plus rows in my table i figured i have to use batch update. The code is this but i get syntax error and a message saying there is no such row:
NSString *addProductColQuery = #"ALTER TABLE Sale ADD COLUMN Product text";
[self.dbManager loadDataFromDB:addProductColQuery];
NSString *batchstart = #"BEGIN IMMEDIATE TRANSACTION";
[self.dbManager loadDataFromDB:batchstart];
for (int i = 0; i<self.productInfo.count; i++)
{
NSString *addValue = [NSString stringWithFormat:#"INSERT INTO Sale (Product) VALUES (%#)",[tempProductArray objectAtIndex:i]];
[self.dbManager loadDataFromDB:addValue];
}
NSString *batchComit = #"COMMIT TRANSACTION";
[self.dbManager loadDataFromDB:batchComit];
Update:
I have managed to get the above code to work but now i am getting 90% CPU Usage on iphone6! I actually saw this coming since im using a for loop which is dead wrong to loop a query. Is there a way to batch insert the values to the rows in a specific column?
Are the values in tempProductArray text values? If so, you have to quote them in your SQL:
NSString *addValue = [NSString stringWithFormat:#"INSERT INTO Sale (Product) VALUES ('%#')", tempProductArray[i]];
Having shown you a quick solution, that's actually the wrong way to do it. It is inadvisable to use stringWithFormat to build SQL with text values. If any of those strings themselves have apostrophe characters in them, the SQL will fail. Also, if any of this data was provided by the end-user or from the network, you're exposed to SQL injection attacks.
The correct solution is to use sqlite3_prepare_v2 of the SQL without quotes, but with ? placeholder:
NSString *addValue = #"INSERT INTO Sale (Product) VALUES (?)";
And then, before calling sqlite3_step, you would call sqlite3_bind_text to bind a text value to the ? placeholder.
If your dbManager doesn't offer that capability, you'll have to add it. Or use a library, like FMDB, that offers this capability out of the box.
I have a NSMutableArray of NSDictionaries. I would like to be able to filter dynamically.
For example, I have 3 types of filters
- area
- item
- type
if area is chossen then I would like to filter the Array with the area predicate, However if the user then chooses to filter item too, the currently filtered area array will then have the item filter applied too it.
However if the area filter is removed then I would like to show the new item filter.
It gets even more complicated when type is introduced, however I am struggling to get this to work correctly and don't really know where to start with the logic of it.
I can get the array to filter based off the last selected predicate. So if I choose area then items the current filter will only be items not both.
Try something like:
NSMutableArray *predicates = [NSMutableArray array];
if (...) {
[predicates addObject:...];
}
NSCompoundPredicate *p = [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:predicates];
Where you add the individual predicates to the array if they are required. This is assuming that at least one predicate does need to be applied.
I have an array of data that is displayed in a table. The array has multiple fields, including two specific ones I want to filter, the "call type" and the "county". The value for "call type" is either an "f" or "e" and the value for the county is either "w" or "c". I want to have 4 UISwitch's to to either turn on/off the "w", turn on/off the "c" etc. Its hard to explain but if you go to this website and look at the top right corner, its exactly what I want to do. http://www.wccca.com/PITS/ Out of the 4 filters, two filters control the county field, and two filters control the call type field. but they all operate independently. How would I go about accomplishing this? Would I use NSPredicate to create a new array each time something is filtered or what? Thanks.
You could definitely use an NSPredicate for this. Probably the easiest thing to do would be to use the same IBAction for all four switches and have it do a recalculation:
- (IBAction)anySwitchDidChange:(id)sender
{
// make a set of all acceptable call types
NSMutableSet *acceptableCallTypes = [NSMutableSet set];
if(self.fSwitch.on) [acceptableCallTypes addObject:#"f"];
// ... etc, to create acceptableCallTypes and acceptableCounties
NSPredicate *predicate =
[NSPredicate predicateWithFormat:
#"(%# contains callType) and (%# contains county)",
acceptableCallTypes, acceptableCounties];
/*
this predicate assumes your objects have the properties 'callType' and
'county', and that you've filled the relevant sets with objects that would
match those properties via isEqual:, whether strings or numbers or
anything else.
NSDictionaries are acceptable since the internal mechanism used here is
key-value coding.
*/
NSArray *filteredArray = [_sourceArray filteredArrayUsingPredicate:predicate];
// send filteredArray to wherever it needs to go
}
Using predicateWithFormat: causes the text to be parsed right there and then. In this case that should be no problem whatsoever but in general you can create the predicates in advance and supply only the parameters at the relevant moment, should you ever end up using one in a really time critical area.