惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
V
V2EX
Cloudbric
Cloudbric
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
量子位
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
K
Kaspersky official blog
博客园 - 【当耐特】
T
Tenable Blog
L
Lohrmann on Cybersecurity
The Cloudflare Blog
S
Schneier on Security
A
Arctic Wolf
Latest news
Latest news
C
Cyber Attacks, Cyber Crime and Cyber Security
罗磊的独立博客
T
The Exploit Database - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
Simon Willison's Weblog
Simon Willison's Weblog
雷峰网
雷峰网
NISL@THU
NISL@THU
人人都是产品经理
人人都是产品经理
月光博客
月光博客
J
Java Code Geeks
V
Visual Studio Blog
S
Security Affairs
博客园 - Franky
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
H
Heimdal Security Blog
有赞技术团队
有赞技术团队
V2EX - 技术
V2EX - 技术
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
T
Troy Hunt's Blog
SecWiki News
SecWiki News
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 聂微东

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger Code Signing and Notarization: Sparkle and Tears | Peter Steinberger Vibe Meter: Monitor Your AI Costs | Peter Steinberger Claude Code is My Computer | Peter Steinberger Stop Over-thinking AI Subscriptions | Peter Steinberger Introducing Demark: HTML in. MD out. Blink-fast. | Peter Steinberger The Future of Vibe Coding: Building with AI, Live and Unfiltered | Peter Steinberger MCP Best Practices | Peter Steinberger Finding My Spark Again | Peter Steinberger Top-Level Menu Visibility in SwiftUI for macOS | Peter Steinberger Fixing keyboardShortcut in SwiftUI | Peter Steinberger On Using Apple Silicon Mac Mini for Continuous Integration | Peter Steinberger Apple Silicon M1: A Developer's Perspective | Peter Steinberger Gardening Your Twitter: Curating Your Timeline | Peter Steinberger Gardening Your Twitter: Growing Your Followers | Peter Steinberger Forbidden Controls in Catalyst: Optimize Interface for Mac | Peter Steinberger Disabling Keyboard Avoidance in SwiftUI's UIHostingController | Peter Steinberger The State of SwiftUI | Peter Steinberger Logging in Swift | Peter Steinberger Building with Swift Trunk Development Snapshots | Peter Steinberger Calling Super at Runtime in Swift | Peter Steinberger zld — A Faster Version of Apple's Linker | Peter Steinberger How to Fix LLDB: Couldn't IRGen Expression | Peter Steinberger Updating macOS on a Hackintosh | Peter Steinberger InterposeKit — Elegant Swizzling in Swift | Peter Steinberger The Great Mac Catalyst Text Input Crash Hunt | Peter Steinberger Jailbreaking for iOS Developers | Peter Steinberger Network Kernel Core Dump | Peter Steinberger How to macOS Core Dump | Peter Steinberger Kernel Panics and Surprise boot-args | Peter Steinberger The LG UltraFine 5K, kernel_task, and Me | Peter Steinberger Let's Try This Again | Peter Steinberger How We Work at PSPDFKit | Peter Steinberger Swizzling in Swift | Peter Steinberger WWDC for First-Timers, 2019 Edition | Peter Steinberger Challenges of Adopting Drag and Drop | Peter Steinberger Marzipan: Porting iOS Apps to the Mac | Peter Steinberger How to Use Slack and Not Go Crazy | Peter Steinberger Hardcore Debugging - Heavy Weapons for Hard Bugs | Peter Steinberger Binary Frameworks in Swift | Peter Steinberger Even Swiftier Objective-C | Peter Steinberger The Case for Deprecating UITableView | Peter Steinberger Running tests with Clang Address Sanitizer | Peter Steinberger UI testing on iOS, without busy waiting | Peter Steinberger Hiring a distributed team | Peter Steinberger Writing Good Bug Reports | Peter Steinberger Real-time collaboration, Apple, and you | Peter Steinberger Converting Xcode Test Runs to JUnit, the Fast Way | Peter Steinberger Efficient iOS Version Checking | Peter Steinberger Investigating Thread Safety of UIImage | Peter Steinberger Swifty Objective-C | Peter Steinberger Running UI Tests on iOS With Ludicrous Speed | Peter Steinberger A Pragmatic Approach to Cross-Platform | Peter Steinberger Surprises with Swift Extensions | Peter Steinberger Using ccache for Fun and Profit | Peter Steinberger UITableViewController designated initializer woes | Peter Steinberger Researching ResearchKit | Peter Steinberger The curious case of rotation with multiple windows on iOS 8 | Peter Steinberger UIKit Debug Mode | Peter Steinberger Retrofitting containsString: on iOS 7 | Peter Steinberger A Story About Swizzling "the Right Way™" and Touch Forwarding | Peter Steinberger Hacking with Aspects | Peter Steinberger Fixing UITextView On iOS 7 | Peter Steinberger Fixing What Apple Doesn't | Peter Steinberger How To Inspect The View Hierarchy Of Third-Party Apps | Peter Steinberger Fixing UISearchDisplayController On iOS 7 | Peter Steinberger Smart Proxy Delegation | Peter Steinberger Adding Keyboard Shortcuts To UIAlertView | Peter Steinberger How To Center Content Within UIScrollView | Peter Steinberger UIAppearance for Custom Views | Peter Steinberger Hacking Block Support Into UIMenuItem | Peter Steinberger
Supporting Both Tap and Long Press on a Button in SwiftUI | Peter Steinberger
Peter Steinberger · 2021-01-28 · via Peter Steinberger

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

Naive First Version

Here’s my first naive iteration:

Button(action: {
    openWebsite(.pspdfkit)
}) {
    Image("pspdfkit-powered")
        .renderingMode(.template)
        .onLongPressGesture(minimumDuration: 2) {
            print("Secret Long Press Action!")
        }
}

While the above works to detect a long press, when adding a gesture to the image, the button no longer fires. Alright, not quite what we want. Let’s move the gesture out of the label and to the button.

Moving Things Around Version

Here’s my next attempt:

Button(action: {
    openWebsite(.pspdfkit)
}) {
    Image("pspdfkit-powered")
        .renderingMode(.template)
}
.onLongPressGesture(minimumDuration: 2) {
    print("Secret Long Press Action!")
}

Great! Now the button tap works again — unfortunately the long-press gesture doesn’t work anymore. OK, let’s use simultaneousGesture to tell SwiftUI that we really care about both gestures.

Getting Fancy with simultaneousGesture

Take three:

Button(action: {
    openWebsite(.pspdfkit)
}) {
    Image("pspdfkit-powered")
        .renderingMode(.template)
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
    print("Secret Long Press Action!")
})
Spacer()

Great — that works. However, now we always trigger both the long press and the action, which isn’t quite what we want. We want either/or, so let’s try adding a second gesture instead.

Two Gestures Are Better Than One

Here we go again:

Button(action: {
	// ignore
}) {
    Image("pspdfkit-powered")
        .renderingMode(.template)
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
    print("Secret Long Press Action!")
})
.simultaneousGesture(TapGesture().onEnded {
    print("Boring regular tap")
    openWebsite(.pspdfkit)
})
Spacer()

It… works! It does exactly what we expect, and it’s nicely calling either tap or long press. Woohoo! So let’s do some QA and test everywhere. iOS 13: check. iOS 14: check. Let’s compile the Catalyst version to be sure. And: It does not work. Neither tap nor long tap. The button has no effect at all.

Catalyst… Always Catalyst!

If we can ignore the long press on Catalyst, then this combination works at least for the regular action:

    @State var didLongPress = false

    var body: 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
            print("Secret Long Press Action!")
        })
        .simultaneousGesture(TapGesture().onEnded {
            didLongPress = false
        })
    }

In our case, we really want the long press though, so what to do? I remembered a trick I used in my Presenting Popovers from SwiftUI article: We can use a ZStack and just use UIKit for what doesn’t work in SwiftUI.

The Nuclear Option

The use is simple:

