SwiftUI ScrollView not following the chained animation - swift

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! :)

Related

Marquee with buttons inside a view SwiftUI

I'm trying to have a marquee horizontal scrolling effect but my buttons are not clickable. The view renders well, but when I tap the buttons it should print out 'tapped user1', for example but there is no effect.
EDIT: If I put the marquee modifier on the ScrollView as suggested, it causes the extremely buggy scrolling behavior. Ideally, this would just be a marquee'd HStack with a bunch of clickable buttons in it with no scrolling behavior built in, but the module doesn't seem to work without the ScrollView wrapping it.
I used this link to create a Marquee view modifier: https://swiftuirecipes.com/blog/swiftui-marquee
My code for the view is below:
struct MyView: View {
var body: some View {
let users = ["user1", "user2", "user3", "user4", "user5", "user6"]
return ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(users, id: \.self) { user in
if user == users.first {
Button(action: {
print("tapped \(user)")
}, label: {
Text(user)
})
} else {
Text("•")
Button(action: {
print("tapped \(user)")
}, label: {
Text(user)
})
}
}
}.frame(height: 20)
.marquee(duration: 10)
}
}
}
The code from the marquee tutorial is below:
struct Marquee: ViewModifier {
let duration: TimeInterval
let direction: Direction
let autoreverse: Bool
#State private var offset = CGFloat.zero
#State private var parentSize = CGSize.zero
#State private var contentSize = CGSize.zero
func body(content: Content) -> some View {
// measures parent view width
Color.clear
.frame(height: 0)
// measureSize from https://swiftuirecipes.com/blog/getting-size-of-a-view-in-swiftui
.measureSize { size in
parentSize = size
updateAnimation(sizeChanged: true)
}
content
.measureSize { size in
contentSize = size
updateAnimation(sizeChanged: true)
}
.offset(x: offset)
// animationObserver from https://swiftuirecipes.com/blog/swiftui-animation-observer
.animationObserver(for: offset, onComplete: {
updateAnimation(sizeChanged: false)
})
}
private func updateAnimation(sizeChanged: Bool) {
if sizeChanged || !autoreverse {
offset = max(parentSize.width, contentSize.width) * ((direction == .leftToRight) ? -1 : 1)
}
withAnimation(.linear(duration: duration)) {
offset = -offset
}
}
enum Direction {
case leftToRight, rightToLeft
}
}
extension View {
func marquee(duration: TimeInterval,
direction: Marquee.Direction = .rightToLeft,
autoreverse: Bool = false) -> some View {
self.modifier(Marquee(duration: duration,
direction: direction,
autoreverse: autoreverse))
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct MeasureSizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self,
value: geometry.size)
})
}
}
extension View {
func measureSize(perform action: #escaping (CGSize) -> Void) -> some View {
self.modifier(MeasureSizeModifier())
.onPreferenceChange(SizePreferenceKey.self, perform: action)
}
}
public struct AnimationObserverModifier<Value: VectorArithmetic>: AnimatableModifier {
// this is the view property that drives the animation - offset, opacity, etc.
private let observedValue: Value
private let onChange: ((Value) -> Void)?
private let onComplete: (() -> Void)?
// SwiftUI implicity sets this value as the animation progresses
public var animatableData: Value {
didSet {
notifyProgress()
}
}
public init(for observedValue: Value,
onChange: ((Value) -> Void)?,
onComplete: (() -> Void)?) {
self.observedValue = observedValue
self.onChange = onChange
self.onComplete = onComplete
animatableData = observedValue
}
public func body(content: Content) -> some View {
content
}
private func notifyProgress() {
DispatchQueue.main.async {
onChange?(animatableData)
if animatableData == observedValue {
onComplete?()
}
}
}
}
public extension View {
func animationObserver<Value: VectorArithmetic>(for value: Value,
onChange: ((Value) -> Void)? = nil,
onComplete: (() -> Void)? = nil) -> some View {
self.modifier(AnimationObserverModifier(for: value,
onChange: onChange,
onComplete: onComplete))
}
}
Put the modifier on scrollview, it will fix your issue
Like this.
import SwiftUI
struct MyView: View {
var body: some View {
let users = ["user1", "user2", "user3", "user4", "user5", "user6"]
return ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(users, id: \.self) { user in
if user == users.first {
Button(action: {
print("tapped \(user)")
}, label: {
Text(user)
})
} else {
Text("•")
Button(action: {
print("tapped \(user)")
}, label: {
Text(user)
})
}
}
}.frame(height: 20)
}
.marquee(duration: 10)
}
}
Try adding the modifier on the button .buttonStyle(PlainButtonStyle()) or similar with other button styles. If that doesn't work, then try using just the button label that you desire and add the modifier .onTapGesture {your code}

