Swift Accidental Infinite ForEach Loop - arrays

I'm trying to use a foreach loop to show multiple Elements in an [[String]] with the help of an incremented Index in a list. But when I want to generate the list the loop repeats infinity regardless of the specified loop-count.
This is my function that gets called in the loop, to return the wanted Array of Strings:
func getPlaces (PlaceNumber: Int) -> [String]{
if allPlaces.count - 1 >= PlaceNumber{
return allPlaces[PlaceNumber]
} else{
return ["Not found",
"Not found",
"https://www.wikipedia.com",
"Not found",
"Not found"]
}
}
The Variable 'allPlaces' is an [[String]] and has 25 String-Arrays in it (0-24):
let allPlaces: [[String]] =
[["no sight",
"No information",
"https://www.wikipedia.com",
"No information",
"No information"],
["Tower of London",
"The Tower of London, officially Her Majesty's Royal Palace and Fortress of the Tower of London, is a historic castle on the north bank of the River Thames in central London. It lies within the London Borough of Tower Hamlets, which is separated from the eastern edge of the square mile of the City of London by the open space known as Tower Hill. It was founded towards the end of 1066 as part of the Norman Conquest. The White Tower, which gives the entire castle its name, was built by William the Conqueror in 1078 and was a resented symbol of oppression, inflicted upon London by the new ruling elite. The castle was also used as a prison from 1100 until 1952, although that was not its primary purpose. A grand palace early in its history, it served as a royal residence.",
"https://en.wikipedia.org/wiki/London_Bridge",
"1066",
"4"],
usw...
This is my view and the function to increment (I use the index to access the different Elements in the [[String]]). I thought that the loop should trigger just as often as the 'places.allPlaces'- Array count is. But it triggers infinitely. Even when I use '0...24' instead of "places.allPlaces'.
struct ListOfPlacesView: View {
#StateObject var location = Location()
#StateObject var places = Places()
#State var index = 0
var body: some View {
NavigationView{
List{
ForEach (places.allPlaces, id: \.self) { _ in
Text(incrementIndex())
}
}
}
.navigationTitle("All Places")
}
func incrementIndex() -> String{
index += 1
print(index)
return (places.getPlaces(PlaceNumber: index)[0])
}
}
But when I start this, the 'print(index)' counts to infinity in the console and the view doesn't even load. When I delete the 'index += 1' the loop prints the first Element of the Array 25 times, like it should.
I don't want to include the first Element of the [[String]] in the list, that's why im starting at an index of 0 and increment first.
Do you have an idea why this occurs and how to stop the app from doing that? Or know a better way to increment?
Sorry if this is described badly, ask if you can't figure out something.

Your incrementIndex() function updates the variable #State var index. This will cause the view to re-render and in turn call the incrementIndex() again. This causes the infinite loop.
Currently, you are not using the function parameter ForEach supplies, but instead discard it by naming it _. Instead of an index, I'd suggest using the value ForEach already supplies:
ForEach(places.allPlaces, id: \.self) { place in
Text(place[0])
}

Related

how do I get rid of this simple SwiftUI error?

I just wanna get all the records from the transaction variable and save it to an array.i tried and all I am getting is this constant error. please help me, I just wanna all models(records) to be saved on an array.
Type '()' cannot conform to 'View'
#State private var transactions: [Transactions] = [Transactions]()
ForEach(transactions, id: \.self) { transaction in
timexxx[0] = transaction.timexx ?? "0"
Text(timexxx[0] ?? "0")
}
enter image description here
Like what #multiverse has suggested
ForEach loop expects some sort of View but you are giving it or attempting to give an "array" (it only wants View)
Here is an updated code where you give the ForEach what it wants and you append to your timexxx array
ForEach(Array(transactions.enumerated()), id: \.offset) { (offset, transaction) in
Text(transaction.timexx ?? "\(offset)")
.onAppear {
timexxx[offset] = transaction.timexx ?? "\(offset)"
}
}
Update
for your question
"how do I do this with a simple "For" loop ? let's say I wanna do this operation in a simple class."
This is how it's done.
I removed the view Text.
for (i, transaction) in transactions.enumerated() {
timexxx[i] = transaction.timexx ?? "0"
}
Ok , so this is an error i faced as well, when i was learning SwiftUI( i am still a beginner), so now we need to understand , what does this error actually means, in this case the ForEach loop expects some sort of View but you are giving it or attempting to give an "array" (it only wants View)....
If you want values to be transferred to an array just simply create a function and do it .....
say you have a class and inside of which you do
#Published var song = [Song]()
then what you do is inside a function like loadData()
objects is the array whose elements you want transferred and most likely those elements belong to a Struct like Song here(if not its even simpler just use what ever type it has like Int, String etc.), this way all your elements will get transferred to song from objects
func loadData() {
song = objects.map {
artist in
Song(album: artist.album, artistImage: artist.artistImage)
}
}
Here i add the simplest possible way to transfer from one array to other
var objects = [1,2,3,4,5]
var song = [Int]()
func loadData() {
song = objects.map { $0 }
}
loadData()
print(song)
//[1,2,3,4,5]

Cant figure out how to go through array in Swift and find specific data

Hello I have question about arrays.
I have an array with following data, also I have corresponding Struct for SpiritRelation():
var spiritRelations = [
SpiritRelation(relationName: "Thunder Lantern", relationSpirit1: "Razor", relationSpirit2: "Lamp Genie", relationSpirit3: "", relationSpirit4: "", relationStats: "Double resist +5%, ATK +1600", relationSpiritIcons: ["razor", "genie"]),
SpiritRelation(relationName: "Illusive Fantasy", relationSpirit1: "Heavenly Maiden", relationSpirit2: "Lamp Genie", relationSpirit3: "", relationSpirit4: "", relationStats: "Excellent strike +15%, Dmg Penetration +15%, Max HP +11500", relationSpiritIcons: ["maiden", "genie"]),
SpiritRelation(relationName: "Grand Demonlord Gathering", relationSpirit1: "Sand Golem", relationSpirit2: "Lamp Genie", relationSpirit3: "", relationSpirit4: "", relationStats: "Excellency Resist +20%, Double Dmg +5%, ATK +1600", relationSpiritIcons: ["golem", "genie"])
}
array which contains data which will be selected by user:
var selectedSpiritsForRelation = [String]()
array of type String because I put there values which corresponds to image names in Assets. I need that to display images
array where I want to keep found relations and use it to display all found relationStats in UI
var foundRelations = [SpiritRelation]()
My problems is:
lets say user has selected 2 spirits for example: selectedSpiritsForRelation["golem", "genie"]
I’m able to find and save correctly found relation by
let result3 = spiritRelations.filter{$0.relationSpiritIcons == (selectedSpiritsForRelation) } // = 3rd relation in spiritRelations[]
foundRelations.append(contentsOf: result3)
but after user select another one spirit and array become: selectedSpiritsForRelation["golem", "genie", "maiden"]
same code as for result3 does not work anymore, because how I understand it tries to filter exactly combination of 3, but my expectation is that 2nd relation from spiritRelation[] will be found also
and here is my problem, I cant figure out how to correctly go through spiritRelations[] and find all relations related to selectedSpiritsForRelation[] every time user selects new spirit
You need to use allSatisfy in your filter by checking that all relationSpiritIcons elements exists in selectedSpiritsForRelation
foundRelations = spiritRelations.filter {
$0.relationSpiritIcons.allSatisfy { icon in
selectedSpiritsForRelation.contains(icon)
}
}

Generate random Strings without a shuffle and repetition of previous ones?

My code already generates a random String of an array when I press a button, but sometimes a String gets repeated. What do I have to do so that the String "Mango" only gets called again when all the other Strings where already called without using a shuffle, I want to call one String at a time?
Example: "Mango", "Kiwi", "Banana", "Pineapple", "Melon", "Mango", "Kiwi",.....
Here is my code:
var array = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
let fruits = Int(arc4random_uniform(UInt32(array.count)))
print(array[fruits])
In order to avoid repetitions you need to keep track of which fruits have previously been seen. There are several ways to do that an all of the proposed solutions do it in one way or another.
For your specific use case, you will need this tracking to be retained outside of the code executed by the button (in your view controller for example).
Here is a generalized structure that could help with this:
(you can define it inside the view controller if this is a one-time thing or outside of it if you intend to reuse the mechanism elsewhere)
struct RandomItems
{
var items : [String]
var seen = 0
init(_ items:[String])
{ self.items = items }
mutating func next() -> String
{
let index = Int(arc4random_uniform(UInt32(items.count - seen)))
let item = items.remove(at:index)
items.append(item)
seen = (seen + 1) % items.count
return item
}
}
To use it, you would declare a variable (in your VC) that will keep track of your fruits:
var fruits = RandomItems(["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"])
And in the button's code use that variable (fruits) to print a non-repeating fruit name at each execution
func buttonPressed() // <- use your function here
{
print( fruits.next() )
}
You need to implement some logic. It's quite easy if you think harder. Run this in your Playground, or if you fully understand this block of code, you can do this in your project already.
var array = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
var selectedIndices = [Int]()
for _ in 1...20 {
let randomFruitIndex = Int(arc4random_uniform(UInt32(array.count)))
// Print only if not yet printed once
if !selectedIndices.contains(randomFruitIndex) {
print(array[randomFruitIndex])
selectedIndices.append(randomFruitIndex)
}
// Reset
if selectedIndices.count == array.count {
print("----- CLEARING SELECTED INDICES----")
selectedIndices.removeAll()
}
}
So as you can see, we are adding each generated random number (in your case, it's the fruits variable.) into an array of Int. Then if the number of selectedIndices is equal to the count of the array of fruits, clear all the stored selectedIndices.
OUTPUT:
Pineapple
Melon
Mango
Kiwi
Banana
Apple
----- CLEARING SELECTED INDICES----
Mango
Melon
This is an adaption from the accepted answer of the linked topic in my comment:
var source = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
var usedElements = [String]()
func choosePseudoRandomElement() -> String {
if source.count == 0 {
source = usedElements
usedElements = []
}
let randomIndex = Int(arc4random_uniform(UInt32(source.count)))
let randomItem = source[randomIndex]
usedElements.append(randomItem)
source.remove(at: randomIndex)
return randomItem
}
for _ in 1...18 {
print("Item: \(choosePseudoRandomElement())")
}
One potential issue with this solution is that it may happen, that the last element of one complete iteration also occurs as the first element of the second iteration. You can handle that case by comparing the randomly chosen item with the item which was chosen before (use a while loop until the items doesn't match anymore).
Also, this does remove elements from the source array. If you do not want that, create a copy of the source array.

Horizontal Scroll Array of JSON Objects

Ok so i have the results from the JSON array all lined up nicely. They dont seem to move across the screen though. Is it because my txtX = 0; is still in draw? Ive played around but cant seem to remedy this and as a result, still have static text :-(
var scores;
var txtX;
var txtY;
function preload() {
scores = loadJSON("stats.json");
}
function setup() {
createCanvas(700, 700);
}
function draw() {
background(254);
var txtX = 0;
var txtY = 550;
var stats = scores.results;
for (var i = 0; i < stats.length; i++) {
textSize(12);
text(stats[i], txtX, txtY);
var wordWidth = textWidth(stats[i]);
var currentOffset = 15;
txtX = txtX + wordWidth + currentOffset;
}
txtX = txtX - 1;
}
Here is my JSON:
{
"description" : "FA Cup results; 7th January 2017.",
"source" : "http://www.bbc.co.uk/sport/football/results",
"results":[
"Manchester United 4-0 Reading",
"Accrington Stanley 2-1 Luton Town",
"Barrow 0-2 Rochdale",
"Birmingham City 1-1 Newcastle United",
"Blackpool 0-0 Barnsley",
"Bolton Wanderers 0-0 Crystal Palace",
"Brentford 5-1 Eastleigh",
"Brighton & Hove Albion 2-0 Milton Keynes Dons",
"Bristol City 0-0 Fleetwood Town",
"Everton 1-2 Leicester City",
"Huddersfiled Town 4-0 Port Vale",
"Hull City 2-0 Swansea City",
"Ipswich Town 2-2 Lincoln City",
"Millwall 3-0 Bournemouth",
"Norwich City 2-2 Southampton",
"Queens Park Rangers 1-2 Blackburn Rovers",
"Rotherham United 2-3 Oxford United",
"Stoke City 0-2 Wolverhampton Wanderers",
"Sunderland 0-0 Burnley",
"Sutton United 0-0 AFC Wimbledon",
"Watford 2-0 Burton Albion",
"West Bromwich Albion 1-2 Derby County",
"Wigan Athletic 2-0 Nottingham Forest",
"Wycombe Wanderers 2-1 Stourbridge",
"Preston North End 1-2 Arsenal"
]
}
Any help would be greatly appreciated. Thanks.
There's a lot about your code that doesn't make sense:
Why are you using a nested for loop? Your inner for loop seems to iterate over each word in an individual string. Why not just pass the whole string into the text() function instead of each word one at a time?
Why are you only changing currentOffset after that inner loop completes? You're drawing a whole string but only changing the offset for a single word.
Why are you adding 500 to the currentOffset?
Why don't you ever use the txtX variable?
You need to take a step back and rethink exactly what you're doing. If I were you, I would do the following:
You need to keep track of two variables: the txtX variable at the sketch level that keeps track of the start, which scrolls to the left, and a currentWordX variable that keeps track of the offset of the current string relative to the txtX variable.
You only need a single for loop. Even if your eventual goal requires drawing each word separately, start with a single for loop that iterates over the results array.
For each string in the results array, you draw that string. Its X position is txtX + currentWordX. Then you get that string's width and add that value to the currentWordX variable.
After the for loop, you need to decrease the txtX variable to cause the whole thing to scroll to the left.

Sorted array: how to get position before and after using name? as3

I have been working on a project and Stack Overflow has helped me with a few problems so far, so I am very thankful!
My question is this:
I have an array like this:
var records:Object = {};
var arr:Array = [
records["nh"] = { medinc:66303, statename:"New Hampshire"},
records["ct"] = { medinc:65958, statename:"Connecticut"},
records["nj"] = { medinc:65173, statename:"New Jersey"},
records["md"] = { medinc:64596, statename:"Maryland"},
etc... for all 50 states. And then I have the array sorted reverse numerically (descending) like this:
arr.sortOn("medinc", Array.NUMERIC);
arr.reverse();
Can I call the name of the record (i.e. "nj" for new jersey) and then get the value from the numeric position above and below the record in the array?
Basically, medinc is medium income of US states, and I am trying to show a ranking system... a user would click Texas for example, and it would show the medinc value for Texas, along with the state the ranks one position below and the state that ranks one position above in the array.
Thanks for your help!
If you know the object, you can use the array.indexOf().
var index:int = records.indexOf(records["nj"]);
var above:Object;
var below:Object;
if(index + 1 < records.length){ //make sure your not already at the top
above = records[index+1];
}
if(index > 0){ //make sure your not already at the bottom
below = records[index-1];
}
I think this is the answer based on my understanding of your data.
var index:int = arr.indexOf(records["nh"]);
That will get you the index of the record that was clicked on and then for find the ones below and above just:
var clickedRecord:Object = arr[index]
var higherRecord:Object = arr[index++]
var lowerRecord:Object = arr[index--]
Hope that answers your question
Do you really need records to be hash?
If no, you can simply move key to record field and change records to simple array:
var records: Array = new Array();
records.push({ short: "nh", medinc:66303, statename:"New Hampshire"}),
records.push({ short: "ct", medinc:65958, statename:"Connecticut"}),
....
This gives you opportunity to create class for State, change Array to Vector and make all of this type-safe, what is always good.
If you really need those keys, you can add objects like above (with "short" field) in the same way you are doing it now (maybe using some helper function which will help to avoid typing shortname twice, like addState(records, data) { records[data.short] = data }).
Finally, you can also keep those records in two objects (or an object and an array or whatever you need). This will not be expensive, if you will create state object once and keep references in array/object/vector. It would be nice idea if you need states sorted on different keys often.
This is not really a good way to have your data set up - too much typing (you are repeating "records", "medinc", "statename" over and over again, while you definitely could've avoided it, for example:
var records:Array = [];
var states:Array = ["nh", "ct", "nj" ... ];
var statenames:Array = ["New Hampshire", "Connecticut", "New Jersey" ... ];
var medincs:Array = [66303, 65958, 65173 ... ];
var hash:Object = { };
function addState(state:String, medinc:int, statename:String, hash:Object):Object
{
return hash[state] = { medinc: medinc, statename: statename };
}
for (var i:int; i < 50; i++)
{
records[i] = addState(states[i], medincs[i], statenames[i], hash);
}
While you have done it already the way you did, that's not essential, but this could've saved you some keystrokes, if you haven't...
Now, onto your search problem - first of all, true, it would be worth to sort the array before you search, but if you need to search an array by the value of the parameter it was sorted on, there is a better algorithm for that. That is, if given the data in your example, your specific task was to find out in what state the income is 65958, then, knowing that array is sorted on income you could employ binary search.
Now, for the example with 50 states the difference will not be noticeable, unless you do it some hundreds of thousands times per second, but in general, the binary search would be the way to go.
If the article in Wiki looks too long to read ;) the idea behind the binary search is that at first you guess that the searched value is exactly in the middle of the array - you try that assumption and if you guessed correct, return the index you just found, else - you select the interval containing the searched value (either one half of the array remaining) and do so until you either find the value, or check the same index - which would mean that the value is not found). This reduces asymptotic complexity of the algorithm from O(n) to O(log n).
Now, if your goal was to find the correspondence between the income and the state, but it wasn't important how that scales with other states (i.e. the index in the array is not important), you could have another hash table, where the income would be the key, and the state information object would be the value, using my example above:
function addState(state:String, medinc:int, statename:String,
hash:Object, incomeHash:Object):Object
{
return incomeHash[medinc] =
hash[state] = { medinc: medinc, statename: statename };
}
Then incomeHash[medinc] would give you the state by income in O(1) time.

Resources