SwiftUI: When I toggle the Bool showAnswer, all the answers to all the questions are displayed. I need to toggle each answer separately - mvvm

I am unable to toggle the answer to each question independently. I tried adding a Bool to the struct holding the question and answer, but the error says the Bool is immutable.
https://github.com/williamallenmd/FlashCardTests
import SwiftUI
struct ItemsView: View {
#StateObject var vm = ItemViewModel()
#State private var showAnswer: Bool = false
var body: some View {
List(vm.items) { item in
VStack(alignment: .leading, spacing: 10) {
Button(item.question) {showAnswer.toggle()}
showAnswer ? Text(item.answer) : Text(" ")
}
}
.onAppear { vm.fetch()}
}
}

you could try this simple approach, works well for me:
With this approach, there is no need for every item to have its own State,
or for an array of variables with keys, or to change your model.
struct ItemsView: View {
#StateObject var vm = ItemViewModel()
#State private var showAnswer: Bool = false
#State private var selectedItem: ItemModel = ItemModel() // <--- here
var body: some View {
List(vm.items) { item in
VStack(alignment: .leading, spacing: 10) {
Button(item.question) {
selectedItem = item // <--- here
showAnswer = true
}
// --- here ---
if showAnswer && selectedItem.id == item.id {
Text(selectedItem.answer)
} else {
Text(" ")
}
}
}
.onAppear {
vm.fetch()
}
}
}

As the comments say, you only have one #State variable and it cannot change each Answer individually by itself. You either have to add a new property to vm.items which would be showAnswer. This would look like this:
import SwiftUI
struct ItemsView: View {
#StateObject var vm = ItemViewModel()
var body: some View {
List(vm.items) { item in
VStack(alignment: .leading, spacing: 10) {
Button(item.question) {item.showAnswer.toggle()}
item.showAnswer ? Text(item.answer) : Text(" ")
}
}
.onAppear { vm.fetch()}
}
}
Otherwise, you can make the showAnswer variable an array, enumerating the items and using their keys.

Related

Tapping on a View to also change its sibling views

Within a VStack, I have 3 views. A view's selection and colour are toggled when tapping on them. I want the previously selected View to be deselected when selecting the next view.
The tapGesture is implemented in each view. I am not sure what is the best way to achieve this.
Thanks.
Here is the code sample:
struct ContentView: View {
#State var tile1 = Tile()
#State var tile2 = Tile()
#State var tile3 = Tile()
var body: some View {
VStack {
TileView(tile: tile1 )
TileView(tile: tile2 )
TileView(tile:tile3 )
}
.padding()
}
}
struct Tile: Identifiable, Equatable{
var id:UUID = UUID()
var isSelected:Bool = false
}
struct TileView: View {
#State var tile:Tile
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill( tile.isSelected ? Color.red : Color.yellow )
.frame(height: 100)
.padding()
.onTapGesture {
tile.isSelected.toggle()
}
}
}
You need to relate the 3 tiles somehow. An Array is an option. Then once they are related you can change the selection at that level.
extension Array where Element == Tile{
///Marks the passed `tile` as selected and deselects other tiles.
mutating func select(_ tile: Tile) {
for (idx, t) in self.enumerated(){
if t.id == tile.id{
self[idx].isSelected.toggle()
}else{
self[idx].isSelected = false
}
}
}
}
Then you can change your views to use the new function.
struct MyTileListView: View {
#State var tiles: [Tile] = [Tile(), Tile(), Tile()]
var body: some View {
VStack {
ForEach(tiles) { tile in
TileView(tile: tile, onSelect: {
//Use the array to select the tile
tiles.select(tile)
})
}
}
.padding()
}
}
struct TileView: View {
//#State just create a copy of the tile `#Binding` is a two-way connection if needed
let tile:Tile
///Called when the tile is selected
let onSelect: () -> Void
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(tile.isSelected ? Color.red : Color.yellow)
.frame(height: 100)
.padding()
.onTapGesture {
onSelect()
}
}
}

Presenting Lists item in detail sheet swiftUI

