Created
September 23, 2021 05:16
-
-
Save rdev/eacfcb2636c0537afab9e18917cbcc9d to your computer and use it in GitHub Desktop.
SwiftUI ClickOutside component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ClickOutside.swift | |
| // ClickOutside | |
| // | |
| // Created by Max Rovensky on 11/07/2021. | |
| // | |
| import SwiftUI | |
| class ClickOutsideHostingView<Content: View>: NSHostingView<Content> { | |
| var frameBounds: CGRect | |
| var action: () -> Void | |
| private var clickMonitor: Any? | |
| private var escMonitor: Any? | |
| init(bounds: CGRect, action: @escaping () -> Void, content: () -> Content) { | |
| self.action = action | |
| self.frameBounds = bounds | |
| super.init(rootView: content()) | |
| } | |
| deinit { | |
| // Clean up monitors | |
| removeClickOutsideListener() | |
| } | |
| @available(*, unavailable) | |
| @MainActor @objc dynamic required init?(coder aDecoder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| @MainActor required init(rootView: Content) { | |
| fatalError("init(rootView:) has not been implemented") | |
| } | |
| func addClickOutsideListener() { | |
| clickMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.leftMouseDown) { event -> NSEvent? in | |
| // GeometryReader bounds in .global coordinate system don't seem to be accounting for the toolbar, | |
| // so we need to correct for this | |
| if let windowFrameHeight = NSApp.keyWindow?.contentView?.frame.height, | |
| let contentLayoutRectHeight = NSApp.keyWindow?.contentLayoutRect.height | |
| { | |
| let toolbarHeight = windowFrameHeight - contentLayoutRectHeight | |
| let bounds = CGRect( | |
| origin: CGPoint(x: self.frameBounds.origin.x, y: self.frameBounds.origin.y - toolbarHeight), | |
| size: self.frameBounds.size | |
| ) | |
| if !bounds.contains(event.locationInWindow) { | |
| self.action() | |
| } | |
| } | |
| return event | |
| } | |
| escMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [self] (event) -> NSEvent? in | |
| if event.keyCode == 53 { // 53 = esc | |
| self.action() | |
| return nil | |
| } | |
| return event | |
| } | |
| } | |
| func removeClickOutsideListener() { | |
| if let click = clickMonitor, let esc = escMonitor { | |
| NSEvent.removeMonitor(click) | |
| NSEvent.removeMonitor(esc) | |
| self.clickMonitor = nil | |
| self.escMonitor = nil | |
| } | |
| } | |
| } | |
| struct ClickOutsideView<Content: View>: NSViewRepresentable { | |
| var active: Binding<Bool> | |
| var bounds: CGRect | |
| var action: () -> Void | |
| var content: () -> Content | |
| func makeNSView(context: Context) -> ClickOutsideHostingView<Content> { | |
| let view = ClickOutsideHostingView(bounds: bounds, action: action, content: content) | |
| view.addClickOutsideListener() | |
| return view | |
| } | |
| func updateNSView(_ view: ClickOutsideHostingView<Content>, context: Context) { | |
| if active.wrappedValue == true { | |
| view.addClickOutsideListener() | |
| } else { | |
| view.removeClickOutsideListener() | |
| } | |
| } | |
| } | |
| struct ClickOutside<Content: View>: View { | |
| var active: Binding<Bool> | |
| var action: () -> Void | |
| var content: () -> Content | |
| var body: some View { | |
| GeometryReader { view in | |
| ClickOutsideView(active: active, bounds: view.frame(in: .global), action: action, content: content) | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| struct MyView: View { | |
| @State var modalVisible = true | |
| var body: some View { | |
| ClickOutside(active: $modalVisible, action: { | |
| self.modalVisible = false | |
| }) { | |
| if modalVisible { | |
| SomeModal() | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment