I have an array of arrays, theMealIngredients = [[]]
I'm creating a newMeal from a current meal and I'm basically copying all checkmarked ingredients into it from the right section and row from a tableview. However, when I use the append, it obviously doesn't know which section to go in as the array is multidimensional. It keeps telling me to cast it as an NSArray but that isn't what I want to do, I don't think.
The current line I'm using is:
newMeal.theMealIngredients.append((selectedMeal!.theMealIngredients[indexPath.section][indexPath.row])
You should re-model your data to match its meaning, then extract your tableview from that. That way you can work much more easily on the data without having to fuss with the special needs of displaying the data. From your description, you have a type, Meal that has [Ingredient]:
struct Meal {
let name: String
let ingredients: [Ingredient]
}
(None of this has been tested; but it should be pretty close to correct. This is all in Swift 3; Swift 2.2 is quite similar.)
Ingredient has a name and a kind (meat, carbs, etc):
struct Ingredient {
enum Kind: String {
case meat
case carbs
var name: String { return self.rawValue }
}
let kind: Kind
let name: String
}
Now we can think about things in terms of Meals and Ingredients rather than sections and rows. But of course we need sections and rows for table views. No problem. Add them.
extension Meal {
// For your headers (these are sorted by name so they have a consistent order)
var ingredientSectionKinds: [Ingredient.Kind] {
return ingredients.map { $0.kind }.sorted(by: {$0.name < $1.name})
}
// For your rows
var ingredientSections: [[Ingredient]] {
return ingredientSectionKinds.map { sectionKind in
ingredients.filter { $0.kind == sectionKind }
}
}
}
Now we can easily grab an ingredient for any given index path, and we can implement your copying requirement based on index paths:
extension Meal {
init(copyingIngredientsFrom meal: Meal, atIndexPaths indexPaths: [IndexPath]) {
let sections = meal.ingredientSections
self.init(name: meal.name, ingredients: indexPaths.map { sections[$0.section][$0.row] })
}
}
Now we can do everything in one line of calling code in the table view controller:
let newMeal = Meal(copyingIngredientsFrom: selectedMeal,
atIndexPaths: indexPathsForSelectedRows)
We don't have to worry about which section to put each ingredient into for the copy. We just throw all the ingredients into the Meal and let them be sorted out later.
Some of this is code is very inefficient (it recomputes some things many times). That would be a problem if ingredient lists could be long (but they probably aren't), and can be optimized if needed by caching the results or redesigning the internal implementation details of Meal. But starting with a clear data model keeps the code simple and straightforward rather than getting lost in nested arrays in the calling code.
Multi-dimentional arrays are very challenging to use well in Swift because they're not really multi-dimentional. They're just arrays of arrays. That means every row can have a different number of columns, which is a common source of crashing bugs when people run off the ends of a given row.
Related
I have the two lists of AddItem objects. AddItem is a custom object made of data downloaded from the web. The second one list is also AddItem, but this one is saved in the database. I use it to create another list, but in this case user of the app decides which object are important for him.
This want I want to achieve is to mark every single object of the first AddItem list (not saved in the database, create during the start of the view), to show in the TableView which one is saved in the database, so I already use him in another view. You know what I mean. There is a TableView list and if I am interested in a cell I select it and add it to the database.
I hope I have described it clearly. If not, ask for questions.
The first AddItem list (not saved in the database):
func setAddItemList(stations: [Station], sensors: [Sensor]) {
var addItems = [AddItem]()
var sensorItems = [SensorItem]()
let falseValue = RealmOptional<Bool>(false)
addList = try persistenceService.fetchAddItems().toArray(ofType: AddItem.self) //The second list with saved data in the database
let addItem = stations.map { station in
AddItem(
id: station.id,
stationId: station.id,
cityName: station.city?.name ?? "",
addressStreet: station.addressStreet!,
added: falseValue,
sensor: [])
}
addItems.append(contentsOf: addItem)
As you can see, it's create by already downloaded data. I decided to add the property - added, which is the bool property and mark it as true if selected the right cell. Unfortunately I don't know how to do this when creating a list of AddItem objects. The saved array is almost the same. There is only more data, but ids, names, addresses and so on are same, so there are loads of the same data for comparison
I made the solution myself:
addItem.forEach { item in
guard let index = addList2.firstIndex(where: { $0.id == item.id})
else {
print("Failed to find the SavedAddItem for the AddItem \(item.id)")
return
}
addItems[index + 1].added = trueValue
}
There is almost certainly a better way of doing this and I'd love to know but I can't phrase it in a question so essentially here is my problem:
I am creating an app that presents a list of items(In a table view) which have various bits of data that come along with the item(String Int Date ect). I've decided that the best way to store this data is in a struct because it allows me to store lost of different types of data as well as run processes on it.
The problem is that I want to have theoretically an infinite number of items in the list and so I need to make lost of instances of the Item struct without predetermining the names of each instance.
I would then store these instance names in an array so that they can be listed in the table view.
I'm completely stuck at this point I've spent hours looking and I just can't make sense of it I'm sure its stupidly easy because hundreds of apps must need to do this. I'm Open to anything thanks.
Currently, I have a struct:
struct Item() {
var data1: String
var data2: String // (But Should be Int)
var data3: String
func setDate() {
// code
}
func returnDate() {
// code
}
}
and then in the view controller I have:
#IBAction func SubmitButton(_ sender: UIButton) {
var textField1 = Item(data1: textField1.text!, data2: textFeild2.text!, data3: "Units")
print(textField1.data1)
}
I am not completely sure what your goal is but I guess the final goal is to have an array of your Item Objects, which then can be used to populate an UITableView??? Without setting up a name for this Instances?
The most easiest solution would be to create a array as a Class Property for storing all the Items which is empty like this:
var items : [Item] = []
And then in viewDidLoad(), you can call a function which populates the item array with all the available Items.
This Item Array can then be used to populate the UITableView.
Second Part:
You got this IBAction which creates a new Item. I guess this is supposed to add a new Item to the existing ones.
You can add this new Item to the existing Item Array by using
items.append(Item(<parameters>))
And don't forget to reload the UITableView to include the new data.
tableView.reloadData()
I have an array with over 300k objects which I'm showing in a UITableView. When filtering by prefix match using the filter method, the first search takes a bit over 60s! After that, the search is way faster taking around 1s, which I'd still want to improve a bit more.
The object looks like this:
struct Book {
let id: Int
let title: String
let author: String
let summary: String
}
This is how I'm filtering at the moment:
filteredBooks = books.filter { $0.title.lowercased().hasPrefix(prefix.lowercased()) }
The data comes from a JSON file which I decode using Codable (which takes a bit longer than I would like as well). I'm trying to achieve this without a database or any kind of framework implementation nor lazy loading the elements. I'd like to be able to show the 300k objects in the UITableView and realtime filter with a decent performance.
I've Googled a bit and found the Binary Search and Trie search algorithms but didn't know how to implement them to be able to use them with Codable and my struct. Also, maybe replacing the struct with another data type would help but not sure which either.
Because I liked the challenge I put something together.
It is basically a tree with each layer of the tree containing a title prefix plus the elements with that exact match plus a list of lower trees with each having the same prefix plus one more letter of the alphabet:
extension String {
subscript (i: Int) -> String {
let start = index(startIndex, offsetBy: i)
return String(self[start...start])
}
}
struct Book {
let id: Int
let title: String
let author: String
let summary: String
}
class PrefixSearchable <Element> {
let prefix: String
var elements = [Element]()
var subNodes = [String:PrefixSearchable]()
let searchExtractor : (Element) -> String
private init(prefix: String, searchExtractor:#escaping(Element) -> String) {
self.prefix = prefix
self.searchExtractor = searchExtractor
}
convenience init(_ searchExtractor:#escaping(Element) -> String) {
self.init(prefix: "", searchExtractor: searchExtractor)
}
func add(_ element : Element) {
self.add(element, search: searchExtractor(element))
}
private func add(_ element : Element, search : String) {
if search == prefix {
elements.append(element)
} else {
let next = search[prefix.count]
if let sub = subNodes[next] {
sub.add(element, search: search)
} else {
subNodes[next] = PrefixSearchable(prefix: prefix + next, searchExtractor: searchExtractor)
subNodes[next]!.add(element, search: search)
}
}
}
func elementsWithChildren() -> [Element] {
var ele = [Element]()
for (_, sub) in subNodes {
ele.append(contentsOf: sub.elementsWithChildren())
}
return ele + elements
}
func search(search : String) -> [Element] {
print(prefix)
if search.count == prefix.count {
return elementsWithChildren()
} else {
let next = search[prefix.count]
if let sub = subNodes[next] {
return sub.search(search: search)
} else {
return []
}
}
}
}
let searchable : PrefixSearchable<Book> = PrefixSearchable({ $0.title.lowercased() })
searchable.add(Book(id: 1, title: "title", author: "", summary: ""))
searchable.add(Book(id: 2, title: "tille", author: "", summary: ""))
print(searchable.search(search: "ti")) // both books
print(searchable.search(search: "title")) // just one book
print(searchable.search(search: "xxx")) // no books
It can probably be improved in terms of readability (my swift is quite rusty right now). I would not guarantee that it works in all corner cases.
You would probably have to add a "search limit" which stops recursively returning all children if no exact match is found.
Before your start changing anything, run Instruments and determine where your bottlenecks are. It's very easy to chase the wrong things.
I'm very suspicious of that 60s number. That's a huge amount of time and suggests that you're actually doing this filtering repeatedly. I'm betting you do it once per visible row or something like that. That would explain why it's so much faster the second time. 300k is a lot, but it really isn't that much. Computers are very fast, and a minute is a very long time.
That said, there are some obvious problems with your existing filter. It recomputes prefix.lowercased() 300k times, which is unnecessary. you can pull that out:
let lowerPrefix = prefix.lowercased()
filteredBooks = books.filter { $0.title.lowercased().hasPrefix(lowerPrefix) }
Similarly, you're recomputing all of title.lowercased() for every search, and you almost never need all of it. You might cache the lowercased versions, but you also might just lowercase what you need:
let lowerPrefix = prefix.lowercased()
let prefixCount = prefix.count // This probably isn't actually worth caching
filteredBooks = books.filter { $0.title.prefix(prefixCount).lowercased() == lowerPrefix }
I doubt you'll get a lot of benefit this way, but it's the kind of thing to explore before exploring novel data structures.
That said, if the only kind of search you need is a prefix search, the Trie is definitely designed precisely for that problem. And yeah, binary searching is also worth considering if you can keep your list in title order, and prefix searching is the only thing you care about.
While it won't help your first search, keep in mind that your second search can often be much faster by caching recent searches. In particular, if you've searched "a" already, then you know that "ap" will be a subset of that, so you should use that fact. Similarly, it is very common for these kinds of searches to repeat themselves when users make typos and backspace. So saving some recent results can be a big win, at the cost of memory.
At these scales, memory allocations and copying can be a problem. Your Book type is on the order of 56 bytes:
MemoryLayout.stride(ofValue: Book()) // 56
(The size is the same, but stride is a bit more meaningful when you think about putting them in an array; it includes any padding between elements. In this case the padding is 0. But if you added a Bool property, you'd see the difference.)
The contents of strings don't have to be copied (if there's no mutation), so it doesn't really matter how long the strings are. But the metadata does have to be copied, and that adds up.
So a full copy of this array is on the order of 16MB of "must copy" data. The largest subset you would expect would be 10-15% (10% of words start with the most common letter, s, in English, but titles might skew this some). That's still on the order of a megabyte of copying per filter.
You can improve this by working exclusively in indices rather than full elements. There unfortunately aren't great tools for that in stdlib, but they're not that hard to write.
extension Collection {
func indices(where predicate: (Element) -> Bool) -> [Index] {
indices.filter { predicate(self[$0]) }
}
}
Instead of copying 56 bytes, this copies 8 bytes per result which could significantly reduce your memory churn.
You could also implement this as an IndexSet; I'm not certain which would be faster to work with.
The main idea is to have all sections in an array or you may suggest other solution to build a table. I had this code below to prepare data for a table:
enum ExerciseSection {
case empty
case exerciseGroup(group: ExerciseGroup)
}
struct ExerciseGroup {
var muscleGroupName: String
var exercises: [ExerciseEntity]
var selectedExercisesIndexes: [Int]
}
As you see using this ExerciseSection enum I can simple check if the section is static empty or it should display some muscle group name. Also Group contains exercises. So I can simple a build needed cell.
So I prepare data for my table by creating an array of ExerciseSection.
In this concert example my empty cell is a cell which redirect me to the other screen.
Looks like this:
[empty cell for section 0, group for section 1, group for section 2... and etc]
Now I changed mind of preparing my own sections and instead I started using CoreStore.monitorSectionedList
typealias ListEntityType = ExerciseEntity
let monitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
)
So now my data is grouped automatically by relationships muscle name.
I can simple access instance of monitor and see how many sections it has and how many rows it has for appropriate section. Pretty awesome!
But my question now how can I combine monitor object which has all needed info about grouped objects and about groups with my static cells.
In my example above I have the firs element empty cell for section 0 but monitor already has section 0 as well.
So I need to have a hack to add 1 + which I really don't like as this is a magical number and some day it will surprise me.
func numberOfSections(in tableView: UITableView) -> Int {
return 1 + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
In the previous time I just had array of all my sections [ExerciseSection] so there is no needs to control code via 1 +
I need to glue somehow my static section info and monitor.sections
You may never used CoreStore before, so never mind you can just think about monitor object as an object that has some groups to represent sections and these groups has items to represent rows. So I just need to combine it.
In my case you can simple see that the static cell is a first one item in the list but I am looking for flexible solution I even can't imagine how to show static cell at the middle of list for example.
Maybe as a solution I can loop through monitor objects and create my enum from it. Not sure.
Hmm... The "easiest" way would be to have a computed property sections or similar.
Something along the lines of
var sections: [ExerciseSection] {
return [.empty] + monitor.sections
}
But if the monitor doesn't have a direct way to get sections, then maybe the best way would be to simply have a list of "pre-sections".
let presections: [ExerciseSection] = [.empty]
func numberOfSections(in tableView: UITableView) -> Int {
return presections.count + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
You could add a couple of functions to help you such as
func section(at indexPath: IndexPath) -> ExerciseSection {
guard indexPath.section >= presections.count else {
return presections[indexPath.section]
}
return monitor.section(at: indexPath.section - presections.count)
}
You do mention looping through the monitor objects, and this can be nice for smaller datasets. The downside is that you suddenly store data in memory. I don't know how the monitor works.
With Realm I've done this but only stored the id and maybe some simple data for each row.
I've got some [NSDictionary] filled with books that I'm checking values for to better display the content in a UICollectionView.
I'm doing a check on a key if it contains several ISBN numbers. If it do I'll want to display them as separate books. The key is 'isbn' and if the value are something like '["9788252586336", " 9788203360510"]' I'll want to display them as separate books. So they'll become two "equal" books, but with different isbn numbers.
Here's what happens:
Heres the code
func parserFinishedSuccesfully(data: NSMutableArray){
self.tempArray = []
if !data.isEqual(nil) && data.count > 0
{
for i in (0 ..< data.count)
{
var currentBook = NSDictionary()
if !((data[i] as? NSDictionary)!.valueForKey("isbn") as? String)!.isEmpty
{
//If the book do have isbn then I'll want to display it
currentBook = data[i] as! NSDictionary
if let isbn = currentBook.valueForKey("isbn") as? String
{ //isbn could be "9788252586336; 9788203360510"
let isbnNumbersRecieved = isbnNumbers(isbn)
//gives: ["9788252586336", " 9788203360510"]
if isbnNumbersRecieved.count >= 2
{
//We are dealing with several isbn numbers on one book
for isbn in isbnNumbersRecieved
{ //The problem lies here!
print("This is the isbn: \(isbn)")
currentBook.setValue(isbn, forKey: "isbn")
self.tempArray.append(currentBook)
}
}
else
{
//Only one isbn
//currentBook.setValue(isbnNumbersRecieved.first, forKey: "isbn")
//self.tempArray.append(currentBook)
}
}else{
//No isbn, no want
print("No isbn")
}
}
}
print(self.tempArray)
self.collectionView.reloadData()
}
In code it says: "problem lies here". Well...the problem is there.
When it enters that loop with this array ["9788252586336", " 9788203360510"] it will first print the first number, then the second as it should.
Then when it takes currentBook. I change the value that is there before which would be "9788252586336; 9788203360510" with just the first isbn number "9788252586336". And then I take the entire currentBook and append it to the array (tempArray) that will be used to display the books in a UICollectionView later.
When the second iteration starts the isbn number will be "9788203360510" and I'll set the isbn number to be that in currentBook, just like the first iteration.
The weird thing happens now. When currentBook get's appended to (tempArray) it should contain the same book, but with different isbn numbers. But it actually contains the same fu#%$ book with same isbn number.
Somehow the second iteration adds 2 books with the same isbn number and the first book that should be there is gone...
Picture 1 is after the first iteration and picture 2 is after the second iteration. If you look at the key "isbn", you can see that there is something messed up with the code...
Picture 1
Picture 2
How can this happen and how would I fix it?
This is happening because currentBook is a reference type, not a value type. You are adding two references to the same book, currentBook, to the array.
As a simple fix, you could copy() the dictionary before modifying it and adding it to the array. A more robust fix would be to make all of your models value types instead.
Further reading:
Swift Blog: Value and Reference Types
Ray Wenderlich: Reference vs. Value Types in Swift
You are using the same dictionary object for each item in your for loop and hence this result. Try below and let me know if it works.
//We are dealing with several isbn numbers on one book
for isbn in isbnNumbersRecieved
{ //The problem lies here!
print("This is the isbn: \(isbn)")
let tempDict = NSDictionary()
tempDict.setValue(isbn, forKey: "isbn")
//currentBook.setValue(isbn, forKey: "isbn")
self.tempArray.append(tempDict)
}