top of page

Solving Frame Rate Issues and App Hangs in SwiftUI iOS Apps


Solving Frame Rate Issues and App Hangs in SwiftUI iOS Apps

Developing a smooth and responsive iOS app is crucial for providing a great user experience. Frame rate issues and app hangs can be frustrating for users and can lead to negative reviews and decreased app usage.


In this blog post, we will explore common causes of frame rate issues and app hangs in SwiftUI iOS apps and provide solutions and code examples to address them.


Understanding Frame Rate Issues


Frame rate issues occur when an app struggles to render frames at the desired rate, usually 60 frames per second (FPS) on most iOS devices. When the frame rate drops, animations become choppy, and the app feels less responsive. There are several common reasons for frame rate issues:

  1. Inefficient View Updates: SwiftUI's declarative nature encourages frequent view updates. If not optimized, this can lead to excessive rendering and reduced frame rates.

  2. Heavy Computation on the Main Thread: Performing CPU-intensive tasks on the main thread can block the UI, making the app feel unresponsive.

  3. Large Images and Assets: Loading or rendering large images or assets can consume significant memory and processing power, leading to frame rate drops.

Solving Frame Rate Issues in SwiftUI


1. Optimize View Updates


You can optimize view updates by:

  • Using the .onAppear and .onDisappear modifiers to load data only when necessary.

  • Implementing the .id modifier to identify views uniquely and avoid unnecessary updates.

  • Reducing the complexity of SwiftUI view hierarchies.

Example:

struct ContentView: View {
    var body: some View {
        Text("Optimize your views")
            .onAppear {
                // Load data when the view appears
                loadData()
            }
    }
}


2. Offload Heavy Computation


Move CPU-intensive tasks to background threads using DispatchQueue or Combine. Ensure that UI updates occur on the main thread.


Using DispatchQueue:

DispatchQueue.global().async {
    // Perform heavy computation
    let result = performHeavyComputation()
    
    DispatchQueue.main.async {
        // Update the UI on the main thread
        self.resultLabel = result
    }
}

Combine is a powerful framework for handling asynchronous and event-driven code in Swift. You can use Combine to perform background operations in SwiftUI seamlessly. In this example, we'll demonstrate how to use Combine to execute a background operation and update the SwiftUI view when the operation completes.


Let's say you want to fetch some data from a network API in the background and update your SwiftUI view when the data is ready. Here's a step-by-step guide:


1. Import Combine in your SwiftUI view file:

import SwiftUI
import Combine

2. Define a ViewModel to handle your data and background operations. Create an ObservableObject class that will hold your data and expose a publisher for notifying view updates.

class MyViewModel: ObservableObject {
    @Published var data: [YourDataType] = [] // Replace YourDataType with the actual data type you're using
    private var cancellables: Set<AnyCancellable> = []
    
    func fetchData() {
        // Simulate a background network request
        fetchDataFromNetwork()
            .receive(on: DispatchQueue.main) // Ensure updates are on the main thread
            .sink { completion in
            // Handle completion or errors if needed
            } receiveValue: { [weak self] newData in
               self?.data = newData 
              // Update the data when received
            }
            .store(in: &cancellables)
    }
    
    private func fetchDataFromNetwork() -> AnyPublisher<[YourDataType], Error> {
        // Implement your network request logic here and return a Combine publisher
        // For example, you can use URLSession's dataTaskPublisher
        let url = URL(string: "https://your-api-url.com/data")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [YourDataType].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

Replace YourDataType with the actual type of data you're fetching from the network.


3. Create a SwiftUI view that observes the changes in your ViewModel and triggers the background operation:

struct ContentView: View {
    @ObservedObject private var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            if viewModel.data.isEmpty {
                Text("Loading...")
            } else {
                List(viewModel.data, id: \.self) { item in
                // Display your data here
                Text(item.name) 
                // Replace with your data properties
                }
            }
        }
        .onAppear {
            viewModel.fetchData() 
            // Trigger the background operation when the view appears
        }
    }
}

In this SwiftUI view, the @ObservedObject property wrapper observes changes to the viewModel, and the onAppear modifier triggers the background operation by calling viewModel.fetchData() when the view appears.


