State variable not update in SwiftUI - swift

I have struct DBScrollViewCellWrapper. that display contain
struct DBScrollViewCellWrapper: View, Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
#State var showSelectionLine: Bool = false
var body: some View {
VStack(spacing: 0){
view
if self.showSelectionLine{
Rectangle()
.frame(width: 10, height: 1)
.foregroundColor(.red)
}
}
}
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id }
}
then generate number of DBScrollViewCellWrapper cell. when tap on cell, display tapped cell selected with line.
struct DBScrollView: View {
let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [DBScrollViewCellWrapper],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { $0 } //DBScrollViewCellWrapper(view: $0)
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(spacing: itemSpacing){
ForEach(self.views) { item in
item
.padding(5)
.border(Color.black)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views, id: \.id) { item in
item
.padding(5)
.border(Color.clear)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
value.showSelectionLine = true
completion(value,index)
}
}
Preview Code:
struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{DBScrollViewCellWrapper(view: AnyView(Text("\($0)")))}
DBScrollView(views: arrView, isHorizontal: false) { (cell, inx) in
cell.showSelectionLine = true
}
}
}
Problem
when tapped on cell, changed the value of cell but that not update.

Redesigned code for selection
struct DBScrollView: View {
private let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var selectedIndex: Int = -1
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [AnyView],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { DBScrollViewCellWrapper(view: AnyView($0))}
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(alignment: .center, spacing: itemSpacing){
ForEach(self.views.indices, id:\.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.foregroundColor(self.selectedIndex == index ? Color.yellow : Color.white)
.padding(5)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views.indices, id: \.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.padding(5)
.border(Color.white)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
completion(value,index)
}
}
struct DBScrollViewCellWrapper: Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id } } struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{AnyView(Text("\($0)"))}
DBScrollView(views: arrView, isHorizontal: true) { (cell, inx) in
}
.background(Color.black)
}
}

Related

How to set variables in a class in a structure that also uses variables from the class Swift

