Filter SwiftUI object Array - swiftui

I'd like to filter an array of object if the todoStatus === true and then return the count but I am unsure of how to go about it,I can already get the count of the entire array, but I'm not sure how to filter it down.
Here is what the structure looks like:
class ToDo: ObservableObject, Identifiable {
var id: String = UUID().uuidString
#Published var todoName: String = ""
#Published var todoStatus: Bool = false
init (todoName: String, todoStatus: Bool) {
self.todoName = todoName
self.todoStatus = todoStatus
}
}
class AppState: ObservableObject {
#Published var todos: [ToDo] = []
init(_ todos: [ToDo]) {
self.todos = todos
}
}
struct ContentView: View {
var name = "Leander"
#ObservedObject var state: AppState = AppState([
ToDo(todoName: "Wash the Dogs", todoStatus: true),
ToDo(todoName: "Wash the Dogs", todoStatus: false),
ToDo(todoName: "Wash the Dogs", todoStatus: true),
ToDo(todoName: "Wash the Dogs", todoStatus: false),
ToDo(todoName: "Wash the Dogs", todoStatus: true),
ToDo(todoName: "Wash the Dogs", todoStatus: false),
])
and how I'm accessing the count
Text("\($state.todos.count)")

try this simple code, no need for the $state, just state, and no need for the $0.todoStatus == true:
Text("\(state.todos.filter{$0.todoStatus}.count)")
Note, you should not nest ObservableObject, like you do, that is,
class AppState: ObservableObject containing an array of class ToDo: ObservableObject. Use
struct ToDo: Identifiable {
let id: String = UUID().uuidString
var todoName: String = ""
var todoStatus: Bool = false
}

You would simply use the .filter function on Array, then .count it like this:
let todoStatusCount = state.todos.filter( { $0.todoStatus } ).count

Related

#Published property not updating view from nested view model - SwiftUI

I'm unsure why my view isn't getting updated when I have a view model nested inside another. My understanding was that a #Published property in the child view model would trigger a change in the parent viewModel, causing that to push changes to the UI.
This is the child view model:
class FilterViewModel : ObservableObject, Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
#Published var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
func changeSelected() {
self.selected = !self.selected
}
}
The following works as expected, on clicking the button the background colour is changed.
struct ContentView: View {
#ObservedObject var filterVM = FilterViewModel(name: "A")
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
However, I want to have an array of filter view models so tried:
class FilterListViewModel: ObservableObject {
#Published var filtersVMS = [FilterViewModel]()
init(){
filtersVMS = [
FilterViewModel(name: "A"),
FilterViewModel(name: "B"),
FilterViewModel(name: "C"),
FilterViewModel(name: "D")
]
}
}
However, the following view is not updated when clicking the button
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
Button(action: { filterListVM[0].changeSelected()}, label: {
Text(filterListVM[0].name)
.background(Color(filterListVM[0].backgroundColour))
})
}
Alternate solution is to use separated view for your sub-model:
struct FilterView: View {
#ObservedObject var filterVM: FilterViewModel
var body: some View {
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
}
so in parent view now we can just use it as
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
// ...
FilterView(filterVM: filterListVM[0])
}
The most simplest way is to define your FilterViewModel as struct. Hence, it is a value type. When a value changes, the struct changes. Then your ListViewModel triggers a change.
struct FilterViewModel : Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
mutating func changeSelected() {
self.selected = !self.selected
}
}

Deleting List Item With A Binding Boolean Value - SwiftUI

