0

I have an empty array of structures. I want to initialize it before I try to display it. I probably did something stupid. In my view I put an init. I ran the debugger and stopped at the append. Down in the debug area it said I had 0 values. That did not change and later when I went to display the data I got the fatal error that the index was out of range. I know when I created the array the 0 element was out of range, but I went through the init code in the, apparently fruitless, hope it would get initialized before it ran.

On another note. I am probably doing it wrong and would not be adverse to changing my code. What I want is an array with 6 elements created when I start the application. The structure that makes up the array does not require an initializer.

The array is a State variable because I am going to change the values for elements in it and when I change them I want a redraw. I am not going to change the number of elements, just data in the existing elements.

Edit: I seem to remember that I can put stuff in the app file that will be available to all. I would not have a problem moving my data there, but it seemed simpler to have it in the view that used it.

Another edit: I changed the definition of the array to create the elements and it apparently worked. That's fine, but it still puzzles me why init did not work. Probably going to run across the iagain.

struct Player {
  var name = ""
}
struct ContentView: View {
  @State private var players: [Player] = []
  init (){
    for _ in 0...5 {
      players.append(Player())
    }
  }
  
  var body: some View {
    HStack {
      TextField ("Player 1", text: $players[0].name)
    }
  }
}
Peter
  • 83
  • 8
  • Use a `ForEach` instead of hardcoding the index. Watch "Demystify SwiftUI" – lorem ipsum Jul 06 '23 at 21:55
  • What if I want just element 2? My problem with Swift is probably that it tries to protect me from myself, I want to do things and if I hurt myself it's my problem. I don't want all the elements in the array I just want 0,1,2,3,4,5 if there is a 6th I don't care. – Peter Jul 08 '23 at 01:35
  • You have to check if the index exists and when it does show what you want – lorem ipsum Jul 08 '23 at 09:05

3 Answers3

1

Never use index subscripting in the body of a SwiftUI view on an empty array because any init method is called after the view in rendered the first time.

A safe swifty alternative to initialize the array is the init(repeating:count:) API

struct ContentView: View {
  @State private var players = Array(repeating: Player(), count: 6)
  
  var body: some View {
  ...
vadian
  • 274,689
  • 30
  • 353
  • 361
0

Init still works, but it doesn't behave as expected because in SwiftUI, the body property of a View is evaluated immediately. Therefore, attempting to access an element from an empty array will result in an 'Index out of range' error.
To address this issue based on your code, you can make a slight modification as follows:

Direct initialization:

@State private var players: [Player] = {
    var array = [Player]()
    for _ in 0...5 {
        array.append(Player())
    }
    return array
}()

or:

@State private var players = Array(repeating: Player(), count: 6)

Indirect initialization:

@State private var players: [Player]

init() {
    var array = [Player]()
    for _ in 0...5 {
        array.append(Player())
    }
    _players = State(wrappedValue: array)
}

or

@State private var players: [Player]

init() {
    _players = State(initialValue: Array(repeating: Player(), count: 6))
}
baohoang
  • 616
  • 1
  • 5
  • 13
  • I am confused. I ran my code with the debugger with breaks in the init and the TextField. The breaks in the init happened and then the break at the TextField. I did not get the error until I let the break at the TextField go. Does it evaluate the body, without popping the debugger, and save the error it got then continue on to the init, where the debugger pops, then on to the TextField, again popping the debugger, then give the error from before? – Peter Jul 08 '23 at 01:11
  • so, the init does NOT run before things happen? Is there some other way for me to run some code before things happen? Let's say I had a view with an array that I knew nothing about at compile time. When the view was about to display I had information and could fill the array. How would I handle that? Build the array with dummy data, so it was available immediately, and at init delete and rebuilt it? – Peter Jul 08 '23 at 01:26
  • Could I define the array with dummy data, avoiding the out of range error, and then in init delete all the members and build new ones? – Peter Jul 08 '23 at 01:30
  • In my opinion, you shouldn't define the array with dummy data, to avoid the out-of-range error. Instead, you should utilize indirect initialization, where you don't need to delete all the members in the init function and rebuild new ones. – baohoang Jul 08 '23 at 13:39
0

The error appears to be that I can not update @State variables in the init. I pointed out in the original post that the count of entries, in the debugger, did not change as I added the entries. I removed the @State, and changed TextField to Text, and it worked like a champ. I had another test where I created the array with entries and in the init I tried to change one of them. The code ran with no errors, but the change did not show up. I removed the @State and it ran fine.

I found this on stack overflow

Peter
  • 83
  • 8
  • Thank you for the link that you shared, and the answer with a high score is precisely the reason for the error you encountered. – baohoang Jul 08 '23 at 13:57