I'm trying to pull the variables from my class, SolutionAggregationViewController, and use them in my structure NewRatingView in place of the current state variables, currentRating, usualRating, and totalRatings. I also have a variable in SolutionAggregationViewController, userCurrentRating, which I want to write to Firebase using the structure of interactive 5 stars I've created in NewRatingView. I'm wondering how I need to have these two items communicate in that way.
struct NewRatingView: View {
var currentRatingTest = SolutionAggregationViewController()
var schools = ["North Avenue", "West Village", "Brittain"]
#State private var location = "West Village"
#State private var userCurrentRating: CGFloat = 0.0
#State private var userUsualRating: CGFloat = 0.0
#State private var isUserRatingCurrent = true
#State private var currentRating: CGFloat = 0
#State private var isUserRatingUsual = true
#State private var usualRating: CGFloat = 0
#State private var totalRatings: CGFloat = 1
var body: some View {
NavigationView{
ScrollView {
VStack(spacing: 16) {
Divider()
Text("Choose a dining location:")
.bold()
Picker("Choose a dining location:", selection: $location) {
ForEach(schools, id: \.self) {
Text($0)
}
}
Text("Current Rating")
.bold()
if self.isUserRatingCurrent {
VStack {
RatingsViewCurrent(onDidRateCurrent: self.userDidRateCurrent, ratingCurrent: self.$userCurrentRating)
.frame(width: .infinity, height: 100, alignment: .center)
}
} else {
VStack {
RatingsViewCurrent(ratingCurrent: self.$currentRating)
.frame(width: .infinity, height: 100, alignment: .center)
if userCurrentRating == 0.0 {
Button("Rate") {
self.isUserRatingCurrent = false
}
}
}
}
Text("Usual Rating")
.bold()
if self.isUserRatingUsual {
VStack {
RatingsViewUsual(onDidRateUsual: self.userDidRateUsual, ratingUsual: self.$userUsualRating)
.frame(width: .infinity, height: 100, alignment: .center)
Button("Submit Rating!") {
self.isUserRatingUsual = true
}
}
} else {
VStack {
RatingsViewUsual(ratingUsual: self.$usualRating)
.frame(width: .infinity, height: 100, alignment: .center)
if userUsualRating == 0.0 {
Button("Rate") {
self.isUserRatingUsual = false
}
}
}
}
}
}.navigationTitle("Add a Rating")
}
}
private func userDidRateCurrent(_ ratingCurrent: Int) {
// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { self.isUserRatingCurrent = false })
self.storeRatingInformation()
}
private func userDidRateUsual(_ ratingUsual: Int) {
self.storeRatingInformation()
}
private func storeRatingInformation() {
let id = location
print("StoredInformation")
let ratingData = ["location": location, "currentRating": currentRating, "usualRating": usualRating, "totalRatings": totalRatings] as [String : Any]
let db = Firestore.firestore()
db.collection("RatingInformation").document(id)
.setData(ratingData) { error in
if let error = error {
print(error)
return
}
}
print("RatingData: \(ratingData)")
}
}
class SolutionAggregationViewController: UIViewController {
var db: Firestore!
struct NewRating {
let id: String
let location: String
let currentRating: Float
let usualRating: Float
let totalRatings: Float
init(id: String, location: String, currentRating: Float, usualRating: Float, totalRatings: Float) {
self.id = id
self.location = location
self.currentRating = currentRating
self.usualRating = usualRating
self.totalRatings = totalRatings
}
}
let NewRating = NewRating(id: "West Village", location: "West Village", currentRating: 2.0, usualRating: 2.0, totalRatings: 4.0)
// [END restaurant_struct]
struct Rating {
let userCurrentRating: Float
}
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
}
func addRatingTransaction(restaurantRef: DocumentReference, userCurrentRating: Float) {
let ratingRef: DocumentReference = restaurantRef.collection("RatingInformation").document()
db.runTransaction({ (transaction, errorPointer) -> Any? in
do {
let restaurantDocument = try transaction.getDocument(restaurantRef).data()
guard var restaurantData = restaurantDocument else { return nil }
// Compute new number of ratings
let numRatings = restaurantData["totalRatings"] as! Float
let newNumRatings = numRatings + 1
// Compute new average rating
let avgRating = restaurantData["currentRating"] as! Float
let oldRatingTotal = avgRating * Float(numRatings)
let newAvgRating = (oldRatingTotal + userCurrentRating) / Float(newNumRatings)
// Set new restaurant info
restaurantData["totalRatings"] = newNumRatings
restaurantData["currentRating"] = newAvgRating
// Commit to Firestore
transaction.setData(restaurantData, forDocument: restaurantRef)
transaction.setData(["userCurrentRating": userCurrentRating], forDocument: ratingRef)
} catch {
}
return nil
}) { (object, err) in
}
}
}

Changing translation of a DragGesture swift

I am working with a slider and dealing with translation. Once the slider performs an action, I mock an api call and then on completion the image is unlocked but now I would like to return the slider to the original position once the api call is completed. Here is what the slider looks like in code.
struct DraggingComponent: View {
#Binding var isLocked: Bool
#Binding var isLoading: Bool
let maxWidth: CGFloat
#State private var width = CGFloat(50)
private let minWidth = CGFloat(50)
init(isLocked: Binding<Bool>, isLoading: Binding<Bool>, maxWidth: CGFloat) {
_isLocked = isLocked
self._isLoading = isLoading
self.maxWidth = maxWidth
}
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.black)
.opacity(width / maxWidth)
.frame(width: width)
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
progressView(isShown: isLoading)
image(name: "lock.open", isShown: !isLocked && !isLoading)
}
.animation(.easeIn(duration: 0.35).delay(0.55), value: !isLocked && !isLoading)
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked || isLoading),
alignment: .trailing
)
.simultaneousGesture(
DragGesture()
.onChanged { value in
guard isLocked else { return }
if value.translation.width > 0 {
width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
}
}
.onEnded { value in
guard isLocked else { return }
if width < maxWidth {
width = minWidth
UINotificationFeedbackGenerator().notificationOccurred(.warning)
} else {
UINotificationFeedbackGenerator().notificationOccurred(.success)
withAnimation(.spring().delay(0.5)) {
isLocked = false
}
}
}
)
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0), value: width)
}
private func image(name: String, isShown: Bool) -> some View {
Image(systemName: name)
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.black)
.frame(width: 42, height: 42)
.background(RoundedRectangle(cornerRadius: 14).fill(.white))
.padding(4)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
private func progressView(isShown: Bool) -> some View {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}
Where it is used:
#State private var isLocked = true
#State private var isLoading = false
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, isLoading: $isLoading, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
.padding(.bottom, 20)
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
}
private func simulateRequest() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isLoading = false
}
}
How can I get the translation back to the initial position.
Pull your width #State up into the containing view and pass it on as #Binding. After simulateRequest set it to its initial state.
In your DraggingComponent use
struct DraggingComponent: View {
#Binding var width: CGFloat
.....
in the View that contains DraggingComponent:
#State private var width = CGFloat(50)
and:
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
self.width = CGFloat(50) //reset it here probably with animation
}

