How to increment an integer in a ForEach loop

I have a fairly simple grid layout:

    @State var gridSize = 3
    @State var selected = 0
    @State var aCount = 0

    var body: some View {

        Grid (horizontalSpacing: 0, verticalSpacing: 0) {
            ForEach(0..<gridSize, id: \.self) { row in
                GridRow{
                    ForEach(0..<gridSize, id: \.self) { col in
                        Rectangle()
                            .foregroundColor(.gray)
                            .overlay(Text("\(aCount)"))
                            .border(Color.black)
                            .bold()
                    }
                }
            }
        }
    }

}

This creates a grid of gridSize x gridsize grey rectangles, so 3x3 in this version

and in a class file I have a fairly simple function that increments an int passed to it by 1 and returns that new value:

    let theCounter = theCount + 1
    return theCounter
}

how do I use that function to increment aCount in the inner foreach loop? This does not seem like it should be impossible, and yet...

any help is appreciated

(Background: aCount is needed because I need to be able to have a click action in button 0,0 read the states of say, buttons 0,1 and 0,2 and have another function possibly change the state of all three depending on what is in 0,0, so aCount is used to help me manage an array of buttons.

(i have been told there's easier ways than an array of buttons to do this, but none of the explanations have worked for my level of inexperience, so an array of buttons is something I at least understand.

Replies

The simplest is to compute directly the numer:

struct ContentView: View {
    @State var gridSize = 3
    @State var selected = 0
    // @State var aCount = 0
    
    func cellNum(row: Int, col: Int) -> Int {
        return row * gridSize + col + 1
    }
    
    var body: some View {
        
        Grid (horizontalSpacing: 0, verticalSpacing: 0) {
            ForEach(0..<gridSize, id: \.self) { row in
                GridRow{
                    ForEach(0..<gridSize, id: \.self) { col in
                        Rectangle()
                            .foregroundColor(.gray)
                            .overlay(Text("\(cellNum(row: row, col: col))"))
                            .border(Color.black)
                            .bold()
                    }
                }
            }
        }
    }
    
}

Trying with increment causes problems as you do not control how SwiftUI draws the cells.

To see it, you can try this, which does not work properly

struct ContentView: View {
    @State var gridSize = 3
    @State var selected = 0
    // @State var aCount = 0
    
    func incremented(_ row: Int, _ col: Int, count: inout Int) -> Int {
        count += 1
        print(row, col, count)
        return count
    }
    
    var body: some View {
        
        Grid (horizontalSpacing: 0, verticalSpacing: 0) {
            ForEach(0..<gridSize, id: \.self) { row in
                var count = row * gridSize
                GridRow{
                    ForEach(0..<gridSize, id: \.self) { col in
                        Rectangle()
                            .foregroundColor(.gray)
                            .overlay(Text("\(incremented(row, col, count: &count))"))
                            .border(Color.black)
                            .bold()
                    }
                }
            }
        }
    }
    
}

You get:

0 0 1
0 1 2
0 2 3
1 0 4
1 1 5
1 2 6
2 0 7
2 1 8
2 2 9
2 2 10
2 1 11
2 0 12
1 2 7
1 1 8
1 0 9
0 2 4
0 1 5
0 0 6

Cells are first drawn properly but then redrawn in reverse order…

(Background: aCount is needed because I need to be able to have a click action in button 0,0 read the states of say, buttons 0,1 and 0,2 and have another function possibly change the state of all three depending on what is in 0,0, so aCount is used to help me manage an array of buttons.

Could you explain precisely what you want ?

You should probably use onTapGesture (if I understood properly your intent)

struct ContentView: View {
    @State var gridSize = 3
    @State var selected = 0
    @State var aCount = 0
    
    func cellNum(row: Int, col: Int, offset: Int) -> Int {
        return row * gridSize + col + 1 + offset
    }
    
    var body: some View {
        
        Grid (horizontalSpacing: 0, verticalSpacing: 0) {
            ForEach(0..<gridSize, id: \.self) { row in
                GridRow{
                    ForEach(0..<gridSize, id: \.self) { col in
                        Rectangle()
                            .foregroundColor(.gray)
                            .overlay(Text("\(cellNum(row: row, col: col, offset: aCount))"))
                            .border(Color.black)
                            .bold()
                            .onTapGesture {
                                aCount += 1
                            }
                    }
                }
            }
        }
    }
    
}

This increments all cells by 1. If you want to increment only cells after the clicked one, you would need a State var with the array of cells content. But please first explain precisely what you want.

  • added explanation at bottom

Add a Comment

Hi! I agree with @Claude31 that I need some more detail on what you're trying to achieve here, but if the goal is to have the numbers 0-8 on the cells, you can do .overlay(Text("\(row * gridSize + col)"))

This will multiply the row number from the outer ForEach by the gridSize and count up as you go across with the inner loop due to the addition of col. Col will reach gridSize - 1, and then the outer loop will move to the next row. The cells will continue in the correct order due to the gridSize multiplier on row.

okay, so the idea here is an "SOS" game (current project at https://github.com/johncwelch/SOS-Swift)

basically, it's an over complicated version of tic-tac-toe, where each player attempts to spell SOS by clicking on the buttons.

the buttons have three options:

  1. Blank (starting default)
  2. "S"
  3. "O"

The buttons can also be enabled or disabled depending on a few things:

  1. if they've already been used, then they're not clickable
  2. if they're blank, but another button is being used by a player, they have to be enabled when that other button is blank, but disabled when the other button is "S" or "O" so that only one button can be used in a move.

So on completion of a move, the following has to happen:

  1. If "S", are any of the buttons next to me "O"? If so, is the button in a line with that "O" an "S". If so, SOS, set the button color of all three to match the player color, (red or blue), disable myself, add a point to the player who made SOS, and change the current player to the other player. If not, just disable the button and change the current player to the other player.
  2. if "O", are the buttons on opposite sides of me both "S"? If so, SOS, set the button color of all three to match the player color, (red or blue), disable myself, add a point to the player who made SOS, and change the current player to the other player. If not, just disable the button and change the current player to the other player.

So i need to be able to both read state of other buttons on the playing field and modify the state of other buttons based on what happens in a click on a current button.

There's also a "new game" option, which blows up any current game (if in the middle), sets scores to 0 and sets all buttons to blank + enabled.

Tracking button state is kind of critical here, both for the current title of the button and its enabled/disabled state.

It is (a little) clearer. And it appears that the problem was not displaying the row and col number !

So, you should

  • define an enum CellState with the possible states (Blank, "S", "O").
  • have a State var as an array that tracks the state of each button.
  • implement the logic you described in a func, by testing the other buttons next to it.
func testCell(row: Int, col: Int) -> CellState {
   // compute the new state
}
  • use this func onTapGesture to change the cell State.
  • use the cell state for display
  • @Claude31 okay, so because I'm having such a hard time:

    i have an enum with three states: blank, s, o (myEnum) I then have a @state var myEnumArray = [myEnum]

    in an onTapGesture, I pass the current location and do whatever to the cell state, and return that new cell state

    That I think I get

    what I'm not getting is how to have button A check the state of button D. that's the part that's been running away from me like an ice cream truck driven by a sadist.

Add a Comment

I think a good option here would be to start by creating a class that holds the properties of the cell. For example,

@Observable
class Cell: Identifiable {
    let id = UUID()
    var title: String = ""
    var buttonToggled = false
}

This creates an observable object that knows when your cell title has changed or the button has been toggled.

Then, you can create each individual cell with the background of your choosing that contains the details I highlighted above. Now, each cell will have its own title and toggle state that you can access.

struct GridCell: View {
    @State var details: Cell = Cell()
    
    var body: some View {
        Button {
            details.title = details.buttonToggle ? "s" : "o" //conditionally changes the title of the button from "s" to "o" when tapped
            details.buttonToggled.toggle()
        } label: {
          //your background here
        }
    }
}

Then, you can use your existing code from your post to lay out the cells. I'd add a State variable containing an array of 9 GridCells, allowing your inner ForEach to look like this:

 ForEach(0..<gridSize, id: \.self) { col in
           let index = row*gridSize + col
           gridCellArr[index] //lay out the cell
           if gridCellArr[index].details.title == "s" {
                    //handle case
             }
 }

Since you can access the cell's details from here, you can check if the cells to the left and right are the correct letters using the index. This is just one option, but it may help you go in a good direction

how to have button A check the state of button D

That's the interest of the array that hold the state (or the class approach as of @sha921).

You know row/col of button A

What you test and change is the state of button D (in fact you look for buttons which row/col meet certain conditions relative to row/col of A). To change Button D, you change the State in the array. And because Array is a State var, that will update the View.

Hope tha's clear.