UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How do I bind Toggle to an item in an array with ForEach?

Forums > SwiftUI

Hi all,

Hope you can help me out.

My question is basically this (answered) question on StackOverflow, but the accepted answer does not compile for me.

I've got an array of BookItems, that I want to show in a List. Every item has the option to enable it with a toggle, but I'm having trouble figuring out how to bind the item in the ForEach loop to the actual source.

Hope someone can help me out :)

Jordi

class BookItem: ObservableObject, Identifiable, Codable, Equatable {
    let id = UUID()
    let name: String
    let enabled: Bool
}
class Books: ObservableObject {
    @Published var items = [BookItem]()

@ObservedObject var books: Books

NavigationView {
    ForEach($books.items) { book in
        HStack {
            Toggle(isOn: $book.enabled.isOn) {
                Text("")
            }
            // ...
        }
    }
}

3      

@mecid  

Hi Jordi,

Let me mention few things,

  1. We have to make BookItem a struct and remove conformance to ObservableObject. There are some limitations, the value of binding should be a value type. You can check docs for ObservedObject.
  2. We need to change let to var for "enabled" field.
  3. In our case we need both book item and binding that we can pass.
import Foundation

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
    typealias Index = Base.Index
    typealias Element = (index: Index, element: Base.Element)

    let base: Base

    var startIndex: Index { base.startIndex }

    var endIndex: Index { base.endIndex }

    func index(after i: Index) -> Index {
        base.index(after: i)
    }

    func index(before i: Index) -> Index {
        base.index(before: i)
    }

    func index(_ i: Index, offsetBy distance: Int) -> Index {
        base.index(i, offsetBy: distance)
    }

    subscript(position: Index) -> Element {
        (index: position, element: base[position])
    }
}

extension RandomAccessCollection {
    func indexed() -> IndexedCollection<Self> {
        IndexedCollection(base: self)
    }
}

Apple provides this collection type in examples. We can use it like this:

import SwiftUI

struct BooksView: View {
    @ObservedObject var books: Books

    var body: some View {
        List {
            ForEach(books.items.indexed(), id: \.1.id) { index, book in
                Toggle(book.name, isOn: self.$books.items[index].enabled)
            }
        }
    }
}

4      

Thanks @mecid! That fixed it.

The only problem I'm having now is that when i try to delete the last item in the array with a swipe gesture, the app crashes.

1 indexes
▿ 1 indexes
  ▿ ranges: 1 element
    ▿ Range(4..<5)
      - lowerBound: 4
      - upperBound: 5
Position to remove is 4
Books count is 5
Fatal error: Index out of range

When i remove other items in the List it works without issues. The index also seems to be correct as it should be in bounds. Any clue?

3      

Hi @jordibruin, I ran into the same problem. This seems to be a solution: https://troz.net/post/2019/swiftui-data-flow/ (scroll down to "ObservableObject & @ObservedObject - Part 2" > "update 3").

3      

@jordibruin This solution worked for me: https://stackoverflow.com/a/62796050/11925891

3      

Thank you for a good hint, which I could use in my project where I have a grid of checkboxes inside of LazyHGrid. There is one problem: when I open an alert, then the contents of LazyHGrid disappears (only background color is seen). Now I have to design this part of code so that no alerts are called in the view which displays the checkbox grid.

The structure of code: in ContentView I open another view, which has some buttons, text and the view with checkbox grid.

If I call the view with checkbox grid directly in the ContentView, then an alert works normally.

In the actual app I have about 20 views which are opened (after selected in menu) when necessary. The view which needs the checkbox grid is one of them.

This is in macOS 11.1, Xcode 12.3

Does anyone have any hints for me in this subject?

3      

Here is the code:

//***************************************
//  ContentView.swift
//****************************************
struct ContentView: View
{
    @State var alertItem: AlertItem?

