My task today was quite simple: adding an optional long-press handler to a push button in SwiftUI. A regular tap opens our website and a long press does… something else. Not and then difficult, right?

Naive Offset Version

Hither'south my first naive iteration:

1 two 3 iv 5 half-dozen 7 8 nine                      
                        Button                        (                        action                        :                        {                        openWebsite                        (                        .                        pspdfkit                        )                        })                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        .                        onLongPressGesture                        (                        minimumDuration                        :                        2                        )                        {                        print                        (                        "Hole-and-corner Long Press Action!"                        )                        }                        }                      

While the to a higher place works to discover a long press, when adding a gesture to the paradigm, the button no longer fires. Alright, not quite what we desire. Let'southward move the gesture out of the characterization and to the push.

Moving Things Around Version

Here's my next attempt:

one 2 iii four five half-dozen 7 8 9                      
                        Button                        (                        action                        :                        {                        openWebsite                        (                        .                        pspdfkit                        )                        })                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        }                        .                        onLongPressGesture                        (                        minimumDuration                        :                        2                        )                        {                        print                        (                        "Secret Long Printing Action!"                        )                        }                      

Great! Now the push button tap works once again — unfortunately the long-press gesture doesn't work anymore. OK, permit'south use simultaneousGesture to tell SwiftUI that we actually care about both gestures.

Getting Fancy with simultaneousGesture

Take iii:

ane 2 iii iv five 6 7 8 9 10                      
                        Push button                        (                        action                        :                        {                        openWebsite                        (                        .                        pspdfkit                        )                        })                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        }                        .                        simultaneousGesture                        (                        LongPressGesture                        ()                        .                        onEnded                        {                        _                        in                        print                        (                        "Undercover Long Printing Action!"                        )                        })                        Spacer                        ()                      

Great — that works. However, at present we always trigger both the long printing and the action, which isn't quite what we want. We want either/or, so allow's endeavor adding a 2nd gesture instead.

2 Gestures Are Ameliorate Than Ane

Hither we go again:

