ScrollView with clipShape or cornerRadius freezes app when reaches safe area.

ScrollView with clipShape or cornerRadius freezes app when offset applied to superview and it reaches safe area.


struct ContentView: View {
    @State var offset: CGSize = .zero
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 17) {
                ForEach(0...10, id: \.self) { i in
                    Color.black
                        .frame(width: 60, height: 60)
                        .clipShape(Circle())
                }
            }
        }
        .clipShape(Capsule())
        .offset(x: 0, y: offset.height)
        .edgesIgnoringSafeArea(.all)
            
        .gesture(
            DragGesture().onChanged { value in
                self.offset = value.translation
            }.onEnded { value in
                self.offset = .zero
            }
        )
    }
}


To test that just try to move scrollview to top or bottom safe area. App gets freezed with no exception or error and never refresh.

Replies

I played with it in Playground (on Mojave) and could not reproduce the problem.

When i move too far to the bottom, the capsule just returns to its central position.


Only get error:

=== AttributeGraph: cycle detected through attribute 37 ===



I modified a bit to better see what's happening.


struct ContentView16: View {
    @State var offset: CGSize = .zero
    let colors = [Color.green, Color.yellow, Color.red]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 17) {
                ForEach(0...10, id: \.self) { i in
                    self.colors[i%3] 
                        .frame(width: 60, height: 60)
                        .clipShape(Circle())
                    .overlay(Text("\(i+1)"))
                }
            }
        }
        .clipShape(Capsule())
        .background(LinearGradient(gradient: Gradient(colors: [.white, .blue]), startPoint: .leading, endPoint: .trailing))
        .offset(x: 0, y: offset.height)
        .edgesIgnoringSafeArea(.all)
             
        .gesture(
            DragGesture().onChanged { value in
                self.offset = value.translation
            }.onEnded { value in
                self.offset = .zero
            }
        )
    }
}

Well, finally tested on device wimulator and got the freeze.


Note: this is on Mojave, maybe the reason ?

Anyway, you should file a bug.


Of course, you can detect the limits (here hard coded, should be computed, as it depends on device)


struct ContentView: View {
    @State var offset: CGSize = .zero
    let colors = [Color.green, Color.yellow, Color.red]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 17) {
                ForEach(0...10, id: \.self) { i in
                    self.colors[i%3] // .color
                        .frame(width: 60, height: 60)
                        .clipShape(Circle())
                    .overlay(Text("\(i+1)"))
                }
            }
        }
        .clipShape(Capsule())
            .background(LinearGradient(gradient: Gradient(colors: [.pink, .white, .blue]), startPoint: .leading, endPoint: .trailing))
        .offset(x: 0, y: offset.height)
        .edgesIgnoringSafeArea(.all)
            
        .gesture(
            DragGesture(coordinateSpace: .global).onChanged { value in
                if value.location.y > 100 && value.location.y < 800 {     // This is Hard Coded for iPhone 11 Pro Max, should be computed
                    self.offset = value.translation
                    print("location", value.location.y)
                } else {
                    print("   off location", value.location.y)
                }
            }.onEnded { value in
                self.offset = .zero
            }
        )
    }
}

And for the fun, I computed the limits (in case of Portrait with a notch


struct ContentView: View {
    @State var offset: CGSize = .zero
    @State var initialPosition: CGPoint = .zero  // Where did we start drag
    let colors = [Color.green, Color.yellow, Color.red]
    let screenHeight = UIScreen.main.bounds.size.height // We should also know where is the initial point
    let centerV = (UIScreen.main.bounds.size.height - 80.0) / 2.0 // Hors Safe area
    @State var topLimit = CGFloat(100)  // Will be computed
    @State var bottomLimit = CGFloat(800)

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 17) {
                ForEach(0...10, id: \.self) { i in
                    self.colors[i%3] // .color
                        .frame(width: 60, height: 60)
                        .clipShape(Circle())
                    .overlay(Text("\(i+1)"))
                }
            }
        }
        .clipShape(Capsule())
            .background(LinearGradient(gradient: Gradient(colors: [.pink, .white, .blue]), startPoint: .leading, endPoint: .trailing))
        .offset(x: 0, y: offset.height)
        .edgesIgnoringSafeArea(.all)
             
        .gesture(
            DragGesture(minimumDistance: 1.0, coordinateSpace: .global).onChanged { value in
                if value.translation.height < 5.0 && value.translation.height > -5.0 { // We have not yet moved
                    self.initialPosition = value.location
                    self.topLimit = 50.0 + (self.initialPosition.y - (self.centerV - 30.0)) // 50: top safe area; 30 = Capsule half height ; (self.initialPosition.y - (self.centerV - 30.0)) from top Capsule -> initial dragpoint
                    self.bottomLimit = self.screenHeight - 50.0 - ((self.centerV + 30.0) - self.initialPosition.y) // self.screenHeight-50: bottom safe area; 30 = Capsule half height; (self.initialPosition.y - (self.centerV + 30.0)) = initial dragpoint -> bottom Capsule
                }
                if value.location.y > self.topLimit && value.location.y < self.bottomLimit { // - 100 { // 100 to cope with 40 for safe area and 60 for the height of Capsule
                    self.offset = value.translation
                } 
            }.onEnded { value in
                self.offset = .zero
            }
        )
    }
}