I have three views: ExerciseList View, ExerciseView, and SetsView. The transition between two of them takes a long time (5-15 seconds), and I’m trying to diagnose and address why.
ExerciseListView lists the exercises and lets the user start a session (whose returned session_id is used by the the later Views), and ExerciseView contains a SetsView for each of the exercises. In addition I have an exerciseViewModel, that GETs the exercises from an API and POSTs a session to the API. Here is the code:
struct ExerciseListView: View {
@StateObject var exerciseViewModel = ExerciseViewModel()
var workout: Workout
var body: some View {
NavigationStack {
ScrollView {
VStack{
ForEach(exerciseViewModel.exercises, id: \.exercise_id) { exercise in
exerciseListRow(exercise: exercise)
}
}.navigationTitle(workout.workout_name)
.toolbar{startButton}
}
}.onAppear {
self.exerciseViewModel.fetch(workout_id: workout.workout_id)
}
}
var startButton: some View {
NavigationLink(destination: ExerciseView(exerciseViewModel: exerciseViewModel, workout: workout)) {
Text("Start Workout")
}
}
}
struct exerciseListRow: View {
let exercise: Exercise
var body: some View {
Text(String(exercise.set_number) + " sets of " + exercise.exercise_name + "s").padding(.all)
.font(.title2)
.fontWeight(.semibold)
.frame(width: 375)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10.0)
}
}
struct Exercise: Hashable, Codable {
var exercise_id: Int
var exercise_name: String
var set_number: Int
}
class ExerciseViewModel: ObservableObject {
var apiManager = ApiManager()
@Published var exercises: [Exercise] = []
@Published var session_id: Int = -1
func fetch(workout_id: Int) {
self.apiManager.getToken()
print("Calling workout data with workout_id " + String(workout_id))
guard let url = URL(string: (self.apiManager.myUrl + "/ExercisesInWorkout"))
else {
print("Error: Something wrong with url.")
return
}
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
urlRequest.allHTTPHeaderFields = [
"Token": self.apiManager.token
]
urlRequest.httpMethod = "POST"
let body = "workout_id="+String(workout_id)
urlRequest.httpBody = body.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
//Convert to json
do {
let exercises = try JSONDecoder().decode([Exercise].self, from: data)
DispatchQueue.main.async {
// print(exercises)
self?.exercises = exercises
}
}
catch {
print("Error: something went wrong calling api", error)
}
}
task.resume()
}
func sendSession(workout_id: Int) {
self.apiManager.getToken()
print("Sending session with workout_id " + String(workout_id))
guard let url = URL(string: (self.apiManager.myUrl + "/Session"))
else {
print("Error: Something wrong with url.")
return
}
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
urlRequest.allHTTPHeaderFields = [
"Token": self.apiManager.token
]
urlRequest.httpMethod = "POST"
let body = "workout_id="+String(workout_id)
urlRequest.httpBody = body.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
do {
let decoded = try JSONDecoder().decode(Int.self, from: data)
DispatchQueue.main.async {
self?.session_id = decoded
}
}
catch {
print("Error: something went wrong calling api", error)
}
}
task.resume()
}
}
struct ExerciseView: View {
@StateObject var exerciseViewModel = ExerciseViewModel()
var workout: Workout
@State var session_started: Bool = false
var body: some View {
NavigationStack {
VStack {
List {
Section(header: Text("Enter exercise data")) {
ForEach(exerciseViewModel.exercises, id: \.exercise_id) { exercise in
NavigationLink(destination: SetsView(workout: workout, exercise: exercise, session_id: exerciseViewModel.session_id)) {
Text(exercise.exercise_name)
}
}
}
}.listStyle(GroupedListStyle())
}.navigationTitle(workout.workout_name)
}.onAppear {
if !self.session_started {
self.exerciseViewModel.sendSession(workout_id: workout.workout_id)
self.session_started = true
}
}
}
}
struct SetsView: View {
var workout: Workout
var exercise: Exercise
var session_id: Int
@ObservedObject var setsViewModel: SetsViewModel
@State var buttonText: String = "Submit All"
@State var lastSetsShowing: Bool = false
init(workout: Workout, exercise: Exercise, session_id: Int) {
print("Starting init for sets view with exercise with session:", session_id, exercise.exercise_name)
self.workout = workout
self.exercise = exercise
self.session_id = session_id
self.setsViewModel = SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise)
}
var body: some View {
ScrollView {
VStack {
ForEach(0..<exercise.set_number) {set_index in
SetView(set_num: set_index, setsViewModel: setsViewModel)
Spacer()
}
submitButton
}.navigationTitle(exercise.exercise_name)
.toolbar{lastSetsButton}
}.sheet(isPresented: $lastSetsShowing) { LastSetsView(exercise: self.exercise, workout_id: self.workout.workout_id)
}
}
var lastSetsButton: some View {
Button("Show Last Sets") {
self.lastSetsShowing = true
}
}
var submitButton: some View {
Button(self.buttonText) {
if entrysNotNull() {
self.setsViewModel.postSets()
self.buttonText = "Submitted"
}
}.padding(.all).foregroundColor(Color.white).background(Color.blue).cornerRadius(10)
}
func entrysNotNull() -> Bool {
if (self.buttonText != "Submit All") {return false}
for s in self.setsViewModel.session.sets {
if ((s.weight == nil || s.reps == nil) || (s.quality == nil || s.rep_speed == nil)) || (s.rest_before == nil) {
return false}
}
return true
}
}
My issue is that there is a large lag after hitting the “Start Workout” button in ExerciseListView before it opens ExerciseView. It has to make an API call and load a view for each exercise in the workout, but considering the most this is is like 7, it is odd it takes so long.
When the Start Button is clicked here is an example response:
Starting init for sets view with exercise with session: -1 Bench Press
Starting init for sets view with exercise with session: -1 Bench Press
Starting init for sets view with exercise with session: -1 Pull Up
Starting init for sets view with exercise with session: -1 Pull Up
Starting init for sets view with exercise with session: -1 Incline Dumbell Press
Starting init for sets view with exercise with session: -1 Incline Dumbell Press
Starting init for sets view with exercise with session: -1 Dumbell Row
Starting init for sets view with exercise with session: -1 Dumbell Row
2023-01-20 00:54:09.174902-0600 BodyTracker[4930:2724343] [connection] nw_connection_add_timestamp_locked_on_nw_queue [C1]
Hit maximum timestamp count, will start dropping eventsStarting init for sets view with exercise with session: -1 Shrugs
Starting init for sets view with exercise with session: -1 Shrugs
Starting init for sets view with exercise with session: -1 Decline Dumbell Press
Starting init for sets view with exercise with session: -1 Decline Dumbell Press
Sending session with workout_id 3
Starting init for sets view with exercise with session: 102 Bench Press
Starting init for sets view with exercise with session: 102 Pull Up
Starting init for sets view with exercise with session: 102 Incline Dumbell Press
Starting init for sets view with exercise with session: 102 Dumbell Row
Starting init for sets view with exercise with session: 102 Shrugs
Starting init for sets view with exercise with session: 102 Decline Dumbell Press
Starting init for sets view with exercise with session: 102 Bench Press
Starting init for sets view with exercise with session: 102 Pull Up
Starting init for sets view with exercise with session: 102 Incline Dumbell Press
Starting init for sets view with exercise with session: 102 Dumbell Row
Starting init for sets view with exercise with session: 102 Shrugs
Starting init for sets view with exercise with session: 102 Decline Dumbell Press
Why does the init statement get repeated so many times? Instead of 6 initializations for the 6 exercises, it’s 24. And I’m assuming the first 12 inits are with -1 because that’s what I instantiate session_id to in exerciseViewModel, but is there a better way to make it work? I’ve tried using DispatchSemaphore, but the App just gets stuck for some reason. Should I pass around the whole viewmodel instead of just the id? Or perhaps the ag is created by something else I’m missing. I’m fairly confident it’s not the API, as non of the other calls take any significant time. Please help me with the right way to set this up.