Some time ago, I was in search of a Go library that would give me access to native macOS APIs, and Macdrive popped up on my radar. It was easy to use and had implemented most of the native macOS APIs. However, I faced an issue with handling callback functions for two macOS methods:
- webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:
- webView:decidePolicyForNavigationAction:decisionHandler:
To handle these callbacks functions, I had to implement a CompletionHandlerDelegate
. You can see the implementation here: https://github.com/decred/dcrdex/blob/master/client/cmd/dexc-desktop/app_darwin.go#L5-L55
The CompletionHandlerDelegate
accepts the completionHandler
or decisionHandler
and it's arguments, then executes the callback function with the arguments in Objc-C.
Note: It will be helpful to learn some C/Objc-C like basic types, method declaration/implementation, type/class 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 in 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 add/implement more functions or methods that accepts the completion handler and it's arguments. Then execute the completion handler in Objc-C.