Add item to array if Bool = false - swiftui

What i want to be able to do is to add items to an array if the Bool attached to them is true.
For example :
Here is my data model
struct ListModel: Identifiable, Hashable {
var id = UUID()
var name:String
var amount:Int
var code:String
var users:[User]
var items:[Item]
}
struct User: Identifiable, Hashable{
var id = UUID()
var foreName:String
var surName:String
var username:String
var pfp:String
}
struct Item:Identifiable, Hashable{
var id = UUID()
var name:String
var image:String?
var showItem:Bool
var amount:String
}
and when an the property showItem is true i want to add it to a new array
I have tried an approach like this
import SwiftUI
struct ListTest: View {
var list:ListModel
#State var nonHiddenItems:[Item] = []
var body: some View {
VStack{
ForEach(0..<list.items.count){item in
ItemView(item: list.items[item])
}
}
.onAppear(){
ForEach(0..<list.items.count){item in
if list.items[item].showItem == true{
nonHiddenItems.append(list.items[item])
}
}
}
}
}
struct ListTest_Previews: PreviewProvider {
static var previews: some View {
ListTest(list: ListModel(name: "Family Shopping", amount: 0, code: "1001", users: [User(foreName: "Elon", surName: "Musk", username: "Elon123", pfp: "Musk"), User(foreName: "Jeff", surName: "Bezos", username: "Jefferey", pfp: "Bezos")], items: [Item(name: "Cheese", image: "Pizza", showItem: true, amount: "5g"), Item(name: "Ham", showItem: false, amount: "5g")]))
}
}
But i get errors such as Type '()' cannot conform to 'View' on my ForEach loop
How can i fix m errors?
Many Thanks
Sean

A ForEach (uppercase) expression is a view builder, not a classic for loop. You cannot use ForEach in onAppear
Instead use forEach (lowercase)
(0..<list.items.count).forEach{ index in
if list.items[index].showItem == true{
nonHiddenItems.append(list.items[index])
}
}
or swiftier
nonHiddenItems = list.items.filter(\.showItem)

You should not use ForEach(...) in .onAppear(...) like you do.
I suggest you read the basics of SwiftUI here: https://developer.apple.com/tutorials/swiftui/
Try this approach:
struct ListTest: View {
var list: ListModel
#State var nonHiddenItems:[Item] = []
var body: some View {
VStack{
ForEach(list.items){ item in // <-- here
ItemView(item: item)
}
}
.onAppear{
for i in list.items.indices { // <-- here
if list.items[i].showItem {
nonHiddenItems.append(list.items[i])
}
}
}
}
}
EDIT-1: my guess is, what you probably wanted to do, is this:
struct ListTest: View {
var list: ListModel
var body: some View {
VStack {
ForEach(list.items.filter{$0.showItem}){ item in
ItemView(item: item)
}
}
}
}
This way you do not "duplicate" your source of truth array in ListModel.

Related

SwiftUI: How to set toggle isOn in a foreach loop