LongPressButton(action: {
    openWebsite(.pspdfkit)
}, longPressAction: {
    print("Secret Long Press Action!")
}, label: {
    Image("pspdfkit-powered")
        .renderingMode(.template)
})

Now, let’s talk about this LongPressButton subclass…

struct LongPressButton<Label>: View where Label: View {
    let label: (() -> Label)
    let action: () -> Void
    let longPressAction: () -> Void

    init(action: @escaping () -> Void, longPressAction: @escaping () -> Void, label: @escaping () -> Label) {
        self.label = label
        self.action = action
        self.longPressAction = longPressAction
    }

    var body: some View {
        Button(action: {
        }, label: {
            ZStack {
                label()
                // Using .simultaneousGesture(LongPressGesture().onEnded { _ in works on iOS but fails on Catalyst
                TappableView(action: action, longPressAction: longPressAction)
            }
        })
    }
}

private struct TappableView: UIViewRepresentable {
    let action: () -> Void
    let longPressAction: () -> Void

    typealias UIViewType = UIView

    func makeCoordinator() -> TappableView.Coordinator {
        Coordinator(action: action, longPressAction: longPressAction)
    }

    func makeUIView(context: Self.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.require(toFail: tapGestureRecognizer)
            $0.addGestureRecognizer(doubleTapGestureRecognizer)
        }
    }

    func updateUIView(_ uiView: UIView, context: Self.Context) { }

    class Coordinator {
        let action: () -> Void
        let longPressAction: () -> Void

        init(action: @escaping () -> Void, longPressAction: @escaping () -> Void) {
            self.action = action
            self.longPressAction = longPressAction
        }

        @objc func handleTap(sender: UITapGestureRecognizer) {
            guard sender.state == .ended else { return }
            action()
        }

        @objc func handleLongPress(sender: UILongPressGestureRecognizer) {
            guard sender.state == .began else { return }
            longPressAction()
        }
    }
}

And here we go. This version works exactly as we expect on iOS 13 and iOS 14, and on Catalyst on Catalina and Big Sur. UIKit is verbose, but it works. And with the power of SwiftUI, we can hide all that code behind a convenient new button subclass.

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

struct LongPressButton<Label>: View where Label: View {
    let label: (() -> Label)
    let action: () -> Void
    let longPressAction: () -> Void
    let longPressDelay: TimeInterval

    init(action: @escaping () -> Void, onLongPress: @escaping () -> Void, longPressDelay: TimeInterval = 2, label: @escaping () -> Label) {
        self.label = label
        self.action = action
        self.longPressAction = onLongPress
        self.longPressDelay = longPressDelay
    }

    var body: some View {
        Button(action: {
        }, label: {
            ZStack {
                label()
                UIViewContainer(UIView().then {
                    let tapGestureRecognizer = UITapGestureRecognizer(name: "Tap") { sender in
                        guard sender.state == .ended else { return }
                        action()
                    }
                    $0.addGestureRecognizer(tapGestureRecognizer)
                    let doubleTapGestureRecognizer = UILongPressGestureRecognizer(name: "Long Press") { sender in
                        guard sender.state == .began else { return }
                        longPressAction()
                    }
                    doubleTapGestureRecognizer.minimumPressDuration = longPressDelay
                    doubleTapGestureRecognizer.require(toFail: tapGestureRecognizer)
                    $0.addGestureRecognizer(doubleTapGestureRecognizer)
                })
            }
        })
    }
}

Addendum: Why Use Button?

Twitter folks have commented that this would all be 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, but it also misses out a few neat default features that Button has:

  • Automatically highlighting on tap; then fading that out if the mouse goes too far away
  • 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 it seems longPress is buggy on Catalyst. If you don’t have to bother with Mac Catalyst, try following sample code.

Conclusion

So what’s really special about the secret long-press action? It does enable the Debug Mode of PDF Viewer, showing various settings that aren’t really useful for regular folks, but that help with QA testing. If you’re curious, download our app (it’s free), long press on our icon in the Settings footer, and see for yourself.