0

I have a SwiftUI app that allows to drag pins on a map.
A pin is first long pressed. On ended, the pin is moved upwards so that its base can be seen. It can then be dragged.
I want to write a UI test that checks correct operation.

However my test fails since the screen points obtained are wrong.
I thus wrote a minimal project that demonstrates the issue.

Here is the ContentView:

import SwiftUI

struct ContentView: View {
    
    @GestureState private var dragAmount = CGSize.zero
    @State private var pinOffset = CGSize.zero
    
    var body: some View {
        
        let longPressGesture = LongPressGesture(minimumDuration: 0.2)
            .onEnded { _ in
                withAnimation(.easeInOut(duration: 0.3)) {
                    pinOffset = CGSize(width: 0, height: -60)
                }
            }
        
        let dragGesture = DragGesture(coordinateSpace: .global)
            .updating($dragAmount) { dragGestureValue, dragAmount, transaction in
                dragAmount = dragGestureValue.translation
            }
            .onEnded { dragGestureValue in
                pinOffset = CGSize(width:  dragGestureValue.translation.width, 
                                   height: dragGestureValue.translation.height - 60)
            }
        
        let combinedGesture = longPressGesture.sequenced(before: dragGesture)
        
        Image(systemName: "pin")
            .offset(CGSize(width:  dragAmount.width  + pinOffset.width, 
                           height: dragAmount.height + pinOffset.height))
            .simultaneousGesture(combinedGesture)
    }
}

And here is my UI test:

func test_dragging() throws {
    let app = XCUIApplication()
    app.launch()
    
    // setup
    let dragAmountX: CGFloat = 100
    let dragAmountY: CGFloat = 100
    let allowedError: CGFloat = 2
    var actualError: CGFloat
    let pinDraggingLiftOff: CGFloat = 60
    let pinLongPressDuration = 1.0
    
    // Long press the pin. On ended, the pin is lifted off. Then drag it to another location.
    
    // given
    let expectedDistance = sqrt(dragAmountX * dragAmountX + (dragAmountY - pinDraggingLiftOff) * (dragAmountY - pinDraggingLiftOff))
    let pinBeforeDrag = app.images.firstMatch
    let pinBaseBeforeDrag   = pinBeforeDrag.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99)).screenPoint
    let pinCenterBeforeDrag = pinBeforeDrag.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
    let pinCenterAfterDrag = pinCenterBeforeDrag.withOffset(CGVector(dx: dragAmountX, dy: dragAmountY))
    // when
    pinCenterBeforeDrag.press(forDuration: pinLongPressDuration, thenDragTo: pinCenterAfterDrag)
    // then
    let pinAfterDrag = app.images.firstMatch
    let pinBaseAfterDrag = pinAfterDrag.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99)).screenPoint
    let actualDistance = sqrt(pow(pinBaseBeforeDrag.x - pinBaseAfterDrag.x, 2) + pow(pinBaseBeforeDrag.y - pinBaseAfterDrag.y, 2))
    actualError = abs(actualDistance - expectedDistance)
    XCTAssert(actualError < allowedError, "Found distance \(actualDistance); expected: \(expectedDistance)")
}

A few values obtained:

dragAmountX dragAmountY expectedDistance    actualDistance  
0           0           60                  60  
0           100         40                  33  
100         0           116.6               110.7  
100         100         107.7               101.7  

I do not understand why all distances are wrong as soon as the pin is dragged.

PS: I have read this question, but it is unrelated.

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116

0 Answers0