I have a viewModel that with an item and a child view, I also present a sheet from a View and pass a selected item to that view. This in turn makes the selected item = item in the child view. The problem now is I have to dismiss the sheet and select a desired item be the value changes in the child view. This is a weird behaviour any help would be appreciated
My view Model
class ItemViewModel: ObservableObject {
#Injected(\.itemLocalRepository) var itemLocalRepository: ItemLocalRepository
#Published var items: [Item] { willSet { objectWillChange.send() } }
init(shoppingList: ShoppingList) {
self.shoppingList = shoppingList.item
// Printing shoppingList prints default value before changing to desired on on second selection
}
}
// Main View
struct FrequentView: View {
#State var selectedShoppingList: ShoppingList = ShoppingList.single
#State private var presentCreateSheet: Bool = false
var itemsList = [...]
var body: some View {
NavigationView {
ZStack {
ScrollView(.vertical, showsIndicators: false, content: {
LazyVStack(alignment: .leading, spacing: 15, pinnedViews: /*#START_MENU_TOKEN#*/[]/*#END_MENU_TOKEN#*/, content: {
ForEach(itemsList, id: \.id) { shoppingList in
Button {
self.selectedShoppingList = shoppingList
self.presentCreateSheet = true
} label: {
HomeRowView(shoppingList: shoppingList)
}
Divider()
.padding(.top, 0)
}
})
})
}
.sheet(isPresented: $presentCreateSheet, onDismiss: {
Task.init {
await viewModel.getList()
}
self.presentCreateSheet = false
}, content: {
ItemView(viewModel: ItemViewModel(shoppingList: selectedShoppingList))
})
}
}
}

Pass view as parameter to Button triggering it as a Modal

I'd like to have a custom button struct that receives a view as a parameter that will be shown as modal when the button is clicked. However, the view parameter is always empty, and I can't find the mistake I'm doing. My button struct looks like that:
struct InfoButton<Content:View>: View {
#State private var showingInfoPage: Bool
private var infoPage: Content
init(infoPage: Content, showingInfoPage: Bool) {
self.infoPage = infoPage
_showingInfoPage = State(initialValue: showingInfoPage)
}
var body: some View {
return
Button(action: {
self.showingInfoPage.toggle()
}) {
Image(systemName: "info.circle")
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(.white)
.padding()
}.sheet(isPresented: self.$showingInfoPage) {
self.infoPage
}.frame(minWidth: 0, maxWidth: .infinity, alignment: .topTrailing)
}
}
This button is placed in a navigation bar from a template I'm creating for multiple other views.
I think the most relevant parts of that template are these:
protocol TrainingView {
var title: String { get }
var subheadline: String { get }
var asAnyView: AnyView { get }
var hasInfoPage: Bool { get }
var infoPage: AnyView { get }
}
extension TrainingView where Self: View {
var asAnyView: AnyView {
AnyView(self)
}
var hasInfoPage: Bool {
false
}
var infoPage: AnyView {
AnyView(EmptyView())
}
}
struct TrainingViewTemplate: View {
#State var showInfoPage: Bool = false
#State var viewIndex: Int = 0
var body: some View {
//the views that conform to the template
let views: [TrainingView] = [
ExerciseView(),
TrainingSessionSummaryView()
]
return NavigationView {
ViewIterator(views, self.$viewIndex) { exerciseView in
VStack {
VStack {
Text(exerciseView.title)
.font(.title)
.fontWeight(.semibold).zIndex(1)
Text(exerciseView.subheadline)
.font(.subheadline)
Spacer()
exerciseView.asAnyView.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}.navigationBarItems(trailing: (exerciseView.hasInfoPage == true ? InfoButton(infoPage: exerciseView.infoPage, showingInfoPage: self.showInfoPage) : nil))
}
}
}
I debugged to the point, where the navigationBarItems are initialized. At that point, the exercise view has content for "hasInfoPage" and "infoPage" itself.
One exemplary Exercise View has a header like that:
struct ExerciseView: View, TrainingView {
var title: String = "Strength Session"
var subheadline: String = "Pushups"
var numberOfExercise: Int = 1
#State var ratingValue: Double = 0
#Environment(\.presentationMode) var presentationMode
var hasInfoPage: Bool = true
var infoPage = ExerciseDetailView()
So in this view, the infoPage gets initialized with the ExercieDetailView() which I receive in the TemplateView, but as soon as the InfoButton is clicked, the debugger shows an empty infoPage, even though the "showingInfoPage" variable contains the right value.
You don't confirm to protocol, so default infoPage from extension TrainingView is shown.
The solution is
struct ExerciseView: View, TrainingView {
// .. other code here
var infoPage = AnyView(ExerciseDetailView()) // << here !!
``

How to pass a string to a child view?

I want to pass the text in the textBox to the child view and create a scrollable Button there. As for the output status, we hope that 'a ~ c' are arranged vertically and that each is a button.
struct ContentView: View {
var textBox = ["a","b","c"]
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<textBox.count) { number in
ScrollText(text: self.textBox[number].lowercased())
}
}
}
}
}
struct ScrollText: View {
#Binding var text: String
#State private var flag: Bool = false
var body: some View {
Button(action: {
self.flag.toggle()
}) {
Text(text)
}
}
}
I'm not totally clear what the problem is, or what you want, but I solved some compiler errors in your code, and it's showing three buttons as expected:
struct ContentView : View {
var textBox = ["a","b","c"]
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false){
ForEach(textBox, id: \.self) { letter in
ScrollText(text: letter)
}
}
}
}
}
struct ScrollText: View {
var text: String
#State private var flag: Bool = false
var body: some View {
Button(action: {
self.flag.toggle()
}, label: {
Text(text)
})
}
}
Your question was how to pass a string, so you don't need #Binding for that. Just pass a string :)
If you're going to keep ScrollText untouched the here is possible modifications in ContentView which uses it
struct ContentView: View {
#State private var textBox = ["a","b","c"] // < make State, so modifiable
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<textBox.count) { number in
ScrollText(text: self.$textBox[number]) // < pass Binding as intended
}
}
}
}
}

Passing filtered #Bindable objects to multiple views in SwiftUI

I’m trying to pass a filter array to multiple views, but the filtering is not working. If I remove the filter, you can pass the array to the next view, but that leads to another error during the ForEach loop. I've posted all the code below.
Does anyone know how you can pass a filter version of a #Bindable array? Also why can't I print sport.name and sport.isFavorite.description in the ForEach loop?
I’m using swiftUI on Xcode 11.0 beta 5.
import SwiftUI
import Combine
struct Sport: Identifiable{
var id = UUID()
var name : String
var isFavorite = false
}
final class SportData: ObservableObject {
#Published var store =
[
Sport(name: "soccer", isFavorite: false),
Sport(name: "tennis", isFavorite: false),
Sport(name: "swimming", isFavorite: true),
Sport(name: "running", isFavorite: true)
]
}
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store.filter({$0.isFavorite}))
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.name)
Spacer()
Text(sport.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
struct TestingThree: View {
#Binding var sport : Sport
var body: some View {
VStack {
Text(sport.isFavorite.description)
.onTapGesture {
self.sport.isFavorite.toggle()
}
}
}
}
#if DEBUG
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
#endif
Filtering in your case might be better placed in the navigation view, due to your binding requirements.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store)
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
#State var onlyFavorites = false
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
if !self.onlyFavorites || sport.value.isFavorite {
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.value.name)
Spacer()
Text(sport.value.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
}
Now you can switch the isFavorite state either within the action implementation of a button, or while specifying the integration of you TestingTwo view.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store, onlyFavorites: true)
}
}
}
Regarding the second part of your question: Note the value addendum in the ForEach loop. You're dealing with as binding here (as ForEach($sports) indicates), hence sport is not an instance of Sport.
You can't get a #Binding from a computed property, since the computed property is computed dynamically. A typical way to avoid this is to pass in ids of the sports objects and the data store itself, whereby you can access the sports items via id from the store.
If you really want to pass a #Binding in you have to remove the filter (pass in an actually backed array) and modfy the ForEach like the following:
ForEach($sports.store) { (sport: Binding<Sport>) in