Swipe to delete function

using my code below how can I swipe right to left on a Task to have an option to be able to delete it.
I have tried using .SwipeAction but have gotten no luck with using that and am stuck with what to use
Here is my view, I am using core data
import CoreData
import SwiftUI
struct TaskView: View {
#FetchRequest(sortDescriptors: []) var tasks: FetchedResults<Tasks>
#State private var isPresented = false
#EnvironmentObject var taskModel: TaskViewModel
#Environment(\.managedObjectContext) var moc
#State var TaskStatus: [String] = ["Todays Tasks", "Upcomming", "Completed"]
#Namespace var animation
var body: some View {
ScrollView{
VStack{
HStack{
Spacer()
Text(TaskStatus[0])
.font(.system(size: 30))
.fontWeight(.semibold)
Button {
print("")
} label: {
Text("May 2022")
.foregroundColor(.black)
.padding(.horizontal)
.font(.system(size: 25))
}
Spacer()
}
.padding()
.padding(.top)
customBar()
.padding()
ForEach(tasks,id: \.self){item in
Task(name: item.title ?? "Task Name", desc: item.desc ?? "Task Desc", image: item.icon ?? "sportscourt", image1: "clock", Time: "10", Colour: item.colour ?? "70d6ff", deadline: item.deadline ?? Date())
}
}
}
.overlay(alignment: .bottom){
HStack{
Spacer()
Button {
isPresented.toggle()
} label: {
Image(systemName: "plus.square")
.font(.system(size: 50))
.foregroundColor(.black)
}
.fullScreenCover(isPresented: $isPresented, content: AddTask.init)
.padding()
}
.padding(.horizontal)
}
}
func deleteTask(at offsets: IndexSet) {
for offset in offsets {
let tasky = tasks[offset]
moc.delete(tasky)
}
try?
moc.save()
}
#ViewBuilder
func customBar()-> some View{
let tabs = ["Today", "Upcoming"," Completed"]
HStack(spacing:10){
ForEach(tabs,id: \.self){tab in
Text(tab)
.font(.system(size: 20))
.scaleEffect(0.9)
.foregroundColor(taskModel.currentTab == tab ? .white:.black)
.padding(.vertical,6)
.frame(maxWidth:.infinity)
.background{
if taskModel.currentTab == tab{
Capsule()
.fill(Color(hex: "ff006e"))
.matchedGeometryEffect(id: "TAB", in: animation)
}
}
.contentShape(Capsule())
.onTapGesture{
withAnimation{taskModel.currentTab = tab}
}
}
}
}
}
struct TaskView_Previews: PreviewProvider {
static var previews: some View {
TaskView()
.previewDevice(PreviewDevice(rawValue: "iPhone 13"))
TaskView()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
}
}
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
I would like someone to be able to point me in the right direction of what I need to do please.
Many Thanks
Here comes my version of a custom SwipeAction for Non-Lists:
It's close to the original, only can't cancel on taps outside of the container. Any improvements or suggestions are welcome.
Usage:
ItemView(item)
.mySwipeAction { // red + trash icon as default
deleteMyItem(item)
}
ItemView(item)
.mySwipeAction(color: .green, icon: "flag" ) { // custom color + icon
selectMyItem(item)
}
Example:
Code:
extension View {
func mySwipeAction(color: Color = .red,
icon: String = "trash",
action: #escaping () -> ()) -> some View {
return self.modifier(MySwipeModifier(color: .red, icon: "trash", action: action ))
}
}
struct MySwipeModifier: ViewModifier {
let color: Color
let icon: String
let action: () -> ()
#AppStorage("MySwipeActive") var mySwipeActive = false
#State private var contentWidth: CGFloat = 0
#State private var isDragging: Bool = false
#State private var isDeleting: Bool = false
#State private var isActive: Bool = false
#State private var dragX: CGFloat = 0
#State private var iconOffset: CGFloat = 40
let miniumDistance: CGFloat = 20
func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
.overlay( GeometryReader { geo in Color.clear.onAppear { contentWidth = geo.size.width }})
.offset(x: -dragX)
Group {
color
Image(systemName: icon)
.symbolVariant(.fill)
.foregroundColor(.white)
.offset(x: isDeleting ? 40 - dragX/2 : iconOffset)
}
.frame(width: max(dragX, 0))
// tap on red area after being active > action
.onTapGesture {
withAnimation { action() }
}
}
.contentShape(Rectangle())
// tap somewhere else > deactivate
.onTapGesture {
withAnimation {
isActive = false
dragX = 0
iconOffset = 40
mySwipeActive = false
}
}
.gesture(DragGesture(minimumDistance: miniumDistance)
.onChanged { value in
// if dragging started new > reset dragging state for all (others)
if !isDragging && !isActive {
mySwipeActive = false
isDragging = true
}
if value.translation.width < 0 {
dragX = -min(value.translation.width + miniumDistance, 0)
} else if isActive {
dragX = max(80 - value.translation.width + miniumDistance, -30)
}
iconOffset = dragX > 80 ? -40+dragX/2 : 40-dragX/2
withAnimation(.easeOut(duration: 0.3)) { isDeleting = dragX > contentWidth*0.75 }
// full drag > action
if value.translation.width <= -contentWidth {
withAnimation { action() }
mySwipeActive = false
isDragging = false
isActive = false
return
}
}
.onEnded { value in
withAnimation(.easeOut) {
isDragging = false
// half drag > change to active / show icon
if value.translation.width < -60 && !isActive {
isActive = true
mySwipeActive = true
} else {
isActive = false
mySwipeActive = false
}
// in delete mode > action
if isDeleting { action() ; return }
// in active mode > show icon
if isActive {
dragX = 80
iconOffset = 0
return
}
dragX = 0
isDeleting = false
}
}
)
// reset all if swipe in other cell
.onChange(of: mySwipeActive) { newValue in
print("changed", newValue)
if newValue == false && !isDragging {
withAnimation {
dragX = 0
isActive = false
isDeleting = false
iconOffset = 40
}
}
}
}
}

Questions are not updated after "Next" button is pressed

I'm working on a quiz app, and stacked with updating the UI with the next set of questions. The first set is loading just fine, but when I'm trying to get the next set of question by clicking on the "Next" button - nothing happened. When I tried debugging, I noticed that instead of updating the questions, the questions are added to the previous set of questions. Please help me figure out what am I doing wrong.
Here is my code:
import Foundation
import Combine
final class QuizManager: ObservableObject {
#Published var quizQuestions: [QuizModel] = Bundle.main.decode("file.json")
var imageIndex = 0
var possibleAnswers = [String]()
var correctAnswers = 0
var questionsAsked = 0
init() {
getRandomQuestion()
}
func getRandomQuestion() {
imageIndex = Int.random(in: 0..<quizQuestions.count)
if quizQuestions[imageIndex].isCompleted {
imageIndex = Int.random(in: 0..<quizQuestions.count)
}
possibleAnswers.append(quizQuestions[imageIndex].description)
var index1 = Int.random(in: 0..<quizQuestions.count)
var index2 = Int.random(in: 0..<quizQuestions.count)
if index1 == imageIndex && index1 == index2 {
index1 = Int.random(in: 0..<quizQuestions.count)
} else {
possibleAnswers.append(quizQuestions[index1].description)
}
if index2 == imageIndex && index1 == index2 {
index2 = Int.random(in: 0..<quizQuestions.count)
} else {
possibleAnswers.append(quizQuestions[index2].description)
}
possibleAnswers.shuffle()
}
func checkAnswer(answer: String) -> Bool {
questionsAsked += 1
if quizQuestions[imageIndex].description == answer {
correctAnswers += 1
}
quizQuestions[imageIndex].isCompleted = true
return quizQuestions[imageIndex].description == answer
}
}
import SwiftUI
struct QuizQestionsView: View {
#ObservedObject private var quizManager = QuizManager()
#State private var isCorrect = false
#State private var correctAnswer = 0
#State private var answerSelected = ""
#State private var isTapped = false
var body: some View {
ScrollView {
VStack {
ImageView(name: quizManager.quizQuestions[quizManager.imageIndex].image,
contentMode: .scaleAspectFit,
tintColor: .black)
.frame(minWidth: 150, idealWidth: 200, maxWidth: 250, minHeight: 150, idealHeight: 200, maxHeight: 250)
Spacer()
VStack {
ForEach(quizManager.possibleAnswers, id: \.self) { answer in
QuestionsView(answer: answer) {
self.isCorrect = self.quizManager.checkAnswer(answer: answer)
self.answerSelected = answer
self.isTapped = true
}
.disabled(isTapped)
.overlay(
RoundedRectangle(cornerRadius: 16.0)
.stroke(getColor(answer), lineWidth: 1)
)
}
}
Spacer()
Button(action: {
self.quizManager.getRandomQuestion()
}) {
Text("NEXT")
}
}
}
}
func getColor(_ tag: String) -> Color {
if answerSelected == tag {
if isCorrect {
return Color.green
} else {
return Color.red
}
} else {
if isTapped && !isCorrect {
if tag == quizManager.quizQuestions[quizManager.imageIndex].description {
return Color.green
}
}
}
return Color.accentColor
}
The QuestionsView looks like this:
var answer: String
var onTap: () -> Void
var body: some View {
Button(action: {
self.onTap()
}) {
Text(answer)
}
}
}
In getRandomQuestion(), as you've already figured out, all of your code appends to the end of the array.
At the beginning of the function, you could clear out the array:
func getRandomQuestion() {
possibleAnswers = []
//the rest of the existing code here
}

