SwiftUI - Using the Drag and Magnification Gestures Sequenced

Hello,

I have a view in SwiftUI that has both a Drag and Magnification Gesture. Before iOS 15 my app worked with both gestures on the same view. This is how they are composed:

let dragGesture = DragGesture()

            .onChanged { value in

                //self.offset = value.translation

                self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)

        }

        .onEnded { value in

            withAnimation {

                self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)

                self.newPosition = self.currentPosition

                self.isDragging = false
            }

        }

        let pressGesture = LongPressGesture()

            .onEnded { value in

                withAnimation {

                    self.isDragging = true

                }

        }

        let pressGestureDelete = LongPressGesture(minimumDuration: 3)

            .onEnded { value in

                self.deleteBtn = true

        }

        let resizeGesture = MagnificationGesture(minimumScaleDelta: 0.1)

            .onChanged { value in

                self.scale *= value

            }

        .onEnded { value in

            self.scale *= value
        }

        

        let combined = pressGesture.sequenced(before: dragGesture).simultaneously(with: pressGestureDelete).simultaneously(with: resizeGesture)

And then on my view I am adding the gesture as .gesture(combined)

Since iOS 15 this no longer works. Instead I can drag the view around after the long press, but as soon as I attempt a resize using the magnification gesture the whole app freezes. I have tried attaching the magnification gesture to different pieces of the view thinking that maybe it needs to be at a different level (parent/child) from the drag gesture, but I get the same behavior if there is a drag gesture and a magnification gesture in the same view. It doesn't seem to matter how I attach them, if they both exist in the same view it causes the whole app to become unresponsive.

Does anyone know how to overcome this? Is this the new "intended" functionality? If so what do we do for the users who are accustomed to being able to seamlessly drag something and then resize it?

Thank you in advance.

Answered by jforward5 in 707301022

OH SNAP!!

I figured it out! I was putting things in the wrong order in my view definition!

Here is the final working view, that is both draggable and resizable!!

import SwiftUI



struct MovableResizableView<Content>: View where Content: View {

    

    @GestureState private var dragState = DragState.inactive

    @State private var currentScale: CGFloat = 0

    @State private var finalScale: CGFloat = 1

    @State var width: CGFloat = 150

    @State var height: CGFloat = 150

    @State private var newPosition: CGSize = .zero

    @State private var isDragging = false
    

    var content: () -> Content

    

    var body: some View {

        content()

            .opacity(dragState.isPressing ? 0.5 : 1.0)

            .scaleEffect(finalScale + currentScale)

            .offset(x: dragState.translation.width + newPosition.width, y: dragState.translation.height + self.newPosition.height)

            .animation(Animation.easeInOut(duration: 0.1), value: 0)

            .gesture(LongPressGesture(minimumDuration: 1.0)

                        .sequenced(before: DragGesture())

                        .updating($dragState, body: { (value, state, transaction) in



                switch value {

                case .first(true):

                    state = .pressing

                case .second(true, let drag):

                    state = .dragging(translation: drag?.translation ?? .zero)

                default:

                    break

                }



            })

                        .onEnded({ (value) in



                guard case .second(true, let drag?) = value else {

                    return

                }



                self.newPosition.height += drag.translation.height

                self.newPosition.width += drag.translation.width

            }))

            .gesture(MagnificationGesture()

                        .onChanged{ newScale in

                currentScale = newScale

            }

                        .onEnded { scale in

                finalScale = scale

                currentScale = 0

            })

            

        

    }

}





struct MovableResizableView_Previews: PreviewProvider {

    static var previews: some View {

        MovableResizableView() {

            Image(systemName: "star.circle.fill")

                .resizable()

                .frame(width: 100, height: 100)

                .foregroundColor(.green)

        }

    }

}

This seems to be the same problem I encountered early in the iOS 15 betas: https://developer.apple.com/forums/thread/689117 but the code where you use the scale value is missing from your example so I could not really say.

Apple has changed the documentation since then, so changing the frame inside the scale gesture seems not to be allowed any more.

