Last active
July 17, 2023 11:05
-
-
Save ukane-philemon/161467e65b91eaade6561a395cc1d568 to your computer and use it in GitHub Desktop.
How to handle MacOS (`AppKit`,`Webkit`) `decisionHandlers` or `completionHandlers` in Go using Macdrive
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
Sometime ago, I was in search of a Go library that would give me access to Native MacOS APIs and Macdrive pop'ed on my radar. It was easy to use, and has impelemented most of the Native MacOS APIs. But I faced an issue with handling callbacks functions for two macOS methods: | |
1. [webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:](https://developer.apple.com/documentation/webkit/wkuidelegate/1641952-webview?language=objc) | |
2. [webView:decidePolicyForNavigationAction:decisionHandler:](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview?language=objc) | |
To handle these callbacks functions, I had to implement a `CompletionHandlerDelegate`. See: https://github.com/decred/dcrdex/blob/master/client/cmd/dexc-desktop/app_darwin.go#L5-L55 | |
The `CompletionHandlerDelegate` takes the method and the arguments for the method then executes the callback function with the arguments. You can see how the Objective C methods are executed in the file above. | |
Note: It will be helpful to learn some [C](https://www.tutorialspoint.com/cprogramming/index.htm)/[Objc-C](https://www.tutorialspoint.com/objective_c/index.htm) like basic types, method declaration/implementation, Object declaration etc. | |
I'm assuming you have good knowlege of Go, and already have Go installed on your system so lets dig into the actual implementaion. | |
Run this your terminal: | |
``` | |
go get github.com/progrium/macdriver/cocoa | |
go get github.com/progrium/macdriver/core | |
go get github.com/progrium/macdriver/objc | |
``` | |
In your go file: | |
``` | |
package main // Package declaration | |
/* | |
#cgo CFLAGS: -x objective-c // Tells compiler we are using objective-c | |
#cgo LDFLAGS: -lobjc -framework WebKit -framework AppKit // Tells linker which framework to dynamically link to | |
// Import required Objc-C headers. We need these to access classes and types etc. | |
#import <objc/runtime.h> | |
#import <WebKit/WebKit.h> | |
#import <AppKit/AppKit.h> | |
// NavigationActionPolicyCancel is an integer used in Go code to represent | |
// WKNavigationActionPolicyCancel | |
const int NavigationActionPolicyCancel = 1; | |
// CompletionHandlerDelegate implements methods required for executing | |
// completion and decision handlers. | |
@interface CompletionHandlerDelegate:NSObject | |
- (void)completionHandler:(void (^)(NSArray<NSURL *> * _Nullable URLs))completionHandler withURLs:(NSArray<NSURL *> * _Nullable)URLs; | |
- (void)decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler withPolicy:(int)policy; | |
- (void)authenticationCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler withChallenge:(NSURLAuthenticationChallenge *)challenge; | |
@end | |
@implementation CompletionHandlerDelegate | |
// "completionHandler:withURLs" accepts a completion handler function from | |
// "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:" and | |
// executes it with the provided URLs. | |
- (void)completionHandler:(void (^)(NSArray<NSURL *> * _Nullable URLs))completionHandler withURLs:(NSArray<NSURL *> * _Nullable)URLs { | |
completionHandler(URLs); | |
} | |
// "decisionHandler:withPolicy" accepts a decision handler function from | |
// "webView:decidePolicyForNavigationAction:decisionHandler" and executes | |
// it with the provided policy. | |
- (void)decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler withPolicy:(int)policy { | |
policy == NavigationActionPolicyCancel ? decisionHandler(WKNavigationActionPolicyCancel) : decisionHandler(WKNavigationActionPolicyAllow); | |
} | |
@end | |
void* createCompletionHandlerDelegate() { | |
return [[CompletionHandlerDelegate alloc] init]; | |
} | |
*/ | |
import "C" // Import the Cgo pseudo package | |
import ( | |
"net/url" | |
"github.com/progrium/macdriver/cocoa" | |
"github.com/progrium/macdriver/core" | |
"github.com/progrium/macdriver/objc" | |
) | |
// Initialized when the app has been started | |
var appURL *url.URL | |
// completionHandler handles Objective-C callback functions for some | |
// delegate methods. | |
var completionHandler = objc.Object_fromPointer(C.createCompletionHandlerDelegate()) | |
func main() { | |
// ..... | |
// Assuming you are using the macdrive DefaultDelegateClass | |
cocoa.DefaultDelegateClass.AddMethod("webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:", func(_ objc.Object, webview objc.Object, param objc.Object, fram objc.Object, completionHandlerFn objc.Object) { | |
panel := objc.Get("NSOpenPanel").Send("openPanel") // Create a new panel. As of writing this class has not been implemented in macdrive but there is a PR for that. | |
openFiles := panel.Send("runModal").Bool() // Start the open panel modal and convert the response to bool. "runModal" will block until the user either cancels or selects a file | |
if !openFiles { | |
completionHandler.Send("completionHandler:withURLs:", completionHandlerFn, nil) // Execute the completion handler with nil if the open panel was cancelled. | |
return | |
} | |
completionHandler.Send("completionHandler:withURLs:", completionHandlerFn, panel.Send("URLs")) // Execute the completion handler with selected URLs. | |
}) | |
cocoa.DefaultDelegateClass.AddMethod("webView:decidePolicyForNavigationAction:decisionHandler:", func(delegate objc.Object, webview objc.Object, navigation objc.Object, decisionHandler objc.Object) { | |
reqURL := core.NSURLRequest_fromRef(navigation.Send("request")).URL() // Fetch the request URL | |
destinationHost := reqURL.Host().String() // Get the request URL host | |
var decisionPolicy int | |
if appURL.Hostname() != destinationHost { // Check if the host match the app's host | |
decisionPolicy = NavigationActionPolicyCancel // Set to NavigationActionPolicyCancel for external links then open the link using macOS native active APIs below. | |
// See: https://developer.apple.com/documentation/appkit/nsworkspace?language=objc | |
cocoa.NSWorkspace_sharedWorkspace().Send("openURL:", core.NSURL_Init(reqURL.String())) | |
} | |
completionHandler.Send("decisionHandler:withPolicy:", decisionHandler, decisionPolicy) // Execute the completion handler with our decision | |
}) | |
// ..... | |
} | |
``` | |
If you need more completion handlers, you can implement a function or method that accepts the completion handler as the first argument and the arguments for it as a second argument. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment