Questions are not updated after "Next" button is pressed - swift

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
}

Related

Want to "deal" cards in Swift: how to stop cards from changing sizes as more cards are dealt

I need help with this animation. Basically, I want to deal 12 cards when I tap on the "deck." However, I want to deal it so that the sizes of each card is already set; I don't want the card size to change as more cards come onto screen. Put differently, to put 12 cards on the screen, at the end, we have 3 rows of 4 cards. But the Swift system tries to put 3 cards per row, and then moves to 4 cards per row. This changing of card size looks very weird, so I was hoping to put in some code that makes it so the card size doesn't change.
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: GameViewModel
#Namespace private var dealingNamespace
var body: some View {
VStack {
newGameButton
gameBody
HStack {
deckBody
discardedDeckBody
}
shuffleButton
}
}
var newGameButton: some View {
Button(action: { viewModel.newGame() }) { Text("New Game")
.font(.caption) }
}
var gameBody: some View {
AspectVGrid(items: viewModel.playingCards, aspectRatio: 2/3) { card in
CardView(viewModel: viewModel, card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.padding(4)
// .transition(AnyTransition.asymmetric(insertion: .identity, removal: .scale))
.onTapGesture {
withAnimation {
viewModel.choose(card)
}
}
.foregroundColor(viewModel.borderColor(for: card))
}
.padding(.horizontal)
}
private func dealAnimation(for cardNumber: Int, _ totalCardsToDeal: Int) -> Animation {
let delay = Double(cardNumber) * (CardConstants.totalDealDuration / Double(totalCardsToDeal))
return Animation.easeInOut(duration: CardConstants.dealDuration).delay(delay)
}
var deckBody: some View {
ZStack {
ForEach(viewModel.undealtCards) { card in
CardView(viewModel: viewModel, card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
// .transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
}
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.foregroundColor(CardConstants.color)
.onTapGesture {
for cardNumber in Array(0..<viewModel.numberOfCardsToDeal()) {
withAnimation(dealAnimation(for: cardNumber, viewModel.numberOfCardsToDeal())) {
viewModel.deal()
}
}
}
}
var discardedDeckBody: some View {
ZStack {
ForEach(viewModel.discardedCards()) { card in
CardView(viewModel: viewModel, card: card)
}
}
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.foregroundColor(CardConstants.color)
}
private struct CardConstants {
static let color = Color.red
static let aspectRatio: CGFloat = 2/3
static let dealDuration: Double = 0.5
static let totalDealDuration: Double = 2
static let undealtHeight: CGFloat = 90
static let undealtWidth = undealtHeight * aspectRatio
}
var shuffleButton: some View {
Button("Shuffle") {
withAnimation {
viewModel.shuffle()
}
}
}
}
struct CardView: View {
#ObservedObject var viewModel: GameViewModel
let card: GameModel.Card
var body: some View {
GeometryReader { geometry in
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
shape.fill().foregroundColor(.white)
shape.stroke(lineWidth: DrawingConstants.lineWidth)
viewModel.symbol(for: card)
.padding(.all)
shape.foregroundColor(viewModel.cover(for: card))
}
}
}
}
struct DrawingConstants {
static let cornerRadius: CGFloat = 10
static let lineWidth: CGFloat = 3
static let fontScale: CGFloat = 0.7
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = GameViewModel()
ContentView(viewModel: game)
.previewInterfaceOrientation(.portrait)
}
}
AspectVGrid
import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
var items: [Item]
var aspectRatio: CGFloat
var content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, #ViewBuilder content: #escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
let computedWidth: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
let width: CGFloat = computedWidth > 65 ? computedWidth : 65
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) { item in
content(item).aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = (itemCount + (columnCount - 1)) / columnCount
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
Model
import Foundation
import SwiftUI
struct GameModel {
private(set) var cards: Array<Card>
var undealtCards: Array<Card>
var discardedCards: Array<Card> = []
private(set) var playingCards: Array<Card>
private var selectedCards: Array<Card> = []
private static var colors = [ContentColor.red, ContentColor.green, ContentColor.purple]
private static var shapes = [ContentShape.squiggly, ContentShape.circle, ContentShape.diamond]
private static var fillings = [ContentFilling.empty, ContentFilling.full, ContentFilling.striped]
private static var numbers = [1, 2, 3]
func numberOfUndealtCards() -> Int {
return undealtCards.count
}
mutating func deal() {
playingCards.append(undealtCards[0])
undealtCards.remove(at: 0)
}
func numberOfCardsToDeal() -> Int {
if numberOfUndealtCards() == 81 {
return 12
} else {
return 3
}
}
mutating func resetCards() {
if !playingCards.first(where: { $0 == selectedCards.first })!.isMatched! {
selectedCards.forEach { card in
let unmatchedIndex = playingCards.firstIndex(of: card)!
playingCards[unmatchedIndex].isMatched = nil
}
selectedCards = []
}
}
mutating func shuffle() {
playingCards = playingCards.shuffled()
}
mutating func choose(_ card: Card) {
if let chosenIndex = playingCards.firstIndex(where: { $0.id == card.id }) {
if selectedCards.count < 3 || selectedCards.count == 3 && !selectedCards.contains(playingCards[chosenIndex]) {
if selectedCards.count == 3 { resetCards() }
if !playingCards[chosenIndex].isSelected {
playingCards[chosenIndex].isSelected = true
selectedCards.append(playingCards[chosenIndex])
if selectedCards.count == 4 {
let chosenCard = playingCards[chosenIndex]
for card in selectedCards {
if card != chosenCard {
discardedCards.append(card)
let matchedIndex = playingCards.firstIndex(of: card)!
playingCards.remove(at: matchedIndex)
}
}
selectedCards = [chosenCard]
}
if selectedCards.count == 3 {
if formSet(by: selectedCards) {
selectedCards.forEach { card in
let index = playingCards.firstIndex(of: card)!
playingCards[index].isMatched = true
playingCards[index].isSelected = false
}
} else {
selectedCards.forEach { card in
let index = playingCards.firstIndex(of: card)!
playingCards[index].isMatched = false
playingCards[index].isSelected = false
}
}
}
}
else {
playingCards[chosenIndex].isSelected = false
selectedCards.removeAll(where: { $0 == playingCards[chosenIndex] })
}
}
}
}
private func formSet(by cards: [Card]) -> Bool {
return feature(\Card.color, isValidFor: cards) && feature(\Card.shape, isValidFor: cards) && feature(\Card.filling, isValidFor: cards) && feature(\Card.number, isValidFor: cards)
}
private func feature<F: Hashable>(_ keyPath: KeyPath<Card, F>, isValidFor cards: [Card]) -> Bool {
let count = cards.reduce(into: Set<F>()) { $0.insert($1[keyPath: keyPath]) }.count
return count == 1 || count == 3
}
init() {
cards = []
var counter = 0
for id1 in GameModel.colors.indices {
for id2 in GameModel.shapes.indices {
for id3 in GameModel.fillings.indices {
for id4 in GameModel.numbers.indices {
cards.append(Card(color: GameModel.colors[id1], shape: GameModel.shapes[id2], filling: GameModel.fillings[id3], number: GameModel.numbers[id4], id: counter))
counter += 1
}
}
}
}
playingCards = []
undealtCards = cards
}
enum ContentColor {
case red
case green
case purple
}
enum ContentShape {
case squiggly
case circle
case diamond
}
enum ContentFilling {
case empty
case full
case striped
}
struct Card: Identifiable, Equatable {
var color: ContentColor
var shape: ContentShape
var filling: ContentFilling
var number: Int
var id: Int
var isSelected = false
var isUndealt = false
var isMatched: Bool?
static func == (lhs: Card, rhs: Card) -> Bool {
return lhs.id == rhs.id
}
}
}
Code is untestable and unclear due to missed many components, but the issue is because use use withAnimation, like
.onTapGesture {
withAnimation {
viewModel.choose(card)
}
}
which animates everything animatable.
There are next directions to fix observed behavior
disable animations everywhere they are unneeded, for example animating of frames
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.animation(nil, value: viewModel.undealtCards) // << like this !!
.foregroundColor(CardConstants.color)
remove all explicit generic withAnimation and use animation(_, value) specifically in those places which are going to be animatable, like
VStack {
newGameButton
gameBody
HStack {
deckBody
discardedDeckBody
}
shuffleButton
}
.animation(.default, value: viewModel.playingCards) // << here !!
probably combine 1-2

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

State variable not update in SwiftUI

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)
}
}

View is not updating on Category Menu Item Selection SwiftUI

This is my first SwiftUI app. I have done a lot of searches and could not find a solution to this.
I want to display Products from different categories. For that, I have top bar horizontally scrollable menu. When selecting any category, I want to display each category's products. When on loading first category products are displaying correctly, but selecting any other category, products are not updating accordingly. However, When I navigate to any other view and come back to this view, products are displaying correctly.
What I tried
I tried different views to display products like List/ScrollView. No luck.
Here is the complete code of my View
struct ProductListView: View {
#State var data:[Product]
#State private var categoryItems:[Product] = []
#State private var uniqueCategories:[Product] = []
#State private var numberOfRows:Int = 0
#State private var selectedItem:Int = 0
#State private var image:UIImage?
#EnvironmentObject var authState: AuthenticationState
#ViewBuilder
var body: some View {
NavigationView{
VStack{
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0..<self.data.removingDuplicates(byKey: { $0.category }).count) { i in
Text(self.data.removingDuplicates(byKey: { $0.category })[i].category)
.underline(self.selectedItem == i ? true:false, color: Color.orange)
.foregroundColor(self.selectedItem == i ? Color.red:Color.black)
//.border(Color.gray, width: 2.0)
.overlay(
RoundedRectangle(cornerRadius: 0.0)
.stroke(Color.init(red: 236.0/255.0, green: 240.0/255.0, blue: 241.0/255.0), lineWidth: 1).shadow(radius: 5.0)
).padding(.horizontal)
//.shadow(radius: 5.0)
.onTapGesture {
self.selectedItem = i
_ = self.getSelectedCategoryProducts()
print("Category Items New Count: \(self.categoryItems.count)")
}
Spacer()
Divider().background(Color.orange)
}
}
}
.frame(height: 20)
Text("My Products").foregroundColor(Color.red).padding()
Spacer()
if(self.data.count == 0){
Text("You didn't add any product yet.")
}
if (self.categoryItems.count>0){
ScrollView(.vertical){
ForEach(0..<Int(ceil(Double(self.categoryItems.count)/2.0)), id: \.self){ itemIndex in
return HStack() {
NavigationLink(destination:ProductDetailView(index: self.computeIndexes(currentIndex: itemIndex,cellNo: 1), data: self.categoryItems, image: UIImage())){
ProductTile(index: self.computeIndexes(currentIndex: itemIndex, cellNo: 1), data: self.categoryItems, image: UIImage())
}
NavigationLink(destination:ProductDetailView(index: self.computeIndexes(currentIndex: itemIndex,cellNo: 2), data: self.categoryItems, image: UIImage())){
ProductTile(index: self.computeIndexes(currentIndex: itemIndex, cellNo: 2), data: self.categoryItems, image: UIImage())
}
}.padding(.horizontal)
}
}.overlay(
RoundedRectangle(cornerRadius: 10.0)
.stroke(Color.init(red: 236.0/255.0, green: 240.0/255.0, blue: 241.0/255.0), lineWidth: 1).shadow(radius: 5.0)
)
}
}
}
.onAppear(perform: {
_ = self.getSelectedCategoryProducts()
//print("Loading updated products....")
if (self.authState.loggedInUser != nil){
FireStoreManager().loadProducts(userId: self.authState.loggedInUser!.uid) { (isSuccess, data) in
self.data = data
}
}
})
.padding()
}
func populateUniqueCategories() -> [Product] {
let uniqueRecords = self.data.reduce([], {
$0.contains($1) ? $0 : $0 + [$1]
})
print("Unique Items: \(uniqueRecords)")
return uniqueRecords
}
func getSelectedCategoryProducts() -> [Product] {
var categoryProducts:[Product] = []
self.data.forEach { (myProduct) in
if(myProduct.category == self.populateUniqueCategories()[selectedItem].category){
categoryProducts.append(myProduct)
}
}
self.categoryItems.removeAll()
self.categoryItems = categoryProducts
return categoryProducts
}
func computeIndexes(currentIndex:Int, cellNo:Int) -> Int {
var resultedIndex:Int = currentIndex
if (cellNo == 1){
resultedIndex = resultedIndex+currentIndex
print("Cell 1 Index: \(resultedIndex)")
}else{
resultedIndex = resultedIndex + currentIndex + 1
print("Cell 2 Index: \(resultedIndex)")
}
return resultedIndex
}
}
And this is the array extension
extension Array {
func removingDuplicates<T: Hashable>(byKey key: (Element) -> T) -> [Element] {
var result = [Element]()
var seen = Set<T>()
for value in self {
if seen.insert(key(value)).inserted {
result.append(value)
}
}
return result
}
}
I would really thankful if you help me to sort out this issue.
Best Regards
The reason is in using static range ForEach constructor, which does not expect any changes, by design, so not updated.
So instead of using like
ForEach(0..<self.data.removingDuplicates(byKey: { $0.category }).count) { i in
^^^^^^ range !!
and
ForEach(0..<Int(ceil(Double(self.categoryItems.count)/2.0)), id: \.self){
^^^^ range
it needs to use direct data container, like
ForEach(self.data.removingDuplicates(byKey: { $0.category }), id: \.your_product_id) { product in
^^^^^^^^^ array
Note: if you need index during iteration, it is possible by using .enumerated() to container, eg: in this post

SwiftUI Table Custom Swipe?

Is there a way to swipe table rows to the left and to the right? I haven't found something for the new Framework SwiftUI so maybe there is no chance to use SwiftUI for this? I need to delete rows and use custom Swipes
It is possible to implement a delete action and the ability to reorder list items quite simply.
struct SwipeActionView: View {
#State var items: [String] = ["One", "two", "three", "four"]
var body: some View {
NavigationView {
List {
ForEach(items.identified(by: \.self)) { item in
Text(item)
}
.onMove(perform: move)
.onDelete(perform: delete)
}
.navigationBarItems(trailing: EditButton())
}
}
func delete(at offsets: IndexSet) {
if let first = offsets.first {
items.remove(at: first)
}
}
func move(from source: IndexSet, to destination: Int) {
// sort the indexes low to high
let reversedSource = source.sorted()
// then loop from the back to avoid reordering problems
for index in reversedSource.reversed() {
// for each item, remove it and insert it at the destination
items.insert(items.remove(at: index), at: destination)
}
}
}
Edit: There is this article by apple that I cannot believe I didn't find previously. Composing SwiftUI Gestures. I haven't experimented with it yet, but the article seems to do a great job!
I wanted the same and have now the following implementation.
The SwipeController checks when to execute a swipe action and performs the SwipeAction, for now you can add your swipe actions under the print lines in the executeAction function. But it is better make an abstract class from this.
Then in the SwipeLeftRightContainer struct we have most of the logic in the DragGesture. What it does is while your dragging its gonna change the offset and then make calls to the SwipeController to see if the threshold for swipe left or right are reached. Then when you finish the dragging it will come into the onEnded callback of the DragGesture. Here we will reset the offset and let the SwipeController decide to execute an action.
Keep in mind lot of the variables in the view are static for an iPhone X so you should change them to what fits best.
import SwiftUI
/** executeRight: checks if it should execute the swipeRight action
execute Left: checks if it should execute the swipeLeft action
submitThreshold: the threshold of the x offset when it should start executing the action
*/
class SwipeController {
var executeRight = false
var executeLeft = false
let submitThreshold: CGFloat = 200
func checkExecutionRight(offsetX: CGFloat) {
if offsetX > submitThreshold && self.executeRight == false {
Utils.HapticSuccess()
self.executeRight = true
} else if offsetX < submitThreshold {
self.executeRight = false
}
}
func checkExecutionLeft(offsetX: CGFloat) {
if offsetX < -submitThreshold && self.executeLeft == false {
Utils.HapticSuccess()
self.executeLeft = true
} else if offsetX > -submitThreshold {
self.executeLeft = false
}
}
func excuteAction() {
if executeRight {
print("executed right")
} else if executeLeft {
print("executed left")
}
self.executeLeft = false
self.executeRight = false
}
}
struct SwipeLeftRightContainer: View {
var swipeController: SwipeController = SwipeController()
#State var offsetX: CGFloat = 0
let maxWidth: CGFloat = 335
let maxHeight: CGFloat = 125
let swipeObjectsOffset: CGFloat = 350
let swipeObjectsWidth: CGFloat = 400
#State var rowAnimationOpacity: Double = 0
var body: some View {
ZStack {
Group {
HStack {
Text("Sample row")
Spacer()
}
}.padding(10)
.zIndex(1.0)
.frame(width: maxWidth, height: maxHeight)
.cornerRadius(5)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.gray))
.padding(10)
.offset(x: offsetX)
.gesture(DragGesture(minimumDistance: 5).onChanged { gesture in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = gesture.translation.width
}
swipeController.checkExecutionLeft(offsetX: offsetX)
swipeController.checkExecutionRight(offsetX: offsetX)
}.onEnded { _ in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = 0
swipeController.prevLocX = 0
swipeController.prevLocXDiff = 0
self.swipeController.excuteAction()
}
})
Group {
ZStack {
Rectangle().fill(Color.red).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityDelete)
Image(systemName: "multiply").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.trailing, 150)
}
}.zIndex(0.9).offset(x: swipeObjectsOffset + offsetX)
Group {
ZStack {
Rectangle().fill(Color.green).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityLike)
Image(systemName: "heart").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.leading, 150)
}
}.zIndex(0.9).offset(x: -swipeObjectsOffset + offsetX)
}
}
var opacityDelete: Double {
if offsetX < 0 {
return Double(abs(offsetX) / 50)
}
return 0
}
var opacityLike: Double {
if offsetX > 0 {
return Double(offsetX / 50)
}
return 0
}
}
struct SwipeListView: View {
var body: some View {
ScrollView {
ForEach(0..<10) { index in
SwipeLeftRightContainer().listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}
struct SwipeLeftRight_Previews: PreviewProvider {
static var previews: some View {
SwipeListView()
}
}