SwiftUI: How to set toggle isOn in a foreach loop - swiftui

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] = []
}

Related

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.

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

Issue with viewModel and TextField

I'm not sure whether it's a SwiftUI bug or it's my fault:
When I type some text in a TextField and press the return button on my keyboard (in order to hide my keyboard), the typed text is removed and the TextField is empty again. I've tried this solution on different simulators and on a real device as well. The issue appears every time. I'm using iOS 14.3, Xcode 12.4
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: CreateNewCardViewModel())
})
}
}
#SwiftPunk: Here is my second question:
Let's say my view model has an additional parameter (id):
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
This parameter needs to be passed when I create the view to my viewModel. For this example let's say we iterate over some elements that have the id:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// is the same problem as before
// because now I create every time a new viewModel, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
Your issue is here, that you did not create a StateObject in main View, and every time you pressed the key on keyboard you created a new model which it was empty as default!
import SwiftUI
struct ContentView: View {
#State var showNew = false
#StateObject var viewModel: CreateNewCardViewModel = CreateNewCardViewModel() // <<: Here
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: viewModel)
})
}
}
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}

SwiftUI .sheet Unexpectedly found nil while unwrapping an Optional value

The Problem
The following example highlights my issue better than I can explain it. I explicitly give an optional variable a value before presenting a sheet. This sheet, which requires a non-optional variable to init, doesn't register the value and says it is nil. I can't understand why this would be if I only ever call the sheet after the optional has been given a value. Any help would be greatly appreciated. Thanks!
What I have tried
In the example I replaced:
.sheet(isPresented: $showModalView, content: {
EditBookView(book: editingBook!) //Fatal error here
})
with:
.sheet(isPresented: $showModalView, content: {
if let book = editingBook {
EditBookView(book: book)
}
})
However, this just shows an empty sheet (implying that editingBook is empty). But, interestingly when I close this empty sheet and select another item in the list, the view appears as intended.
Reproducible example
import SwiftUI
struct Book: Identifiable {
var id: UUID
var title: String
init(title: String){
self.title = title
self.id = UUID()
}
}
struct ContentView: View {
#State var books = [Book]()
#State var showModalView = false
#State var editingBook: Book? = nil
var body: some View {
List{
ForEach(books){ book in
VStack(alignment: .leading){
Text(book.title)
.font(Font.title.bold())
Text("id: \(book.id.uuidString)")
.foregroundColor(.gray)
Button(action: {
editingBook = book
showModalView = true
}){
Text("Edit")
.foregroundColor(.accentColor)
}
.buttonStyle(PlainButtonStyle())
.padding(.top)
}
}
}
.padding()
.onAppear{
for i in 0...50 {
books.append(Book(title: "Book #\(i)"))
}
}
.sheet(isPresented: $showModalView, content: {
EditBookView(book: editingBook!) //Fatal error here
})
}
}
struct EditBookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit:
enum SheetChoice: Hashable, Identifiable {
case addContentView
case editContentView
var id: SheetChoice { self }
}
...
.sheet(item: $sheetChoice){ item in
switch item {
case .addContentView:
AddContentView()
.environmentObject(model)
case .editContentView:
//if let selectedContent = selectedContent {
ContentEditorView(book: selectedContent!, editingFromDetailView: false)
.environmentObject(model)
//}
}
}
Make sure you also use editingBook inside your body (not only sheet building block).
SwiftUI tracks which State variables are used in its body. When it’s not used, you might come into this weird situations when your body is called with ignored changes to that state variable.
So basically add this line at the beginning of your body:
var body: some View {
_ = editingBook
return <your view>
}
Alternatively, you can use this .sheet modifier version:
https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)
Following the answer from #msmialko, I suspect this is a compiler problem.
_ = self.<your_variable>
inside body solves the problem.
One possible workaround is moving out the sheet content into another View, and pass the Binding to the #Stete to it:
struct Book: Identifiable {
var id: UUID
var title: String
init(title: String){
self.title = title
self.id = UUID()
}
}
enum SheetChoice: Hashable, Identifiable {
case addContentView
case editContentView
var id: SheetChoice { self }
}
class MyModel: ObservableObject {
}
struct ContentView: View {
#State var books = [Book]()
#State var selectedContent: Book? = nil
#State var sheetChoice: SheetChoice? = nil
#StateObject var model = MyModel()
var body: some View {
List{
ForEach(books){ book in
VStack(alignment: .leading){
Text(book.title)
.font(Font.title.bold())
Text("id: \(book.id.uuidString)")
.foregroundColor(.gray)
Button(action: {
selectedContent = book
sheetChoice = .editContentView
}){
Text("Edit")
.foregroundColor(.accentColor)
}
.buttonStyle(PlainButtonStyle())
.padding(.top)
}
}
}
.padding()
.onAppear{
for i in 0...50 {
books.append(Book(title: "Book #\(i)"))
}
}
.sheet(item: $sheetChoice){
item in
SheetContentView(item: item, selectedContent: $selectedContent)
.environmentObject(model)
}
}
}
struct SheetContentView: View {
var item: SheetChoice
var selectedContent: Binding<Book?>
var body: some View {
switch item {
case .addContentView:
AddContentView()
case .editContentView:
ContentEditorView(book: selectedContent.wrappedValue!,
editingFromDetailView: false)
}
}
}
struct ContentEditorView: View {
var book: Book
var editingFromDetailView: Bool
var body: some View {
Text(book.title)
}
}
struct AddContentView: View {
var body: some View {
Text("AddContentView")
}
}

How to add an observable property when other properties change

I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.