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 events
Starting 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.
CodePudding user response:
ExerciseViewModel
should be a state object at the top of your view hierarchy, and an observable object thereafter, so
@StateObject var exerciseViewModel = ExerciseViewModel()
In ExerciseView
should be
@ObservedObject var exerciseViewModel: ExerciseViewModel
You want the same instance to be used everywhere.
ExerciseListView.workout
seems a bit of a strange property, it's not clear where that comes from but if it's something immutable that's set from outside, make it a let
.
Don't concern yourself much with the number of times a SwiftUI view is initialised. The framework will be rebuilding the view hierarchy whenever it observes a change in a published or state or other special property. Your views should essentially be free to create, in that you shouldn't be doing anything heavy in the init methods.
Which makes me suspect that this line:
self.setsViewModel = SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise)
Could be part of your problem. You don't show the implementation of this, but if you're creating it on initialisation then it should be a state object, which you create like this:
@StateObject var setsViewModel: SetsViewModel
...
//in your init
_setsViewModel = StateObject(wrappedValue: SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise))
This ensures the view model will only be created once.
The last thing that looks a bit suspect is apiManager.getToken()
- this looks like it may well involve an API call, but you are not treating it as asynchronous.
To really work out what's happening, you're off to a good start with the logging, but you could add more. You can also put some breakpoints and step through the code, perhaps pause the program in the debugger while it's hanging, and check the CPU usage or profile in instruments.