Now, your SwiftUI view will fetch data from the network in the background using Combine and update the view when the data is ready, providing a smooth and responsive user experience.


3. Efficiently Manage Images and Assets


Load images lazily and use asset catalogs for managing image resources. Resize images to appropriate dimensions to reduce memory usage.


In SwiftUI, you can load images lazily using the AsyncImage view. AsyncImage allows you to load and display images asynchronously, which is especially useful for large images or images fetched from the network. Here's how you can use AsyncImage to load images lazily in SwiftUI:

import SwiftUI

struct LazyLoadingImageView: View {
    let imageURL: URL
    var body: some View {
        AsyncImage(url: imageURL) { phase in
        switch phase {
            case .empty:
                // Placeholder while loading (optional)
                ProgressView()
            case .success(let image):
                // Successfully loaded image
                image
                    .resizable()
                    .scaledToFit()
            case .failure(_):
                // Handle the failure (e.g., show an error message)
                Image(systemName: "xmark.octagon")
                    .resizable()
                    .scaledToFit()
                    .foregroundColor(.red)
            @unknown default:
                // Handle other unknown states
                Text("Unknown state")
            }
        }
    }
}

In the code above:

  1. AsyncImage is used to load the image asynchronously from the specified URL.

  2. The closure inside AsyncImage receives a Phase parameter, which represents the current state of the image loading process.

  3. In the .empty phase, you can display a placeholder (e.g., a ProgressView) to indicate that the image is being loaded.

  4. In the .success phase, you can display the loaded image, making it resizable and scaling it to fit the available space.

  5. In the .failure phase, you can handle the failure by displaying an error image or a message.

  6. The @unknown default case is used to handle any unknown states that might be introduced in future SwiftUI versions.

To use the LazyLoadingImageView in your SwiftUI view, simply provide the URL of the image you want to load:

struct ContentView: View {
    var body: some View {
        LazyLoadingImageView(imageURL: URL(string: "https://example.com/image.jpg")!)
            .frame(width: 200, height: 200)
    }
}

Make sure to replace "https://example.com/image.jpg" with the actual URL of the image you want to load.


With AsyncImage, you can efficiently load and display images in a lazy manner, ensuring a smooth user experience, especially when dealing with large images or images from remote sources.


Addressing App Hangs


App hangs occur when the app becomes unresponsive for 250 milli seconds or more due to various reasons, such as blocking the main thread or network requests taking too long. Here are some strategies to prevent app hangs:


1. Use Background Threads for Network Requests


Perform network requests on background threads to avoid blocking the main thread. Combine or URLSession can be used for this purpose.


Example:

let cancellable = URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data)
    .decode(type: MyModel.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { _ in }) { data in
      // Process data and update UI
    }

2. Implement Error Handling


Handle errors gracefully, especially in asynchronous operations, to prevent app hangs and crashes.

do {
    let result = try performRiskyOperation()
    // Handle the result
} catch {
    // Handle the error
}


3. Use Xcode Instruments and APM tools


Use Xcode's Instruments to profile your app's performance, identify bottlenecks, and monitor memory usage. Debugging tools like LLDB can help trace and fix specific issues causing frame rate issues and app hangs.


APM tools are a very much helpful in detecting frame rate issues and App Hangs. Finotes is such an APM tool that helps in detecting these issues and provides all relevant data points to the developer to fix the issue.


Conclusion


Frame rate issues and app hangs can significantly impact the user experience of your SwiftUI iOS app. By optimizing view updates, offloading heavy computation, efficiently managing assets, and addressing app hangs through proper threading and error handling, you can create a smooth and responsive app that users will love.


Remember that performance optimization is an ongoing process. Regularly test your app on different devices and keep an eye on performance metrics to ensure a consistently great user experience.

Blog for Mobile App Developers, Testers and App Owners

 

This blog is from Finotes Team. Finotes is a lightweight mobile APM and bug detection tool for iOS and Android apps.

In this blog we talk about iOS and Android app development technologies, languages and frameworks like Java, Kotlin, Swift, Objective-C, Dart and Flutter that are used to build mobile apps. Read articles from Finotes team about good programming and software engineering practices, testing and QA practices, performance issues and bugs, concepts and techniques. 

bottom of page