Since I can't edit my question, and the comments only allow in-line code I am going to post my other attempt at making this work using Apple's documented way (doesn't work either):

import SwiftUI



struct MovableResizableView<Content>: View where Content: View {

    

    //@ObservedObject var imageModel: YearImageViewModel

    @GestureState private var dragState = DragState.inactive

    @GestureState private var scale: CGFloat = 1.0

    @State var image = UIImage()

    @State var width: CGFloat = 150

    @State var height: CGFloat = 150

    @State private var currentPosition: CGSize = .zero

    @State private var newPosition: CGSize = .zero

    @State private var isDragging = false

    @State private var deleteBtn = false

    @State private var deleted = false

    

    var content: () -> Content

    

    var body: some View {

        content()

            .opacity(dragState.isPressing ? 0.5 : 1.0)

            .offset(x: dragState.translation.width + newPosition.width, y: dragState.translation.height + self.newPosition.height)

            .animation(Animation.easeInOut(duration: 0.1), value: 0)

            .gesture(LongPressGesture(minimumDuration: 1.0)

                        .sequenced(before: DragGesture())

                        .updating($dragState, body: { (value, state, transaction) in



                switch value {

                case .first(true):

                    state = .pressing

                case .second(true, let drag):

                    state = .dragging(translation: drag?.translation ?? .zero)

                default:

                    break

                }



            })

                        .onEnded({ (value) in



                guard case .second(true, let drag?) = value else {

                    return

                }



                self.newPosition.height += drag.translation.height

                self.newPosition.width += drag.translation.width

            }))

            .gesture(MagnificationGesture()

                        .updating($scale, body: { (value, scale, trans) in

                            scale = value.magnitude

                        }))

            .scaleEffect(scale)

        

    }

}





struct MovableResizableView_Previews: PreviewProvider {

    static var previews: some View {

        MovableResizableView() {

            Image(systemName: "star.circle.fill")

                .resizable()

                .frame(width: 100, height: 100)

                .foregroundColor(.green)

        }

    }

}
Accepted Answer

OH SNAP!!

I figured it out! I was putting things in the wrong order in my view definition!

Here is the final working view, that is both draggable and resizable!!

import SwiftUI



struct MovableResizableView<Content>: View where Content: View {

    

    @GestureState private var dragState = DragState.inactive

    @State private var currentScale: CGFloat = 0

    @State private var finalScale: CGFloat = 1

    @State var width: CGFloat = 150

    @State var height: CGFloat = 150

    @State private var newPosition: CGSize = .zero

    @State private var isDragging = false
    

    var content: () -> Content

    

    var body: some View {

        content()

            .opacity(dragState.isPressing ? 0.5 : 1.0)

            .scaleEffect(finalScale + currentScale)

            .offset(x: dragState.translation.width + newPosition.width, y: dragState.translation.height + self.newPosition.height)

            .animation(Animation.easeInOut(duration: 0.1), value: 0)

            .gesture(LongPressGesture(minimumDuration: 1.0)

                        .sequenced(before: DragGesture())

                        .updating($dragState, body: { (value, state, transaction) in



                switch value {

                case .first(true):

                    state = .pressing

                case .second(true, let drag):

                    state = .dragging(translation: drag?.translation ?? .zero)

                default:

                    break

                }



            })

                        .onEnded({ (value) in



                guard case .second(true, let drag?) = value else {

                    return

                }



                self.newPosition.height += drag.translation.height

                self.newPosition.width += drag.translation.width

            }))

            .gesture(MagnificationGesture()

                        .onChanged{ newScale in

                currentScale = newScale

            }

                        .onEnded { scale in

                finalScale = scale

                currentScale = 0

            })

            

        

    }

}





struct MovableResizableView_Previews: PreviewProvider {

    static var previews: some View {

        MovableResizableView() {

            Image(systemName: "star.circle.fill")

                .resizable()

                .frame(width: 100, height: 100)

                .foregroundColor(.green)

        }

    }

}

Cannot find 'DragState' in scope

It is not working like I expected but as I recreate it to test, here is the DragState for you.

enum DragState {
  case inactive
  case pressing
  case dragging(translation: CGSize)

  var isPressing: Bool {
    guard case .pressing = self else { return false }
    return true
  }
  var translation: CGSize {
    guard case .dragging(let size) = self else { return .zero }
    return size
  }
}
SwiftUI - Using the Drag and Magnification Gestures Sequenced
 
 
Q