State variable that keeps track of list of published items - swiftui

I am learning SwiftUI and having trouble with data flow.
I can't figure out how to create a "state"-variable to keep track of a list of published child items.
I need a variable that is true if at least one child item is selected. These items are mapped to SwiftUI toggle-switches, which works. (Making a computed property is not allowed)
Property wrapper cannot be applied to a computed property
class Main : ObservableObject {
#Published var items : [Item]
//This is what i want, but not allowed
#Published var selectedItemsExist : Bool {
get {
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
return exists
}
}
init(items: [Item]) {
self.items = items
}
}
class Item : Identifiable, ObservableObject {
var id = UUID()
var name : String!
#Published var selected : Bool = false
}
And then in my view be able to do something like
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
Any guidance appreciated!

Please find below the demo of approach how this could be done. Tested as worked with Xcode 11.2 / iOS 13.2
class Main : ObservableObject {
#Published var items : [Item] {
didSet { // << as item is value array will be updated on item changed
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
self.selectedItemsExist = exists
}
}
#Published var selectedItemsExist : Bool = false
init(items: [Item]) {
self.items = items
}
}
struct Item : Identifiable { // << value type, so changed on any property change
var id = UUID()
var name : String!
var selected : Bool = false
init(_ name: String) {
self.name = name
}
}
struct TestCalculablePublishing: View {
#ObservedObject var main = Main(items: [Item("1"), Item("2"), Item("3")])
var body: some View {
VStack {
Button("Test") {
self.main.items[1].selected.toggle() // << just for test purpose
}
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
}
}
}
struct TestCalculablePublishing_Previews: PreviewProvider {
static var previews: some View {
TestCalculablePublishing()
}
}

Related

Left side of mutating operator isn't mutable: 'self' is immutable [duplicate]

Basically what i want to do is if you press the Button then entries should get a new CEntry. It would be nice if someone could help me out. Thanks!
struct AView: View {
var entries = [CEntries]()
var body: some View {
ZStack {
VStack {
Text("Hello")
ScrollView{
ForEach(entries) { entry in
VStack{
Text(entry.string1)
Text(entry.string2)
}
}
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp")) <-- Error
}) {
someButtonStyle()
}
}
}
}
The Class CEntries
class CEntries: ObservableObject, Identifiable{
#Published var string1 = ""
#Published var string2 = ""
init(string1: String, string2: String) {
self.string1 = string1
self.string2 = string2
}
}
Views are immutable in SwiftUI. You can only mutate their state, which is done by changing the properties that have a #State property wrapper:
#State var entries: [CEntries] = []
However, while you could do that, in your case CEntries is a class - i.e. a reference type - so while you could detect changes in the array of entries - additions and removals of elements, you won't be able to detect changes in the elements themselves, for example when .string1 property is updated.
And it doesn't help that it's an ObservableObject.
Instead, change CEntries to be a struct - a value type, so that if it changes, the value itself will change:
struct CEntries: Identifiable {
var id: UUID = .init()
var string1 = ""
var string2 = ""
}
struct AView: View {
#State var entries = [CEntries]()
var body: some View {
VStack() {
ForEach(entries) { entry in
VStack {
Text(entry.string1)
Text(entry.string2)
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp"))
}) {
someButtonStyle()
}
}
}
}

Using ForEach inside a Picker

I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style

Update text with Slider value in List from an array in SwiftUI

I have a list of sliders, but I have a problem updating the text that shows the slider value.
The app workflow is like this:
User taps to add a new slider to the list.
An object that defines the slider is created and stored in an array.
The class that has the array as a property (Db) is an ObservableObject and triggers a View update for each new item.
The list is updated with a new row.
So far, so good. Each row has a slider whose value is stored in a property in an object in an array. However, the value text doesn't update as soon as the slider is moved, but when a new item is added. Please see the GIF below:
The Slider doesn't update the text value when moved
How can I bind the slider movements to the text value? I thought that by defining
#ObservedObject var slider_value: SliderVal = SliderVal()
and binding that variable to the slider, the value would be updated simultaneously but that is not the case. Thanks a lot for any help.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var db: Db
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value)) //<-- Problem here
}
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
struct Criteria: Identifiable {
var id = UUID()
var name: String
#ObservedObject var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
class SliderVal: ObservableObject {
#Published var value:Double = 50
}
The #ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the #Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.
You can fix this by turning your model into a struct:
struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
struct SliderVal {
var value:Double = 50
}
The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.
If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:
struct ContentView: View {
#ObservedObject var db: Db = Db()
private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
Now, because the models are all value types (structs), the View and #Published know when to update and your sliders work as expected.
try something like this:
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
.onChange(of: criteria.slider_value.value) { newVal in
DispatchQueue.main.async {
criteria.slider_value.value = newVal
}
}

Delete entire list in SwiftUI

Is there a way to delete all the list items in SwiftUI?
I'm using a ForEach() inside a List() and I want to have a clear all button to remove all the items from the list, is there a way to do it?
struct SwiftUIView: View {
#State var filters : [filter] = [filter(name: "new"), filter(name: "old"), filter(name: "some")]
#State var afterFilters : [someFilter] = []
var body: some View {
List{
ForEach(0..<self.filters.count, id:\.self){ i in
filterRepresent(string: self.$afterFilters[i].filter.name, isOn: self.$afterFilters[i].isOn)
}
}.onAppear {
for filter in self.filters {
self.afterFilters.append(someFilter(filter: filter))
}
}
}
}
struct filterRepresent : View {
#Binding var string : String
#Binding var isOn : Bool
var body : some View {
HStack{
Text(string)
Toggle("",isOn: $isOn)
}
}
}
struct filter {
var name : String
var isOn : Bool
init(name: String){
self.name = name
self.isOn = false
}
}
struct someFilter : Identifiable{
var id : Int
var filter : filter
var isOn : Bool
init(filter : filter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isOn
}
}
As you can see, in the example above, I'm using a #Binding to change the data I store based on the Toggle state, I want to have a button that deletes the entire list (in the real app the data to the list is uploaded from a server side into a temp array just like in the above) when I do it with .removeall() I get thrown with "out of index" error.
The button I use :
Button(action: {
self.afterFilters.removeAll()
}, label: {
Text("Clear all").font(Font.custom("Quicksand-Medium", size: 15))
})
The error I'm getting:
Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.13/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
You have to clean up model and view will be refreshed automatically.
Here is a simple demo:
struct DemoCleanUpList: View {
#State private var persons = ["Person 1", "Person 2", "Person 3"]
var body: some View {
VStack {
Button("CleanUp") { self.persons.removeAll() }
List {
ForEach(persons, id: \.self) { person in
Text(person)
}
}
}
}
}

How to manipulate the ion-list item using an ActionSheet in SwiftUI?

I am trying to use an ActionSheet to manipulate items of a List. How can I call a function (in this example deleteItem) that is part of the data model, using an ActionSheet and manipulte the selected item, similar to what .onDelete does?
My view presents items from a model using the following code:
struct ItemManager: View {
#ObservedObject var model: ItemModel
var body: some View {
List {
ForEach(model.items) { item in
ItemCell(item: item)
}
.onDelete { self.model.deleteItem(at: $0) }
}
}
}
struct ItemCell: View {
var item: Item
#State private var isActionSheetVisible = false
private var actionSheet: ActionSheet {
let button1 = ActionSheet.Button.default(Text("Delete")){
self.isActionSheetVisible = false
}
let button2 = ActionSheet.Button.cancel(){
self.isActionSheetVisible = false
}
let buttons = [button1, button2]
return ActionSheet(title: Text("Actions"), buttons: buttons)
}
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.isActionSheetVisible = true
}) {
Text(item.title).font(.headline)
}.actionSheet(isPresented: self.$isActionSheetVisible) {
self.actionSheet
}
}
}
}
My model has some simple properties and a function that deletes items from the collection:
struct Item: Identifiable, Equatable {
let title: String
var id: String {
title
}
}
class ItemModel: ObservableObject {
#Published var items: [Item] = [Item(title: "temp.1"), Item(title: "temp.2")]
public func deleteItem(at indices: IndexSet) {
indices.forEach { items.remove(at: $0) }
}
}
extension Item {
static let previewItem = Item(title: "temp.3")
}
Update: Added Equatable in the Item declaration to comform.
You could try passing the ItemModel to the ForEach() like so:
ForEach(model.items) { item in
ItemCell(item: item, model: self.model)
}
Then in your ItemCell you can:
struct ItemCell: View {
var item: Item
var model: ItemModel // Add the model variable
#State private var isActionSheetVisible = false
private var actionSheet: ActionSheet {
let button1 = ActionSheet.Button.default(Text("Delete")) {
// Get the index
if let index = self.model.items.firstIndex(of: self.item) {
// Delete the item based on the index
self.model.items.remove(at: index)
// Dismiss the ActionSheet
self.isActionSheetVisible = false
} else {
print("Could not find item!")
print(self.item)
}
}
}
}