Escaping closure captures mutating 'self' parameter Error in Swift - swift

Creating a simple card game (Set) and I have a function in the model that deals X cards onto the deck. Currently, when I click the deal card button they all show up at once so I added the timer so that they would appear one after another. This gives the error "Escaping closure captures mutating 'self' parameter" Any ideas on what I can fix?
mutating func deal(_ numberOfCards: Int) {
for i in 0..<numberOfCards {
Timer.scheduledTimer(withTimeInterval: 0.3 * Double(i), repeats: false) { _ in
if deck.count > 0 {
dealtCards.append(deck.removeFirst())
}
}
}
}

A timer is not even required. You can use transitions in combination with an animation to get the desired effect. Here the transition is delayed based on the index of the card:
class CardModel: ObservableObject {
#Published var cards: [Int] = []
func deal(_ numberOfCards: Int) {
cards += (cards.count ..< (cards.count + numberOfCards)).map { $0 }
}
func clear() {
cards = []
}
}
struct ContentView: View {
#ObservedObject var cardModel = CardModel()
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
ForEach(0 ..< self.cardModel.cards.count, id: \.self) { index in
CardView(cardNumber: self.cardModel.cards[index])
.transition(.offset(x: geometry.size.width))
.animation(Animation.easeOut.delay(Double(index) * 0.1))
}
}
Button(action: { self.cardModel.deal(2) }) {
Text("Deal")
}
Button(action: { self.cardModel.clear() }) {
Text("Clear")
}
}
}
}
}
struct CardView: View {
let cardNumber: Int
var body: some View {
Rectangle()
.foregroundColor(.green)
.frame(width: 9, height: 16)
}
}
Or a bit simpler (without the CardModel):
struct ContentView: View {
#State var cards: [Int] = []
func deal(_ numberOfCards: Int) {
cards += (cards.count ..< (cards.count + numberOfCards)).map { $0 }
}
func clear() {
cards = []
}
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
ForEach(0 ..< self.cards.count, id: \.self) { index in
CardView(cardNumber: self.cards[index])
.transition(.offset(x: geometry.size.width))
.animation(Animation.easeOut.delay(Double(index) * 0.1))
}
}
Button(action: { self.deal(2) }) {
Text("Deal")
}
Button(action: { self.clear() }) {
Text("Clear")
}
}
}
}
}
struct CardView: View {
let cardNumber: Int
var body: some View {
Rectangle()
.foregroundColor(.green)
.frame(width: 9, height: 16)
}
}
Note this approach works fine if you're centering the cards, because the previous cards will need to shift too. If you left-align the cards however (using a spacer) the animation delay will be present for the cards that do not need to shift (the animation starts with an awkward delay). If you need to account for this case, you'll need to make the newly inserted index part of the model.

