swiftuiresizedragsplitview

SwiftUI - How to create an adjustable split view without using SplitView?


I am trying to create a View that acts as a split view, with an adjustable handle in the center of the two views.

Here's my current code:

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)
        }
    }
}

And what that code produces: [split view screenshot1

This partially 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 advise what went wrong. Thank you.


Solution

  • 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.