Skip to content

Instantly share code, notes, and snippets.

@ukane-philemon
Last active July 17, 2023 11:05
Show Gist options
  • Save ukane-philemon/161467e65b91eaade6561a395cc1d568 to your computer and use it in GitHub Desktop.
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

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:

  1. webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:
  2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment