SwiftUI not updating ObservedObject in child-view - swift

So I tried to learn SwiftUI from Stanford CS193p. This works great, however, I can't get my head around why this is not working. I have the same exact view as the instructor has:
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
HStack {
ForEach(self.viewModel.cards) { card in
CardView(card: card).onTapGesture {
self.viewModel.chooseCard(card: card)
}
}
.aspectRatio(2/3, contentMode: .fit)
}
.foregroundColor(.orange)
.padding()
.font(viewModel.numberOfPairsOfCards >= 5 ? .callout : .largeTitle)
}
}
struct CardView: View {
var card: MemoryGame<String>.Card
var body: some View {
VStack {
ZStack {
if card.isFaceUp {
RoundedRectangle(cornerRadius: 10.0).fill(Color.white)
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
Text(card.content)
} else {
RoundedRectangle(cornerRadius: 10.0).fill(Color.orange)
}
}
}
}
}
The issue is that this does not update the view, it's as if the published information from the model does not get passed down the hierarchy. I know it works since if I change the code to this:
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
HStack {
ForEach(self.viewModel.cards) { card in
ZStack {
if card.isFaceUp {
RoundedRectangle(cornerRadius: 10.0).fill(Color.white)
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
Text(card.content)
} else {
RoundedRectangle(cornerRadius: 10.0).fill(Color.orange)
}
}
.onTapGesture {
self.viewModel.chooseCard(card: card)
}
}
.aspectRatio(2/3, contentMode: .fit)
}
.foregroundColor(.orange)
.padding()
.font(viewModel.numberOfPairsOfCards >= 5 ? .callout : .largeTitle)
}
}
all works well.
All help is greatly appreciated!
class EmojiMemoryGame: ObservableObject {
#Published private var game: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static private func createMemoryGame() -> MemoryGame<String> {
let emojis = ["🎃", "👻", "🕷", "😈", "🦇"]
return MemoryGame(numberOfPairsOfCards: Int.random(in: 2...5)) { emojis[$0] }
}
//MARK: - Access to the Model
var cards: Array<MemoryGame<String>.Card> {
game.cards
}
var numberOfPairsOfCards: Int {
game.cards.count / 2
}
//MARK: - Intents
func chooseCard(card: MemoryGame<String>.Card) {
game.choose(card)
}
}
struct MemoryGame<CardContent> {
var cards: Array<Card>
mutating func choose(_ card: Card) {
if let indexOfCard = cards.firstIndex(of: card) {
cards[indexOfCard].isFaceUp.toggle()
}
}
init(numberOfPairsOfCards: Int, cardContentFactory: (Int) -> CardContent) {
cards = Array<Card>()
for pairIndex in 0..<numberOfPairsOfCards {
let content = cardContentFactory(pairIndex);
cards.append(Card(content: content, id: pairIndex * 2))
cards.append(Card(content: content, id: pairIndex * 2 + 1))
}
cards.shuffle()
}
struct Card: Identifiable, Equatable {
static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
lhs.id == rhs.id
}
var isFaceUp = true
var isMatched = false
var content: CardContent
var id: Int
}
}

The implementation of the equality operator in your Card struct only compares ids. The CardView is not updated because SwiftUI deduces the card hasn't changed.
Note that you may want to check for the other properties of card as well (CardContent would need to conform to Equatable).
struct Card: Identifiable, Equatable {
static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
return lhs.id == rhs.id && lhs.isFaceUp == rhs.isFaceUp
}
var isFaceUp = true
var isMatched = false
var content: CardContent
var id: Int
}

Related

Comparing An Object to Itself Returning False

