Optimization Opportunities Detection
Welcome to Cinematica, where you’ll explore the intricacies of optimizing app performance! Cinematica is a sleek one-screen view app that brings you the latest upcoming movies from The Movie Database (TMDb) API. In this lesson, you’ll use Cinematica to delve into the world of performance optimization, focusing primarily on SwiftUI views.
Open the starter project for this lesson. Build and run the project. As you navigate through Cinematica, you’ll notice its minimalist design, allowing you to seamlessly scroll through a list of upcoming movies. But behind its elegant interface lie opportunities for performance enhancement. You’ll take a closer look at the app’s SwiftUI views to identify potential bottlenecks and learn how to address them effectively.
First, check Xcode’s debug metrics for possible performance opportunities. While you’re still running the app, tap the Debug View Hierarchy button in Xcode to see the current view’s View Hierarchy.
Next, open the Debug Navigator. You’ll see some important metrics like CPU and memory usage. You’ll focus on this part on the purple alert shown in the hierarchy, indicating the presence of an optimization opportunity. By default, Xcode enables you to see these opportunities. But if you want to ensure they’re visible, open the Editor menu, then check the Show Optimization Opportunities option.
Unfold the hierarchy to see the exact location of this issue. You’ll see that it’s related to the ListCollectionViewCell
. But what’s the issue exactly? To answer this question, open the Issue Navigator. This navigator holds the details of all the performance opportunities. By reading the issue, you can spot that it’s related to triggering the offscreen rendering through shadowing. Now you know that this issue is related to the cell view and shadow. It’s time to fix it.
Open MovieCellView, then scroll down to the bottom of the view until you spot the line where you set the shadow for this view. SwiftUI draws the shadows around your view objects dynamically at Runtime based on their current positions and bounds. That rendering follows the view throughout its lifecycle.
It’s a math-intensive process that involves many draw-calls to the GPU. Replace this code with a background
modifier that has a shadow in it. You could also remove the shadow if it isn’t important to your UI.
.background(
Rectangle()
.fill(Color.white)
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
.padding(1)
)
Build and run the app. Then tap the Debug View Hierarchy button in Xcode. Next, open the Issue Navigator. Notice how all the performance opportunities have disappeared.
Congratulations, you’re on track to improve your app’s performance!
MovieCellView Performance Optimizations
Open MovieCellView again. You’ll make a few other performance improvements in this view.
First, remove the GeometryReader
from the placeholder of the AsyncImage
since you don’t need it. Replace this code with a similar one as the image inside the AsyncImage
. GeometryReader
was making unnecessary calculations, and you replaced it with a fixed frame and aspect ratio for the placeholder.
Image(.imagePlaceholder)
.resizable()
.aspectRatio(0.67, contentMode: .fit)
.frame(height: 100)
Finally, replace the Label
s used in the VStack
with the lightweight Text
since you don’t have an image to show beside this text.
Text(movie.originalTitle ?? "")
.font(.title)
Text(movie.overview ?? "")
.font(.subheadline)
Wow! You’ve become an expert in performance optimization, and you’ve done a great job so far. Notice that there is another optimization opportunity here. The movie
property costs a lot of rendering because its located inside the body
. You’ll fix this with another major change but first you’ll move to another performance measurement tool to detect a new optimization opportunity.
Using Printing Checks
Open MovieCellView, then add this printing line inside the view’s body. This line is typically used for debugging or logging purposes, where the _printChanges()
method might print out or log changes or updates within the current view.
let _ = Self._printChanges()
Build and run the app. Monitor the console output and observe the print statements generated. Each cell appearing on the screen initially triggers a print statement, which is expected behavior. Now, scroll down precisely one cell. You might anticipate only one additional print statement corresponding to the newly displayed MovieCellView
. However, multiple print statements are generated, indicating changes in other cells that shouldn’t occur.
This issue arises from how MovieListViewModel
is passed to MovieCellView
and used to retrieve movie data. This approach results in any modification to MovieListViewModel
triggering a rerender of MovieCellView
, leading to unnecessary updates when scrolling.
Open MovieCellView and replace all the properties with only one Movie
property. Then, use this property throughout your view. Finally, make sure to fix the initializer for your preview.
@State var movie: Movie
Next, open MovieListView and replace the ForEach
with a new one that passes only the movie
property to the MovieCellView
. This change ensures that the MovieCellView
has only the needed data, which will prevent unnecessary updates.
ForEach(movieListViewModel.movies) { movie in
MovieCellView(movie: movie)
.frame(height: 100)
}
While you’r in this view, lets do another performance improvement. Replace the List
with a LazyVGrid
inside a ScrollView
. You know the benefits of lazy loading to reduce memory usage and improve rendering performance.
ScrollView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(movieListViewModel.movies) { movie in
MovieCellView(movie: movie)
.frame(height: 100)
}
.padding(.horizontal)
}
Also add the columns
property above the body
:
var columns: [GridItem] = [
GridItem(.flexible(), spacing: 0)
]
Finally, add the needed columns for this LazyVGrid
to draw the grid smoothly as previously.
var columns: [GridItem] = [
GridItem(.flexible(), spacing: 0)
]
Build and run the app. Scroll down precisely one cell and notice how it prints only one statement as expected. You’ve made many improvements, but there is only one to go.
Enhancing SwiftUI Performance with @Observable Macro
You already know the importance of using the @Observable
Macro over ObservableObject
. Now, you’ll implement this change.
Open MovieListViewModel and import the Observation
library. Next, add the @Observable
Macro above the MovieListViewModel
class. After that, remove the ObservableObject
protocol and @Published
property wrapper from all properties.
import Observation
@Observable
class MovieListViewModel
Next, open MovieListView, replace @ObservedObject
with @State
. Build and run the app. Notice that the app has the same behavior, but unnecessary redraws are now minimized, enhancing the overall efficiency of your app.
@State var movieListViewModel: MovieListViewModel
Congratulations! Your app has made great improvements in SwiftUI views. In the next lessons, you’ll address other optimization opportunities.