Search code examples
swiftswiftui

TabView style causes jitter during gesture


I have a basic tabview that contains photos, when I add the page tab view style or any style to the tabview then the image jitters when dragged down. If I remove the tab view style modifier the jitter goes away when the view is dragged. How can I fix this defect so there is no image jitter on drag? I used multiple image rendering libraries to make sure the issue was with the tab view and not the image.

import SwiftUI

struct StoryViewMain: View {
    var body: some View {
        TabView {
            AsyncImage(url: URL(string: "https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?cs=srgb&dl=pexels-anjana-c-169994-674010.jpg&fm=jpg"))
                .aspectRatio(contentMode: .fill)
        }
        .tabViewStyle(.page(indexDisplayMode: .never))  // HERE ---
        .background(.black)
    }
}

struct ContentView22: View {
    @State private var offset: CGSize = .zero
    @State private var startOfDragLocation: CGPoint = .zero
    @State private var circleSize: CGFloat = .zero
    @State var backOpac = 1.0

    var body: some View {
        GeometryReader { proxy in
            let h = proxy.size.height
            ZStack {
                VStack {
                    StoryViewMain()
                        .frame(width: widthOrHeight(width: true))
                        .offset(y: -top_Inset())
                }
                .padding(.top, top_Inset())
                .padding(.bottom, bottom_Inset())
                .frame(width: widthOrHeight(width: true), height: widthOrHeight(width: false) - bottom_Inset())
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background {
                Color.black.padding(-h)
            }
            .mask(alignment: .topLeading) {
                if offset == .zero {
                    Rectangle().ignoresSafeArea()
                } else {
                    Circle()
                        .position(startOfDragLocation)
                        .frame(width: circleSize, height: circleSize)
                }
            }
            .offset(offset)
            .background(content: {
                Color.black.opacity(backOpac).ignoresSafeArea()
            })
            .simultaneousGesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged { value in
                        if value.translation.height >= 0 {
                            if startOfDragLocation != value.startLocation {
                                startOfDragLocation = value.startLocation
                            }
                            offset = value.translation
                            let newH = h * 1.5
                            circleSize = max(100, newH - (newH * (value.translation.height / 300.0)))
                            backOpac = max(0.0, min(1.0, 1.0 - (value.translation.height / 500.0)))
                        }
                    }
                    .onEnded({ value in
                        withAnimation(.easeIn(duration: 0.15)){
                            circleSize = h + h
                            offset = CGSize(width: 0, height: 0.001)
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
                            offset = .zero
                        }
                    })
            )
        }
    }
}

#Preview(body: {
    ContentView22()
})

func widthOrHeight(width: Bool) -> CGFloat {
    let scenes = UIApplication.shared.connectedScenes
    let windowScene = scenes.first as? UIWindowScene
    let window = windowScene?.windows.first
    
    if width {
        return window?.screen.bounds.width ?? 0
    } else {
        return window?.screen.bounds.height ?? 0
    }
}

Solution

  • You are using AsyncImage inside ZStack which its size will automatically fit to its parent. So you need to update the size (maxWidth, maxHeight) after the image appear with a GeometryReader. Here is the solution:

    struct StoryViewMain: View {
        @State var sizeOnLoaded: CGSize? = nil // Will update on AsyncImage.appear
        var body: some View {
            TabView {
                ZStack(content: {
                    GeometryReader(content: { geometry in
                        AsyncImage(url: URL(string: "https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?cs=srgb&dl=pexels-anjana-c-169994-674010.jpg&fm=jpg"), content: {
                            phase in
                            if let image = phase.image {
                                image
                                .resizable(resizingMode: .stretch)
                                .scaledToFill()
                                
                            } else {
                                Color.black
                            }
                        })
                        .frame( // constraint the size here ---
                            maxWidth: sizeOnLoaded?.width ?? .infinity,
                            maxHeight: sizeOnLoaded?.height ?? .infinity
                        )
                        .onAppear(perform: {
                            sizeOnLoaded = geometry.size // --- Fix the size
                        })
                    })
                })
            }
            .tabViewStyle(.page(indexDisplayMode: .never))  // HERE ---
            .background(.black)
        }
    }