(This may be duplicating what Jack Goossen has written; go look there first, and if it's not clear, this may give some more explanation.)
The core problem here is that you appear to be treating a struct as a reference type. A struct is a value. That means that each holder of it has its own copy. If you pass a value to a Timer, then the Timer is mutating its own copy of that value, which can't be self.
In SwiftUI, models are typically reference types (classes). They represent an identifiable "thing" that can be observed and changes over time. Changing this type to a class would likely address your problem. (See Jack Goossen's answer, which uses a class to hold the cards.)
This is backwards of the direction that Swift had been moving in with UIKit, where views were reference types and the model was encouraged to be made of value types. In SwiftUI, views are structs, and the model is usually made of classes.
(Using Combine with SwiftUI, it's possible to make both view and model into value types, but that's possibly beyond what you were trying to do here, and is a bit more complex if you haven't studied Combine or reactive programming already.)

Related

Change values in struct model

I'm stuck with adding items from struct to favorites. The idea:
I have a json with data for cards
On the main screen app shows a random card
The user could pick another random card or save it to favorites.
Code below.
Creating a CardModel (file 1):
struct CardModel: Hashable, Codable, Identifiable {
let id: Int
let topic: String
let category: String
var saved: Bool
}
Retrieving data from json and creating an array of structs (file 2):
var cardsModelArray: [CardModel] = load("LetsTalkTopics.json")
Func to pickup random item from the array (file 3):
func pickRandomCard() -> CardModel {
let randomCard = cardsModelArray.randomElement()!
return randomCard
}
Func to change the "saved" bool value (file 4)
func saveCard(card: CardModel) {
let index = card.id
cardsModelArray[index] = CardModel(id: index, topic: card.topic, category: card.category, saved: !card.saved)
}
View file (file 5, simplified)
import SwiftUI
struct StackOverFlow: View {
#State var currentCard = pickRandomCard()
var body: some View {
VStack{
CardViewStackOver(cards: currentCard)
Button("Show random card") {
currentCard = pickRandomCard()
}
Button("Save item") {
saveCard(card: currentCard)
}
}
}
struct StackOverFlow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
StackOverFlow()
}
}
}
struct CardViewStackOver: View {
let cards: CardModel
var body: some View {
VStack {
Text(cards.topic)
Text(cards.category)
Text(String(cards.id))
HStack {
if cards.saved {
Image(systemName: "heart.fill")
.font(.title)
.padding(15)
.padding(.bottom, 20)
} else {
Image(systemName: "heart")
.font(.title)
.padding(15)
.padding(.bottom, 20)
}
}
}
}
}
But I definitely making something wrong, in a separate view I'm showing saved cards but it doesn't work (it shows some random cards, and some saved cards have duplicates). With my research, I found out that structs are immutable and when I'm trying to edit a value, basically swift creates a copy of it and makes changes in the copy. If so, what would be the right approach to create this favorite feature?
A solution that fixed the problem:
func saveCardMinus(currentCard: CardModel) {
var index = currentCard.id - 1
cardsModelArray[index].saved.toggle()
}
But I'm sure that the whole solution is bad. What is the right/more proper way to solve it?
(and btw, now I face another problem: the icon for bool value updates is not in real-time, you need to open this card again to see a new value (filled heart/unfilled heart))
You could use a Binding to pass the selected card on. But I restructured the whole code as there were multiple bad practices involved:
struct StackOverFlow: View {
//store the current selected index and the collection as state objects
#State var currentCardIndex: Int?
#State var cards: [CardModel] = []
var body: some View {
VStack{
//if there is an index show the card
if let index = currentCardIndex{
CardViewStackOver(card: $cards[index])
}
Button("Show random card") {
// just create a random index
currentCardIndex = (0..<cards.count).randomElement()
}
Button("Save / delete item") {
if let index = currentCardIndex{
//you save delete as favorite here
cards[index].saved.toggle()
}
}
}.onAppear{
//donĀ“t exactly know where this function lives
if cards.count == 0{
cards = load("LetsTalkTopics.json")
}
}
}
}
struct CardViewStackOver: View {
//use binding wrapper here
#Binding var card: CardModel
var body: some View {
VStack {
Text(card.topic)
Text(card.category)
Text(String(card.id))
HStack {
Image(systemName: card.saved ? "heart.fill" : "heart")
.font(.title)
.padding(15)
.padding(.bottom, 20)
.onTapGesture {
card.saved.toggle()
}
}
}
}
}

List Views overlapping SwiftUI

Recently, i had been using a LazyVStack to load views Lazy from dynamic data however as you may know, when the Views get complex, it can cause major performance issues and so ive started using a List however on the iOS 15 simulator, it seems to mess up and causes the views to overlap whereas on my normal device everything is just fine, any help as to what i could be doing wrong?
Struct HomeView: View {
var body: some View {
GeometryReader { geometry in
List {
getTopView(geometry).anyView
.listRowBackground(Color.clear)
.listRowInsets(.init())
.padding(5)
ForEach(getDataViews) { view in
view.anyView
.listRowBackground(Color.clear)
.listRowInsets(.init())
}
Color.clear
.id("bottom-2")
.frame(height: bottomInsetHeight() + 100)
.listRowBackground(Color.clear)
}
.listStyle(.plain)
.overlay(NavigationBar(title: "Home"))
.overlay(ZStack {
...
}
)
}
}
}
And then in the getTopView method I just have a vstack within a custom struct named DataView, which just contains an AnyView and id Property(I did this to try resolve the issue with lists as i thought it was an issue with Ids but same effect)
func getTopView(_ geometry: GeometryProxy) -> DataView {
return DataView(anyView: AnyView(
VStack {
...
}
)
}
The actual data(posts), are loaded in a computed property returning an Array of DataView:
var getDataViews: [DataView] {
var anyViews: [DataView] = []
anyViews = viewModel.events.map { event in
return DataView(anyView: AnyView(
ZStack {
EventCell(event: event)
.padding()
.padding(.bottom)
Navigator.navigate(route: .eventDetail(event)) {
Rectangle()
}
.opacity(0)
}
)
)
}
return anyViews
}
The Data view struct:
struct DataView: Identifiable {
var id: String = UUID().uuidString
var anyView: AnyView
}
If anyone is wondering what the geometry reader is for, it is being used in presenting a 'stories', i doubt it could be causing the issue
Yes I tried putting sections as well, same effect

Initializer 'xxx' requires that 'Int' conform to 'BinaryFloatingPoint'

I am making a Slider based on a view model, but I am facing this error message Initializer 'init(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
It is strange because converting the integer from view model into Double doesn't quite do the trick.
I found very similar question and read the SO answer (How can I make Int conform to BinaryFloatingPoint or Double/CGFloat conform to BinaryInteger?), but it doesn't seem like I can implementation the solution for my case, probably because I am using ObservedObject for the view model.
If I remove $ in front of setInformationVM.elapsedRestTime, I would see another error message saying Cannot convert value of type 'Int' to expected argument type 'Binding<Int>'
They said "Binding are generally used when there is a need for 2-way communication" - would that mean the Slider needs a way to communicate/update back to the View Model? Why is it that the Slider was accepting #State private var xx: Double for the value in general , but not a simple integer from my view model?
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
#State var showingLog = false
var body: some View {
GeometryReader { geometry in
ZStack() {
(view content removed for readability)
}
.sheet(isPresented: $showingLog) {
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
setLoggingView(setInformationVM: setInformatationVM, restfullness: 3, stepValue: 10)
}
}
}
setLoggingView
struct setLoggingView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var setInformationVM: SetInformationTestClass
#State var restfullness: Int
var stepValue: Int
var body: some View {
GeometryReader { geometry in
let rect = geometry.frame(in: .global)
ScrollView {
VStack(spacing: 5) {
Text("Rested \(Int(setInformationVM.elapsedRestTime)) sec")
Slider(value: $setInformationVM.elapsedRestTime,
in: 0...setInformationVM.totalRestTime,
step: Int.Stride(stepValue),
label: {
Text("Slider")
}, minimumValueLabel: {
Text("-\(stepValue)")
}, maximumValueLabel: {
Text("+\(stepValue)")
})
.tint(Color.white)
.padding(.bottom)
Divider()
Spacer()
Text("Restfullness")
.frame(minWidth: 0, maxWidth: .infinity)
restfullnessStepper(rect: rect, maxRestFullness: 5, minRestFullness: 1, restfullnessIndex: restfullness)
Button(action: {
print("Update Button Pressed")
//TODO
//perform further actions to update restfullness metric and elapsed rest time in the viewmodels before dismissing the view, and also update the iOS app by synching the view model.
dismiss()
}) {
HStack {
Text("Update")
.fontWeight(.medium)
}
}
.cornerRadius(40)
}
.border(Color.yellow)
}
}
}
SetInformationTestClass view model
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
You can create a custom binding variable like :
let elapsedTime = Binding(
get: { Double(self.setInformationVM.elapsedRestTime) },
set: { self.setInformationVM.elapsedRestTime = Int($0) } // Or other custom logic
)
// then you reference it in the slider like:
Slider(elapsedTime, ...)

Text updating with ObservedObject but not my custom View

I am trying to code the board game Splendor as a simple coding project in SwiftUI. I'm a bit of a hack so please point me in the right direction if I've got this all wrong. I'm also attempting to use the MVVM paradigm.
The game board has two stacks of tokens on it, one for unclaimed game tokens on and one for the tokens of the active player. There is a button on the board which allows the player to claim a single token at a time.
The tokens are drawn in a custom view - TokenView() - which draws a simple Circle() offset by a small amount to make a stack. The number of circles matches the number of tokens. Underneath each stack of tokens is a Text() which prints the number of tokens.
When the button is pressed, only the Text() updates correctly, the number of tokens drawn remains constant.
I know my problem something to do with the fact that I'm mixing an #ObservedObject and a static Int. I can't work out how to not use the Int, as TokenView doesn't know whether it's drawing the board's token collection or the active players token collection. How do I pass the count of tokens as an #ObservedObject? And why does Text() update correctly?
Model:
struct TheGame {
var tokenCollection: TokenCollection
var players: [Player]
init() {
self.tokenCollection = TokenCollection()
self.players = [Player()]
}
}
struct Player {
var tokenCollection: TokenCollection
init() {
self.tokenCollection = TokenCollection()
}
}
struct TokenCollection {
var count: Int
init() {
self.count = 5
}
}
ViewModel:
class MyGame: ObservableObject {
#Published private (set) var theGame = TheGame()
func collectToken() {
theGame.tokenCollection.count -= 1
theGame.players[0].tokenCollection.count += 1
}
}
GameBoardView:
struct GameBoardView: View {
#StateObject var myGame = MyGame()
var body: some View {
VStack{
TokenStackView(myGame: myGame, tokenCount: myGame.theGame.tokenCollection.count)
.frame(width: 100, height: 200, alignment: .center)
Button {
myGame.collectToken()
} label: {
Text ("Collect Token")
}
TokenStackView(myGame: myGame, tokenCount: myGame.theGame.players[0].tokenCollection.count) .frame(width: 100, height: 200, alignment: .center)
}
}
}
TokenStackView:
struct TokenStackView: View {
#ObservedObject var myGame: MyGame
var tokenCount: Int
var body: some View {
VStack {
ZStack {
ForEach (0..<tokenCount) { index in
Circle()
.stroke(lineWidth: 5)
.offset(x: CGFloat(index * 10), y: CGFloat(index * 10))
}
}
Spacer()
Text("\(tokenCount)")
}
}
}
If you take a look at your console you'll see the error:
ForEach<Range<Int>, Int, OffsetShape<_StrokedShape<Circle>>> count (2) != its initial count (1). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
The fix is pretty easy in this case, just add id, like this:
ForEach(0..<tokenCount, id: \.self) {

SwiftUI - ForEach deletion transition always applied to last item only

I'm trying to add a delete animation to my ForEach, so that each Card inside it scales out when removed. This is what I have so far:
The problem is that no matter which Card is pressed, it's always the last one that animates. And sometimes, the text inside each card has a weird sliding/morphing animation. Here's my code:
/// Ran into this problem: "SwiftUI ForEach index out of range error when removing row"
/// `ObservableObject` solution from https://stackoverflow.com/a/62796050/14351818
class Card: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
init(name: String) {
self.name = name
}
}
struct ContentView: View {
#State var cards = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cards.indices, id: \.self) { index in
CardView(card: cards[index], removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.transition(.scale)
}
}
}
}
}
struct CardView: View {
#ObservedObject var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
How can I scale out the Card that is clicked, and not the last one?
The reason you're seeing this behavior is because you use an index as an id for ForEach. So, when an element is removed from the cards array, the only difference that ForEach sees is that the last index is gone.
You need to make sure that the id uniquely identifies each element of ForEach.
If you must use indices and have each element identified, you can either use the enumerated method or zip the array and its indices together. I like the latter:
ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in
//...
}
The above uses the object itself as the ID, which requires conformance to Hashable. If you don't want that, you can use the id property directly:
ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}
For completeness, here's the enumerated version (technically, it's not an index, but rather an offset, but for 0-based arrays it's the same):
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
//...
}
New Dev's answer was great, but I had something else that I needed. In my full code, I had a button inside each Card that scrolled the ScrollView to the end.
/// the ForEach
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(
card: cards[index],
scrollToEndPressed: {
proxy.scrollTo(cards.count - 1, anchor: .center) /// trying to scroll to end... not working though.
},
removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
}
)
.transition(.scale)
}
/// CardView
struct CardView: View {
#ObservedObject var card: Card
var scrollToEndPressed: (() -> Void)?
var removePressed: (() -> Void)?
var body: some View {
VStack {
Button(action: {
scrollToEndPressed?() /// scroll to the end
}) {
VStack {
Text("Scroll to end")
}
}
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
}
With the above code, the "Scroll to end" button didn't work.
I fixed this by assigning an explicit ID to each CardView.
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(card: cards[index], scrollToEndPressed: {
withAnimation(.easeOut) { /// also animate it
proxy.scrollTo(cards.last?.id ?? card.id, anchor: .center) /// scroll to the last card's ID
}
}, removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.id(card.id) /// add ID
.transition(.scale)
}
Result:
I recommend you to rethink and use Card as struct rather than a class and confirm to Identifiable and Equatable.
struct Card: Hashable, Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}
And then create a View Model that holds your cards.
class CardViewModel: ObservableObject {
#Published
var cards: [Card] = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
}
Iterate over cardViewModel.cards and pass card to the CardView. Use removeAll method of Array instead of remove. It is safe since Cards are unique.
ForEach(viewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { $0 == card}
}
}
}
A complete working exammple.
struct ContentView: View {
#ObservedObject var cardViewModel = CardViewModel()
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cardViewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { $0 == card}
}
}
.transition(.scale)
}
}
}
}
}
struct CardView: View {
var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?()
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
If for some reason you need index of card in ContentView, do this.
Accessing and manipulating array item in an EnvironmentObject
Removing items from a child view generated by For Each loop causes Fatal Error
Both of them are similar to this tutorial of Apple.
https://developer.apple.com/tutorials/swiftui/handling-user-input