How for `SwiftUI.ForEach.init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)` compiler is able to warn if `Range` is not constant?

Since Xcode Version 13.3 beta 2 (13E5095k), I'm getting this warning:

ForEach(0..<someInt) { index in  // ⚠️ Non-constant range: not an integer range
   ...
}

I think it's only starting to show this warning with this latest Xcode beta. Looking at the header "source", the type is just Rang<Int>, seem it should just accept any Range<Int>, but it's now showing this warning.

How does this warning raise?

extension ForEach where Data == Range<Int>, ID == Int, Content : View {

    /// Creates an instance that computes views on demand over a given constant
    /// range.
    ///
    /// The instance only reads the initial value of the provided `data` and
    /// doesn't need to identify views across updates. To compute views on
    /// demand over a dynamic range, use ``ForEach/init(_:id:content:)``.
    ///
    /// - Parameters:
    ///   - data: A constant range.
    ///   - content: The view builder that creates views dynamically.
    public init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}
2 Likes

That version of ForEach.init has the undocumented annotation @_semantics("swiftui.requires_constant_range"). Xcode doesn't show it in the “generated interface”, but you can find it in the .swiftinterface file.

/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64e-apple-macos.swiftinterface
4 Likes

I see. Thanks!

So this annotation is at the function level. Shouldn't it be on the parameter? What if you have another Range<Int> parameter that can be non-constant?

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension SwiftUI.ForEach where Data == Swift.Range<Swift.Int>, ID == Swift.Int, Content : SwiftUI.View {
  @_semantics("swiftui.requires_constant_range") public init(_ data: Swift.Range<Swift.Int>, @SwiftUI.ViewBuilder content: @escaping (Swift.Int) -> Content)
}

I'm just curious why it's this way. I doesn't really matter anyways since this annotation is not for public use.

My guess is it's easier to implement this way, so it's just quick & dirty hack.

1 Like

I'm getting this warning with Xcode 13.3

let kLoops = 6

and then in a view:

ForEach(0..<kLoops) {

And getting: Non-constant range: argument must be an integer literal

Am I doing anything wrong? Or is there a way to suppress this warning?

Change to:

ForEach(0..<kLoops, id: \.self) {
…
}

Or do what the warning tells you: give it a constant range:

ForEach(0..<6) {
…
}
3 Likes

Thanks!

Am I silly in thinking that using a let to create a range isn't a constant range?

A let variable isn't a compile time constant. Here is a great explanation (it takes global variable as an example):

Global variables are semantically always lazily initialized: every access does an atomic operation (implemented with something like pthread_once ) to check if the global is initialized. This is silly for something like let foobarCount = 42 . To offset this, the Swift optimizer has heuristics for promoting initializers to constants. These heuristics are currently very simple and they are also opaque to the developer. With constant expressions, we could chose to provide a model like @constexpr let foobarCount = 42 which reliably folds to a constant or produces an error if the initializer is not a constant expression. This would also allow later uses of foobarCount to be treated as a compile-time constant.

Thanks.

Even though a let may not be the same as a literal, its value can never change so it's constant, so the range created from either a let or int literal when pass into ForEach.init(_:) are both the same constant range. So I think @_semantics("swiftui.requires_constant_range") should not issue warning either ways.

I'm getting this warning with Xcode 13.4 for the following code:

warning: non-constant range: not an integer range

ForEach(Range(1...130)) { id in 
}

How is Range(1...130) not constant? btw this is in a SwiftUI preview, just to get a bunch of Texts in a scroll view.

The compiler is unable to determine at the relevant stage of compilation that Range.init doesn’t have side effects and gives the same result each time it’s called. It’s just an ordinary initializer.

There’s no reason to insist on writing Range(1...130) because you and I both know it’s 1..<131, even if the compiler isn’t sure.

2 Likes