1

I hope I can explain my question clearly.

I want to select some courses from list via toggle but whatever I've tried it didn't work.

What am I supposed to do?

Thank you for your time. Bests, Murat

struct SubjectCardsView: View {
    // MARK: - Properties
    @State var courses: [Course] = Bundle.main.decode("courses.json")
    
    @State private var toggle: Bool = false
    
    // MARK: - Body
    var body: some View {
        
        NavigationView {
            
            List {
                
                ForEach(courses) { course in
                    Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
                        ForEach(course.courseName, id: \.name) { item  in
                            Toggle(isOn: $toggle, label: {
                                Text(item.name)
                            })
                            
                        }
                    }
                }
            }
            
            .listStyle(InsetGroupedListStyle())
            .navigationBarTitle("Choose your subject", displayMode: .inline).font(.system(size: 16, weight: .medium, design: .rounded))
            .navigationBarItems(leading: Button(action: {
                
            }, label: {
                Text("Cancel")
            }), trailing: Button(action: {
                
            }, label: {
                Text("Save")
            }))
            
            
        } // NavigationView
    }
}

Course part!

import Foundation
import SwiftUI

struct Course: Codable, Identifiable {
    
    var id: Int
    var title: String
    var subjectCount: String
    var courseName: [Content]
    var isToggled = false
    
    private var imageName: String
    var image: Image {
        Image(imageName)
    }

    enum LessonSegment: String, CaseIterable, Identifiable {
        case overview
        case resources

        var id: String { self.rawValue }
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case title
        case subjectCount
        case imageName
        case courseName
   
    }
}

struct Content: Codable {
    
    var id: Int
    var name: String
    var content: String
    var assessment: String
    var notify: String
}
aheze
  • 24,434
  • 8
  • 68
  • 125
muradi
  • 15
  • 6
  • Can you include the code for `Course`? – jnpdx Apr 12 '21 at 19:14
  • Yes. I edited the original message. – muradi Apr 12 '21 at 19:26
  • So you want to toggle whether each `Content` is selected? Is there a certain property on `Content` that should respond to the toggle? Looks like you have a `isToggled` on `Course`, but your `Toggle` control is under a `ForEach` for `courseName` so it's a little unclear. Secondly, are you stuck with `Int` ids or can you use something more unique like UUIDs? – jnpdx Apr 12 '21 at 19:38
  • Each courseName has a toggle so I can understand which on is selected. After selection, it will be saved and filtered in content view. I try to UUID and Int ids but they don't work. – muradi Apr 12 '21 at 19:56

1 Answers1

4

Your @State private var toggle: Bool = false doesn't make sense. You have many courses, not a single course. Each course should have it's own toggle on/off, which is what you started to do with:

struct Course: Codable, Identifiable {
    var isToggled = false /// here!

    ...
}

To use this, you can reference each course's isToggled inside the ForEach, like this:

ForEach(courses) { course in

    Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
        ForEach(course.courseName, id: \.name) { item  in

            ///          here!
            Toggle(isOn: course.isToggled, label: {
                Text(item.name)
            })
            
        }
    }
}

However, this won't work. course.isToggled is a Bool, not a Binding<Bool>, which the Toggle expects.

Where can you get Binding<Bool>? From the @State var courses: [Course], of course! sorry for pun


The Binding<> part comes from the @State declaration.

Properties that are marked with @State, like your @State var courses: [Course], include a projectedValue that has the Binding<> type.

You can access the projectedValue by adding a $ to the property. So, if you write $courses, that will have type Binding<[Course]>.

Xcode autocompletion for $courses

But, your toggle expects Binding<Bool>, not Binding<[Course]>.

Toggle(isOn: Binding, label: { Text("A cool toggle") })

This is where the Bool part comes in.

You will need to replace the Binding's value, [Course], with a Bool. Well, we had a Bool before, right?

struct Course: Codable, Identifiable {
    var isToggled = false /// this is a Bool!

Each course has a isToggled, which is a Bool. From earlier on in this answer, we got this inside the ForEach:

ForEach(courses) { course in

    ...

    ///          getting the Bool, which unfortunately doesn't work (yet)
    Toggle(isOn: course.isToggled, label: {

... We need to somehow combine the Binding<> with the Bool. This means that we must

  • reference $courses (to get the Binding<>)
  • get each courses' isToggled

And... tada!

$courses[index].isToggled /// has type Binding<Bool>

To get index, we'll need to loop over courses.indices instead of directly looping over courses.

ForEach(courses.indices) { index in

    ...

    ///          this works! 
    Toggle(isOn: $courses[index].isToggled, label: {

Then, just replace every occurrence of course in your old code's ForEach with courses[index]. Here's the full working example:

ForEach(courses.indices) { index in
    Section(header: Text(courses[index].title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
        ForEach(courses[index].courseName, id: \.name) { item  in

            /// $courses[index].isToggled is a Binding<Bool>
            Toggle(isOn: $courses[index].isToggled, label: {
                Text(item.name)
            })
        }
    }
}

As a convenience so you don't have to do courses[index] every time you want the current course, you can use Array(zip as shown in this answer to loop over a (Int, Course). This also assigns a unique id for every Section inside the loop, so any transitions you add will work out smoothly.

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in

    Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
        ForEach(course.courseName, id: \.name) { item  in

            Toggle(isOn: $courses[index].isToggled, label: {
                Text(item.name)
            })
        }
    }
}

Well (Int, Course) is actually (Range<Array<Course>.Index>.Element, Course) but that's pretty much the same thing.

Final result:

Toggles inside each row of the ForEach working

Edit for isToggled inside Content, not Course:

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in
    Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
        ForEach(Array(zip(course.courseName.indices, course.courseName)), id: \.1.id) { (itemIndex, item) in

            ///          here!
            Toggle(isOn: $courses[index].courseName[itemIndex].isToggled, label: {
                Text(item.name)
            })
        }
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
  • 1
    Thank you for editing my question and your answer. It works. Almost two day, I've worked on it. I learned something today. : ) – muradi Apr 12 '21 at 20:05
  • @muradi I added some extra clarification to my answer :) – aheze Apr 12 '21 at 20:57
  • Thank you again. I want to ask one more question. Each course has two or more courseNames and toggle is working as a block again. Sorry to bother you. – muradi Apr 12 '21 at 20:58
  • @muradi sure, what is it? – aheze Apr 12 '21 at 21:00
  • I'm trying to upload the screen sharing. [link](https://imgur.com/o0wXuHd) – muradi Apr 12 '21 at 21:12
  • @muradi, check what [jnpdx said](https://stackoverflow.com/questions/67064272/toggle-selection-in-a-list-swiftui/67064699#comment118542768_67064272). Move the `isToggled` from `Course` into `Content`. – aheze Apr 12 '21 at 21:23
  • @muradi np, after you move `isToggled`, use the code in my edited answer – aheze Apr 12 '21 at 21:30