[Programming Language]/[SwiftUI]

[SwiftUI] Non-constant range: argument must be an integer literal

Semincolon 2024. 11. 1. 09:19

[머릿말]

임시로 뷰를 그려보기 위해 10가지 정도의 데이터를 담은 Array를 만들고 이를 ForEach를 사용하여 하나씩 나타나게 하는 과정에서

이 경고를 접하게 되었다. ForEach(0..<데이터.count) { ... } 이것처럼 범위에 count 또는 indices 를 사용한 사람들은 이 경고를

한 번쯤은 보았을 것 같다.


1. 문제 상황

위 코드처럼 ForEach문에서 범위에 count 또는 indices를 사용했을 때 이 경고를 접하게 되었다. 변하지 않는 값인 상수 범위가 사용되지 않았기 때문이다. count, indices데이터의 추가 또는 삭제가 발생함에 따라 충분히 변할 수 있는 값이기에 문제가 되는 것이다.

 

2. 이 Warning을 무시했을 때 문제가 발생하는 경우

이는 어디까지나 Warning이기에 일단 뷰는 잘 나타난다. 그러나 Warning특정 상황에서 예기치 않는 결과를 불러올 수 있다.

 

이를 테스트해보기 위해 뷰를 그리고 나서 버튼을 눌러 배열의 첫 번째 데이터를 삭제해보도록 하겠다.

import SwiftUI

struct Student: Identifiable {
    var id: Int
    var name: String
    var age: Int
}

struct ContentView: View {
    
    @State private var students: [Student] = [
        .init(id: 1, name: "John", age: 21),
        .init(id: 2, name: "Jane", age: 22),
        .init(id: 3, name: "Jack", age: 23),
        .init(id: 4, name: "Jill", age: 24),
        .init(id: 5, name: "Jone", age: 25)
    ]
    
    @State private var colorStrings: [String] = [
        "red", "blue", "green"
    ]
    
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            ForEach(0..<students.count) { i in
                HStack {
                    Image(systemName: "person.crop.circle.fill")
                    Text(students[i].name)
                    Text("\(students[i].age)")
                }
            }
            
            ForEach(colorStrings.indices) { c in
                Text(colorStrings[c])
            }
            
            Button("첫 번째 값 삭제") {  ❗️데이터 삭제 버튼
                students.removeFirst()
                colorStrings.removeFirst()
            }
        }
    }
    
}

위 예제를 통해 데이터의 삭제가 일어나니 앱에 충돌이 발생하여 종료되는 것을 확인할 수 있다. 이러한 문제가 발생하는 이유는 뭘까?

 

"데이터의 변경이 이뤄져도 ForEach에서 count, indices는 재계산되지 않는다."

  • 삭제 전 studentscolorStrings의 개수는 각각 5개, 3개였지만 삭제 후에는 4개, 2개가 되었다.
  • 그러나 ForEach에서는 countindices재계산되지 않는다. 즉, 처음의 값이 그대로 사용된다.
  • ForEach에서 접근하는 데이터의 개수가 삭제 후의 개수보다 1개 더 많으므로 Index out of range가 발생하여 앱이 종료된다.

 

3. 해결 방법

Xcode에서 ForEach의 정의를 보면 위 내용을 확인할 수 있다.

  • Instance는 오직 제공된 `data`의 초깃값만을 읽는다.
  • 업데이트 전반에 걸쳐 View를 식별할 필요가 없다(최초 1번 그리고 나면 값이 변해도 영향을 받지 않는다는 말인 것 같음).
  • 동적으로 변하는 값에 따라 View를 다시 그리고 싶으면 `ForEach/init(_:id:content:)`를 사용해라.

 

위 내용에 따르면 View를 식별하기 위한 id를 추가해주면 되는 것이다! 이에 근거하여 앞서 다룬 코드를 다음과 같이 수정해보았다.

...
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            ForEach(0..<students.count, id: \.self) { i in // ❗️id: \.self 추가❗️
                HStack {
                    Image(systemName: "person.crop.circle.fill")
                    Text(students[i].name)
                    Text("\(students[i].age)")
                }
            }
            
            ForEach(colorStrings.indices, id: \.self) { c in // ❗️id: \.self 추가❗️
                Text(colorStrings[c])
            }
            
            Button("첫 번째 값 삭제") {
                students.removeFirst()
                colorStrings.removeFirst()
            }
        }
    }
...

View를 식별하기 위한 id로 자기 자신을 사용한 것이다.

 

이제 데이터를 삭제해도 앱이 종료되지 않는다!

 


이런 Warning이나 Error에 대한 내용을 찾을 때마다 느끼는 거지만 StackoverFlow와 같은 외국 커뮤니티 사이트에는 정말 똑똑한 사람들이 많은 것 같다... 영어의 중요성을 간과해서는 안되겠다는 생각이 든다...

 

끝!


참고 : https://www.hackingwithswift.com/forums/swiftui/compiler-warning-non-constant-range-argument-must-be-an-integer-literal/14878