Make a grid of `TextField`s in SwiftUI

Code that reproduces the issue

import SwiftUI

@main
struct TextFieldsGridApp: App {
	@State private var controller = Controller()
	
	var body: some Scene {
		WindowGroup {
			GridView()
				.environment(controller)
		}
	}
}

struct GridView: View {
	@Environment(Controller.self) private var c
	
	var body: some View {
		VStack {
			ForEach(0..<c.strings.count, id: \.self) { r in
				HStack {
					ForEach(0..<4, id: \.self) { c in
						TextField(
							"",
							text: c.strings[r][c]
						)
						.textFieldStyle(.roundedBorder)
					}
				}
			}
		}
		.padding()
	}
}

#Preview {
	GridView()
		.environment(Controller())
}

@Observable
class Controller {
	private(set) var strings: [[String]] = Array(
		repeating: Array(repeating: "A", count: 4),
		count: 4,
	)
}

Error

Cannot convert value of type 'Range<Int>' to expected argument type 'Binding<C>', caused by ForEach(0..<4, id: \.self) { c in.

Which I do not get if I say, for example:

struct GridView: View {
  @State private var text = "H"
  
  // ...

  TextField("", text: $text)

Environment

MacBook Air M1 8GB iOS Sequoia 15.5 Xcode 16.4 Preview: iPhone 16 Pro, iOS 18.5.

Answered by BabyJ in 848743022

The issue is this part despite what the error message says:

TextField("", text: c.strings[r][c])


The first quick point is that you are using c here as the environment object and the ForEach closure parameter, so Swift is getting confused and doesn't know which one you are referring to. I will be referencing the environment object property as controller.


The second point is illustrated further by the example you gave.

TextField("", text: $text)

TextField expects to be passed a Binding<String> which is why you prepend the $ symbol to obtain one from a variable.

However in your code, you are passing in controller.strings[r][c] which is just a regular String and not a binding, which is why the error occurs.


To solve this, you need a way of getting a Binding<String> from your current String property. There are two ways of approaching this — pick whichever one you prefer.

  1. Use the Bindable property wrapper which facilitates creating bindings from the mutable properties of observable objects.

    var body: some View {
        @Bindable var controller = controller
    
        TextField("", text: $controller.strings[r][c])
    }
    


  2. Manually create a Binding and pass that to the text field.

    TextField("", text: .init {
        controller.strings[r][c]
    } set: {
        controller.strings[r][c] = $0
    })
    
Accepted Answer

The issue is this part despite what the error message says:

TextField("", text: c.strings[r][c])


The first quick point is that you are using c here as the environment object and the ForEach closure parameter, so Swift is getting confused and doesn't know which one you are referring to. I will be referencing the environment object property as controller.


The second point is illustrated further by the example you gave.

TextField("", text: $text)

TextField expects to be passed a Binding<String> which is why you prepend the $ symbol to obtain one from a variable.

However in your code, you are passing in controller.strings[r][c] which is just a regular String and not a binding, which is why the error occurs.


To solve this, you need a way of getting a Binding<String> from your current String property. There are two ways of approaching this — pick whichever one you prefer.

  1. Use the Bindable property wrapper which facilitates creating bindings from the mutable properties of observable objects.

    var body: some View {
        @Bindable var controller = controller
    
        TextField("", text: $controller.strings[r][c])
    }
    


  2. Manually create a Binding and pass that to the text field.

    TextField("", text: .init {
        controller.strings[r][c]
    } set: {
        controller.strings[r][c] = $0
    })
    
Make a grid of &#96;TextField&#96;s in SwiftUI
 
 
Q