SwiftUI #Binding reloading on push/pop with different navigation items

I've got a very simple app example that has two views: a MasterView and a DetailView. The MasterView is presented inside a ContentView with a NavigationView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView(viewModel: MasterViewModel())
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton()
)
}
}
}
struct MasterView: View {
#ObservedObject private var viewModel: MasterViewModel
init(viewModel: MasterViewModel) {
self.viewModel = viewModel
}
var body: some View {
print("Test")
return DataStatusView(dataSource: self.$viewModel.result) { texts -> AnyView in
print("Closure")
return AnyView(List {
ForEach(texts, id: \.self) { text in
NavigationLink(
destination: DetailView(viewModel: DetailViewModel(stringToDisplay: text))
) {
Text(text)
}
}
})
}.onAppear {
if case .waiting = self.viewModel.result {
self.viewModel.fetch()
}
}
}
}
struct DetailView: View {
#ObservedObject private var viewModel: DetailViewModel
init(viewModel: DetailViewModel) {
self.viewModel = viewModel
}
var body: some View {
self.showView().onAppear {
self.viewModel.fetch()
}
.navigationBarTitle(Text("Detail"))
}
func showView() -> some View {
switch self.viewModel.result {
case .found(let s):
return AnyView(Text(s))
default:
return AnyView(Color.red)
}
}
}
The DataStatusView is a simple view to manage some state:
public enum ResultState<T, E: Error> {
case waiting
case loading
case found(T)
case failed(E)
}
struct DataStatusView<Content, T>: View where Content: View {
#Binding private(set) var dataSource: ResultState<T, Error>
private let content: (T) -> Content
private let waitingContent: AnyView?
#inlinable init(dataSource: Binding<ResultState<T, Error>>,
waitingContent: AnyView? = nil,
#ViewBuilder content: #escaping (T) -> Content) {
self._dataSource = dataSource
self.waitingContent = waitingContent
self.content = content
}
var body: some View {
self.buildMainView()
}
private func buildMainView() -> some View {
switch self.dataSource {
case .waiting:
return AnyView(Color.red)
case .loading:
return AnyView(Color.green)
case .found(let data):
return AnyView(self.content(data))
case .failed:
return AnyView(Color.yellow)
}
}
}
and the view models are a very simple "pretend to make a network call" vm:
final class MasterViewModel: ObservableObject {
#Published var result: ResultState<[String], Error> = .waiting
init() { }
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(["This", "is", "a", "test"])
}
}
}
final class DetailViewModel: ObservableObject {
#Published var result: ResultState<String, Error> = .waiting
private let stringToDisplay: String
init(stringToDisplay: String) {
self.stringToDisplay = stringToDisplay
}
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(self.stringToDisplay)
}
}
}
Now the problem I'm having is that every time I go from Master -> Detail view the block inside the DataStatusView is called. This is a problem because the "DetailView" is constantly re-created (and therefore its vm too, which causes the loading of the detail's data to fail).
This is happening because when I go from master -> detail the buttons in the navigation bar change (or at least that's the hypothesis). When I remove the lines:
.navigationBarItems(
leading: EditButton()
)
This works as "expected".
What is the "SwiftUI" way of dealing with this? A sample project that shows this issue is here: https://github.com/kerrmarin/swiftui-mvvm-master-detail

#State updates view but #ObservedObject does not

I have a view:
struct Form: View {
#ObservedObject var model = FormInput()
var body: some View {
Form {
TextArea("Details", text: $model.details.value)
.validation(message: model.details.message)
}
}
}
Where TextArea is a custom view and .validation(message: model.details.message) is a custom view modifier. My FormInput looks as follows:
final class FormInput: ObservableObject {
#Published var details: TextInput
// ... other code
}
TextInput is a custom struct.
Now when I run the above code the validation never triggers because the Form never re-renders, but if I change the Form to:
struct MyForm: View {
#State var details = TextInput()
var body: some View {
Form {
TextArea("Details", text: $details.value)
.validation(message: details.message)
}
}
}
Then everything works as expected. Why would the view render for the second version of Form but not for the first? Shouldn't the Form in the first case update when details changes since details is #Published and the Form is observing the changes?
ADDITIONAL CONTEXT
Below is additional code for the above components
final class FormInput: ObservableObject {
#Published var details: TextInput
init(details: String = "") {
self.details = TextInput(value: details, isValid: false, validations: [.length(12)])
}
// other code
}
struct TextInput {
var value: String {
didSet {
self.validate()
}
}
var validations: [TextValidation] // this is just an enum of different types of validations
var isValid: Bool
var message: String
mutating func validate() {
for validation in validations {
// run validation
}
}
}
struct TextArea: View {
#Binding var text: String
#State var height: CGFloat = 12
init(text: Binding<String>) {
self._text = text
}
var body: some View {
TextAreaField(text: $text)
}
}
struct TextAreaField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
func makeUIView(context: Context) -> UITextView {
let textField = UITextView()
textField.isEditable = true
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = false
// ..other initializers removed for brevity
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextView, context: Context) {
calculateHeight(uiView)
}
func calculateHeight(_ uiView: UIView) {
let newSize = uiView.sizeThatFits(CGSize(width: uiView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if self.height != newSize.height {
DispatchQueue.main.async {
self.height = newSize.height
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject, UITextViewDelegate {
var parent: TextAreaField
init(_ parent: TextAreaField) {
self.parent = parent
}
func textViewDidChange(_ uiView: UITextView) {
self.parent.text = uiView.text
self.parent.calculateHeight(uiView)
}
}
}
struct Validation: ViewModifier {
let message: String
func body(content: Content) -> some View {
let isValid = message == ""
print(message)
return VStack(alignment: .leading) {
content
if isValid == false {
Text(message)
.font(.footnote)
.italic()
.foregroundColor(Color.red)
.frame(height: 24)
}
}
.padding(.bottom, isValid ? 24 : 0)
}
}
extension View {
func validation(message: String) -> some View {
self.modifier(Validation(message: message))
}
}
I suppose the issue is in absent custom components. Because below simple demo replication of provided infrastructure works well with ObservableObject, actually as expected.
Tested with Xcode 11.4 / iOS 13.4. Comparing with below demo might be helpful to find what is missed in your code.
struct TextInput {
var value: String = "" {
didSet {
message = value // just duplication for demo
}
}
var message: String = ""
}
// simple validator highlighting text when too long
struct ValidationModifier: ViewModifier {
var text: String
func body(content: Content) -> some View {
content.foregroundColor(text.count > 5 ? Color.red : Color.primary)
}
}
extension View { // replicated
func validation(message: String) -> some View {
self.modifier(ValidationModifier(text: message))
}
}
struct MyForm: View { // avoid same names with standard views
#ObservedObject var model = FormInput()
var body: some View {
Form {
TextField("Details", text: $model.details.value) // used standard
.validation(message: model.details.message)
}
}
}
final class FormInput: ObservableObject {
#Published var details: TextInput = TextInput()
}
struct TestObservedInModifier: View {
var body: some View {
MyForm()
}
}

SwiftUI not updating ObservedObject in child-view

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
}

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.