Performance Optimization

Sep 21 2025 · Swift 6, iOS 26, Xcode 26

Lesson 03: Thread Optimization & Memory Management

Demo

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll use the Cinematica app from the second lesson, where you’ve already made efficiency enhancements and tackled Network and Data Optimization. Now, it’s time to dive into Thread Optimization. You’ll aim to boost app responsiveness and performance, ultimately delivering a smoother user experience.

Thread Optimization

Multi threading

Building upon what you learned in the previous lesson about fetching upcoming movies, you will now also fetch top rated and popular movies concurrently thanks to multi threading to improve the information available to your app users without compromising performance.

You can start adding the new network requests. Open APIsPaths to add the new endpoints:

static let topRatedPath = moviePath + "/top_rated"
static let popularPath = moviePath + "/popular"

Then, open MoviesRequests and add the following cases to the enum:

case fetchTopRated(page: Int)
case fetchPopular(page: Int)

Then, update the path property to support the new endpoint.

case .fetchTopRated:
  return APIsPaths.topRatedPath
case .fetchPopular:
  return APIsPaths.popularPath

Finally, update the urlParams property to add the correct parameters, and remove the exsiting .fetchUpcoming case since you’ll be handling it along with the new cases.

case .fetchUpcoming(let page), .fetchTopRated(let page), .fetchPopular(let page):
  return ["page": "\(page)", "sort_by": "popularity.desc"]

Your app is now ready to fetch top rated and popular movies. It’s time to add this data to your screen.

You can start adding the logic in your view model. Open MovieListViewModel and replace the property movies with corresponding property for each movie type.

var upcomingMovies: [Movie] = []
var topRatedMovies: [Movie] = []
var popularMovies: [Movie] = []

Then, replace the fetchMovies method with 3 methods for each corresponding type of movies. Each method will fetch the right movies for its category. Then it’ll append the movies to the corresponding property.

func fetchUpcomingMovies() async throws {
  let moviePaginatedResponse: MoviePaginatedResponse = try await
  requestManager.perform(MoviesRequests.fetchUpcoming(page: currentPage))
  self.upcomingMovies.append(contentsOf: moviePaginatedResponse.results ?? [])
}

func fetchTopRatedMovies() async throws {
  let moviePaginatedResponse: MoviePaginatedResponse = try await
  requestManager.perform(MoviesRequests.fetchTopRated(page: currentPage))
  self.topRatedMovies.append(contentsOf: moviePaginatedResponse.results ?? [])
}

func fetchPopularMovies() async throws {
  let moviePaginatedResponse: MoviePaginatedResponse = try await
  requestManager.perform(MoviesRequests.fetchPopular(page: currentPage))
  self.popularMovies.append(contentsOf: moviePaginatedResponse.results ?? [])
}

With the latest update, the view model can fetch upcoming, top rated, and popular movies and store them in local arrays. Now, it’s time to present this data to the view.

Create a new file MovieSectionView.swift which will contain the items of each movie list:

import SwiftUI

struct MovieSectionView: View {
  let movies: [Movie]
  let title: String

  var body: some View {
    VStack(alignment: .leading, spacing: 5) {
      Text(title)
        .font(.title)
        .padding(.top, 15)
        .padding(.leading, 15)

      ScrollView(.horizontal, showsIndicators: false) {
        LazyHStack(spacing: 10) {
          ForEach(movies, id: \.id) { movie in
            MovieCellView(movie: movie)
              .frame(width: 150, height: 200)
          }
        }
      }
    }
    .background(Color(.secondarySystemBackground))
  }
}

Then, open MovieListView and replace the content of ScrollView with the three sections for the different movie categories.

VStack(spacing: 20) {
  MovieSectionView(movies: movieListViewModel.upcomingMovies, title: "Upcoming")
  MovieSectionView(movies: movieListViewModel.topRatedMovies, title: "Top Rated")
  MovieSectionView(movies: movieListViewModel.popularMovies, title: "Popular")
}
.padding()

Then, remove the excessive part in handling the error.

if movieListViewModel.errorManager.errorMessage != nil

Next, you’ll need to replace the action of the retry button and the content of .task to fetch all the movie categories. You could do this by calling a single line for each method with await, try, and catch. But this way will make the api calls happen sequentially where each call await to get its results before proceeding on the next call. Instead, you’ll use withThrowingTaskGroup with do-catch blocks to make sure that all the three api calls happen concurrently and to stop loading after all the three api calls succeeded.

Open MovieListViewModel again then add the fetchAllMovies method. This method uses withThrowingTaskGroup to group the api calls together. You add tasks or calls to the group containing the do-catch block of your call. Then after fetching all the three api calls, you make sure to stop the loading.

func fetchAllMovies() async {
  isLoading = true
  do {
    await withThrowingTaskGroup(of: Void.self) { group in
      group.addTask {
        do {
          try await self.fetchUpcomingMovies()
        } catch {
          self.errorManager.handleError(error)
        }
      }
      group.addTask {
        do {
          try await self.fetchTopRatedMovies()
        } catch {
          self.errorManager.handleError(error)
        }
      }
      group.addTask {
        do {
          try await self.fetchPopularMovies()
        } catch {
          self.errorManager.handleError(error)
        }
      }
    }
  }
  isLoading = false
}

Now, Open MovieListView. Replace the action of the retry button and the content of .task with the new method to fetch movies.

Task {
  await movieListViewModel.fetchAllMovies()
}

And, finally, you can delete the method fetchMovies which is no longer needed.

Build and run your app now to witness the simultaneous fetching of three distinct movie lists without any interruption to the user interface. This lists here are open for more edits and improvements like including pagination for all lists. This tutorial put you on the first step and you can go further to more advanced features.

Thread hangs

The code you previously added involves the app retrieving three movie lists in a background thread and subsequently refreshing the UI on the main thread using the view model properties with the help of @Observable. This approach ensures a seamless user interface experience without blocking the UI.

Neglecting to optimize the threading of your iOS app and executing all tasks in the main thread can lead to unresponsive UI whenever heavy or pending tasks, such as computations and network requests, are initiated. Unresponsiveness occurs when the main thread is occupied with completing its tasks. Luckily, to address such issues, Xcode offers an Instrument tool for debugging.

Let’s simulate heavy computation by fetching data in the main thread with a 10-second sleep for demo purposes. Open APIManager, add @MainActor and a sleep to the method perform.

@MainActor // 1
public func perform(_ request: RequestProtocol) async throws -> Data {
  sleep(5) // 2
  // ...
  1. As you already knew, @MainActor will run the perform method on the main thread.
  2. sleep simulates a heavy computation sleeping the thread for 5 seconds.

As soon as the user launches the app and it starts fetching the movie lists, this code will cause the main thread to hang. To identify and debug this hang, you can utilize Instruments:

  1. Launch Instrument from Xcode with either the shortcut ⌘ + I or from the menu Product > Profile
  2. Select Time Profiler
  3. Start recoding the app with either the shortcut ⌘ + R or pressing the record button on top left of the profiler window

When Instruments starts recording the app, the row labeled Hangs will display several instances of hanging due to the sleep we added in the main thread. To identify the source of this hang, select the last row labeled Cinematica. Instruments will then show a call tree of your application. Within the call tree, you will notice a call named completeTaskWithClosure that is taking some time. Although the name may not be very helpful initially, clicking on it will reveal a list called Heaviest Stack Trace on the right side of the call tree. This stack trace will highlight the specific method that is causing the hang in the main thread, which in our case is APIManager.perform(_:).

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion