Performance Optimization

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

Lesson 02: Networking Optimization & Caching

Demo

Episode complete

Play next episode

Next
Transcript

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

Networking Optimization

Open the starter project for this lesson. It’s the same as the final version you reached in lesson one, with the addition of a few files that you’ll use later in this demo. Build and run the app to check the latest status from the previous lesson.

Pagination

The first improvement you’ll make is to handle pagination in your API calls. Open MovieListViewModel.swift, then add the pagination properties to your properties list. The currentPage property will handle the latest page downloaded from these movie lists. The total pages property will have this movie list’s total number of pages. Finally, you’ll add isFetching to prevent calling the same API twice during an ongoing call.

private var currentPage = 1
private var totalPages = 1
private var isFetching = false

Next, replace the implementation of the fetchMovies method to handle the pagination:

func fetchMovies() async {
  // 1
  guard !isFetching && currentPage <= totalPages else { return }
  isFetching = true

  do {
    let moviePaginatedResponse: MoviePaginatedResponse = try await
    requestManager.perform(MoviesRequests.fetchUpcoming(page: currentPage))
    let newMovies = moviePaginatedResponse.results ?? []
    await MainActor.run {
      self.movies.append(contentsOf: newMovies)
      self.isLoading = false
      // 2
      self.totalPages = moviePaginatedResponse.totalPages ?? 1
      self.currentPage += 1
      self.isFetching = false
    }
  } catch {
    await MainActor.run {
      self.isLoading = false
      // 3
      self.isFetching = false
    }
  }
}

Here’s a code breakdown:

  1. You add a check to prevent fetching the movies when there’s an ongoing API or if you reached the last page for this movie list.
  2. After fetching the movies, you update the pagination properties with the new values and enable the fetching.
  3. You also enable fetching movies if an error happens to the last API.

Check the pagination functionality. Build and run the app.

Now, as you scroll down to the last movie on the list, you may notice nothing happening beyond that point. Wondering why the remaining movies don’t load? It’s because you haven’t implemented pagination yet after the initial call of the fetchMovies method.

One common approach to address this issue is to trigger the fetchMovies method again when the user scrolls near the end of the current list. Proceed with implementing this functionality.

Open MovieListView.swift and add the pagination check in the onAppear of the MovieCellView inside the ForEach:

.onAppear {
  if movie.id == movieListViewModel.movies.last?.id {
    fetchMovies()
  }
}

This code checks if the user reaches the last movie and then fetches the next page. You can change this to call it when the user reaches the last three or four elements.

Finally, remember to add the fetchMovies method inside the MovieListView.swift to call the fetchMovies method from the ViewModel within the Task block.

private func fetchMovies() {
  Task {
    await movieListViewModel.fetchMovies()
  }
}

Build and run the app. Now, as you scroll down to the last movie in the list, you may notice that the subsequent page loads, and the list becomes larger, showing more movies.

Now that you’ve implemented pagination in your app, it’s essential to consider how to handle potential errors that may occur during API calls. How would you communicate these errors to the user, and what strategies would you employ to address various types of errors? You’ll address this aspect now.

Error Handling

Open APIManager.swift, then replace the implementation of the perform method to handle errors:

public func perform(_ request: RequestProtocol) async throws -> Data {
  let (data, response) = try await urlSession.data(for: request.createURLRequest())
  // 1
  guard let httpResponse = response as? HTTPURLResponse else {
    throw NetworkError.invalidServerResponse
  }

  // 2
  switch httpResponse.statusCode {
  case 200...299:
    return data
  case 400...499:
    throw NetworkError.clientError
  case 500...599:
    throw NetworkError.serverError
  default:
    throw NetworkError.unknownError
  }
}

Here’s a code breakdown:

  1. You check if the response is of type HTTPURLResponse. If not, you throw the invalidServerResponse error. This error type is part of the newly added NetworkError enum, which holds different types of errors and their error messages that you throw and show to your user according to the corresponding error.

  2. If the response is of the right type, you check the statusCode in the second part. Then, according to it, you throw the right error or return the data.

Currently, you handle which error you throw back in every API request. Next, you need to handle showing this error to your user.

Open the Utilities folder and create a new file named ErrorManager.swift. Next, add the ErrorManager class to it. This manager holds two methods to handle or show the error and clear it. Use this new class in your ViewModel.

@Observable
class ErrorManager {
  var errorMessage: String?

  func handleError(_ error: Error?) {
    errorMessage = error?.localizedDescription
  }

  func clearError() {
    errorMessage = nil
  }
}

Open MovieListViewModel.swift, then add the errorManager property to manage showing or hiding errors between your ViewModel and your view.

var errorManager = ErrorManager()

Next, inside the catch block in the fetchMovies function, add the handling for this error manager to show the user an error that gets caught inside the block.

errorManager.handleError(error)

Finally, open MovieListView.swift and add the alert view to show this error message whenever it appears. You show or hide the alert view according to the value of the errorMessage. Then when the user dismisses the alert, you clear the errorMessage.

.alert(item: $movieListViewModel.errorManager.errorMessage) { errorMessage in
    Alert(
        title: Text("Error"),
        message: Text(errorMessage),
        dismissButton: .default(Text("OK")) {
            // Clear the error message when dismissed
          movieListViewModel.errorManager.clearError()
        }
    )
}

Congratulations—you’ve added the error-handling feature to your app! But to test it, you must first add the network reachability check.

Network Reachability

You’ll use the NetworkReachability class you saw in the previous section to check your app’s network connectivity and then show it to your user. This class is in the NetworkManager folder under the Managers folder. But to make this active, you must start monitoring it when the app launches and stop monitoring it when it terminates.

Open AppDelegate.swift, then replace it’s contents with the following:

func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
  NetworkReachability.startMonitoring()
  return true
}

func applicationWillTerminate(_ application: UIApplication) {
  NetworkReachability.stopMonitoring()
}

Next, check the network reachability in your network manager. Open APIManager.swift. Then, add the networking check at the beginning of the perform method. Here, you check if the network connection is unreachable and throw the network error before you even call the API, ensuring you reduce unnecessary network requests.

if !NetworkReachability.isConnected {
  throw NetworkError.network
}

To check this functionality and the error handling functionality you applied before, disconnect your internet or wifi connection. Then, build and run the app.

Notice that the app will show an error message regarding network connectivity, asking the user to check their internet connection. Once you reconnect to the internet and rerun the app, the error message for network connectivity disappears, and the app resumes its normal functionality, fetching and displaying movie data as expected.

Congratulations on following the instructions and testing the Network Reachability and the error-handling functionality! You did a great job enhancing the app’s network performance. Now, it’s your turn to focus on the data optimization.

Data Optimization

From the last section, you know the importance of data optimization in maintaining app performance and responsiveness. You’ll handle data optimization in this app through image and disk caching.

Image Caching

AsyncImage is limited in its support for image caching. However, fear not—you’ve got a solution ready for this.

You’ll use a custom image view for caching: the RemoteImage class, conveniently located under the Common Views folder. Combining this class with ImageCache, you can efficiently manage image caching, ensuring improved performance and a seamless user experience.

Open RemoteImage.swift to explore it. This View has the same implementation you had in the AsyncImage with an image and a placeholder. However, the difference lies in checking which one to show.

The loadImage method inside the ImageLoader checks if a cached image with this name is saved. If there is one, it returns it. But if there isn’t, it calls an API request to fetch this image and then caches it locally.

Now, check the ImageCache class to see its implementation. Open ImageCache.swift and notice how it uses the NSCache to handle both setting and getting the cached image if there is one saved locally. Now, it’s your turn to use RemoteImage to cache images in your movies list.

Open MovieCellView.swift, then replace the AsyncImage with the RemoteImage view. Notice the error that appears when you need to convert the value of imageUrl to a string. It’s time to fix this.

RemoteImage(url: movie.imagePath ?? "")
  .aspectRatio(0.67, contentMode: .fit)
  .frame(height: 100)
  .padding(.trailing, 5)

Open the Movie model, then replace the imageUrl with the imagePath property to return a string for the image path:

var imagePath: String? {
  return AppConstants.imageBaseUrl + (posterPath ?? "")
}

Build and run your app. Wait for a few images to load, then scroll down until some cells disappear. Then, scroll up again and notice how the images for the first cells don’t load again since they were cached before and loaded quickly from the local cache.

Disk Caching

Finally, disk caching is the last part you’ll handle in this demo. You’ll apply the simplest way to handle network call caching, which is URLSession caching.

Open RequestManager.swift, then change the configuration inside the getUrlSession method to handle the disk caching. The configuration should be default since ephemeral prevents caching.

private static func getUrlSession() -> URLSession {
  let configuration = URLSessionConfiguration.default
  configuration.timeoutIntervalForRequest = 90
  configuration.timeoutIntervalForResource = 90
  configuration.requestCachePolicy = .useProtocolCachePolicy
  return URLSession(configuration: configuration)
}

Finally, to apply this to every network call, open APIManager.swift. Then, edit the beginning of the perform method to ignore the cached data if the network is reachable. However, you’ll show the cached data if the network is unreachable.

if NetworkReachability.isConnected {
  urlSession.configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
} else {
  urlSession.configuration.requestCachePolicy = .returnCacheDataElseLoad
}

Before diving into this test, ensure you’ve built and run your app at least once. Once that’s done, disconnect your internet or WiFi connection. Upon rerunning your app, you’ll notice a delightful surprise: part of the movie collection, cached in the disk cache, will still be accessible. This showcases your excellent handling of app performance, leveraging network optimization and data caching. Your efforts have truly elevated the user experience. Kudos to you for your thorough approach!

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