I am new to SwiftUI and here is what I want to achieve:
I have a Form
with sections. Each section has a list and each row has a Toggle
I can select/deselect. Below the list, I have a clear button. With a click, I would like to reset all the Toggles
to a deselected state.
Model (each ListSection represents Section
in Form
):
struct RowItem: Identifiable {
let id = UUID()
var isSelected: Bool
var rowName: String
var amount: Int
}
struct ListSection: Identifiable, Equatable {
static func == (lhs: ListSection, rhs: ListSection) -> Bool {
lhs.id == rhs.id
}
let id = UUID()
let title: String
let items: [RowItem]
let visible: Int
}
class HomeModel: ObservableObject {
private let items1 = [RowItem(isSelected: false, rowName: "Energy", amount: 24),
RowItem(isSelected: true, rowName: "Automotive", amount: 70),
RowItem(isSelected: true, rowName: "Materials", amount: 56),
RowItem(isSelected: false, rowName: "Industrials", amount: 109),
RowItem(isSelected: true, rowName: "Consumer1", amount: 209),
RowItem(isSelected: true, rowName: "Consumer2", amount: 12),]
private let items2 = [RowItem(isSelected: true, rowName: "Europe", amount: 302),
RowItem(isSelected: true, rowName: "America", amount: 589),
RowItem(isSelected: false, rowName: "Asia", amount: 67),
RowItem(isSelected: false, rowName: "Africa", amount: 207),
RowItem(isSelected: true, rowName: "Oceania", amount: 9)]
@Published var sections: [ListSection]
init() {
self.sections = [ListSection(title: "Sectors", items: items1, visible: 4),
ListSection(title: "Regions", items: items2, visible: 5)]
}
}
struct ExpandableList: View {
private var title: String
private var visibleElements: Int
private var showAllButtonVisible: Bool
@State private var items: [RowItem]
@State private var isExpanded = false
init(title: String, items: [RowItem], visible: Int) {
self.title = title.lowercased()
self.items = items
self.visibleElements = visible
self.showAllButtonVisible = visible < items.count
}
var body: some View {
List {
ForEach(isExpanded ? 0..<items.count : 0..<Array(items[0..<visibleElements]).count, id: \.self) { index in
HStack {
Text(items[index].rowName).fontWeight(.light)
Spacer()
Text("\(items[index].amount)").foregroundColor(.gray).fontWeight(.light)
Toggle(isOn: Binding(
get: { return items[index].isSelected },
set: { items[index].isSelected = $0 }
)){}.toggleStyle(.checklist)
}.listRowSeparator(.hidden)
}
if showAllButtonVisible {
HStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
if isExpanded {
Label("Show less \(title)", systemImage: "chevron.up").foregroundColor(Color(red: 55/255, green: 126/255, blue: 83/255, opacity: 1)).labelStyle(ImageToRightSideLabelStyle())
} else {
Label("Show all \(title)", systemImage: "chevron.down").foregroundColor(Color(red: 55/255, green: 126/255, blue: 83/255, opacity: 1)).labelStyle(ImageToRightSideLabelStyle())
}
}
}
}
}
}
}
struct MyForm: View {
@ObservedObject var model = HomeModel()
var body: some View {
NavigationView {
VStack {
Form {
ForEach(model.sections, id: \.id) { section in
Section {
ExpandableList(title: section.title, items: section.items, visible: section.visible)
} header: {
Text(section.title).font(.title3).foregroundColor(.black).textCase(nil)
} footer: {
if section != model.sections.last {
VStack {
Divider().padding(.top, 15)
}
}
}
}
}.navigationTitle(Text("Filter stocks"))
HStack {
Button {
for section in model.sections {
for var item in section.items {
item.isSelected = false
print("Tag1 false")
}
}
} label: {
Text("Clear filters").foregroundColor(.black)
}.padding(.leading, 30)
Spacer()
Button(action: {
print("sign up bin tapped")
}) {
Text("Show 1506 assets")
.font(.system(size: 18))
.padding()
.foregroundColor(.black)
}
.background(Color.green) // If you have this
.cornerRadius(10)
.padding(.trailing, 30)
}
}
}
}
}
I assume I need to use Binding
somewhere, but not sure where.
CodePudding user response:
I made a small project to address your issue. The main takeaway is that you need to let SwiftUI
know when to redraw the view after the model or model element's properties change. That can be done with @Published
and/or objectWillChange
which is available to any class conforming to the ObservableObject
protocol.
import SwiftUI
struct ContentView: View {
@StateObject var model = Model()
class Model: ObservableObject {
@Published var items = [
Item(zone: "America", count: 589),
Item(zone: "Asia", count: 67),
Item(zone: "Africa", count: 207),
Item(zone: "Oceania", count: 9)
]
class Item: ObservableObject, Hashable {
static func == (lhs: ContentView.Model.Item, rhs: ContentView.Model.Item) -> Bool {
lhs.id == rhs.id
}
let id = UUID()
let zone: String
let count: Int
@Published var selected: Bool = false
init(zone: String, count: Int) {
self.zone = zone
self.count = count
}
func toggle() {
selected.toggle()
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}
var body: some View {
VStack {
ForEach(Array(model.items.enumerated()), id: \.element) { index, item in
HStack {
Text(item.zone)
Spacer()
Text(String(item.count))
Button {
item.toggle()
print(index, item.selected)
} label: {
Image(systemName: item.selected ? "checkmark.square" : "square")
.resizable()
.frame(width: 24, height: 24)
.font(.system(size: 20, weight: .regular, design: .default))
}
}
.padding(10)
.onReceive(item.$selected) { isOn in
model.objectWillChange.send()
}
}
Button {
model.items.forEach { item in
item.selected = false
}
model.objectWillChange.send()
} label: {
Text("Clear")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
https://github.com/aibo-cora/SwiftUI/blob/main/ResetCheckBoxes.md