Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining DragGesture and MagnificationGesture on a SwiftUI View in iOS14

I am pulling my hair out on this and I did not find an answer that seems to fit.

I have got a View (see below) - and I need to support 2 gestures (Drag and Magnification) somehow on this control. The View is a Knob and drag modifies the value, Magnification is supposed to modify the precision of the knob.

I have tried the following:

  • Composing a gesture with drag.simultanously(with:magnification), which kinda works, but the problem seems to be that the MagnificationGesture does not end when one finger is lifted and therefore the drag does not continue - it also never gets a .onEnded call. (I don't know why - I would think this is a bug?) The effect is a rather strange experience where the knob still magnified while a user would expect to change values.
  • Adding a .gesture(drag).gesture(magnification) which seems to do the same
  • magnification.exclusively(before:drag) never calls the drag.onChange block, but only the .onEnded for some reason. Effectively the drag does not work...
  • drag.exclusively(before:magnification) also combines both with the magnification never ending and passing back to drag.
  • I have also tried to put the magnification gesture on the surrounding VStack and keep the drag gesture on the inner Path view, but somehow this also seems to result in the same result as drag.simultanously(with:magnification) on the inner area. I have not figured out how I can prevent the drag gesture from propagating through and combining with the magnification on the inner view.

I would very much appreciate feedback, as I am out of ideas at least for the moment...

struct VirtualKnobView<Content:View>: View {
    
    init(model:VirtualKnobModel, contentSize:CGSize, @ViewBuilder _ contentView:()-> Content ){
        self.model = model
        self.contentSize = contentSize
        self.contentView = contentView()
    }
    
    init(contentSize:CGSize, @ViewBuilder _ contentView:()-> Content){
        self.model = VirtualKnobModel(inner: 0.7, outer: 0.8, ext: 0.05, angle: 30.0)
        self.contentSize = contentSize
        self.contentView = contentView()
    }
    
    @ObservedObject var model:VirtualKnobModel
    
    @State var lastMagnitude:CGFloat = 1.0
    @State var isDragging:Bool = false
    
    var contentSize:CGSize
    var contentView:Content
    
    var body: some View {
        
        let size = model.calclulateSize(for: contentSize)
        
        let drag = DragGesture(minimumDistance: 0)
            .onChanged({ state in
                print ("Drag Changed")
                let point = state.location
                let refPoint = CGPoint(x: (point.x - size/2)/size,
                                       y: (point.y - size/2)/size)
                model.setTouchPoint(point: refPoint)
            })
            .onEnded({ _ in
                print ("Drag ended")
                model.reset()
            })
        
        let magnification = MagnificationGesture()
            .onChanged({ (magnitude:CGFloat) in
                print ("Magnification changed")
                let delta = magnitude / lastMagnitude
                lastMagnitude = magnitude
                let angle = model.clickAngle
                print ("Magnitude: \(magnitude)")
                let magnified = angle * delta
                if magnified >= model.minClick && magnified <= model.maxClick {
                    model.clickAngle = magnified
                }
            })
            .onEnded({ _ in
                print("Magnification ended")
                lastMagnitude = 1.0
                model.reset()
            })
        
        let scaler = CGAffineTransform(scaleX: size, y: size)
        
        let gesture = magnification.simultaneously(with: drag)
        
        ZStack {
            HStack {
                Spacer()
                VStack{
                    Spacer()
                    Path { path in
                        model.segmentList.forEach { segment in
                            let inner = segment.inner
                            let outer = segment.outer
                            let innerScaled = inner.applying(scaler)
                            let outerScaled = outer.applying(scaler)
                            path.move(to: innerScaled)
                            path.addLine(to: outerScaled)
                        }
                        
                    }
                    .stroke(model.strokeColor, lineWidth: model.lineWidth)
                    .background(Color.black)
                    .frame(width: size, height: size)
                    Spacer()
                }
                Spacer()
            }
            .background(Color.black)
            .gesture(gesture)
            
            HStack {
                Spacer()
                VStack{
                    Spacer()
                    contentView
                        .frame(width: contentSize.width,
                               height: contentSize.height,
                               alignment: .center)
                    Spacer()
                }
                Spacer()
            }
        }
    }
}
like image 377
Michael J Avatar asked Dec 28 '25 19:12

Michael J


1 Answers

So here is the solution I ended up with for today:

  • I recognise drag on my knob
  • I recognise magnification on the empty space around. (which I am lucky to have)

I found no way to implement the behavior I had on UiKit where pinch and drag worked simultanously.

If you come a across a way - please let me know.

Interesting detail: I figured that gestures only work on pixels that are not transparent. So everything needs to have a background. No way to attach a gesture to a Color(.clear) or anything that would not show. That gave me some headache on the Path view, as it would only triggure the gesture where the Path actually painted something.

struct VirtualKnobView<Content:View>: View {
    
    init(model:VirtualKnobModel, contentSize:CGSize, @ViewBuilder _ contentView:()-> Content ){
        self.model = model
        self.contentSize = contentSize
        self.contentView = contentView()
    }
    
    init(contentSize:CGSize, @ViewBuilder _ contentView:()-> Content){
        self.model = VirtualKnobModel(inner: 0.7, outer: 0.8, ext: 0.05, angle: 30.0)
        self.contentSize = contentSize
        self.contentView = contentView()
    }
    
    @ObservedObject var model:VirtualKnobModel
    
    @State var lastMagnitude:CGFloat = 1.0
    @State var isDragging:Bool = false
    
    var contentSize:CGSize
    var contentView:Content
    
    // The bgcolor is needed for the views to receive gestures.
    let bgColor = Color(UIColor.black.withAlphaComponent(0.001))
    
    var body: some View {
        
        let size = model.calclulateSize(for: contentSize)
        
        
        let drag = DragGesture(minimumDistance: 0)
            .onChanged({ state in
                let point = state.location
                let refPoint = CGPoint(x: (point.x - size/2)/size,
                                       y: (point.y - size/2)/size)
                model.setTouchPoint(point: refPoint)
            })
            .onEnded({ _ in
                model.reset()
            })
        
        let magnification = MagnificationGesture()
            .onChanged({ (magnitude:CGFloat) in
                let delta = magnitude / lastMagnitude
                lastMagnitude = magnitude
                let angle = model.clickAngle
                let magnified = angle * delta
                if magnified >= model.minClick && magnified <= model.maxClick {
                    model.clickAngle = magnified
                }
            })
            .onEnded({ _ in
                lastMagnitude = 1.0
                model.reset()
            })
        
        let scaler = CGAffineTransform(scaleX: size, y: size)
        
        ZStack {
            HStack(spacing:0) {
                Rectangle()
                    .foregroundColor(bgColor)
                    .gesture(magnification)
                VStack(spacing:0){
                    Rectangle()
                        .foregroundColor(bgColor)
                    
                    Path { path in
                        model.segmentList.forEach { segment in
                            let inner = segment.inner
                            let outer = segment.outer
                            let innerScaled = inner.applying(scaler)
                            let outerScaled = outer.applying(scaler)
                            path.move(to: innerScaled)
                            path.addLine(to: outerScaled)
                        }
                        
                    }
                    .stroke(model.strokeColor, lineWidth: model.lineWidth)
                    .foregroundColor(bgColor)
                    .gesture(drag)
                    .frame(width: size, height: size)
                    
                    Rectangle()
                        .foregroundColor(bgColor)
                }
                Rectangle()
                    .foregroundColor(bgColor)
                    .gesture(magnification)
            }
            
            HStack {
                Spacer()
                VStack{
                    Spacer()
                    contentView
                        .frame(width: contentSize.width,
                               height: contentSize.height,
                               alignment: .center)
                    Spacer()
                }
                Spacer()
            }
        }
    }
}
like image 174
Michael J Avatar answered Dec 31 '25 00:12

Michael J