SwiftUI - Fatal error: index out of range on deleting element from an array

I have an array on appstorage. I'm displaying its elements with foreach method. It has swipe to delete on each element of the array. But when i delete one, app is crashing. Here is my first view;
struct View1: View {
#Binding var storedElements: [myElements]
var body: some View{
GeometryReader {
geometry in
VStack{
ForEach(storedElements.indices, id: \.self){i in
View2(storedElements: $storedElements[i], pic: $storedWElements[i].pic, allElements: $storedElements, index: i)
}
}.frame(width: geometry.size.width, height: geometry.size.height / 2, alignment: .top).padding(.top, 25)
}
}
}
And View 2;
struct View2: View {
#Binding var storedElements: myElements
#Binding var pic: String
#Binding var allElements: [myElements]
var index: Int
#State var offset: CGFloat = 0.0
#State var isSwiped: Bool = false
#AppStorage("pics", store: UserDefaults(suiteName: "group.com.some.id"))
var arrayData: Data = Data()
var body : some View {
ZStack{
Color.red
HStack{
Spacer()
Button(action: {
withAnimation(.easeIn){
delete()}}) {
Image(systemName: "trash").font(.title).foregroundColor(.white).frame(width: 90, height: 50)
}
}
View3(storedElements: $storedElements).background(Color.red).contentShape(Rectangle()).offset(x: self.offset).gesture(DragGesture().onChanged(onChanged(value:)).onEnded(onEnd(value:)))
}.frame(width: 300, height: 175).cornerRadius(30)
}
}
func onChanged(value: DragGesture.Value) {
if value.translation.width < 0 {
if self.isSwiped {
self.offset = value.translation.width - 90
}else {
self.offset = value.translation.width
}
}
}
func onEnd(value: DragGesture.Value) {
withAnimation(.easeOut) {
if value.translation.width < 0 {
if -value.translation.width > UIScreen.main.bounds.width / 2 {
self.offset = -100
delete()
}else if -self.offset > 50 {
self.isSwiped = true
self.offset = -90
}else {
self.isSwiped = false
self.offset = 0
}
}else {
self.isSwiped = false
self.offset = 0
}
}
}
func delete() {
//self.allElements.remove(at: self.index)
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
}
}
OnChange and onEnd functions are for swiping. I think its the foreach method that is causing crash. Also tried the commented line on delete function but no help.
And I know its a long code for a question. I'm trying for days and tried every answer here but none of them solved my problem here.
In your myElements class/struct, ensure it has a unique property. If not add one and upon init set a unique ID
public class myElements {
var uuid: String
init() {
self.uuid = NSUUID().uuidString
}
}
Then when deleting the element, instead of
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
Use
self.allElements.removeAll(where: { a in a.uuid == storedElements.uuid })
This is array bound safe as it does not use an index