I am trying to make a grid view in SwiftUI, composed of a varying amount of view elements. I used this thread as an orientation: UICollectionView and SwiftUI?
I have an collection that holds my elements, chunked into snippets of 3 each:
let test = allTheRewards.filter({ !$0.completed }).chunked(into: 3)
Then, in my "mother view", I'm iterating through them like this to pass the data to the child view element:
VStack {
ForEach(self.test.indices, id:\.self) { idx in
HStack {
ForEach(self.test[idx].indices, id:\.self) { index in
HStack {
TestReward(name: self.test[idx][index].name, description: self.test[idx][index].description, shape: self.test[idx][index].shape, bgStart: self.test[idx][index].bgStart, bgEnd: self.test[idx][index].bgEnd, isComplete: self.test[idx][index].completed)
}
}
}
}
}
The problem here is that the compiler doesn't seem to like that many uses of the indexes ([idx][index]).
The compiler is unable to type-check this expression in reasonable
time; try breaking up the expression into distinct sub-expressions
It works fine when I'm only using it a few times in the call to pass my arguments, but it doesn't work with the current 6 times. The compiling time increases a lot and it eventually fails. So I think this may be a pretty inefficient way to go about it. But I don't know what would be a better solution. Any ideas?
Further info:
It is no problem to use the amount of indexes in separate elements, like this:
Text("\(self.uncompletedRewards[idx][index].name)")
Text("\(self.uncompletedRewards[idx][index].description)")
Text("\(self.uncompletedRewards[idx][index].shape)")
Text("\(String(describing: (self.uncompletedRewards[idx][index].bgStart)))")
Text("\(String(describing: (self.uncompletedRewards[idx][index].bgEnd)))")
Text("\(String(self.uncompletedRewards[idx][index].completed))")
I just seem not able to put this all into one call...
TestReward(model: model, row: row, column: column)
or
TestReward(row: row, column: column).environmentObject(model)
should solve your trouble and dramatically increase readability (and next maintenance) of your codebase.
Related
I have an iOS app that uses core data and CloudKit. The data model is simple and it contains a date and a string element that stores the date formatted as EEEE, MMM d yyyy.
When I run a SectionedFetchRequest and display the results in a list, after I have more than two sections the sort order all the sudden "jumps" and mixes the entries under each section. If I delete the app and reinstall it, the data that comes from iCloud is correctly formatted, but when I close the app and reopen it, again the sort order gets all mixed up. This happens with three or more sections and the order mix-up is not always the same (at least that I can observe).
Here's what I mean:
Two sections
Adding another entry, creating a third section
Closing the app and reopening, order gets mixed up
I have tried to look at how the data gets sorted via FetchedResults and SectionedFetchResults, I have tried to change how the list is created, and I have gone through the debug messages from CloudKit, but I can't figure out what's going on.
This is the data model:
And the code to fetch the data and display it on the list:
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Item.date, ascending: false)])
var entries: FetchedResults<Item>
#SectionedFetchRequest<String, Item>(
sectionIdentifier: \.dateText!,
sortDescriptors: [SortDescriptor(\.date, order: .reverse)]
)
var sections: SectionedFetchResults<String, Item>
NavigationView {
List {
ForEach(sections) { section in
Section(header: Text(section.id)) {
ForEach(section) { entry in
Text(entry.text!)
}
}
}
}
}
At this point I'm out of ideas of what else can it be. Since it's coming from iCloud formatted correctly, I'm assuming the issue is during the fetch requests and the sorting happening there, but I don't know enough to understand whether I'm doing something wrong in how I fetch the data, or whether there is another bug. Any help is appreciated.
Section order has to match sort order, the weekdays are being sorted in alphabetical order which is a different order from the dates. Try yyyy-MM-dd for the dateText. Then you will need to format it again for displaying the section text and usually that would be localised.
I have the following formula: =ArrayFormula(INDEX(Items!F2:F,MATCH(C2,Items!E2:E,0)))
I would like to extend it such that the entire C column runs the same formula for values. Please help. If a script is necessary to achieve this, I'd like to explore that option too.
Use Apps Script!
Sheet functions (formulae) work great (especially if you are a master like player0), but I find it much easier to work within Apps Script for anything much more complicated than a simple INDEX MATCH. If you are willing to learn some JavaScript, I highly recommend learning some.
Custom Functions
You can write custom sheet functions in Apps Script that you can call with the traditional =FUNCTION() from a cell.
The way it works is that you write a function in Apps Script that returns a two dimensional array corresponding to the area that it needs to fill.
For example, if wanted a function to fill a 2 x 2 block with 1, you would need to make your function return:
[[1,1],[1,1]]
Or you can write it like this:
[
[1, 1],
[1, 1]
]
Implementing Index Match
There are many ways you can implement it, here is an example.
The example spreadsheet has 2 tabs, "Ledger" and "Items".
The goal of the function that follows is to get the costs of the items from the "Items" tab.
function ledgerIndexMatch(){
// Initializing the location of data
let ss = SpreadsheetApp.getActive();
let ledger = ss.getSheetByName("Ledger");
let source = ss.getSheetByName("Items");
let ledgerRange = ledger.getDataRange();
let sourceRange = source.getDataRange();
// Getting the values into a 2D array
let ledgerValues = ledgerRange.getValues();
let sourceValues = sourceRange.getValues();
// Discarding the first row (headers)
ledgerValues.shift();
sourceValues.shift();
// Initializing the output array
let output = [];
// This is where the INDEX MATCH happens
// For each row in ledger
ledgerValues.forEach(ledgerRow => {
// Get the second column (index 1)
let item = ledgerRow[1];
// Initialize the column
let value = [];
// For each row in the source
sourceValues.some(sourceRow => {
// Check if the item is there
if (item == sourceRow[0]) {
// if so, add to value
value.push(sourceRow[1]);
// stop looking for values
return true
// if not matched, keep looking
} else return false
})
// Add the found value (or blank if not found)
// to the output array.
output.push(value);
})
return output;
}
Which can be used like this:
Whats nice about Apps Script is that you can customize it to your heart's content. In this example, the function automatically detects the height of the respective tables, so you don't need to fiddle around with ranges.
You might want to extend this function with arguments so that its more flexible. Or you could just have a few versions of it for different operations, if you don't have too many. Or refactor it... its up to you.
References
Apps Script
Custom Functions
Tutorials
SpreadsheetApp
use:
=ARRAYFORMULA(IFNA(VLOOKUP(C2:C, Items!E2:F, 2, 0)))
tNum={[2]=true , [3]=true,[4]=true, [5]=true ,[6]=true }
#tNum-->0
tNum={}
tNum[2]=true
tNum[3]=true
tNum[4]=true
tNum[5]=true
tNum[6]=true
#tNum-->6
why such a difference in size?
are both examples identical?
Your two tables are semantically identical, but using # on them is ambiguous. Both 0 and 6 are correct lengths. Here's an abridged version of the docs:
The length operator applied on a table returns a border in that table. A border in a table t is any natural number that satisfies the following condition:
(border == 0 or t[border] ~= nil) and t[border + 1] == nil
A table with exactly one border is called a sequence.
When t is not a sequence, #t can return any of its borders. (The exact one depends on details of the internal representation of the table, which in turn can depend on how the table was populated and the memory addresses of its non-numeric keys.)
This is an example of undefined behavior (UB). (That may not be the right word, because the behavior is partially defined. UB in Lua can't launch nuclear weapons, as it can in C.) Undefined behavior is important, because it gives the devs the freedom to choose the fastest possible algorithm without worrying about what happens when a user violates their assumptions.
To find a length, Lua makes, at most, log n guesses instead of looking at every element to find an unambiguous length. For large arrays, this speeds things up a lot.
The issue is that when you define a table as starting at index [2], the length operator breaks because it assumes that tables start at index [1].
The following code works as intended:
tNum = {[1]=false, [2]=true, [3]=true, [4]=true, [5]=true, [6]=true}
#tNum => 6
The odd behaviour is caused because when you initialize an array with tNum={} it initializes by assigning every index to nil, and the first index is [1] (It doesn't actually initialize every value to nil, but it's easier to explain that way).
Conversely, when you initialize an array with tNum={[2]=true} you are explicitly telling the array that tNum[1] does not exist and the array begins at index 2. The length calculation breaks when you do this.
For a more thorough explanation, see this section of the lua wiki near the bottom where it explains:
For those that really want their arrays starting at 0, it is not difficult to write the following:
days = {[0]="Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
Now, the first value, "Sunday", is at index 0. That zero does not affect the other fields, but "Monday" naturally goes to index 1, because it is the first list value in the constructor; the other values follow it. Despite this facility, I do not recommend the use of arrays starting at 0 in Lua. Remember that most functions assume that arrays start at index 1, and therefore will not handle such arrays correctly.
The Length operator assumes your array will begin at index [1], and since it does not, it doesn't work correctly.
I hope this was helpful, good luck with your code!
I am writing an app to simulate the nba lottery. I have already written the codes to generate the random combinations, and assigned them to each team.
Here is my method to simulate the drawings and assign the draft positions to each team. standingsArray is an array of Team items of type ObjectWrapper, with values of name, seed, wins, losses, draft position exc... for each team. So basically what Im doing is I have 14 balls and randomly choose 4 balls, which constitute a combination (order doesn't matter). So essentially there are a total of 1001 total possible combinations, but one is thrown out. (you can ignore the first while loop because that is just there so that the thrown out combination isnt selected). A number of combinations is assigned to the 14 lottery teams based on record (250 for worst team, 199 for second worst exc...). The argument in my method standingsArray already has the number of possibilities assigned to each team. Next, I randomly pull 4 balls from the total possibilities, and the team with that combination gets the first pick. But because all the combinations for that team selected cant be chosen again for the second pick, I have to remove all of those combinations, but that is very complicated so instead, i make a new array called tempPossibilities which appends all the combinations for every team except the one just selected, which then allows me to generate a new combination to select from.
However, I am getting an error at this line for j in 0...(standingsArray[i].possibilities?.count)!-1{ It says bad instruction error, and I cannot figure out why I am getting this error. And what else doesnt make sense is that the for loop works and the tempPossibilities array is fully populated with the correct amount of combinations (without the lottery team), even though the error happens at the for loop?
Code is below: any help is appreciated, thank you, and sorry for the really long paragraph
func setDraftPositions(var standingsArray: [Team])->[Team]{
var lottery: [Team]=[]
var totalPossibilities: [[Int]]=combosOfLength(14, m: 4)
var tempPossibilities = []
var rand = Int(arc4random_uniform(UInt32(totalPossibilities.count)))
var draw = totalPossibilities[rand]
while (draw==(unused?.first)!) {
rand = Int(arc4random_uniform(UInt32(totalPossibilities.count)))
draw = totalPossibilities[rand]
}
s: for x in 0...13{
for a in 0...(standingsArray[x].possibilities?.count)!-1{
if(draw==standingsArray[x].possibilities![a]){
standingsArray[x].setDraftingPosition(1)
standingsArray[x].isLottery=true;
lottery.append(standingsArray[x])
for i in 0...(standingsArray.count-1) {
if(standingsArray[i].firstName != standingsArray[x].firstName!) {
for j in 0... (standingsArray[i].possibilities?.count)!-1{ //ERROR is happening here
tempPossibilities.append(standingsArray[i].possibilities![j])
}
}
}
standingsArray.removeAtIndex(x)
break s;
}
}
}
(repeat this for the next 2 picks)
Try this:
for j in 0...(standingsArray[i].possibilities?.count)!-1{
should be written like this:
for j in 0...(standingsArray[i].possibilities?.count)! - 1{
it needs proper spacing.
I have an Item model which has an attribute category. I want the items count grouped by category. I wrote a map reduce for this functionality. It was working fine. I recently wrote a script to create 5000 items. Now I realize my map reduce only gives the result for the last 80 records. The following is the code for the mapreduce function.
map = %Q{
function(){
emit({},{category: this.category});
}
}
reduce = %Q{
function(key, values){
var category_count = {};
values.forEach(function(value){
if(category_count.hasOwnProperty(value.category))
category_count[value.category]++;
else
category_count[value.category] = 1
})
return category_count;
}
}
Item.map_reduce(map,reduce).out(inline: true).first.try(:[],"value")
After researching a bit and I discovered mongodb invokes reduce function multiple times. How can achieve the functionality I intended for?
There is a rule you must follow when writing map-reduce code in MongoDB (a few rules, actually). One is that the emit (which emits key/value pairs) must have the same format for the value that your reduce function will return.
If you emit(this.key, this.value) then reduce must return the exact same type that this.value has. If you emit({},1) then reduce must return a number. If you emit({},{category: this.category}) then reduce must return the document of format {category:"string"} (assuming category is a string).
So that clearly can't be what you want, since you want totals, so let's look at what reduce is returning and work out from that what you should be emitting.
It looks like at the end you want to accumulate a document where there is a keyname for each category and its value is a number representing the number of its occurrences. Something like:
{category_name1:total, category_name2:total}
If that's the case then the correct map function would emit({},{"this.category":1}) in which case your reduce will need to add up the numbers for each key corresponding to a category.
Here is what the map should look like:
map=function (){
category = { };
category[this.category]=1;
emit({},category);
}
And here is the correct corresponding reduce:
reduce=function (key,values) {
var category_count = {};
values.forEach(function(value){
for (cat in value) {
if( !category_count.hasOwnProperty(cat) ) category_count[cat]=0;
category_count[cat] += value[cat];
}
});
return category_count;
}
Note that it satisfies two other requirements for MapReduce - it works correctly if the reduce function is never called (which will be the case if there is only one document in your collection) and it will work correctly if the reduce function gets called multiple times (which is what's happening when you have more than 100 documents).
A more conventional way to do that would be to emit category name as key and the number as value. This simplifies map and reduce:
map=function() {
emit(this.category, 1);
}
reduce=function(key,values) {
var count=0;
values.forEach(function(val) {
count+=val;
}
return count;
}
This will sum the number of times each category appears. This also satisfies requirements for MapReduce - it works correctly if the reduce function is never called (which will be the case for any category that only appears once) and it will work correctly if the reduce function gets called multiple times (which will happen if any category appears more than 100 times).
As others pointed out, aggregation framework makes the same exercise much simpler with:
db.collection.aggregate({$group:{_id:"$category",count:{$sum:1}}})
although that matches the format of the second mapReduce I showed, and not the original format that you had which is outputting category names as keys. However aggregation framework will always be significantly faster than MapReduce.
I agree with Neil Lunn's comment.
What I can see from the info that is provided is that if you are on a version of MongoDB greater or equal than 2.2 you can use the aggregation framework instead of map-reduce.
db.items.aggregate([
{ $group: { _id: '$category', category_count: { $sum: 1 } }
])
Which is a lot simpler and performant (see Map/Reduce vs. Aggregation Framework )