|
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("")
}
// ...
}
}
}
|
|
Hi Jordi,
Let me mention few things,
- 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.
- We need to change let to var for "enabled" field.
- 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)
}
}
}
}
|
|
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?
|
|
|
|
|
|
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?
|
|
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)
}
}
|
|
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)
}
}
|