Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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()
    }
}
like image 219
mintuz Avatar asked Nov 06 '22 07:11

mintuz


1 Answers

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.

like image 58
Cuneyt Avatar answered Nov 15 '22 04:11

Cuneyt