ane ii 3 4 5 6 seven 8 9 10 eleven 12 13 14                      
                        Button                        (                        action                        :                        {                        // ignore                        })                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        }                        .                        simultaneousGesture                        (                        LongPressGesture                        ()                        .                        onEnded                        {                        _                        in                        impress                        (                        "Secret Long Press Action!"                        )                        })                        .                        simultaneousGesture                        (                        TapGesture                        ()                        .                        onEnded                        {                        impress                        (                        "Boring regular tap"                        )                        openWebsite                        (                        .                        pspdfkit                        )                        })                        Spacer                        ()                      

It… works! It does exactly what we expect, and it'southward nicely calling either tap or long press. Woohoo! And then let's do some QA and examination everywhere. iOS 13: cheque. iOS 14: bank check. Let'southward compile the Catalyst version to be sure. And: Information technology does not piece of work. Neither tap nor long tap. The button has no outcome at all.

Goad… Always Catalyst!

If nosotros can ignore the long printing on Catalyst, so this combination works at least for the regular action:

i ii three 4 5 6 7 8 nine ten 11 12 13 14 xv 16 17 18 19 20 21 22 23                      
                        @State                        var                        didLongPress                        =                        false                        var                        trunk                        :                        some                        View                        {                        Button                        (                        action                        :                        {                        if                        didLongPress                        {                        didLongPress                        =                        false                        }                        else                        {                        print                        (                        "Boring regular tap"                        )                        openWebsite                        (                        .                        pspdfkit                        )                        }                        })                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        }                        // None of this ever fires on Mac Catalyst :(                        .                        simultaneousGesture                        (                        LongPressGesture                        ()                        .                        onEnded                        {                        _                        in                        didLongPress                        =                        true                        impress                        (                        "Secret Long Printing Action!"                        )                        })                        .                        simultaneousGesture                        (                        TapGesture                        ()                        .                        onEnded                        {                        didLongPress                        =                        false                        })                        }                      

In our instance, we really want the long press though, so what to practice? I remembered a trick I used in my Presenting Popovers from SwiftUI article: Nosotros tin can use a ZStack and only utilize UIKit for what doesn't work in SwiftUI.

The Nuclear Choice

The use is elementary:

1 2 3 4 v 6 vii 8                      
                        LongPressButton                        (                        action                        :                        {                        openWebsite                        (                        .                        pspdfkit                        )                        },                        longPressAction                        :                        {                        impress                        (                        "Secret Long Press Activity!"                        )                        },                        characterization                        :                        {                        Image                        (                        "pspdfkit-powered"                        )                        .                        renderingMode                        (                        .                        template                        )                        })                      

Now, permit's talk about this LongPressButton subclass…

1 2 3 4 five 6 7 viii 9 ten eleven 12 13 14 15 16 17 eighteen 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                      
                        struct                        LongPressButton                        <                        Label                        >                        :                        View                        where                        Characterization                        :                        View                        {                        let                        label                        :                        (()                        ->                        Label                        )                        let                        activeness                        :                        ()                        ->                        Void                        let                        longPressAction                        :                        ()                        ->                        Void                        init                        (                        action                        :                        @escaping                        ()                        ->                        Void                        ,                        longPressAction                        :                        @escaping                        ()                        ->                        Void                        ,                        characterization                        :                        @escaping                        ()                        ->                        Label                        )                        {                        self                        .                        label                        =                        label                        cocky                        .                        action                        =                        activity                        self                        .                        longPressAction                        =                        longPressAction                        }                        var                        body                        :                        some                        View                        {                        Button                        (                        action                        :                        {                        },                        label                        :                        {                        ZStack                        {                        label                        ()                        // Using .simultaneousGesture(LongPressGesture().onEnded { _ in works on iOS just fails on Catalyst                        TappableView                        (                        activeness                        :                        activity                        ,                        longPressAction                        :                        longPressAction                        )                        }                        })                        }                        }                        private                        struct                        TappableView                        :                        UIViewRepresentable                        {                        allow                        action                        :                        ()                        ->                        Void                        let                        longPressAction                        :                        ()                        ->                        Void                        typealias                        UIViewType                        =                        UIView                        func                        makeCoordinator                        ()                        ->                        TappableView                        .                        Coordinator                        {                        Coordinator                        (                        activity                        :                        action                        ,                        longPressAction                        :                        longPressAction                        )                        }                        func                        makeUIView                        (                        context                        :                        Cocky                        .                        Context                        )                        ->                        UIView                        {                        UIView                        ()                        .                        then                        {                        let                        tapGestureRecognizer                        =                        UITapGestureRecognizer                        (                        target                        :                        context                        .                        coordinator                        ,                        action                        :                        #selector(                        Coordinator.handleTap(sender:)                        )                        )                        $0                        .                        addGestureRecognizer                        (                        tapGestureRecognizer                        )                        let                        doubleTapGestureRecognizer                        =                        UILongPressGestureRecognizer                        (                        target                        :                        context                        .                        coordinator                        ,                        action                        :                        #selector(                        Coordinator.handleLongPress(sender:)                        )                        )                        doubleTapGestureRecognizer                        .                        minimumPressDuration                        =                        2                        doubleTapGestureRecognizer                        .                        crave                        (                        toFail                        :                        tapGestureRecognizer                        )                        $0                        .                        addGestureRecognizer                        (                        doubleTapGestureRecognizer                        )                        }                        }                        func                        updateUIView                        (                        _                        uiView                        :                        UIView                        ,                        context                        :                        Self                        .                        Context                        )                        {                        }                        class                        Coordinator                        {                        let                        activity                        :                        ()                        ->                        Void                        let                        longPressAction                        :                        ()                        ->                        Void                        init                        (                        activity                        :                        @escaping                        ()                        ->                        Void                        ,                        longPressAction                        :                        @escaping                        ()                        ->                        Void                        )                        {                        cocky                        .                        action                        =                        action                        self                        .                        longPressAction                        =                        longPressAction                        }                        @objc                        func                        handleTap                        (                        sender                        :                        UITapGestureRecognizer                        )                        {                        guard                        sender                        .                        land                        ==                        .                        ended                        else                        {                        return                        }                        activeness                        ()                        }                        @objc                        func                        handleLongPress                        (                        sender                        :                        UILongPressGestureRecognizer                        )                        {                        guard                        sender                        .                        state                        ==                        .                        began                        else                        {                        return                        }                        longPressAction                        ()                        }                        }                        }                      

And hither we get. This version works exactly as we look on iOS 13 and iOS xiv, and on Catalyst on Catalina and Big Sur. UIKit is verbose, but it works. And with the power of SwiftUI, we tin hide all that code behind a convenient new push button subclass.

In our project, this lawmaking is much smaller, as we use small categories to allow block-based gesture recognizers and automatic wrapping of UIViews:

i ii three iv 5 6 7 8 9 10 11 12 13 14 15 sixteen 17 18 19 twenty 21 22 23 24 25 26 27 28 29 thirty 31 32 33 34 35 36                      
                        struct                        LongPressButton                        <                        Label                        >                        :                        View                        where                        Label                        :                        View                        {                        let                        characterization                        :                        (()                        ->                        Label                        )                        allow                        action                        :                        ()                        ->                        Void                        let                        longPressAction                        :                        ()                        ->                        Void                        let                        longPressDelay                        :                        TimeInterval                        init                        (                        action                        :                        @escaping                        ()                        ->                        Void                        ,                        onLongPress                        :                        @escaping                        ()                        ->                        Void                        ,                        longPressDelay                        :                        TimeInterval                        =                        2                        ,                        label                        :                        @escaping                        ()                        ->                        Label                        )                        {                        self                        .                        label                        =                        label                        cocky                        .                        activity                        =                        action                        self                        .                        longPressAction                        =                        onLongPress                        cocky                        .                        longPressDelay                        =                        longPressDelay                        }                        var                        torso                        :                        some                        View                        {                        Push                        (                        action                        :                        {                        },                        label                        :                        {                        ZStack                        {                        label                        ()                        UIViewContainer                        (                        UIView                        ()                        .                        and then                        {                        let                        tapGestureRecognizer                        =                        UITapGestureRecognizer                        (                        name                        :                        "Tap"                        )                        {                        sender                        in                        guard                        sender                        .                        state                        ==                        .                        concluded                        else                        {                        return                        }                        activeness                        ()                        }                        $0                        .                        addGestureRecognizer                        (                        tapGestureRecognizer                        )                        let                        doubleTapGestureRecognizer                        =                        UILongPressGestureRecognizer                        (                        name                        :                        "Long Printing"                        )                        {                        sender                        in                        guard                        sender                        .                        state                        ==                        .                        began                        else                        {                        render                        }                        longPressAction                        ()                        }                        doubleTapGestureRecognizer                        .                        minimumPressDuration                        =                        longPressDelay                        doubleTapGestureRecognizer                        .                        crave                        (                        toFail                        :                        tapGestureRecognizer                        )                        $0                        .                        addGestureRecognizer                        (                        doubleTapGestureRecognizer                        )                        })                        }                        })                        }                        }                      

Addendum: Why Employ Button?

Twitter folks have commented that this would all exist much easier if I didn't use Button but — like here — the Image struct directly. This indeed makes the SwiftUI tap gestures work much better, just it also misses out a few smashing default features that Push has:

  • Automatically highlighting on tap; and so fading that out if the mouse goes too far abroad
  • Automatically tinting the image when the window is active and using gray when the window is inactive again (especially noticeable on Catalyst)
  • Automatically adding some click padding around the content

I've tried various variations, but information technology seems longPress is buggy on Catalyst. If you don't have to bother with Mac Goad, try post-obit sample code.

Conclusion

And so what'southward really special about the secret long-press action? It does enable the Debug Fashion of PDF Viewer, showing diverse settings that aren't really useful for regular folks, only that help with QA testing. If you're curious, download our app (it'southward free), long press on our icon in the Settings footer, and come across for yourself.