    var body: some View
    {
        VStack
        {
            Workview(alertItem: $alertItem)
        }
        .alert(item: $alertItem)
        {
            alertItem in
            let ebtn = alertItem.ekaBtn
            let tbtn = alertItem.tokaBtn
            if ebtn == nil
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                dismissButton: tbtn!)
            }
            else if tbtn == nil
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                             primaryButton: ebtn!, secondaryButton: .default(Text("Ei")))
            }
            else
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                             primaryButton: ebtn!, secondaryButton: tbtn!)
            }
        }
    }
}

// Values of one checkbox
struct Ruksi: Identifiable, Codable, Equatable
{
     var id = UUID()
     var nimiz: String = ""
     var checked: Bool = false
}

// All checkboxes
class Ruksit: ObservableObject
{
     @Published var items = [Ruksi]()
}

struct AlertItem: Identifiable
{
    var id = UUID()
    var otsa = Text("")   // headline
    var sisus: Text?         // contents
    var ekaBtn: Alert.Button?
    var tokaBtn: Alert.Button?
}

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection
{
     typealias Index = Base.Index
     typealias Element = (index: Index, element: Base.Element)

     let base: Base
     var startIndex: Index { base.startIndex }
     var endIndex: Index { base.endIndex }

     func index(after i: Index) -> Index {   base.index(after: i)    }

     func index(before i: Index) -> Index {  base.index(before: i)   }

     func index(_ i: Index, offsetBy distance: Int) -> Index {  base.index(i, offsetBy: distance)    }

     subscript(position: Index) -> Element {  (index: position, element: base[position])     }
}

extension RandomAccessCollection
{
    func indexed() -> IndexedCollection<Self>
    {
        IndexedCollection(base: self)
    }
}

//***************************************
//  Workview.swift
//****************************************
struct Workview: View
{
    @ObservedObject var chekit = Ruksit()
    @State var kopio = [Ruksi()]   // copy of orginal values
    @Binding var alertItem: AlertItem?      // Alertti esiin

    var body: some View
    {
        VStack
        {
            Button("Close the app")
            {
                exit(0)
            }
            Text("Change some check values, checks 2...5 are listed on debug output:")
            Button("Show some values")
            {
                print("\(chekit.items[2].nimiz) = \(chekit.items[2].checked)")
                print("\(chekit.items[3].nimiz) = \(chekit.items[3].checked)")
                print("\(chekit.items[4].nimiz) = \(chekit.items[4].checked)")
                print("\(chekit.items[5].nimiz) = \(chekit.items[5].checked)")
            }
            Button("Cancel the changes")
            {
                chekit.items = kopio
            }
            Button("ALERT TEST")
            {
                let otz = "MYSTERIES HAPPEN"
                let sisuz = "The contents of checkbox grid disappears"
                alertItem = AlertItem(otsa: Text(otz), sisus: Text(sisuz), tokaBtn: .default(Text("OK")))
            }
            HStack()
            {
                Text("  ")    // some empty space on the left of the grid
                Checkmatrix(chekit: chekit)
            }
        }
        .frame(minWidth: 220, minHeight: 300)
        .frame(maxWidth: .infinity)
        .onAppear()
        {
            // Collect the check boxes and give then names of differing lengths
            for i in 0...55
            {
                var bite = Ruksi()
                let ranint = Int.random(in: 1..<8)
                let zzz = String(repeating: "x", count: ranint)
                bite.nimiz = "Plant\(zzz) \(i)"
                bite.checked = ((i / 2) * 2 == i)
                chekit.items.append(bite)
            }
            kopio = chekit.items  // make backup of the original values
        }
    }
}

//***************************************
//  Checkmatrix.swift
//****************************************
struct Checkmatrix: View
{
    @ObservedObject var chekit: Ruksit

    var gridItemLayout = [GridItem(.adaptive(minimum: 20), spacing: 0, alignment: .leading)]

    var body: some View
    {
        ScrollView(.horizontal)
        {
            LazyHGrid(rows: gridItemLayout, alignment: .top, spacing: 10)
            {
                ForEach(chekit.items.indexed(), id: \.1.id)
                {
                    index, ruks in
                    Toggle(ruks.nimiz, isOn: self.$chekit.items[index].checked)
                }
            }
            .padding(.horizontal, 4)
        }
        .background(Color.yellow)
    }
}

