Home > Enterprise >  Why are more views being instantiated than expected?
Why are more views being instantiated than expected?

Time:01-21

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.

  • Related