Swiftui: How to Iterate Over an Array of Bindable Objects

SwiftUI ForEach with array or sub-array

You don’t have to use $ sign in front of your arrays.

ForEach(isExpanded ? items[..<items.count] : items[..<4], id: \.self) { sub in

}

@Binding and ForEach in SwiftUI

You can use something like the code below. Note that you will get a deprecated warning, but to address that, check this other answer: https://stackoverflow.com/a/57333200/7786555

import SwiftUI

struct ContentView: View {
@State private var boolArr = [false, false, true, true, false]

var body: some View {
List {
ForEach(boolArr.indices) { idx in
Toggle(isOn: self.$boolArr[idx]) {
Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
}
}
}
}
}

In SwiftUI, how to iterate over array that is and attribute of a custom object

you mean like this?

import SwiftUI

struct Role : Identifiable, Hashable {
var id = UUID().uuidString

var roleName: String
var roleActions: [String]
}

struct RowView : View {

var role : Role

var body: some View {

ScrollView(.horizontal, content: {
HStack {
ForEach(role.roleActions, id: \.self) { action in
Text(action)
}
}
})
}
}

struct ContentView: View {

var roles = [Role(roleName: "Witch", roleActions: ["Action 1","Action 2"]),
Role(roleName: "Hero", roleActions: ["Action 3", "Action4"])]

var body: some View {

List (roles, id: \.self) { role in
Text(role.roleName)
.font(.title).fontWeight(.heavy)

VStack() {
Text("Choose Your Actions")
.font(.headline)

RowView(role: role)

}
}.environment(\.defaultMinListRowHeight, 140)
}
}

Using ForEach with a an array of Bindings (SwiftUI)

Trying a different approach. The FormField maintains it's own internal state and publishes (via completion) when its text is committed:

struct FormField : View {
@State private var output: String = ""
let viewModel: FormFieldViewModel
var didUpdateText: (String) -> ()

var body: some View {
VStack {
TextField($output, placeholder: Text(viewModel.placeholder), onCommit: {
self.didUpdateText(self.output)
})

Line(color: Color.lightGray)
}.padding()
}
}
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: vm) { (output) in
vm.output = output
}
}

Using a protocol array with ForEach and bindable syntax

This is a thorny problem with your data types.

If you can change your data types, you can make this easier to solve.

For example, maybe you can model your data like this instead, using an enum instead of a protocol to represent the variants:

struct Testable {
let id: UUID
var name: String
var variant: Variant

enum Variant {
case animal(Animal)
case human(Human)
}

struct Animal {
var owner: String
}

struct Human {
var age: Int
}
}

It will also help to add accessors for the two variants' associated data:

extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}

var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}

Then you can write your view like this:

class ContentViewModel: ObservableObject {
@Published var testables: [Testable] = []
}

struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: \.id) { $testable in
VStack {
TextField("Name", text: $testable.name)

if let human = Binding($testable.human) {
Stepper("Age: \(human.wrappedValue.age)", value: human.age)
}

else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}

HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}

SwiftUI - Using an Array of Ints that is wrapped with @Binding

How about trying something like this example code,
using .onAppear{...} for updating each episode with the showTitle.count.
Also removing this "appending" from analyzeShowTitle function. This append(...) should not be used within the ForEach loop, because it triggers a view refresh,
which then triggers another append(...) etc...

 struct PlayButton: View {
// input API
@Binding var isPlaying: Bool
var episodes: [Episode]

// output API
@Binding var charactersInShowTitle: [Int]

var body: some View {
Button(action: {
isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
ForEach(episodes) { episode in
Text("CHILD: \(analyzeShowTitle(episode))")
}
.onAppear { // <-- here
for episode in episodes {
charactersInShowTitle.append(episode.showTitle.count)
}
}
}

func analyzeShowTitle( _ episode: Episode ) -> String {
return "\( episode.showTitle ) - \( episode.showTitle.count ) chars" // <-- here
}
}

Iterating over array of Codable in SwiftUI

Your code works fine the way it is for printing to the console, but ForEach requires that GeoResult conforms to either Identifiable (preferred) or at least Hashable. Given that you didn't include the property id in your code, let's have that struct conforming to Hashable.

So, assuming that each GeoResult is different because formatted_address is never the same (you must check if that's true), you can add two functions to ensure conformance. You will get the following:

struct GeoResult: Codable, Hashable {    // <- Conform to Hashable

// Differentiating
static func == (lhs: GeoResult, rhs: GeoResult) -> Bool {
lhs.formatted_address == rhs.formatted_address
}

// Hashing
func hash(into hasher: inout Hasher) {
hasher.combine(formatted_address)
}


struct Geometry: Codable {

struct Location: Codable {

let lat: Float
let lng: Float

init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}

In the view, add an array of GeoResult, that will be the @State variable to iterate over. Place the .task() modifier on the outermost view.

    // This is the list
@State private var geoArray: [GeoResult] = []

var body: some View {
NavigationView {
VStack {

// GeoResult is not Identifiable, so it is necessary to include id: \.self
ForEach(geoArray, id: \.self) { result in
NavigationLink {
Text("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
} label: {
Text("Address: \(result.formatted_address)")
}
}
.navigationTitle("Quotes")
}
}

// Attach the task to the outermost view, in this case the NavigationView
.task {
await handleData()
}
}

Finally, change the @State variable in your function, after decoding:

    func handleData() async {

// ...

let decoder = JSONDecoder()
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)

// Add this
geoArray = obj.results
} catch {
print("Did not work :(\n\(error)")
}
}

How to bind an array and List if the array is a member of ObservableObject?

The fix

Change your ForEach block to

ForEach(model.results, id: \.self) { text in
Text(text)
}

Explanation

SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text) to Text(text as String) and remove the $ before model.results), is "Generic parameter 'ID' could not be inferred".

In other words, to use ForEach, the elements that you are iterating over need to be uniquely identified in one of two ways.

  1. If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property var id: Hashable. You don't need the id parameter in this case.
  2. The other option is to specifically tell ForEach what to use as a unique identifier using the id parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.

In this case, we chose option 2 and told ForEach to use the String element itself as the identifier (\.self). We can do this since String conforms to the Hashable protocol.

What about the $?

Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:

  • A Toggle needs to update a Bool value in response to a switch
  • A Slider needs to update a Double value in response to a slide
  • A TextField needs to update a String value in response to typing

The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>. So a Toggle requires you to pass it a Binding<Bool>, a Slider requires a Binding<Double>, and a TextField requires a Binding<String>.

This is where the @State property wrapper (or @Published inside of an @ObservedObject) come in. That property wrapper "wraps" the value it contains in a Binding (along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable, but if we need the binding, we can use the shorthand $myVariable.

So, in this case, your original code contained ForEach($model.results). In other words, you were telling the compiler, "Iterate over this Binding<[String]>", but Binding is not a collection you can iterate over. Removing the $ says, "Iterate over this [String]," and Array is a collection you can iterate over.

Binding in a ForEach in SwiftUI

Assuming your elements is state of items array, it can be

List {
ForEach(elements.indices, id: \.self) { i in
CheckBoxView(checked: $elements[i].checked)
}
}


Related Topics



Leave a reply



Submit