SwiftUI ScrollView Jittery/Glitchy Scrolling in iOS 17

Evening All and hope you had a good weekend.

I'm experiencing significant scrolling issues in my SwiftUI weather app running on iOS 17. When scrolling through the main content, the scrolling is glitchy and not smooth, particularly around transitions between different subviews in the scroll view. The Issue The scrolling behavior has these problems:

Stuttering/jittery movement during normal scrolling The issue seems most noticeable around the transitions between the header sections and the content sections (WindDetailsView and WeatherDataView) Smooth deceleration is affected, with visible "jumps" during momentum scrolling The problem appears to be worse on devices with iOS 17 compared to previous iOS versions

Current Implementation My app uses a standard SwiftUI ScrollView with offset tracking to create a parallax effect for the header when scrolling. I'm calculating offset values to animate the header and adjust its opacity as the user scrolls. Here's the core structure:

Answered by DTS Engineer in 836706022

A great start to debugging hangs and hitches in your SwiftUI View is by profiling your app using the SwiftUI template in Instruments.

This will enable you to assess potential runtime performance optimizations and identify any view properties or view body computations that may be impacting your data flow.

The SwiftUI template in Instruments shows detailed runtime performance characteristics of your app in the following tracks:

  • View Body: The timing summary and intervals of each body computation per module and view type.
  • View Properties: The current values, summary and updates of each dynamic property for a view and module.
  • Core Animation Commits: The summary, profile, samples and intervals of each Core Animation transaction commit for the process.
  • Time Profile: The profile and samples of running threads on all cores at regular intervals for all processes.

For more information, see the following WWDC sessions:

opps. here is code

import CoreLocation

struct Home: View {
    // MARK: - Incoming Parameters
    var topEdge: CGFloat
    var size: CGSize
    
    // MARK: - Environment & State
    @EnvironmentObject var weatherViewModel: WeatherViewModel
    @StateObject private var trainingViewModel = TrainingViewModel()
    
    @State private var offset: CGFloat = 0
    @State private var isFavoriteToggled: Bool = false
    @State private var scrollToTop: UUID = UUID()
    @State private var lastStationID: String? = nil
    
    // Adjustable bottom padding for the main content
    @State private var bottomPadding: CGFloat = 30
    
    // MARK: - Body
    var body: some View {
        let displayedStation = weatherViewModel.currentStation
        
        // Calculate offsets used in WeatherHeaderView and TempView
        let baseOffset = -offset
        let positiveOffset = offset > 0 ? (offset / size.width) * 100 : 0
        let titleOffset = getTitleOffset() // Subtle upward shift based on scroll
        let finalOffset = baseOffset + positiveOffset + titleOffset
        
        ZStack {
            // MARK: - Gradient Background
            LinearGradient(
                            gradient: Gradient(colors: [
                                Color(red: 0.0, green: 0.1, blue: 0.1), // Dark blue/gray
                                Color(red: 0.1, green: 0.1, blue: 0.2), // Medium blue
                                Color(red: 0.1, green: 0.2, blue: 0.5) // Light blue
                            ]),
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
            .ignoresSafeArea(.container, edges: .top)
            .blur(radius: min(10, offset / 15))
            .overlay(
                Color.black.opacity(0.15) // Slight darkening overlay
                    .ignoresSafeArea()
            )
            
            // MARK: - Main Scroll
            ScrollViewReader { proxy in
                ScrollView(.vertical, showsIndicators: false) {
                    VStack {
                        // Dummy view to scroll to top
                        Color.clear
                            .frame(height: 0)
                            .id(scrollToTop)
                        
                        // MARK: - Weather Header
                        WeatherHeaderView(displayedStation: displayedStation)
                            .offset(y: finalOffset)
                        
                        // MARK: - Temperature View
                        TempView(
                            displayedStation: displayedStation,
                            isFavoriteToggled: $isFavoriteToggled
                        ) {
                            toggleFavorite(for: displayedStation)
                        }
                        .offset(y: finalOffset)
                        .opacity(getTempViewOpacity())
                        
                        // MARK: - Wind Details
                        if let stationID = displayedStation?.stationID {
                            WindDetailsView(stationID: stationID)
                                .padding(.top, 10)
                                .environmentObject(weatherViewModel)
                            
                            // MARK: - Weather Data
                            WeatherDataView(
                                stationID: stationID,
                                stations: weatherViewModel.weatherData,
                                trainingViewModel: trainingViewModel
                            )
                            .environmentObject(weatherViewModel) // Provide the environment object
                            .id(stationID) // Force a refresh if the station ID changes
                            .onChange(of: weatherViewModel.currentStation?.stationID) { oldID, newID in
                                guard let newID = newID else { return }
                                guard newID != lastStationID else { return }
                                lastStationID = newID
                                withAnimation {
                                    proxy.scrollTo(scrollToTop, anchor: .top)
                                }
                            }
                            .padding(.bottom, bottomPadding)
                            .environmentObject(weatherViewModel)
                        } else {
                            // Loading indicator if no station is displayed
                            ProgressView("Loading station data...")
                                .foregroundColor(.white)
                                .padding()
                                .background(Color.black.opacity(0.3))
                                .cornerRadius(10)
                        }
                    }
                    .padding(.top, 25)
                    .padding(.top, topEdge)
                    .padding(.horizontal)
                    .safeAreaInset(edge: .bottom) {
                        Color.clear.frame(height: 10)
                    }
                    .offsetChangeHome { rect in
                        offset = rect.minY
                    }
                }
                // Also update this one to the TWO-parameter closure
                .onChange(of: weatherViewModel.currentStation?.stationID) { oldID, newID in
                    guard let newID = newID else { return }
                    guard newID != lastStationID else { return }
                    lastStationID = newID
                    withAnimation {
                        proxy.scrollTo(scrollToTop, anchor: .top)
                    }
                }
            }
        }
        // MARK: - Favorite Limit Alert
        .alert(isPresented: $weatherViewModel.showFavoriteLimitAlert) {
            Alert(
                title: Text("Favorite Limit Reached"),
                message: Text("You can only have up to 5 favorite stations."),
                dismissButton: .default(Text("OK"))
            )
        }
    }
}

// MARK: - Private Methods
extension Home {
    private func toggleFavorite(for station: RawsWeatherStation?) {
        guard let station = station else { return }
        weatherViewModel.toggleFavoriteStatus(for: station)
    }

    private func getTitleOffset() -> CGFloat {
        let progress = max(0, min(1, -offset / 120))
        return -progress * 34
    }

    private func getTempViewOpacity() -> CGFloat {
        let fadeOutStart: CGFloat = 0
        let fadeOutEnd: CGFloat = 100
        let progress = (offset - fadeOutEnd) / (fadeOutStart - fadeOutEnd)
        return max(2 - progress, 0)
    }
}```

Accepted Answer

A great start to debugging hangs and hitches in your SwiftUI View is by profiling your app using the SwiftUI template in Instruments.

This will enable you to assess potential runtime performance optimizations and identify any view properties or view body computations that may be impacting your data flow.

The SwiftUI template in Instruments shows detailed runtime performance characteristics of your app in the following tracks:

  • View Body: The timing summary and intervals of each body computation per module and view type.
  • View Properties: The current values, summary and updates of each dynamic property for a view and module.
  • Core Animation Commits: The summary, profile, samples and intervals of each Core Animation transaction commit for the process.
  • Time Profile: The profile and samples of running threads on all cores at regular intervals for all processes.

For more information, see the following WWDC sessions:

SwiftUI ScrollView Jittery/Glitchy Scrolling in iOS 17
 
 
Q