In my app, I have a screen where the user can switch filters (of a map) on and off using the Swift Toggle, and also, delete them (with Edit Button). I use a Binding Boolean variable to connect the toggles so that I can react to each change, here is the code:
//The filter data model
class DSFilter{
var filterId : Int
var filterTitle : String
var isActive : Bool
init(filterId : Int, filterTitle : String, isActive : Bool){
self.filterId = filterId
self.filterTitle = filterTitle
self.isActive = isActive
}
}
//The data model representable
struct filterItem : View {
#Binding var filter : DSFilter
#Binding var isOn : Bool
#State var image = Image("defPerson")
var body : some View {
HStack {
image.resizable().frame(width: 45, height: 45).clipShape(Circle())
VStack(alignment: .leading, spacing: 8) {
Text(filter.filterTitle).font(Font.custom("Quicksand-Bold",size: 15))
Text(filter.filterTitle).font(Font.custom("Quicksand-Medium",size: 13)).foregroundColor(.gray)
}.padding(.leading)
Spacer()
HStack{
Toggle("",isOn: $isOn).padding(.trailing, 5).onReceive([self.isOn].publisher.first()) { (value) in
self.filter.isActive = self.isOn
}.frame(width: 30)
}
}.frame(height: 60)
.onAppear(){
self.isOn = self.filter.isActive
}
}
}
//The view where the user has the control
struct FilterView: View {
#State var otherFilters : [addedFilter] = []
#Binding var isActive : Bool
var body: some View {
VStack{
Capsule().fill(Color.black).frame(width: 50, height: 5).padding()
ZStack{
HStack{
Spacer()
Text("Filters").font(Font.custom("Quicksand-Bold", size: 20))
Spacer()
}.padding()
HStack{
Spacer()
EditButton()
}.padding()
}
List{
ForEach(0..<self.otherFilters.count, id:\.self){ i in
filterItem(filter: self.$otherFilters[i].filter, isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
}
Spacer()
}
}
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.otherFilters.remove(at: Array(offsets)[0]) //I use it like so because you can only delete 1 at a time anyway
}
}
//The class I use to bind the isOn value so I can react to change
struct addedFilter : Identifiable{
var id : Int
var filter : DSFilter
var isOn : Bool
init(filter : DSFilter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isActive
}
}
When I use the delete as it is right now I get the following error (Xcode 12.0):
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
2020-10-06 17:42:12.567235+0300 Hynt[5207:528572] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
What do I need to change so I can delete the items? Should I take a different approach to the Binding problem with the Toggle?
Hi instead of keeping three different array to manage addedFilter, DSFilter, isOn. why don't you try the following one single model
class FFilter: Identifiable {
var id: Int
var addedFilter: addedFilter
var dsFilter: DSFilter
var isOn: Bool
init(addedFilter: addedFilter, dsFilter: DSFilter, isOn: Bool) {
self.id = addedFilter.id
self.addedFilter = addedFilter
self.dsFilter = dsFilter
self.isOn = isOn
}
}
And ViewModel change code like this:
class ViewModel: ObservableObject {
#Published var superFilters: [FFilter] = [
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true)
),
dsFilter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false)
),
dsFilter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false),
isOn: false),
FFilter(
addedFilter: addedFilter(filter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true)),
dsFilter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true)
),
dsFilter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false)
),
dsFilter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false),
isOn: false)
]}
then change your list code
List {
ForEach(viewModel.superFilters) { filter in
filterItem(filter: .constant(filter.dsFilter), isOn: .constant(filter.isOn))
}.onDelete(perform: removeRows(at:)) }
and removeRows method:
func removeRows(at offsets: IndexSet) {
print("deleting at \(offsets)")
self.viewModel.superFilters.remove(atOffsets: offsets)}
I hope it will work. let me know if you still face any issues
I suggest you to change your line in removeRows() function from
self.otherFilters.remove(at: Array(offsets)[0]
to
self.otherFilters.remove(atOffsets: offsets)
So, after a lot of try and fail I came to the answer.
The problem seems to be in the binding part - the compiler can't handle a change in the array size if I bind an element from it so I did the following:
First, I change the filterItem struct so the filter property is #State and not #Binding :
struct filterItem : View {
#State var filter : DSFilter
#Binding var isOn : Bool
Secondly, I have changed the addedFilter struct to the following:
struct addedFilter : Identifiable{
var id : Int
var isOn : Bool
init(filter : DSFilter){
self.id = filter.filterId
self.isOn = filter.isActive
}
}
And the ForEach loop to the following:
ForEach(0..<self.normOthFilters.count, id:\.self){ i in
filterItem(filter: self.normOthFilters[i], isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
To the main view I have added :
#State var normDefFilters : [DSFilter] = []
#State var normOthFilters : [DSFilter] = []
And finally the onDelete function to the following:
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.normOthFilters.remove(atOffsets: offsets)
}
In conclution I am not changing the value of the binded value and so I am not receiving any errors!

Swiftui #EnvironmentObject update in View

I am using an #EnvironmentObject (which is the ViewModel) and I have a demoData() function.
When I press it the data does change but my View is not updated.
How do I get the data to change in the View?
Thank you.
The view information:
import Combine
import SwiftUI
struct MainView: View {
#EnvironmentObject var entry:EntryViewModel
var body: some View {
TextField("Beg Value", text: self.$entry.data.beg)
TextField("Beg Value", text: self.$entry.data.end)
Button(action: { self.entry.demoData() }) { Text("Demo Data") }
}
}
ViewModel:
class EntryViewModel: ObservableObject {
#Published var data:EntryData = EntryData()
func demoData() {
var x = Int.random(in: 100000..<120000)
x = Int((Double(x)/100).rounded()*100)
data.beg = x.withCommas()
x = Int.random(in: 100000..<120000)
x = Int((Double(x)/100).rounded()*100)
data.end = x.withCommas()
}
Model:
EntryData:ObservableObject {
#Published var beg:String = ""
#Published var end:String = ""
}
This is because EntryData is a class and if you change its properties it will still be the same object.
This #Published will only fire when you reassign the data property:
#Published var data: EntryData = EntryData()
A possible solution is to use a simple struct instead of an ObservableObject class:
struct EntryData {
var beg: String = ""
var end: String = ""
}
When a struct is changed, it's copied and therefore #Published will send objectWillChange.

How to pass State variables as parameters to a model class which is of ObservableObject type?

I want to save some data from a SwiftUI view to a Model so that I can use these data into another SwiftUI view. However, I came up with some error when I try to call the Model class and pass all the data as parameters. The error says:
Cannot use instance member 'expectedEndDate' within property initializer; property initializers run before 'self' is available"
Here is my SearchBikeDataModel() code:
import Foundation
class SearchBikeDataModel: ObservableObject {
#Published var startDate: Date = Date()
#Published var startTime: Date = Date()
#Published var endDate: Date = Date()
#Published var endTime: Date = Date()
#Published var bikeType: String = ""
#Published var collectionPoint: String = ""
#Published var returnPoint: String = ""
init(selectedStartDate: Date, selectedStartTime: Date, selectedEndDate: Date, selectedEndTime: Date, bikeType: String, collectionPoint: String, returnPoint: String) {
self.startDate = selectedStartDate
self.startTime = selectedStartTime
self.endDate = selectedEndDate
self.endTime = selectedEndTime
self.bikeType = bikeType
self.collectionPoint = collectionPoint
self.returnPoint = returnPoint
}
}
And here is the code where I try to pass data as parameters:
import SwiftUI
struct BikeSearchFormView: View {
#Binding var isDateTimeShown: Bool
#Binding var isEndDateTimePickerShown: Bool
#State var expectedStartDate = Date()
#State var expectedStartTime = Date()
#State var expectedEndDate = Date()
#State var expectedEndTime = Date()
#State var isBikeTypePickerExpand: Bool = false
#State var isDropOffPointPickerExpand: Bool = false
#State var isPickUpPointPickerExpand: Bool = false
#State var selectedBikeType: String = "BIKE TYPE"
#State var selectedDropOffPoint: String = "DROP OFF POINT"
#State var selectedPickUpPoint: String = "PICKUP POINT"
#State var findBikeError: String = ""
#State var isActive: Bool = false
#ObservedObject var bikeTypeViewModel = VehicleTypeViewModel()
#ObservedObject var findBikeViewModel = FindBikeViewModel()
#ObservedObject var dataModel = SearchBikeDataModel(selectedStartDate: expectedStartDate, selectedStartTime: expectedStartTime, selectedEndDate: expectedEndDate, selectedEndTime: expectedEndTime, bikeType: selectedBikeType, collectionPoint: selectedPickUpPoint, returnPoint: selectedDropOffPoint)
var body: some View {
Text("Hello, World")
}
}
I have omitted codes of my UI as the question is just about am I following the right way to pass the data into parameters.

*SwiftUI Beta 7* How to give item in ForEach Loop an active state

I have a data model of the type:
struct fruit: Identifiable{
var id = UUID()
var a: String
var b: String
var isActive: Bool
}
and an array:
let fruitData=[
Model(a:"appleImg", b:"Apple", isActive: true),
Model(a:"pearImg", b:"Pear", isActive: false),
Model(a:"bananaImg", b:"Banana", isActive: false),
]
There's a RowView that looks like this:
struct RowView{
var a = "appleImg"
var b = "Apple"
var isActive = true
var body: some View{
HStack(spacing:8){
Image(a)
Text(b)
Spacer()
}
}
}
I then created a view to use ModelArray in and looped that in a ForEach in the main view. So something like:
let fruits = fruitData
ForEach(fruits){fruitPiece in
RowView(a:fruitPiece.a, b:fruitPiece.b, isActive:
fruitPiece.isActive)
}
I want to change the isActive based on the user tapping on the selected row - trick is it should be a single select, so only 1 active state at a time. Still new to SwiftUI so any help is super appreciated :)
The code below does what you asked. But some comments on your code first:
By convention, variable names should never begin with an uppercase (such as in your let ModelArray). It makes it harder to read. If not to you, to others. When sharing code (such as in stackoverflow), try to follow proper conventions.
You are calling ForEach(models), but you have not defined a models array (you did create ModelArray though).
Inside your ForEach, you are calling Model(...). Inside ForEach you need a view to display your data. But Model, in your code, is a data type. Does not make sense.
Here's the code that I think, does what you asked. When you tap the view, its isActive flag toggles. To show that it works, the text color changes accordingly.
struct Model: Identifiable {
var id = UUID()
var a: String
var b: String
var c: String
var isActive: Bool
}
struct ContentView: View {
#State private var modelArray = [
Model(a:"hi", b:"I like", c: "potatoes", isActive: true),
Model(a:"hi", b:"I like", c: "potatoes", isActive: false),
Model(a:"hi", b:"I like", c: "potatoes", isActive: false),
]
var body: some View {
ForEach(modelArray.indices, id: \.self){ idx in
Text("\(self.modelArray[idx].a) \(self.modelArray[idx].b) \(self.modelArray[idx].c)")
.foregroundColor(self.modelArray[idx].isActive ? .red : .green)
.onTapGesture {
self.modelArray[idx].isActive = true
for i in self.modelArray.indices {
if i != idx { self.modelArray[i].isActive = false }
}
}
}
}
}