3      

Having done some more digging I found a working solution to this problem. And the code is now simpler. A good startpoint was the article here: [https://swiftui.diegolavalle.com/posts/on-demand-bindings/]

This is for macOS 11.1, coded in Xcode 12.3. And here is the code:

// ***************************
//  ContentView.swift
// ***************************

struct ContentView: View
{
    @State var alertItem: AlertItem?    

    var body: some View
    {
        VStack
        {
            Workview(alertItem: $alertItem)
        }
        .alert(item: $alertItem)
        {
            alertItem in
            let ebtn = alertItem.ekaBtn
            let tbtn = alertItem.tokaBtn
            if ebtn == nil
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                dismissButton: tbtn!)
            }
            else if tbtn == nil
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                             primaryButton: ebtn!, secondaryButton: .default(Text("NO")))
            }
            else
            {
                return Alert(title: alertItem.otsa, message: alertItem.sisus,
                             primaryButton: ebtn!, secondaryButton: tbtn!)
            }
        }
    }
}

struct AlertItem: Identifiable
{
    var id = UUID()
    var otsa = Text("")   // headline
    var sisus: Text?      // contents
    var ekaBtn: Alert.Button?
    var tokaBtn: Alert.Button?
}

struct Ruksi: Identifiable
{
    var id = UUID()
    var nimiz: String = ""
    var checked: Bool = false

    init(nz: String, ison: Bool)
    {
        nimiz = nz
        checked = ison
    }
}

// *******************************
//  Workview.swift
// *******************************

struct Workview: View
{
    @Binding var alertItem: AlertItem?
    @State var chekit = [Ruksi]()
    @State var kopio = [Ruksi]()   // backup of checklist

    var body: some View
    {
        VStack
        {
            Button("Close the app")
            {
                exit(0)
            }
            Text("Change some check values, checks 2...5 are listed on debug output:")
            Button("Show some values")
            {
                print("\(chekit[2].nimiz) = \(chekit[2].checked)")
                print("\(chekit[3].nimiz) = \(chekit[3].checked)")
                print("\(chekit[4].nimiz) = \(chekit[4].checked)")
                print("\(chekit[5].nimiz) = \(chekit[5].checked)")
            }
            Button("Cancel the changes")
            {
                chekit = kopio
            }
            Button("ALERT TEST")
            {
                let otz = "ALERT TEST"
                let sisuz = "See that the contents of checkbox grid remains"
                alertItem = AlertItem(otsa: Text(otz), sisus: Text(sisuz), tokaBtn: .default(Text("OK")))
            }
            HStack()
            {
                Text("  ")    // some empty space on the left of the grid
                Checkmatrix(chekit: $chekit)
            }
        }
        .frame(minWidth: 220, minHeight: 300)
        .frame(maxWidth: .infinity)
        .onAppear()
        {
            // Collect the check boxes and give them names of differing lengths
            for i in 0...55   // kootaan ruksijoukko ja niille eripituisia nimiä
            {
                let ranint = Int.random(in: 1..<7)
                let zzz = String(repeating: "x", count: ranint)
                let bite = Ruksi(nz: "kasvi\(zzz) \(i)", ison: ((i / 2) * 2 == i))
                chekit.append(bite)
            }
            kopio = chekit
        }
    }
}

// *******************************
//  Checkmatrix.swift
// *******************************

struct Checkmatrix: View
{
    @Binding var chekit: [Ruksi]
    var gridItemLayout = [GridItem(.adaptive(minimum: 20), spacing: 0, alignment: .leading)]

    var body: some View
    {
        ScrollView(.horizontal)
        {
            LazyHGrid(rows: gridItemLayout, alignment: .top, spacing: 10)
            {
                ForEach(chekit.indices, id: \.self)
                {
                    Toggle(self.chekit[$0].nimiz, isOn: self.$chekit[$0].checked)
                }
            }
            .padding(.horizontal, 4)
        }
        .background(Color.yellow)
    }
}

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.