UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Compiler warning - Non-constant range: argument must be an integer literal

Forums > SwiftUI

With the update to Xcdoe 13.4 previous projects that used the following code:

ForEach(0..<viewModel.recipes.count) { index in

Now throws the warning: Non-constant range: argument must be an integer literal

It can be fixed by adding id: \.self so that the code is now

ForEach(0..<viewModel.recipes.count, id: \.self) { index in

Can anyone throw some light on why id: \.self appears to be necessary now or was the previous code dead wrong in the first place?

9      

It's always been that way. When you use a range in a ForEach like that, it is read initially and never updated, so if you add/remove items from your array (viewModel.recipes in this case), you can run into an index out of range error that will crash your app.

So instead of using this initializer:

init(
    _ data: Range<Int>,
    content: @escaping (Int) -> Content
)

you should use this initializer:

init(
    _ data: Data,
    id: KeyPath<Data.Element, ID>,
    content: @escaping (Data.Element) -> Content
)

which is "Available when Data conforms to RandomAccessCollection, ID conforms to Hashable, and Content conforms to View." Range conforms to RandomAccessCollection, which is why it will work here.

There was previously no warning about this so you would just crash unexpectedly if you were unaware of the above, but I guess Xcode 13.4 now supplies a warning to make you aware of the issue.

8      

This warning also comes up in the Cupcake Corner project in ContentViews ForEach when looping over the Order.types.indices. Adding the id: .self clears the error, but it did not seem to be an issue in Pauls code.

5      

Seeing this issue with Challenge 1: Converter, Advanced project about 34 minutes in for

Picker("Conversion", selection: $selectedUnits) { ForEach(0..<conversions.count) { Text(conversions[$0]) } }

3      

Same as tomtubbs. The following throws the warning:

ForEach(0..<conversions.count) {
  Text(conversions[$0])
}

So I have to use a defined count instead:

ForEach(0..<4) {
  Text(conversions[$0]
}

Using conversions.count throws the warning and won't calculate properly even though it builds.

3      

You can use this instead:

ForEach(0..<conversions.count, id: \.self) {
    Text(conversions[$0])
}

But is conversions an array of String? If so, you should do this:

ForEach(conversions, id: \.self) {
    Text($0)
}

Basically, you should always try to use that kind of ForEach (supplying either a collection whose elements conform to Identifiable or specifying which property should be used as the id) rather than supplying a range for it to loop through. Sometimes it can't be helped, but the ForEach that loops through a collection directly rather than by referencing the collection's count is a better approach.

4      

So awesome. Your first solution worked perfectly, @roosterboy. Thank you very much, friend. 🙌🏻

3      

@roosterboy using the alternative initialiser with the id parameter as \.self does not stop an Index out of range error . The index can still end up out of the range if something is removed from the collection during the iteration - so I am not sure why you are claiming that that is the reason to use that initialiser instead.

Data is the Range which still doesn't update - so it leaves the original question open really... The simple initialiser without the id must either require that the Range be a constant for a different reason...

3      

@roosterboy using the alternative initialiser with the id parameter as \.self does not stop an Index out of range error . The index can still end up out of the range if something is removed from the collection during the iteration - so I am not sure why you are claiming that that is the reason to use that initialiser instead.

Because what I said is true.

Data is the Range which still doesn't update

Data does update in that second form of ForEach. That's why Apple describes that parameter as "The data that the ForEach instance uses to create views dynamically." The key word there is dynamically, meaning that it updates as the source data changes.

This ForEach will crash with an index out of range error as soon as the count drops below its original value:

ForEach(0..<kids.count) {
    Text(kids[$0])
}

That's because the range is read in once when the ForEach is first constructed and then never again. So if you have, say, 6 elements initially, as soon as you remove 1 element, your code will crash when the ForEach tries to iterate over that last index.

(You'll also get a warning in the debugger telling you that the current count doesn't equal the initial count when you add elements but you won't get an index out of range error.)

But this form of ForEach will not crash, even if you take the element count all the way down to 0:

ForEach(0..<kids.count, id: \.self) {
    Text(kids[$0])
}

Because SwiftUI creates Views dynamically with this ForEach, so it reads the source data each time and thus always knows the current element count and won't try to iterate over a no-longer-existent index. If the element count gets to 0, the ForEach just won't do anything, so no crash there either.

Here's an example to prove it:

import SwiftUI

struct TwoForEaches: View {
    @State private var kids = [
        "Charlotte Grote",
        "Shauna Wickle",
        "Mildred Haversham",
        "Linton Baxter",
        "Jack Finch",
        "Sonny Craven"
    ]
    @State private var spares = [
        "Claire Little",
        "Shelley Winters",
        "Amy Beckwith-Chilton",
        "Ryan Beckwith",
        "Tim Jones",
        "Sarah Grote",
    ]

    var body: some View {
        VStack {
            HStack {
                Spacer()
                Group {
                    Button {
                        if let idx = kids.indices.randomElement() {
                            let kid = kids.remove(at: idx)
                            spares.insert(kid, at: spares.endIndex)
                        }
                    } label: {
                        Image(systemName: "minus.square.fill")
                            .foregroundColor(.red)
                    }
                    Text("\(kids.count)")
                    Button {
                        if let idx = spares.indices.randomElement() {
                            let kid = spares.remove(at: idx)
                            kids.insert(kid, at: kids.endIndex)
                        }
                    } label: {
                        Image(systemName: "plus.square.fill")
                            .foregroundColor(.green)
                    }
                }
                Spacer()
            }

            List {
                //using a static range
                //this will immediately crash when you drop an element
                ForEach(0..<kids.count) {
                    Text(kids[$0])
                }

                //using a dynamic range
                //this will not crash
//                ForEach(0..<kids.count, id: \.self) {
//                    Text(kids[$0])
//                }
            }
        }
    }
}

You can comment/uncomment each of the ForEaches in the List to see for yourself.

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.