Simultaneous Drag and Magnification Gestures

I wrote an application that enables simultaneous rotate and magnification gestures on a rectange. This application worked as expected. When I adapted the same application to enable simultaneous drag and magnification gestures on a rectange, I can either drag the rectangle or resize the rectangle, but I cannot do both.


I'm thinking this behavior is normal, because the two gestures are different; that is, drag is a one-finger gesture and magnify is a two-finger gesture. Can someone confirm this behavior for me?


If this is indeed working as expected, then how can I drag and resize a view at the same time?

Accepted Reply

Assuming the platform is iOS...


See the HIGs https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/gestures/


>but I cannot do both.


Yeah, pick one. Apple trains users to expect consistent UI behavior, and even if you found a way to make this work, you risk rejection during review, so...

Replies

Assuming the platform is iOS...


See the HIGs https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/gestures/


>but I cannot do both.


Yeah, pick one. Apple trains users to expect consistent UI behavior, and even if you found a way to make this work, you risk rejection during review, so...

Apple's own maps app has simultaneous drag and magnification

I sort of couldn't believe this was an actual issue, but it seems to be impossible to create a pure SwiftUI view that pans and zooms at the same time!

This is one of the core multitouch behaviors which Apple has supported in Photos, Maps and Safari for fourteen years. Every Apple app which supports zooming an image also supports simultaneous panning.

I filed this serious UI bug as FB9488452 with the following sample code:

struct ContentView: View {
	@GestureState var zoom = CGFloat(1.0)
	@GestureState var pan = CGSize.zero

    var body: some View {
		VStack {
			Image(systemName: "person")
				.font(.largeTitle)
				.padding()
				.foregroundColor(.black)
		}.frame(maxWidth: .infinity, maxHeight: .infinity)
		.background(Color.white)
		.scaleEffect(zoom)
		.offset(pan)
		.animation(.spring())
		.gesture(
			MagnificationGesture().updating($zoom){ (value, state, transaction) in
				state = value
			}.simultaneously(with: DragGesture())
				.updating($pan){ (value, state, transaction) in
					state = value.second?.translation ?? .zero
					print("Translation: \(value.second?.translation ?? .zero)")
				}
		)
	}
}

The above code should compose a gesture which allows simultaneous dragging and zooming, but only one of the gestures will succeed. It's as if they have been specified as Exclusive rather than Simultaneous. This is clearly a bug, as these gestures should absolutely be able to compose.

Any update on this issue ? I also assumed combining 2 drag gestures to pan with 2 fingers not just one should be as easy as this:

var move: some Gesture {
    return DragGesture().simultaneously(with: DragGesture())
      .updating($offset){ value, state, transaction in
          // compute state based on value.first and value.second
    }
  }

However, this gesture only gets triggered when the user drags with 1 finger instead of 2. Another issue is that the pinch example from HIGs https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/gestures/ shows how the zoom is done depending on where the user pinches not always in the center of the image. This isn't currently possible to implement by only using MagnificationGesture as it only provides a CGFloat and no information of the offset we should apply when zooming. Currently I don't see a way for implementing pinch to zoom like in the Photos app using only SwiftUI, please correct me if I'm wrong.

  • You're not wrong! Please file a bug—I believe teams prioritize based on number of duplicate bugs.

Add a Comment

The problems lies way beyond ‘simultaneous or exclusive’ and is in my opinion a serious bug with a lot of side effects and inconsistencies.

You only need one single view with one single drag gesture attached to it!

Set ‘minimumDistance: 0’ so the gesture gets recognized right away (like at touchBegan/touchDown of a tap gesture) and let the .onChange(), .onEnded() and .updating() closures print something to the console for checking when they fire.

Now start dragging (first gesture) and while doing so accidentally touch/tap/press the screen with another finger. The ‘new’ (second) gesture does NOT get reported to you by the system! No callback gets fired…

HOWEVER, your original first gesture gets killed, which means no more updates, changes and - BIG BUMMER - no .onEnded callback!!! And as there is no .onCancel callback like in UIKit, this behavior leaves you with dangling gestures and every state machine that you tried to establish with pairs of ‘started-ended‘ gets screwed up big time.

When you then place yet another ‘new‘ (third) gesture, a fresh drag gesture gets spun up (reporting all callbacks like it’s supposed to) but being totally ignorant of the two former gestures, regardless of whether you lifted the finger of your first and second gesture or one/both fingers are still on the screen !!!

And that is the reason why drag gestures (a one finger gesture) don’t work simultaneously with 2 finger gestures like magnification or rotation. Because the second finger that touches the screen kills the drag gesture but magnification and rotation start and keep on firing…

Attach more drag gestures to the same view, they all keep firing simultaneously without any problem whatsoever. Any combination and number of rotations/magnifications - same correct behavior…

But put a drag gesture in the mix and the whole thing brakes!

That is for sure not as it’s supposed to work…

Sadly, more than one year has passed and..

   var body: some View {
    Image("main")
      .resizable()
      .scaledToFit()
      .offset(
        x: viewState.width + control.translation.width,
        y: viewState.height + control.translation.height
      )
      .scaleEffect(control.zooming)
      .gesture(dragging)
      .simultaneousGesture(zooming)
  }

where:

   var dragging: some Gesture {
    DragGesture()
      .updating($control) { value, state, transaction in
        print(value, state, transaction)
        state = .dragging(translation: value.translation)
      }
  }
   
  var zooming: some Gesture {
    MagnificationGesture()
      .updating($control) { value, state, transaction in
        print(value, state, transaction)
        state = .zooming(zoom: value)
      }
  }

Doesn't work...... Either you Drag or you Magnify. SimultaneousGesture is seriously bugged.

I observed that if to have both a drag and a mangnify gesture on a view (an image in my case), the order of gesture modifiers was critical.

I had to have drag first and magnify second, otherwise onChanged was never fired on drag.

In addition, the offset and scaleEffect have to be called before the gestures…

                    .offset(dragTranslation)
                    .gesture(drag)
                
                    .scaleEffect(some value)
                    .gesture(magnification)