Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

iOS App in QML how to navigate backward through StackView via swipe gestures



  • Hi together,

    I am developing an crossplatform compatible (iOS, Android; maybe later also windows phone) app prototype for our company. I am completely new to QML programming.

    In my main.qml file I use a StackView which is used for the basic navigation through views in the app. For the android like backward navigation I can simply listen to the "Keys.onReleased" event to pop the current view.

    For the iOS implementation of the app I want to use the swipe gesture for navigating to the previous view. What way would you suggest to achieve such functionality?

    I currently have an (not completely working approach) where I am using an "MouseArea" as a child of my StackView (with z: 100) to detect the swipe gesture. The navigation works, but I am in trouble to use the controls (like buttons, list, ...) underneath the MouseArea.

    My current StackView looks like that:

    StackView {
            id: mainStackLayout
            anchors.fill: parent
            focus: true
    
            //handle the android back button
            Keys.onReleased: {
                if (event.key === Qt.Key_Back && mainStackLayout.depth > 1) {
                    mainStackLayout.pop()
                    event.accepted = true
                }
            }
    
            //and the ios back swipe
            MouseArea {
                id: globalMouseArea
                anchors.fill: parent
                hoverEnabled: true
                z: 100
                //propagateComposedEvents: true
                preventStealing: true
                property real velocity: 0.0
                property int xStart: 0
                property int xPrev: 0
                property bool tracing: false
    
                onPressed: {
                    if(mainStackLayout.depth > 1) {
                        xStart = mouse.x
                        xPrev = mouse.x
                        velocity = 0
                        tracing = true
                        mouse.accepted = true
                    } else {
                        mouse.accepted = false
                    }
                }
                onPositionChanged: {
                    print('position changed')
                    //this routine sets the velocity which is checked in the onReleased handler
                    if (!tracing) {
                        mouse.accepted = false
                        return
                    }
                    var currVel = (mouse.x-xPrev)
                    velocity = (velocity + currVel)/2.0
                    xPrev = mouse.x
                    mouse.accepted = true
                }
                onReleased: {
                    print('released')
                    if(!tracing) {
                        mouse.accepted = false
                        return
                    }
                    print('velocity: ' + velocity)
                    tracing = false
                    if (velocity > 15 && mouse.x > parent.width * 0.2 ) {
                        print('Detect swipe')
                        mouse.accepted = true
                        mainStackLayout.pop()
                    }
                }
            }
        }
    

    But as I said it steals the input events from the controls underneath. I tried to get rid of this problem due simulating mouse clicks in case of the user releases the mouse without making an valid gesture, but I failed to implement such a mouse simulating. I tried to send mouseEvents from c++ to several QML objects, but they never received an event.

    For simulating the MouseEvents I tried things like

    QCoreApplication::instance()->sendEvent(findApplicationWindow(), &mouseEvent);
    

    where the mouseEvent is an left click and findApplicationWindow() returns the instance of the "QQuickApplicationWindow". In the net I found something like

        QMouseEvent mouseEvent(QEvent::MouseButtonPress, QPointF(105, 25), Qt::LeftButton, 0, Qt::NoModifier);
    
        QGraphicsSceneMouseEvent pressEvent(QEvent::GraphicsSceneMousePress);
        pressEvent.setScenePos(QPointF(105, 25));
        pressEvent.setButton(Qt::LeftButton);
        pressEvent.setButtons(Qt::LeftButton);
        QApplication::sendEvent(object, &pressEvent);
    
        QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
        releaseEvent.setScenePos(QPointF(105, 25));
        releaseEvent.setButton(Qt::LeftButton);
        releaseEvent.setButtons(Qt::LeftButton);
        QApplication::sendEvent(object, &releaseEvent);
    

    Which is although not working.

    But, all in all, I think it is a bad solution to simulate mouse clicks in my case.

    Maybe someone out here has already an working solution for this case or an advise for me. That would really be helpful :-)

    Thank you in advance!

    Greetings
    eumel1990


  • Moderators

    Hi @eumel1990

    I currently have an (not completely working approach) where I am using an "MouseArea" as a child of my StackView (with z: 100) to detect the swipe gesture.

    Instead of maintaining the z order you can make MouseArea as the root element and make StackView child of it so that you propagate only those events beneath which you require.

    MouseArea {
       ...
       StackView {
       ...
       }
    }
    

    Can SwipeView be an alternative to StackView in your case ?

    Also Since Qt 5 there is no relation between QGraphics* classes and QtQuick. The backend implemenation is now scenegraph based. So sending events in that way wont work.



  • Hi p3c0

    that's brilliant. I just did a short test until now, but changing the parent-child relation like you suggested worked for me.

    Now my StackView looks like that:

    MouseArea {
            id: globalMouseArea
            anchors.fill: parent
            hoverEnabled: true
            //z: 100
            //propagateComposedEvents: true
            preventStealing: true
            property real velocity: 0.0
            property int xStart: 0
            property int xPrev: 0
            property bool tracing: false
    
            onPressed: {
               // mouse.accepted = false
    
                //mouseEventSender.sendMouseEvent(mouse.x, mouse.y, globalMouseArea)
    
                if(mainStackLayout.depth > 1) {
                    xStart = mouse.x
                    xPrev = mouse.x
                    velocity = 0
                    tracing = true
                    mouse.accepted = true
                } else {
                    mouse.accepted = false
                }
            }
            onPositionChanged: {
                print('position changed')
                //this routine sets the velocity which is checked in the onReleased handler
                if (!tracing) {
                    mouse.accepted = false
                    return
                }
                var currVel = (mouse.x-xPrev)
                velocity = (velocity + currVel)/2.0
                xPrev = mouse.x
                mouse.accepted = true
            }
            onReleased: {
                print('released')
                if(!tracing) {
                    mouse.accepted = false
                    return
                }
                print('velocity: ' + velocity)
                tracing = false
                if (velocity > 15 && mouse.x > parent.width * 0.2 ) {
                    print('Detect swipe')
                    mouse.accepted = true
                    mainStackLayout.pop()
                } /*else {
                    mouse.accepted = false
                }*/
            }
    
            StackView {
                id: mainStackLayout
                anchors.fill: parent
                focus: true
    
                //handle the android back button
                Keys.onReleased: {
                    if (event.key === Qt.Key_Back && mainStackLayout.depth > 1) {
                        mainStackLayout.pop()
                        event.accepted = true
                    }
                }
            }
        }
    

    I know the SwipeView, but as far as I understood, it's navigating in a line (like if you swiping through pictures). I am using the StackView for the complete App and, for later implementation steps, the backward swipe should only be enabled for the iOS implementation.

    p3c0 you really helped me a lot, I am really grateful, Thank you!!

    Greetings
    eumel1990


  • Moderators

    @eumel1990 You're Welcome :)



  • Sry, but I have to ask again. What is the reason for this behaviour? Why does the change of the parent/child relation of MouseArea/StackView cause that change of the input event mechanics?

    Can you give an advise for an modern book about QML and/or App development with Qt? I already have "Getting started with Qt Quick" from Paolo Sereno (it's about Qt 5.6) which is like a short tutorial.

    Thanks in advise

    greetings
    eumel1990


  • Moderators

    @eumel1990

    Sry, but I have to ask again. What is the reason for this behaviour? Why does the change of the parent/child relation of MouseArea/StackView cause that change of the input event mechanics?

    It is better to have a the main Item to listen to mouse events and then propagate those to individual items rather than each item grab the events and propagate them to other child or siblings.

    Can you give an advise for an modern book about QML and/or App development with Qt? I already have "Getting started with Qt Quick" from Paolo Sereno (it's about Qt 5.6) which is like a short tutorial.

    Well I guess most of us here have learned Qt/QML through the excellent official documentation so I would first suggest the docs itself. Another one is
    http://qmlbook.github.io/



  • Ok. Thank you for this informations :-)



  • Sorry for replying to a topic so old, however, I needed to implement this behavior and made some modifications to the code in this forum. To help someone avoid loosing time, I'll post my modifications.

    Summary of modifications:

    • Created separate QML item to implement swipe gesture (only enabled on iOS)
    • Played with the "mouse.accepted" assignments to get same behavior without drastically affecting full-page Flickable-based items
    • Instead of filling the whole stack view, we create the object after the rest of the UI items, and anchor it to the left part of the screen.
    • We set a relatively small width to avoid issues with Flickables, ListViews, GridViews, etc.

    First, I created a new QML file named BackGestureDetector.qml, with the following code:

    import QtQuick 2.12
    
    MouseArea {
        width: 32
        hoverEnabled: true
        scrollGestureEnabled: false
        propagateComposedEvents: true
        enabled: Qt.platform.os === "ios"
    
        signal backGestureDetected()
    
        property int xPrev: 0
        property int xStart: 0
        property real velocity: 0.0
        property bool tracing: false
        property bool allowedToWork: false
    
        onPressed: {
            if (allowedToWork) {
                xStart = mouse.x
                xPrev = mouse.x
                velocity = 0
                tracing = true
                mouse.accepted = true
            }
    
            else
                mouse.accepted = false
        }
    
        onPositionChanged: {
            if (!tracing) {
                mouse.accepted = false
                return
            }
    
            var currVel = (mouse.x - xPrev)
            velocity = (velocity + currVel)/2.0
            xPrev = mouse.x
    
            mouse.accepted = false
        }
    
        onReleased: {
            if (!tracing) {
                mouse.accepted = false
                return
            }
    
            tracing = false
    
            if (velocity > 15 && mouse.x > parent.width * 0.2)
                backGestureDetected()
    
            mouse.accepted = false
        }
    }
    

    Finally, I integrate it to the UI as follows:

    import QtQuick 2.12
    import QtQuick.Window 2.12
    import QtQuick.Controls 2.12
    
    ApplicationWindow {
        id: app
    
        //
        // Set window geometry
        //
        width: 800
        height: 600
        visible: true
        title: qsTr("Swipe gestures example")
    
        //
        // Page display
        //
        StackView {
            id: stackView
            focus: true
            initialItem: page1
            anchors.fill: parent
    
            Page {
                id: page1
                visible: false
                width: parent.width
                height: parent.height
    
                Button {
                    text: qsTr("Go to page 2")
                    anchors.centerIn: parent
                    onClicked: stackView.push(page2)
                }
            }
    
            Page {
                id: page2
                visible: false
                width: parent.width
                height: parent.height
    
                Label {
                    text: qsTr("Page 2")
                    anchors.centerIn: parent
                }
            }
        }
    
        //
        // Swipe back to navigate
        //
        BackGestureDetector {
            allowedToWork: stackView.depth > 1
            onBackGestureDetected: stackView.pop()
    
            anchors {
                top: parent.top
                left: parent.left
                bottom: parent.bottom
            }
        }
    }
    

    Any comments are welcome, hope everyone is safe regarding the pandemic.


Log in to reply