Text updating with ObservedObject but not my custom View - swift

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

Related

Save / restore array of booleans to core date

Building my first SwiftUI app, and have some basic knowledge of Swift. So a bit much to chew but I am enjoying learning.
I have a Form with many toggles saving/restoring from core data in my swift app. Works well but the interface is cumbersome with all the toggles.
Instead I want to make an HStack of tappable labels that will be selected / unselected instead. Then when you submit it will map the selected Text objects to the existing State variables I have OR? save an array of selected strings to core data (for restoring later?).
In either case my code for this has been cobbled from a todo list tutorial plus some nice HStack examples I have put in my form. They select/deselect nicely but I do not know how to save their state like I did the toggle switches.
I will paste what I think is relevant code and remove the rest.
#State var selectedItems: [String] = []
#State private var hadSugar = false
#State private var hadGluten = false
#State private var hadDairy = false
let dayvariablesText = [
"Sugar",
"Gluten",
"Dairy"
]
// section 1 works fine
Section {
VStack {
Section(header: Text("Actions")) {
Toggle("Sugar", isOn: $hadSugar)
Toggle("Gluten", isOn: $hadGluten)
Toggle("Dairy", isOn: $hadDairy)
}
}
}
// section 2 trying this
ScrollView(.horizontal) {
LazyHGrid(rows: rows) {
ForEach(0..<dayvariablesText.count, id: \.self) { item in
GridColumn(item: dayvariablesText[item], items: $selectedItems)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
// save
Button("Submit") {
DataController().addMood(sugar: hadSugar, gluten: hadGluten, dairy: hadDairy, context: managedObjContext)
dismiss()
}
This works fine with the toggles shown above - how to do this when selecting gridItems in the next section for example?
I think you need to remodel your code. Having multiple sources of truth like in your example (with the vars and the array for the naming) is a bad practice and will hurt you in the long run.
Consider this solution. As there is a lot missing in your question it´s more general. So you need to implement it to fit your needs. But it should get you in the right direction.
//Create an enum to define your items
// naming needs some improvement :)
enum MakroType: String, CaseIterable{
case sugar = "Sugar", gluten = "Gluten", dairy = "Dairy"
}
//This struct will hold types you defined earlier
// including the bool indicating if hasEaten
struct Makro: Identifiable{
var id: MakroType {
makro
}
var makro: MakroType
var hasEaten: Bool
}
// The viewmodel will help you store and load the data
class Viewmodel: ObservableObject{
//define and create the array to hold the Makro structs
#Published var makros: [Makro] = []
init(){
// load the data either here or in the view
// when it appears
loadCoreData()
}
func loadCoreData(){
//load items
// ..... code here
// if no items assign default ones
if makros.isEmpty {
makros = MakroType.allCases.map{
Makro(makro: $0, hasEaten: false)
}
}
}
// needs to be implemented
func saveCoreData(){
print(makros)
}
}
struct ContentView: View {
// Create an instance of the Viewmodel here
#StateObject private var viewmodel: Viewmodel = Viewmodel()
var body: some View {
VStack{
ScrollView(.horizontal) {
LazyHStack {
// Iterate over the items themselves and not over the indices
// with the $ in front you can pass a binding on to the ChildView
ForEach($viewmodel.makros) { $makro in
SubView(makro: $makro)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
Spacer()
Button("Save"){
viewmodel.saveCoreData()
}.padding()
}
.padding()
}
}
struct SubView: View{
// Hold the binding to the Makro here
#Binding var makro: Makro
var body: some View{
//Toggle to change the hasEaten Bool
//this will reflect through the Binding into the Viewmodel
Toggle(makro.makro.rawValue, isOn: $makro.hasEaten)
}
}

Why does my SwiftUI view not get onChange updates from a #Binding member of a #StateObject?

Given the setup I've outlined below, I'm trying to determine why ChildView's .onChange(of: _) is not receiving updates.
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
#State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
#StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
#Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
Interestingly, if I make the following changes in ChildView, I get the behavior I want.
Change #StateObject to #ObservedObject
Change _viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem)) to viewModel = ViewModel(someItem: someItem)
From what I understand, it is improper for ChildView's viewModel to be #ObservedObject because ChildView owns viewModel but #ObservedObject gives me the behavior I need whereas #StateObject does not.
Here are the differences I'm paying attention to:
When using #ObservedObject, I can tap the black area and see the changes applied to both the white text and red rectangle. I can also tap the red rectangle and see the changes observed in ParentView through the white text.
When using #StateObject, I can tap the black area and see the changes applied to both the white text and red rectangle. The problem lies in that I can tap the red rectangle here and see the changes reflected in ParentView but ChildView doesn't recognize the change (rotation does not change and "Change Detected" is not printed).
Is #ObservedObject actually correct since ViewModel contains a #Binding to a #State created in ParentView?
Normally, I would not write such a convoluted solution to a problem, but it sounds like from your comments on another answer there are certain architectural issues that you are required to conform to.
The general issue with your initial approach is that onChange is only going to run when the view has a render triggered. Generally, that happens because some a passed-in property has changed, #State has changed, or a publisher on an ObservableObject has changed. In this case, none of those are true -- you have a Binding on your ObservableObject, but nothing that triggers the view to re-render. If Bindings provided a publisher, it would be easy to hook into that value, but since they do not, it seems like the logical approach is to store the state in the parent view in a way in which we can watch a #Published value.
Again, this is not necessarily the route I would take, but hopefully it fits your requirements:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
#Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
#StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
#StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}
Actually we don't use view model objects at all in SwiftUI, see [Data Essentials in SwiftUI WWDC 2020]. As shown in the video at 4:33 create a custom struct to hold the item, e.g. ChildViewConfig and init it in an #State in the parent. Set the childViewConfig.item in a handler or add any mutating custom funcs. Pass the binding $childViewConfig or $childViewConfig.item to the to the child View if you need write access. It's all very simple if you stick to structs and value semantics.

Add views to parent swiftui view and then remove them programatically

Right now I've got a parent view written in SwiftUI with some Buttons. What I'd like to happen is to programmatically create and overlay some new child views on top of the entire parent view when the buttons are pressed and then have them fade out, removing them from the view. With SwiftUI being based on state, I'm not sure how to go about this. Would I have to keep track of every view that is created with many at #State variables? My code looks like the following right now:
struct ContentView: View {
var key: String
#State private var touches: [Touch] = []
struct Touch: Identifiable {
let id = UUID()
let location: CGPoint
}
var body: some View {
ZStack {
Color.blue
ripplesLayer
}
.gesture(
DragGesture(minimumDistance: 0)
.onEnded { value in
touches.append(
Touch(location: value.location)
)
}
)
.edgesIgnoringSafeArea(.all)
}
var ripplesLayer: some View {
ForEach(touches.suffix(15)) { touch in
RippleView(key: key)
.position(
x: touch.location.x,
y: touch.location.y
)
}
}
struct RippleView: View {
var key: String
#State private var isHidden = false
#State private var size: CGFloat = 50
var body: some View {
Circle()
.fill(Color.white.opacity(isHidden ? 0 : 0.5))
.frame(width: size, height: size)
.transition(.opacity)
.animation(.easeOut(duration: 0.5))
.onAppear {
Sound.play(file: key, fileExtension: ".wav")
withAnimation {
isHidden = true
size = 200
}
}
}
}
}
I've got this struct that references the ripple child views in the foreach loop as in this answer but don't know how to keep track of multiple disappearing views or even implement it in the first place. Multitouch is enabled and. I want to be able to dynamically create a lot of child views.
struct FreePiano: View {
#State var numberOfPianos = 0
var whiteKeys = ["c1", "d1", "e1", "f1", "g1", "a1", "b1", "c2", "d2", "e2", "f2", "g2", "a2", "b2", "c3"]
var blackKeys = ["c#1", "d#1", "f#1", "g#1", "a#1", "c#2", "d#2", "f#2", "g#2", "a#2"]
var body: some View {
ZStack {
HStack {
VStack {
ForEach(whiteKeys, id: \.self) { whiteKey in
FreePianoKey(numberOfImages: $numberOfPianos, key: whiteKey)
}
}
VStack {
ForEach(blackKeys, id: \.self) { blackKey in
FreePianoKey(numberOfImages: $numberOfPianos, key: blackKey)
}
}
}
ForEach(0 ... numberOfPianos, id, \.self) { _ in
ContentView()
.allowsHitTesting(false)
.frame(width: 300, height: 300)
.position(x: 100, y:100)
}
}
}
}
Would something like SpriteKit be easier for what I'm trying to do?
I do the same thing in my app. In the parent view, I keep a state variable that keeps track of the child views. If your app only allows one view at a time, you could use a state variable that is simply an enum (I call it AppState). Then, as a child view is launched, you would change the AppState.
Alternatively, the state variable could be something more complex, like a stack. Or, in the case of allowing multiple child views at a time, it could be an array of tuples of all child windows and their state (for instance, 0 = closed, 1 = open).

Escaping closure captures mutating 'self' parameter Error in 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.)

SwiftUI: Change #State variable through a function called externally?

So maybe I'm misunderstanding how SwiftUI works, but I've been trying to do this for over an hour and still can't figure it out.
struct ContentView: View, AKMIDIListener {
#State var keyOn: Bool = false
var key: Rectangle = Rectangle()
var body: some View {
VStack() {
Text("Foo")
key
.fill(keyOn ? Color.red : Color.white)
.frame(width: 30, height: 60)
}
.frame(width: 400, height: 400, alignment: .center)
}
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
print("foo")
keyOn.toggle()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So the idea is really simple. I have an external midi keyboard using AudioKit. When a key on the keyboard is pressed, the rectangle should change from white to red.
The receivedMIDINoteOn function is being called and 'foo' is printed to the console, and despite keyOn.toggle() appearing in the same function, this still won't work.
What's the proper way to do this?
Thanks
Yes, you are thinking of it slightly wrong. #State is typically for internal state changes. Have a button that your View directly references? Use #State. #Binding should be used when you don't (or shouldn't, at least) own the state. Typically, I use this when I have a parent view who should be influencing or be influenced by a subview.
But what you are likely looking for, is #ObservedObject. This allows an external object to publish changes and your View subscribes to those changes. So if you have some midi listening object, make it an ObservableObject.
final class MidiListener: ObservableObject, AKMIDIListener {
// 66 key keyboard, for example
#Published var pressedKeys: [Bool] = Array(repeating: false, count: 66)
init() {
// set up whatever private storage/delegation you need here
}
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
// how you determine which key(s) should be pressed is up to you. if this is monophonic the following will suffice while if it's poly, you'll need to do more work
DispatchQueue.main.async {
self.pressedKeys[Int(noteNumber)] = true
}
}
}
Now in your view:
struct KeyboardView: View {
#ObservedObject private var viewModel = MidiListener()
var body: some View {
HStack {
ForEach(0..<viewModel.pressedKeys.count) { index in
Rectangle().fill(viewModel.pressedKeys[index] ? Color.red : Color.white)
}
}
}
}
But what would be even better is to wrap your listening in a custom Combine.Publisher that posts these events. I will leave that as a separate question, how to do that.