How to correctly do up an adjustable split view in SwiftUI?

Viewed 1604

This is my first time trying out SwiftUI, and I am trying to create a SwiftUI view that acts as a split view, with an adjustable handle in the center of the two views.

Here's my current code implementation example:

struct ContentView: View {

    @State private var gestureTranslation = CGSize.zero
    @State private var prevTranslation = CGSize.zero

    var body: some View {

        VStack {
            Rectangle()
                .fill(Color.red)
                .frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
            RoundedRectangle(cornerRadius: 5)
            .frame(width: 40, height: 3)
            .foregroundColor(Color.gray)
            .padding(2)
            .gesture(DragGesture()
                    .onChanged({ value in
                        self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)

                    })
                    .onEnded({ value in
                        self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)

                        self.prevTranslation = self.gestureTranslation
                    })
            )
            Rectangle()
                .fill(Color.green)
                .frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
        }
    }
}

How it looks like now: [split view screenshot[1]

This kinda works, but when dragging the handle, it is very glitchy, and that it seems to require a lot of dragging to reach a certain point.

Please advice me what went wrong. Thank you.

2 Answers

See How to change the height of the object by using DragGesture in SwiftUI? for a simpler solution.

My version of that:

let MIN_HEIGHT = CGFloat(50)

struct DragViewSizeView: View {
    @State var height: CGFloat = MIN_HEIGHT

    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.red)
                .frame(width: .infinity, height: height)

            HStack {
                Spacer()
                Rectangle()
                    .fill(Color.gray)
                    .frame(width: 100, height: 10)
                    .cornerRadius(10)
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                height = max(MIN_HEIGHT, height + value.translation.height)
                            }
                    )
                Spacer()
            }

            VStack {
                Text("my o my")
                Spacer()
                Text("hoo hah")
            }
        }
    }
}

struct DragTestView: View {

    var body: some View {
        VStack {
            DragViewSizeView()

            Spacer() // If comment this line the result will be as on the bottom GIF example
        }
    }
}

struct DragTestView_Previews: PreviewProvider {
    static var previews: some View {
        DragTestView()
    }
}

From what I have observed, the issue seems to be coming from the handle being repositioned while being dragged along. To counteract that I have set an inverse offset on the handle, so it stays in place. I have tried to cover up the persistent handle position as best as I can, by hiding it beneath the other views (zIndex).

I hope somebody else got a better solution to this question. For now, this is all that I have got:

import PlaygroundSupport
import SwiftUI

struct SplitView<PrimaryView: View, SecondaryView: View>: View {

    // MARK: Props

    @GestureState private var offset: CGFloat = 0
    @State private var storedOffset: CGFloat = 0

    let primaryView: PrimaryView
    let secondaryView: SecondaryView


    // MARK: Initilization

    init(
        @ViewBuilder top: @escaping () -> PrimaryView,
        @ViewBuilder bottom: @escaping () -> SecondaryView)
    {
        self.primaryView = top()
        self.secondaryView = bottom()
    }


    // MARK: Body

    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 0) {
                self.primaryView
                    .frame(height: (proxy.size.height / 2) + self.totalOffset)
                    .zIndex(1)

                self.handle
                    .gesture(
                        DragGesture()
                            .updating(self.$offset, body: { value, state, _ in
                                state = value.translation.height
                            })
                            .onEnded { value in
                                self.storedOffset += value.translation.height
                            }
                    )
                    .offset(y: -self.offset)
                    .zIndex(0)

                self.secondaryView.zIndex(1)
            }
        }
    }


    // MARK: Computed Props

    var handle: some View {
        RoundedRectangle(cornerRadius: 5)
            .frame(width: 40, height: 3)
            .foregroundColor(Color.gray)
            .padding(2)
    }

    var totalOffset: CGFloat {
        storedOffset + offset
    }
}


// MARK: - Playground

let splitView = SplitView(top: {
    Rectangle().foregroundColor(.red)
}, bottom: {
    Rectangle().foregroundColor(.green)
})

PlaygroundPage.current.setLiveView(splitView)

Just paste the code inside XCode Playground / Swift Playgrounds

If you found a way to improve my code please let me know.

Related