Michael J
Michael J

Reputation: 437

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:

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

Upvotes: 3

Views: 1598

Answers (1)

Michael J
Michael J

Reputation: 437

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

Upvotes: 1

Related Questions