Hi I have a for each loop which goes through a struct with values and in this for each loop I am trying to create a toggle for each and each needs its own isOn parameter so in my struct I have added 'isToggleOn' and set it to true but i am getting the following error: Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
and this view gets called by another view which provides it with the Sandwich struct
And i am not sure what the best way to approach this and any help would be appreciated.
import SwiftUI
struct IngredientView: View {
var sandwich: Sandwich
var body: some View {
ForEach(sandwich.ingredients) { ingredient in
HStack {
Text(ingredient.name)
Toggle("", isOn: ingredient.isToggleOn)
.toggleStyle(CheckboxToggleStyle(style: .square))
.foregroundColor(.blue)
}
}
}
you could try something like this:
struct IngredientView: View {
#Binding var sandwich: Sandwich
var body: some View {
ForEach($sandwich.ingredients) { $ingredient in
HStack {
Text(ingredient.name)
Toggle("", isOn: $ingredient.isToggleOn)
.toggleStyle(CheckboxToggleStyle(style: .square))
.foregroundColor(.blue)
}
}
}
}
With , for example,
struct ContentView: View {
#State var sandwich = Sandwich()
var body: some View {
IngredientView(sandwich: $sandwich)
.onAppear {
sandwich.ingredients = [
Ingredient(isToggleOn: false, name: "ingredient-1"),
Ingredient(isToggleOn: true, name: "ingredient-2"),
Ingredient(isToggleOn: false, name: "ingredient-3")]
}
}
}
assuming something like this:
struct Ingredient: Identifiable, Codable {
let id = UUID()
var isToggleOn: Bool = false
var name: String = ""
}
struct Sandwich: Codable {
var ingredients: [Ingredient] = []
}

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

SwiftUI #State wierd wrappedValue behaviour

So straight to the point i have this code:
struct DataModel {
var option: String
}
struct ViewModel {
var selected: String? = "1"
var options = [DataModel(option: "1"), DataModel(option: "2"), DataModel(option: "3")]
}
struct ContentView: View {
#State var viewModel: ViewModel
var body: some View {
VStack {
Picker("test", selection: aBinder()) {
ForEach(viewModel.options, id: \.option) { option in
Text(option.option)
}
}
.background(Color.red)
}
}
func aBinder() -> Binding<String?> {
Binding<String?> {
viewModel.selected
} set: { value in
$viewModel.selected.wrappedValue = value
print($viewModel.selected.wrappedValue)
}
}
}
The value of "selected" in the viewModel doesn't change.
This works:
struct DataModel {
var option: String
}
struct ViewModel {
var selected: String = "1"
var options = [DataModel(option: "1"), DataModel(option: "2"), DataModel(option: "3")]
}
struct ContentView: View {
#State var viewModel: ViewModel
var body: some View {
VStack {
Picker("test", selection: $viewModel.selected) {
ForEach(viewModel.options, id: \.option) { option in
Text(option.option)
}
}
Button("press me", action: { print(viewModel.selected) })
}
}
}
But that doesn't make any sense. in both cases i use a binding to store the current value. What is going on? I'm pretty new to swiftUI so i might have missed how something works.
Thanks in advance
The types don’t match selected is a String and the options are a DataModel. The types have to match exactly.

Why does the TextField in the List stop updating?

I started studying SwiftUI and wanted to make a prototype of standard reminders, like in an iPhone. It seems nothing complicated, there is a List, in each cell a TextField.
But I ran into a problem: when we change the text in the TextField using onChange, then we accordingly tell the view model to update our objects.
And when the objects are updated, the entire List is redrawn and the editing of the current TextField is reset (you can neither remove more than one character, nor add). You have to click on the text again to continue editing.
Does anyone know how to treat this?
This is my code:
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
let name: String
func updateName(newName: String) -> Fruit {
return Fruit(name: newName)
}
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
func updateName(newName: String, fruit: Fruit) {
if let index = fruits.firstIndex(where: { $0.id == fruit.id }) {
fruits[index] = fruit.updateName(newName: newName)
}
}
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.fruits) { fruit in
ListViewRow(fruit: fruit)
}
}
.environmentObject(viewModel)
}
}
struct ListViewRow: View {
#EnvironmentObject var viewModel: ViewModel
#State var fruitTextField: String
let fruit: Fruit
init(fruit: Fruit) {
self.fruit = fruit
_fruitTextField = State(initialValue: fruit.name)
}
var body: some View {
TextField("", text: $fruitTextField)
.onChange(of: fruitTextField) { newValue in
viewModel.updateName(newName: newValue, fruit: fruit)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
You can simplify it even more with:
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
That being said, you really need to view the Apple Swift Tutorials that were linked in the comments.
Edit: Full Project Code for Lorem Ipsum:
//
// ContentView.swift
// FruitApp
//
// Created by Developer on 11/27/21.
//
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
struct ContentView: View {
var body: some View {
ListView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How do I update a List in SwiftUI?

My code is a little more complex than this so I created an example that gets the same error.
When I navigate into a view, I have a function I want to perform with a variable passed into this view. That function then produces an array. I then want to put that array into a List, but I get an error.
How do I get the List to show the produced array?
I think the issue is the List can't be updated because it already has the declared blank array.
struct ContentView : View {
#State var array = [String]()
var body: some View {
List(self.array,id: \.self) { item in
Text("\(item)")
}
.onAppear(perform: createArrayItems)
}
func createArrayItems() {
array = ["item1", "item2", "item3", "item4", "item5"]
}
}
You can use ObservableObject data providers(eg : ViewModel) with #Published properties.
struct ListView: View {
#ObservedObject var viewModel = ListViewModel()
var body: some View {
NavigationView {
List(){
ForEach(viewModel.items) { item in
Text(item)
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
#endif
class ListViewModel: ObservableObject {
#Published var items = ["item1", "item2", "item3", "item4", "item5","item6"]
func addItem(){
items.append("item7")
}
}
You can use combine framework to update the list.
Whenever a change is made in DataProvider Object it will automatically update the list.
struct ContentView : View {
#EnvironmentObject var data: DataProvider
var body: some View {
NavigationView {
NavigationButton(destination: SecondPage()) {
Text("Go to Second Page")
}
List {
ForEach(data.array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
}
Add items in the list
struct SecondPage : View {
#State var counter = 1
#EnvironmentObject var tempArray: DataProvider
var body: some View {
VStack {
Button(action: {
self.tempArray.array.append("item\(self.counter)")
self.counter += 1
}) {
Text("Add items")
}
Text("Number of items added \(counter-1)")
}
}
}
It will simply notify the change
import Combine
final class DataProvider: BindableObject {
let didChange = PassthroughSubject<DataProvider, Never>()
var array = [String]() {
didSet {
didChange.send(self)
}
}
}
You also need to do some update in the SceneDelegate. This update ensures that ContentView has a DataProvider object in the environment.
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(DataProvider()))
#txagPman
I too have your problem to understand how to modify a list.
I was able to write this code.
I hope it's useful.
import SwiftUI
struct ContentView: View {
#State private var array = createArrayItems()
// #State private var array = [""] - This work
// #State private var array = [] - This not work
#State private var text = ""
var body: some View {
VStack {
TextField("Text", text: $text, onCommit: {
// self.array = createArrayItems() - This work after press return on textfield
self.array.append(self.text)
}).padding()
List (self.array, id: \.self) {item in
Text("\(item)")
}
}
// .onAppear {
// self.array = createArrayItems() - This not work
// }
}
}
func createArrayItems() -> [String] {
return ["item_01","item_02","item_03","item_04" ]
}
A dumb UI is a good UI
Keep your views dumb try the following code to create a dynamic List
import UIKit
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
#State var array = [String]()
var body: some View {
List{
ForEach(array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
func createArrayItems()->[String] {
return ["item1", "item2", "item3", "item4", "item5","item6"]
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView(array: createArrayItems()))
Use this:
class ObservableArray<T>: ObservableObject {
#Published var array: [T]
init(array: [T] = ) {
self.array = array
}
init(repeating value: T, count: Int) {
array = Array(repeating: value, count: count)
}
}
struct YourView: View {
#ObservedObject var array = ObservableArray<String>()
var body: some View {
}
}