I have a class that implements the Equatable protocol, and it uses a UUID field for comparison:
class MemberViewModel {
private static var entities: [MemberViewModel] = []
private var entity: Member
let id = UUID()
init(_ entity: Member) {
self.entity = entity
}
static func members() -> [MemberViewModel] {
entities.removeAll()
try? fetch().forEach { member in
entities.append(MemberViewModel(member))
}
return entities
}
}
extension MemberViewModel: Equatable {
static func == (lhs: MemberViewModel, rhs: MemberViewModel) -> Bool {
return lhs.id == rhs.id
}
}
I then have a view that creates icons that when tapped should display a stroke to denote it was "selected":
struct MyView: View {
#State var selectedMember: MemberViewModel? = nil
var body: some View {
let members = MemberViewModel.members()
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: 4) {
ForEach (members, id: \.id) { member in
var isSelected: Bool = selectedMember == member
Circle()
.fill(Color(.red))
.frame(width: 48, height: 48)
.overlay() {
if isSelected {
Circle()
.stroke(Color(.black), lineWidth: 2)
}
}
.onTapGesture { selectedMember = member }
}
}
}
}
}
I have tried setting isSelected multiple ways, including the following from another SO question:
let isSelected = Binding<Bool>(get: { self.selectedMember == member }, set: { _ in })
When debugging using breakpoints, the value of isSelected is always false.
I'm using XCode Version 14.0.1 (14A400), and Swift 5.7.
Put let members = MemberViewModel.members() just before
var body: some View {...}, not inside it, like this:
let members = MemberViewModel.members() // <-- here
var body: some View {
...
}
Due to your weird code structure, let members = MemberViewModel.members()
gets re-done evey time the body is refreshed. And since the UUID is re-generated .... you can guess that the ids now are all over the place and not equal to the selectedMember.
In other words, the object comparison is working, but you are not comparing what you think.
Use the sample below hope you get the idea.
Model
struct Member {
let id = UUID()
}
extension Member: Equatable {
static func == (lhs: Member, rhs: Member) -> Bool {
return lhs.id == rhs.id
}
}
class MemberViewModel: ObservableObject {
// each time you update the entities, view vill be updated
#Published var entities: [Member] = []
func fetchMembers() {
entities.removeAll()
// your code to fetch members
try? fetch().forEach { member in
entities.append(member)
}
}
}
MyView
struct MyView: View {
#StateObject private var modelData = MemberViewModel()
#State private var selectedMember: Member? = nil
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: 4) {
ForEach (modelData.entities, id: \.id) { member in
Circle()
.fill(Color(.red))
.frame(width: 48, height: 48)
.overlay() {
if selectedMember == member {
Circle()
.stroke(Color(.black), lineWidth: 2)
}
}
.onTapGesture { selectedMember = member }
}
}
}.onAppear {
modelData.fetchMembers()
}
}
}

Cannot convert value of type '[ASValue<T>]' to expected argument type 'Binding<C>'

I'm building a custom component and using ForEach with a custom generic type. The issue is that this type is throwing this error:
Cannot convert value of type '[ASValue]' to expected argument type 'Binding'
public struct ArrayStepperSection<T: Hashable>: Hashable {
public let header: String
public var items: [ASValue<T>]
public init(header: String = "", items: [ASValue<T>]) {
self.header = header
self.items = items
}
}
public struct ASValue<T: Hashable>: Hashable {
private let id = UUID()
var item: T
public init(item: T) {
self.item = item
}
}
public class ArrayStepperValues<T: Hashable>: Hashable, ObservableObject {
#Published public var values: [ASValue<T>]
#Published public var selected: ASValue<T>
#Published public var sections: [ArrayStepperSection<T>]
public init(values: [ASValue<T>], selected: ASValue<T>, sections: [ArrayStepperSection<T>]? = nil) {
self.values = values
self.selected = selected
if sections != nil {
self.sections = sections!
} else {
self.sections = [ArrayStepperSection(items: values)]
}
}
public static func == (lhs: ArrayStepperValues<T>, rhs: ArrayStepperValues<T>) -> Bool {
return lhs.sections == rhs.sections
}
public func hash(into hasher: inout Hasher) {
hasher.combine(sections)
}
}
struct ArrayStepperList<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var values: ArrayStepperValues<T>
let display: (T) -> String
var body: some View {
List {
ForEach(values.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in // Error happens here
Button(action: {
values.selected.item = item
dismiss()
}) {
HStack {
Text(display(item))
Spacer()
if values.selected.item == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(Text(display(values.selected.item)))
}
}
here is the test code I used to remove the error:
struct ContentView: View {
#StateObject var values = ArrayStepperValues(values: [ASValue(item: "aaa"),ASValue(item: "bbb")], selected: ASValue(item: "aaa"))
var body: some View {
ArrayStepperList(values: values) // for testing
}
}
struct ArrayStepperList<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var values: ArrayStepperValues<T>
// let display: (T) -> String // for testing
var body: some View {
List {
ForEach(values.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
DispatchQueue.main.async {
values.selected.item = item.item // <-- here
// dismiss() // for testing
}
}) {
HStack {
Text("\(item.item as! String)") // for testing
Spacer()
if values.selected.item == item.item { // <-- here
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.listStyle(InsetGroupedListStyle())
// .navigationTitle(Text(display(values.selected.item))) // for testing
}
}

SwiftUI ScrollView not following the chained animation

Back again for some SwiftUI issues. haha
So I have a scroll view, and I simulate a user switching elements in the scrollview pretty fast.
(ex. Music Lyrics that adjust to where the user is listening).
My issue here is the animation are not following the speed and I was wondering if there was a way to prevent that. Cancel the previous animation for example? I haven't found anything atm to fix this problem.
The following code is used to reproduce the Animation issue.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ScrollView(showsIndicators: false, content: {
ScrollViewReader(content: { scrollViewProxy in
LazyVStack(content: {
ForEach(viewModel.fragments, id: \.id) { fragment in
Text(fragment.content + fragment.id)
.background(fragment == viewModel.currentFragment ? Color.yellow : Color.gray)
.frame(height: 100)
.font(.largeTitle)
.id(fragment.id)
}
})
.id("ContentView-LazyVStack-Animation")
.onReceive(viewModel.$currentFragment, perform: { currentFragment in
guard let currentFragment = currentFragment else {
return
}
withAnimation(.easeInOut(duration: 2)) {
scrollViewProxy.scrollTo(currentFragment.id, anchor: .center)
}
})
})
})
.id("ContentView-ScrollView-Animation")
}
}
final class ViewModel: ObservableObject {
#Published var fragments: [Fragment] = [Int](0..<100).map({ Fragment(id: "\($0)", content: "Some text yeah! super cool.") })
#Published var currentFragment: Fragment?
private var scrollingTimer: Timer?
init() {
currentFragment = fragments.first
setupRandomScroll()
}
func setupRandomScroll() {
scrollingTimer = Timer.scheduledTimer(withTimeInterval: 0.2,
repeats: true,
block: { [weak self] _ in
guard let self = self else {
return
}
let newIndex = Int.random(in: 70..<100)
self.currentFragment = self.fragments[newIndex]
})
}
}
final class Fragment: ObservableObject, Equatable, Hashable {
var id: String
#Published var content: String
init(id: String, content: String) {
self.id = id
self.content = content
}
static func == (lhs: Fragment, rhs: Fragment) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Thanks in advance for any help given! :)

TextField in SwiftUI loses focus when I enter a character

I have a problem when I enter a character within a TextField (Within ExerciseSetView), I have to re-click the text box to make it so I can enter another character. If I remove the bindings from the Textfield I can enter text fluidly.
I think it has something to do with my presenter class and the updateSet function recreating a set instance because I have to replace some values two levels deep within an array.
//
// ContentView.swift
// test
//
//
import SwiftUI
import Combine
import CoreData
class WorkoutExerciseSetVM: Hashable, ObservableObject {
#Published public var id: Int
#Published public var reps: String
#Published public var weight: String
init(id: Int, reps: String, weight: String) {
self.id = id
self.reps = reps
self.weight = weight
}
static func ==(lhs: WorkoutExerciseSetVM, rhs: WorkoutExerciseSetVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutExerciseVM: Hashable, ObservableObject {
#Published public var id: UUID
#Published public var name: String
#Published public var sets: [WorkoutExerciseSetVM]
init(id: UUID, name: String, sets: [WorkoutExerciseSetVM]) {
self.id = id
self.name = name
self.sets = sets
}
static func ==(lhs: WorkoutExerciseVM, rhs: WorkoutExerciseVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutVM: Hashable, ObservableObject {
#Published public var id = UUID()
#Published public var name: String
#Published public var exercises: [WorkoutExerciseVM]
#Published public var started: Date? = Date()
#Published public var completed: Date? = Date()
init(id: UUID, name: String, exercises: [WorkoutExerciseVM], started: Date?, completed: Date?) {
self.id = id
self.name = name
self.exercises = exercises
self.started = started
self.completed = completed
}
static func ==(lhs: WorkoutVM, rhs: WorkoutVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutPresenter: ObservableObject {
#Published public var id: UUID
#Published public var exercises: [WorkoutExerciseVM]
#Published public var name: String
#Published public var started: Date?
#Published public var completed: Date?
init(routine: WorkoutVM) {
self.id = UUID()
self.name = routine.name
self.started = Date()
self.completed = nil
self.exercises = routine.exercises.map{ exercise in
return WorkoutExerciseVM(
id: UUID(),
name: exercise.name,
sets: [
WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")
]
)
}
}
func removeExercise(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.filter{$0.id.uuidString != exerciseId}
}
func addSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map {
if ($0.id.uuidString == exerciseId) {
if ($0.sets.count == 0) {
$0.sets.append(WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0"))
}
if let lastSet = $0.sets.last {
$0.sets.append(WorkoutExerciseSetVM(id: lastSet.id + 1, reps: lastSet.reps, weight: lastSet.weight))
}
}
return $0
}
}
func updateSet(id: UUID, set: WorkoutExerciseSetVM) {
let exerciseId = id.uuidString
self.exercises = self.exercises.map{
if $0.id.uuidString == exerciseId {
$0.sets = $0.sets.map{(oldExerciseSet) -> WorkoutExerciseSetVM in
if oldExerciseSet.id == set.id {
return set
}
return oldExerciseSet
}
return $0
}
return $0;
}
}
func removeSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map{(exercise) -> WorkoutExerciseVM in
if exercise.id.uuidString == exerciseId {
let newExercise = exercise
if newExercise.sets.count > 1 {
newExercise.sets.removeLast()
}
return newExercise
}
return exercise;
}
}
}
struct ContentView: View {
var body: some View {
VStack {
WorkoutView(presenter: WorkoutPresenter(routine: WorkoutVM(id: UUID(), name: "Test", exercises: [WorkoutExerciseVM(id: UUID(), name: "Exercise", sets: [WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")])], started: nil, completed: nil)))
}
}
}
struct WorkoutView: View {
#ObservedObject var presenter: WorkoutPresenter
var body: some View {
return GeometryReader { geo in
ZStack {
VStack {
ScrollView {
ForEach(self.presenter.exercises, id: \.self) { exercise in
ExerciseView(presenter: self.presenter, exercise: exercise)
}
}
}
}
}
}
}
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack {
VStack {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(
set: exerciseSet,
onUpdate: { newExerciseSet in
self.presenter.updateSet(id: self.exercise.id, set: newExerciseSet)
}
)
}
}
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
var set: WorkoutExerciseSetVM
var onUpdate: (_ set: WorkoutExerciseSetVM) -> Void
var body: some View {
let repBinding = Binding(
get: {
String(self.set.reps)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: $0 , weight: self.set.weight))
}
}
)
let weightBinding = Binding(
get: {
String(self.set.weight)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: self.set.reps, weight: $0 ))
}
}
)
return HStack {
Spacer()
// textfield that isn't working
TextField("", text: repBinding)
Spacer()
// textfield that isn't working
TextField("", text: weightBinding)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your code has some fundamental errors. Please research about ObservableObject and Published values before going into production with this code. Otherwise, it would be quite hard to deal with this code later.
I have updated your views and that seems to work. You are not using ObservableObject as they should be used. Just pass ObservableObjectss around let them do the bindings for you instead of setting custom bindings.
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
#ObservedObject var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(set: exerciseSet)
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
#ObservedObject var set: WorkoutExerciseSetVM
var body: some View {
HStack {
Spacer()
TextField("", text: $set.reps)
Spacer()
TextField("", text: $set.weight)
}
}
}
Let me know if this works for you.

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}