Performance Optimization

Jun 20 2024 · Swift 5.9, iOS 17.4, Xcode 15.3

Lesson 02: Networking Optimization & Caching

Demo

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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
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
    }
  }
}
.onAppear {
  if movie.id == movieListViewModel.movies.last?.id {
    fetchMovies()
  }
}
private func fetchMovies() {
  Task {
    await movieListViewModel.fetchMovies()
  }
}

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
  }
}
@Observable
class ErrorManager {
  var errorMessage: String?

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

  func clearError() {
    errorMessage = nil
  }
}
var errorManager = ErrorManager()
errorManager.handleError(error)
.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()
        }
    )
}

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.

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

func applicationWillTerminate(_ application: UIApplication) {
  NetworkReachability.stopMonitoring()
}
if !NetworkReachability.isConnected {
  throw NetworkError.network
}

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.

RemoteImage(url: movie.imagePath ?? "")
  .aspectRatio(0.67, contentMode: .fit)
  .frame(height: 100)
  .padding(.trailing, 5)
var imagePath: String? {
  return AppConstants.imageBaseUrl + (posterPath ?? "")
}

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.

private static func getUrlSession() -> URLSession {
  let configuration = URLSessionConfiguration.default
  configuration.timeoutIntervalForRequest = 90
  configuration.timeoutIntervalForResource = 90
  configuration.requestCachePolicy = .useProtocolCachePolicy
  return URLSession(configuration: configuration)
}
if NetworkReachability.isConnected {
  urlSession.configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
} else {
  urlSession.configuration.requestCachePolicy = .returnCacheDataElseLoad
}
See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion