Hi all thanks in advance.
I am having an issue when running app (not in preview), a textfield is not updating the state. I've not continued to expand the MVVM yet as I am getting caught up in this UI/Binding issue.
Not sure what have I missed here? I am passing a StateObject (view model instance) into the EnvironmentObject list, which is then accessed from an EnvironmentObject and the models array of elements in a view is iterated over, then further passing the iterated elements of the array to a Binding in another view which is then bound to a textfield to be edited by the user?
Specifically, the issue is:
When swipe action > edit on an expense in the ContentView to navigate to EditExpenseView, the textfields don't allow editing.
Note:
If I move the textfield up to the ExpenseList View, the binding to edit works. I thought that maybe the List(items) was the issue because it's iterating over an immutable collection.
I am using the index and passing the array binding via $expenses[index] which is avoiding accessing the immutable collection as its only being used to get the index of the list item the user will edit.
If your still reading, thanks for being awesome!
Let me know if I can add any further information or provide clarity.
Expense Model:
struct Expense: Equatable, Identifiable, Codable {
init(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus, budgetId: UUID?) {
self.date = date
self.description = description
self.amount = amount
self.type = type
self.status = status
self.budgetId = budgetId
}
static func == (lhs: Expense, rhs: Expense) -> Bool {
lhs.id == rhs.id
}
var id: UUID = UUID()
var date: Date
var description: String
var amount: Decimal
var type: ExpenseType
var status: ExpenseStatus
var budgetId: UUID?
}
ExpenseViewModel:
class ExpenseViewModel: ObservableObject, Identifiable {
#Published var expenses: [Expense] = []
func insertExpense(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus) -> Void {
expenses.insert(Expense(date: date, description: description, amount: amount, type: type, status: status, budgetId: nil), at:0)
}
func remove(_ expense: Expense) {
expenses.removeAll(where: {$0.id == expense.id})
}
}
App Entry:
import SwiftUI
#main
struct iBudgeteerApp: App {
#StateObject private var expenses = ExpenseViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(expenses)
}
}
}
Initial View:
struct ContentView: View {
#EnvironmentObject private var model: ExpenseViewModel
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
NavigationStack {
VStack {
Button("Add Row") {
model.insertExpense(date: Date(), description: "Groceries", amount: 29.94, type: .Expense, status: .Cleared)
}
ExpenseList(expenses: $model.expenses)
}
}
}
}
Expense List View:
struct ExpenseList: View {
#Binding var expenses: [Expense]
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
index in
HStack {
Text("\(index + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expenses[index].date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expenses[index].description)
}
HStack {
Text(expenses[index].description)
Spacer()
Text("\(expenses[index].amount as NSNumber, formatter: formatter)")
.foregroundColor( expenses[index].type == .Expense ? .red : .green)
Image(systemName: expenses[index].type == .Expense ? "arrow.down" : "arrow.up").foregroundColor( expenses[index].type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { expenses.remove(at: index) } ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: self.$expenses[index])
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
}
}
}
Edit Expense View:
struct EditExpenseView: View {
#Binding var expense: Expense
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
Form {
Section(header: Text("Editing: \(expense.description)")) {
VStack {
DatePicker(
"Date",
selection: $expense.date,
displayedComponents: [.date]
)
HStack {
Text("Name")
Spacer()
TextField("description",text: $expense.description)
.fixedSize().multilineTextAlignment(.trailing)
}
HStack {
Text("Amount")
Spacer()
TextField("0.00", value: $expense.amount, formatter: formatter).fixedSize()
}
Picker("Status", selection: $expense.status) {
ForEach(ExpenseStatus.allCases, id: \.self) {
status in
Text("\(status.rawValue)")
}
}
Picker("Type", selection: $expense.type) {
ForEach(ExpenseType.allCases, id: \.self) {
type in
Text("\(type.rawValue)")
}
}
}
}
}
}
}
UPDATE
It works in:
List ($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
ForEach($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
But not in:
List($expenses) {
$expense in
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer() }
HStack {
Text(expense.description)
Spacer()
Text("\(expense.amount as NSNumber, formatter: formatter)")
.foregroundColor( expense.type == .Expense ? .red : .green)
Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up").foregroundColor(expense.type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { //expenses.remove(expense)
} ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: $expense)
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
Disclaimer:
I couldn´t test this answer properly as your example is missing information and is not reproducible. Please consider posting a minimal reproducible example.
The issue is in these lines:
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
and then doing:
EditExpenseView(expense: self.$expenses[index])
You are not passing a binding reference of Expense on to your EditExpenseView but a binding to a copy of it. You are breaking the binding chain.
The following aproach should yield the desired result:
List ($expenses) { $expense in
HStack {
Text("\(expenses.firstIndex(of: expense) + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expense.description)
}
.....
and passing your Expense on to your subview:
EditExpenseView(expense: $expense)
Related
Usual caveat of being new to swiftui and apologies is this is a simple question.
I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.
I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.
I have the date to be used stored as a "#State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).
Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?
Code below. Thanks
import SwiftUI
struct TrainingSessionListView: View {
#StateObject var viewModel = TrainingSessionsViewModel()
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(session: session)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
Spacer()
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
print("filteredSessions called")
return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks
You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.
ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
sessionListItem(session: session)
}
Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)
For this second option you could do something like this.
Start create the Binding extension for the didSet and willSet
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
func willSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
Introduce the new state variable
#State var filteredSessions: [TrainingSession] = []
// removing the other var
We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value
Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval
self.filterSessions(displayDate)
As I said, this second method might be better for more complex filters
Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's #FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.
New code below seems to be working nicely now.
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionListView: View {
#FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var todayButton : some View {
Button(action: { self.displayDate = Date.now }) {
Text("Today")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
if filteredSessions.isEmpty {
Spacer()
Text("No Training Sessions found")
} else {
List {
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
}
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: todayButton, trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
I'm building an app which makes graphs and calculates some basic stuff. On the main screen, I have a ForEach loop inside a List that shows the saved charts. When entering the NavigationLink inside the List, the destination view does not correspond with the label shown (video below).
I had to use a custom ForEach extension to deal with the bindings https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/ (Apple announced that on iOS 15 ForEach will accept bindings, but I'm developing for iOS 14).
The code of the List:
List{
ForEach($chartData.calibrations) { index, data in
VStack{
NavigationLink(
destination: DetailedChartView(index: index, currentData: data, isDetailShown: $detailVisible),
isActive: $detailVisible,
label: {
SavedListItem(index: index, savedData: self.chartData.calibrations[index])
})
.navigationBarHidden(true)
}
}.onDelete(perform: removeRows)
}.id(UUID())
.listStyle(PlainListStyle())
The code of the label (SavedListItem)
struct SavedListItem: View {
var index: Int
var data: ChartDataObject
var formattedSlope = ""
var formattedOrigin = ""
var formattedCoef = ""
init(index: Int, savedData: ChartDataObject) {
self.data = savedData
self.index = index
self.formattedSlope = String(format: "%.2f", data.slope)
self.formattedOrigin = String(format: "%.2f", abs(data.origin))
self.formattedCoef = String(format: "%.3f", data.regressionCoef)
}
var body: some View {
HStack {
Text("\(index + 1)").bold()
VStack(alignment: .leading,spacing: 10) {
Text("\(data.name)").font(.title3)
Text("\(formatDate(data.date))")
}.padding()
Spacer()
if data.origin > 0 {
VStack {
VStack {
Text("y = \(self.formattedSlope)x + \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin == 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin < 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x - \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else {
Text("There was an error")
}
}
.padding()
}
private func formatDate(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let dateString = dateFormatter.string(from: date)
return dateString
}
}
And the code of DetailedChartView:
struct DetailedChartView: View {
#EnvironmentObject var calibrationsController: SavedCalibrationsController
var index: Int
#Binding var currentData: ChartDataObject
var copyData: ChartDataObject {
return currentData
}
#State var isEditing: Bool = false
#Binding var isDetailShown: Bool
var body: some View {
NavigationView{
VStack(spacing: 10) {
Text("y = \(currentData.origin) + \(currentData.slope)x")
Text("Slope: \(currentData.slope)")
Text("Origin: \(currentData.origin)")
Text("R2: \(currentData.regressionCoef)")
Divider()
RegressionChart(data: currentData).frame(height: 500)
}.navigationBarHidden(true)
}
.navigationBarTitle(currentData.name)
.toolbar(content: {
Button(action: {
self.isEditing = true
}, label: {
Text("Edit")
})
})
.sheet(isPresented: $isEditing, onDismiss: {
currentData = copyData
calibrationsController.saveData()
isEditing = false
isDetailShown = false
}, content: {
EditView(calibrationsController: calibrationsController, editVisible: $isEditing, isDetailShown: $isDetailShown, index: index, data: $currentData).environmentObject(calibrationsController)
})
}
}
When there is only one item the List behaves as expected.
This issue is driving me nuts: if I use a List (without the ForEach), everything works as expected but I cant change the ForEach because I lose the .onDelete() functionality, and deleting the items inside the detailed view (which has an edit button), gives me an index out of range error (another story...).
Sorry for the long post!
EDIT: Minimal reproductible example
https://drive.google.com/file/d/1WN_tGR_kVNVNqEOW054d6kMBlq8HGkF5/view?usp=sharing
remove
isActive: $detailVisible,
in MainList NavigationLink.
I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.
First, sorry for my bad English! I'm absolutely new to SwiftUI and I tried to create a Quiz App with multiple Choices and multiple Answers. I created a Button with a ForEach to Display the possible answers. Now I want to select the correct Answer and tap the check Button to validate the chosen Answer. There can be more then 1 correct Answer.
I tried this function but its only return, if there are one or two
//MARK:- Funktionen
func checkAnswer() {
if validateAnswer == quiz.correctAnswer {
print("Richtig")
} else {
print("Falsch")
}
}
I have no idea how to validate the chosen Answers with the correct answers. Can anyone help me?
Here is my Code:
QuizModel
struct Quiz: Identifiable {
var id: String = UUID().uuidString
var question: String
var howManyAnswers: String
var options: [PossibleAnswer]
var correctAnswer: [String]
var explain: String
}
extension Quiz: Equatable {}
struct PossibleAnswer : Identifiable, Equatable {
let id = UUID()
let text : String
}
ContentView
struct ContentView: View {
var quiz: Quiz
#State var isChecked:Bool = false
#State private var showAlert: Bool = false
#State var validateAnswer: [String] = ["Antwort 3", "Antwort 4"]
//MARK:- Answers
VStack {
ForEach(quiz.options) { answerOption in
QuizButtonView(isChecked: isChecked, title: answerOption.text)
.foregroundColor(.black)
.padding(2.0)
}
Spacer()
Divider()
HStack {
//MARK:- Button Überprüfen & Zurück
Button(action: {
print("Ich gehe zurück")
}, label: {
Text("Zurück")
})
Button(action: {
checkAnswer()
print("Ich überprüfe...")
self.showAlert.toggle()
}, label: {
Text("Überprüfen")
})
.padding(.leading, 200)
And my CheckButtonView
struct QuizButtonView: View {
#State var isChecked:Bool = false
var title:String
func toggle(){
isChecked.toggle()
if self.isChecked == true {
print("Antwort wurde ausgewählt")
} else if self.isChecked == false {
print("Antwort wurde wieder abgewählt")
}
}
var body: some View {
VStack {
Button(action: toggle) {
HStack{
Text(title)
.font(.system(size: 16))
.foregroundColor(.black)
.lineLimit(3)
Spacer()
Image(systemName: isChecked ? "checkmark.square.fill": "square")
}
}
Thank you!
You just need to create a state variable of an array of booleans:
struct ContentView: View {
private let quiz: Quiz
#State private var userSelections: [Bool]
init(quiz: Quiz) {
self.quiz = quiz
_userSelections = State(initialValue: Array(repeating: false, count: quiz.options.count))
}
var body: some View {
VStack {
ForEach(0..<quiz.options.count) { index in
QuizButtonView(isChecked: userSelections[index], title: quiz.options[index].text)
.foregroundColor(.black)
.padding(2.0)
}
}
}
func checkAnswer() {
let userSelectionTexts = Set(userSelections.enumerated().map({ quiz.options[$0.offset].text }))
let correctAnswers = Set(quiz.correctAnswer)
let isAllSelectionsTrue = userSelectionTexts == correctAnswers
let isAllSelectionsFalse = userSelectionTexts.intersection(correctAnswers).isEmpty
let isAnySelectionsTrue = !isAllSelectionsFalse
}
}
I have a NSManagedObject Subclass coming from a core data entity MyEntity with:
#NSManaged public var name: String
#NSManaged public var date: Date
and a list. I prepared a ScrollView that holds Buttons (?) that could be used to filter the list
func groupYears(fetchedResults: FetchedResults<MyEntity>) -> [Int] {
var result = [Int]()
for fetchedResult in fetchedResults {
let component = Calendar.current.dateComponents([.year], from: fetchedResult.date)
if !result.contains(component.year!) {
result.append(component.year!)
}
}
return result
}
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \MyEntity.date, ascending: true)]) var fetchedResults: FetchedResults<MyEntity>
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
Button(action: {}) { Text("all") }
ForEach(groupYears(fetchedResults: fetchedResults), id: \.self) { year in
Button(action: {}) { Text(String(year)) }
}
}
}
List(fetchedResults, id: \.self) { result in
VStack(alignment: .leading) {
Text(result.name)
Text(dateFormatter.string(from: result.date))
}
}
}
I want to click a button and the list should dynamically filter the related year.
How can I filter the list dynamically?
You can add a property to store your selected year:
#State var selectedYear: Int?
Button(action: {
self.selectedYear = nil
}) {
Text("all")
}
ForEach(groupYears(fetchedResults: fetchedResults), id: \.self) { year in
Button(action: {
self.selectedYear = year
}) {
Text(String(year))
}
}
And then filter the list by this selected year:
ForEach(fetchedResults.filter({ isDateInSelectedYear($0.date) }), id: \.self) { ...
func isDateInSelectedYear(_ date: Date) -> Bool {
if selectedYear == nil {
return true
}
return Calendar.current.component(.year, from: date) == selectedYear
}