0

How do I anchor resizing views in a scrollview to the top-leading point so that when they grow, they grow downwards?

Context: I'm trying to make a scrollviews of views that I can resize in height by dragging a handle in the bottom of it up and down. My problem is that when it resizes, it resizes equally much up as down. I want the top to stay put and only adjust how far down it goes.

I don't think the problem lies with the scrollview, as the behaviour is the same if I replace it with a VStack. In the context of the scrollview, though, it resizing upwards makes the user not able to scroll up far enough to see the top of the view.

Full sample code follows under the screenshots. The issue is on both iPad and iPhone simulator

In the following screenshots, the scrollview is scrolled to the top in both. The first screenshot shows the start-state before resizing the topmost item

Start-state before resizing the topmost item

The second screenshot shows the state after resizing the topmost item - the topmost item now goes outside the list so we cannot see scroll up to see the top

State after resizing the topmost item - the topmost item now goes outside the list so we cannot see scroll up to see the top

Here follows the full code, runnable with Xcode 11.0, to show the issue

struct ScaleItem: View {

    static let defaultHeight: CGFloat = 240.0

    @State var heightDiff: CGFloat = 0.0
    @State var currentHeight: CGFloat = ScaleItem.defaultHeight

    var resizingButton: some View {
        VStack {
            VStack {
                Spacer(minLength: 15)
                HStack {
                    Spacer()
                    Image(systemName: "arrow.up.and.down.square")
                        .background(Color.white)
                    Spacer()
                }
            }
            Spacer()
                .frame(height: 11)
        }
        .background(Color.clear)
    }

    var body: some View {

        ZStack {
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    Text("Sample")
                    Spacer()
                }
                Spacer()
            }
            .background(Color.red)
            .overlay(
                RoundedRectangle(cornerRadius: 5.0)
                    .strokeBorder(Color.black, lineWidth: 1.0)
                    .shadow(radius: 3.0)
            )
            .padding()
            .frame(
                minHeight: self.currentHeight + heightDiff,
                idealHeight: self.currentHeight + heightDiff,
                maxHeight: self.currentHeight + heightDiff,
                alignment: .top
            )

            resizingButton
                .gesture(
                    DragGesture()
                        .onChanged({ gesture in
                            print("Changed")
                            let location = gesture.location
                            let startLocation = gesture.startLocation
                            let deltaY = location.y - startLocation.y
                            self.heightDiff = deltaY
                            print(deltaY)
                        })
                    .onEnded { gesture in
                        print("Ended")
                        let location = gesture.location
                        let startLocation = gesture.startLocation
                        let deltaY = location.y - startLocation.y
                        self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
                        self.heightDiff = 0
                        print(deltaY)
                        print(String(describing: gesture))
                    })

        }

    }
}

struct ScaleDemoView: View {

    var body: some View {

        ScrollView {
            ForEach(0..<3) { _ in
                ScaleItem()
            }
        }
    }

}

1 Answer 1

3

One way to resolve this is to redraw the ScrollView to its original position during the dragging. Create an observer object and when your DeltaY will be changed, the parent view will be notified and the view will update accordingly.

  • First, Create an ObservableObject final class DeltaHeight: ObservableObject & pass to the child view.
  • Add .offset(x: 0, y: 0) to your ScrollView

Here is the tested code:

import SwiftUI

struct ScaleDemoView_Previews: PreviewProvider {
    static var previews: some View {
        ScaleDemoView()
    }
}

struct ScaleItem: View {
    @ObservedObject var delta: DeltaHeight

    static let defaultHeight: CGFloat = 200.0

    @State var heightDiff: CGFloat = 0.0
    @State var currentHeight: CGFloat = ScaleItem.defaultHeight

    var resizingButton: some View {
        VStack {
            VStack {
                Spacer(minLength: 15)
                HStack {
                    Spacer()
                    Image(systemName: "arrow.up.and.down.square")
                        .background(Color.white)
                    Spacer()
                }
            }
            Spacer()
                .frame(height: 11)
        }
        .background(Color.clear)
    }

    var body: some View {

        ZStack {
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    Text("Sample")
                    Spacer()
                }
                Spacer()
            }
            .background(Color.red)
            .overlay(
                RoundedRectangle(cornerRadius: 5.0)
                    .strokeBorder(Color.black, lineWidth: 1.0)
                    .shadow(radius: 3.0)
            )
            .padding()
            .frame(
                minHeight: self.currentHeight + heightDiff,
                idealHeight: self.currentHeight + heightDiff,
                maxHeight: self.currentHeight + heightDiff,
                alignment: .top
            )

            resizingButton
                .gesture(
                    DragGesture()
                        .onChanged({ gesture in
                            print("Changed")
                            let location = gesture.location
                            let startLocation = gesture.startLocation
                            let deltaY = location.y - startLocation.y
                            self.heightDiff = deltaY
                            print("deltaY: ", deltaY)
                            self.delta.delta = deltaY
                        })
                    .onEnded { gesture in
                        print("Ended")
                        let location = gesture.location
                        let startLocation = gesture.startLocation
                        let deltaY = location.y - startLocation.y
                        self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
                        self.heightDiff = 0
                        print(deltaY)
                        print(String(describing: gesture))
                    })

        }

    }
}

struct ScaleDemoView: View {
    @ObservedObject var delta = DeltaHeight()
    var body: some View {
        ScrollView(.vertical) {
            ForEach(0..<3) { _ in
                ScaleItem(delta: self.delta)
            }
        }.offset(x: 0, y: 0)
            .background(Color.green)
    }

}

final class DeltaHeight: ObservableObject {
    @Published  var delta: CGFloat = 0.0
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you! :-) That works. :-) Only thing I had to add was also update self.delta.delta in onEnded, otherwise there'd be a similar jump when it was resized to smaller than ScaleItem.defaultHeight. Thanks a lot for your help! :-)
I guess behind the scrollView, ScrollView is working like reloadRows the tableViewCell, As you change the height, the view grows based on the user's best visibility.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.