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

2

5

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.

Bug answered 12/4, 2020 at 10:22 Comment(0)
O
3

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.

Oldwife answered 10/6, 2020 at 15:38 Comment(6)
Previously kind of gave up on this question, but now that I get an answer from you, I think it would be good to revisit. Indeed this solution does not appear to be the best, as the handle would 'disappear' until dragging ends. I'll play around with it and see how to improve itBug
I played around with both codes and I found that actually with my code, if I change DragGesture() to DragGesture(coordinateSpace: .global) the glitches seems to disappear!Bug
Since your solution does work and that it did indirectly made me find the solution that matches what I need, so I will mark this as solved.Bug
If you use DragGesture(coordinateSpace: .global) then you can remove the inverse offset to the handle (.offset(y: -self.offset)) and then everything behaves perfectly.Accomplice
This solution doesn't seem to work anymore. The system appears to keep calling the "updating" callback even after the mouse button is released.Connubial
For reference: Someone made something similar here: github.com/avdyushin/SplitViewIneducation
A
3

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()
    }
}
Agee answered 18/5, 2021 at 6:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.