ios – Why are more views being instantiated than expected?

0
135


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.