How do I intercept downloads from a WKWebView? - objective-c

I am making a Cocoa app that involves users selecting pictures from online for use within the app. I am also looking to collect contextual metadata from those downloads, such as the host the image came from, the website the user was visiting, the exact MIME type in the response headers, etc.
Basically I want to curate my user across the internet, downloading images and metadata into that user's account as I go. Until today I thought this would be impossible.
But just recently I was toying with a WKWebView and I tried right clicking on an image. I saw this...
Is there any way I could connect to that Download Image button, and get notifications when its clicked?

I did figure out how to make "Download Linked File" work and its a doozy that will not fit in a SO answer: https://github.com/kfix/MacPin/commit/9e5f925819f7f54ef29baff1e90783b820e683a3
However implementing those private delegate functions doesn't seem to allow "Download Image" to signal my WkWebView app in any way.

You can intercept those non-working "Download Image" and "Download Linked File" menu items by subclassing the WKWebView class and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(sender:))
menuItem.target = self
}
}
}
func menuClick(sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
Instead of this you can also simply hide the menu items with menuItem.isHidden = true
Detecting the chosen menu item is one thing, but knowing what the user actually clicked in the WKWebView control is the next challenge :)

Related

ObjectiveC Accessibility API: UnMaximize Window

I’m not sure if I am referring to this correctly, but when I use the word “UnMaximize”, I’m referring to:
When you click on the green button which is third on the top left of a
Chrome Window, it Maximizes the Window. When I use the word
“UnMaximize” above, I’m referring to the behavior that clicks that
button again so that it is no longer in full screen.
(By the way, what is the correct word for this in MacOS Terminology?)
I enjoy using the Easy Move+Resize App. While it can move Windows around, unfortunately, it has no effect on windows that are Maximized. Fortunately, the code is available on Github.
I’m curious if anyone can point me how to UnMaximize a Window using the Accessibility API
Does anyone what is the UnMaximize equivalent to kAXCloseButtonAttribute
I’m using MacOs 10.12 if that helps.
I’m grateful to #Willeke - Willeke for pointing me in the correct direction.
As mentioned in my question, I was looking at the code of the Easy Move+Resize App on GitHub. The problem with this code/app is that it does not work for Windows that are currently Maximized i.e. it tries to move these Windows, but it cannot, because they are fixed. (Note: This only has use and is relevant in a multi-monitor setup.) This app works correctly for Windows that are not Maximized.
Here, I am trying to add code that would UnMaximize a window in order to move it, and then Maximize it again after it has been moved. Obviously, the code below is in the context of this app, but I’m sure would be useful to users in other contexts.
I first added a wasMaximized property to EMRMoveResize.h
//EMRMoveResize.h
#property bool wasMaximized;
Next, I moved to EMRAppDelegate.m where the actual Event Callback code is. It should be noted that we are only concerned with moving i.e. only concerned with the Left Mouse Button. (This app uses the Right Mouse Button for resizing, which is not relavent when the Window has been maximized.) So, we are only concerned with kCGEventLeftMouseDown, kCGEventLeftMouseDragged and finally with kCGEventLeftMouseUp. In pseudo code, I have done something like:
If (LeftMouseDown) {
Find out if Window is Maximized
If (Window is Maximized) {
set the wasMaximized property
Click FullScreen Button to UnMaximize the Window in Order to Move it
}
The Window is now UnMaximized would now move as other windows in the LeftMouseDragged event, which I have not made any changes to. Finally,
If(LeftMouseUp) {
If(wasMaximized value was set) {
Click FullScreen Button again to Maximize the Window (Since it started out as Maximized)
Reset the wasMaximized property
}
}
Now for the snippets of code changes to EMRAppDelegate.m
if (type == kCGEventLeftMouseDown
|| type == kCGEventRightMouseDown) {
//..
//Skipped Unchanged Code
//..
//Find out if Window is Maximized
CFTypeRef TypeRef = nil;
if (AXUIElementCopyAttributeValue((AXUIElementRef)_clickedWindow, CFSTR("AXFullScreen"), &TypeRef)) {
if(Debug) NSLog(#"Could not get wasMaximized Value");
} else {
[moveResize setWasMaximized: CFBooleanGetValue(TypeRef)];
if(Debug) NSLog(CFBooleanGetValue(TypeRef) ? #"Updated Maximized to True" : #"Updated Maximized to False");
}
//Click FullScreen Button to UnMaximize the Window in Order to Move it
if([moveResize wasMaximized]) {
AXUIElementRef buttonRef = nil;
AXUIElementCopyAttributeValue(_clickedWindow, kAXFullScreenButtonAttribute, (CFTypeRef*)&buttonRef);
if(Debug) NSLog(#"buttonRef: %p", buttonRef);
AXUIElementPerformAction(buttonRef, kAXPressAction);
CFRelease(buttonRef);
}
//..
//Skipped Unchanged Code
//..
}
if (type == kCGEventLeftMouseUp
|| type == kCGEventRightMouseUp) {
//..
//Skipped Unchanged Code
//..
//Click FullScreen Button again to Maximize the Window (Since it started out as Maximized)
AXUIElementRef _clickedWindow = [moveResize window];
if([moveResize wasMaximized]) {
AXUIElementRef buttonRef = nil;
AXUIElementCopyAttributeValue(_clickedWindow, kAXFullScreenButtonAttribute, (CFTypeRef*)&buttonRef);
if(Debug) NSLog(#"buttonRef: %p", buttonRef);
AXUIElementPerformAction(buttonRef, kAXPressAction);
CFRelease(buttonRef);
[moveResize setWasMaximized: false];
}
//..
//Skipped Unchanged Code
//..
}
This worked for me. But I'm not an expert in Objective C or MacOS, so if you feel something can be improved, feel free to edit this post.

Disable NSDocument's "Revert To" & "Duplicate" menu item

I am creating a Mac app to read a XML document and save it. Everything is working fine except "Revert To" & "Duplicate" menu items. Till i find a solution for that i want to disable both of them, but i didn't found any solution for it, Please let me know how can i disable both the options so that they end user cannot click on them.
I already looked into Menu's from .xib so that i can disable them but i don't see any options.
I tried to somehow manipulate below code, but i didn't found any answers.
override func duplicate() throws -> NSDocument {
return self
}
The general way to disable a menu item in Cocoa is returning false in validateMenuItem(_:) (or validateUserInterfaceItem(_:).)
In this case, put the following code in your NSDocument subclass.
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
guard let action = menuItem.action else { return false }
switch action {
case #selector(duplicate(_:)):
return false
case #selector(revertToSaved(_:)):
return false
default: break
}
return super.validateMenuItem(menuItem)
}
However, according to the Apple's Human Interface Guidelines, you should not leave menu items which are not used. So, if your app doesn't support duplication and revert features at all, I prefer to remove the items rather than to disable.

WKWebView's Context menu "Download Image" menu item not responding....

Image download from right click "Context Menu" not responding in the web page loaded in WKWebview. If any body knows help me to identify which delegate method will receive this call or is there any manual implementation needed to get this option work.
App:Mac Application. Language : Objective C. SDK: Xcode
You can intercept context menu items of the WKWebView class by subclassing it and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.isHidden = true
}
}
}
}

Can I preload the web content for Safari View Controller?

I can create Safari View Controller without problem:
let svc = SFSafariViewController(URL: NSURL(string: remote_url)!, entersReaderIfAvailable: true)
self.presentViewController(svc, animated: true, completion: nil)
Is there any way I can preload the URL before I present the view controller to the user?
For example, I can preload the URL (web content) in the background first, and after the user clicks on something, I can show the Safari View Controller with the content right away. The user will feel the page loading is faster or instant.
P.S. Workarounds/hacks are also acceptable. For example, using cache or starting the view controller in background, etc.
EDIT: please consider SFSafariViewController only.
Here is a solution.
Obviously, if you click on the button right away you'll see the loading.
But basically, I load the Browser and put the view behind another one and I put a button in this other view.
When you press the button, the browser is bring to the front, already loaded.
The only problem here is that I'm not using any transition but that's one solution at least.
import UIKit
import SafariServices
class ViewController: UIViewController {
var svc = SFSafariViewController(URL: NSURL(string: "https://microsoft.com/")!, entersReaderIfAvailable: true)
var safariView:UIView?
let containerView = UIView()
let btn = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
//let tmpView = svc.view
addChildViewController(svc)
svc.didMoveToParentViewController(self)
svc.view.frame = view.frame
containerView.frame = view.frame
containerView.backgroundColor = UIColor.redColor()
safariView = svc.view
view.addSubview(safariView!)
view.addSubview(containerView)
btn.setTitle("Webizer", forState: UIControlState.Normal)
btn.titleLabel!.textColor = UIColor.blackColor()
btn.addTarget(self, action: "buttonTouched:", forControlEvents: .TouchUpInside)
btn.frame = CGRectMake(20, 50, 100, 100)
containerView.addSubview(btn)
view.sendSubviewToBack(safariView!)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTouched(sender: AnyObject) {
view.bringSubviewToFront(safariView!)
//self.presentViewController(svc, animated: true, completion: nil)
}
}
Sadly this behaviour is not supported with the current implementation of SFSafariViewController. I would encourage filing a radar with Apple to add support for this behaviour but like others have suggested your best bet is to use WKWebView and start loading before its added to the hierarchy.
I came across a lovely radar from Twitter that actually mentions exactly what you're asking for. I think you might find the following requests useful:
High Priority:
- Ability to warm the SFSafariViewController before actually presenting it with a URL, URL request, HTML data or file on disk
- Currently, are investing heavily into warming the shared URL cache for high priority Tweets so that if the user hits that Tweet we
will open UIWebView (sadly not WKWebView) with that pre-cached web
page. If we could just warm an SFSafariViewController with the
desired link, this would eliminate an enormous amount of effort on our
end.
You can see in their implementation they simply cache responses using UIWebView since WKWebView seems to obfuscate the caching semantics a bit. The only risk is that UIWebView is a likely candidate for deprecation as you see in their docs "In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView."
So unfortunately it seems that their are many hoops you need to jump through to get this all going so your best bet for now is to just pester Apple and dupe Twitters radar.
You could try using a http cache, but I don't think it would work as the Safari View Controller is working as a separate process (probably the same as Safari), so that's why it e.g. circumvents ATS.
The only way I can think of this working is to somehow force the user's Safari to load it? openURL: or adding to Reading List maybe? This doesn't sound like a viable solution.
You can always experiment with custom presentation of the view controller, attach it the view hierarchy, trigger appearance events, but set its frame to CGRectMake(0,0,1,1) or attach it somewhere off-screen, then wait a while and represent it with a correct frame.
you can download the web page using the following code . and represent it with the help of svc
let data:NSData?
do {
let weatherData = try NSData(contentsOfURL: NSURL(string: remote_url)!, options: NSDataReadingOptions())
data = weatherData
print(weatherData)
} catch {
print(error)
}
and load it when you needed in the svc
While it's technically possible to use the solution above to achieve what you're asking, this may not pass App Store review. Per the SFSafariViewController docs:
In accordance with App Store Review Guidelines, this view controller must be used to visibly present information to users; the controller may not be hidden or obscured by other views or layers. Additionally, an app may not use SFSafariViewController to track users without their knowledge and consent.

Dropping files onto the app's dock icon works except for proxy icons

I want to be able to accept drops on my app's dock icon from files, URLs, and text. Since files (public.file-url) are a subtype of URL (public.url), I added just two Services entries to my Info.plist:
Services
Item 0 (processURL)
Instance method Name = processURL
Send Types
Item 0 = public.url
Menu
Menu item title = Process URL
Item 1 (processString)
Instance method Name = processString
Send Types
Item 0 = public.plain-text
Menu
Menu item title = Process Text
Then I made my -applicationDidFinishLaunching call [NSApp setServicesProvider: self], and wrote a couple methods (-processString:userData:error and -processURL:userData:error) there in my application delegate. The app icon now accepts drops of all three types. In the -processURL:... method, it's easy to check if it's a local file or not, so that handles both of those cases.
One case still eludes me, though. When I try dragging a window's proxy icon to the app, it highlights the icon as if it can accept the drop, but then my method isn't called.
I tried dropping proxy icons from Xcode, Terminal, Preview, and some third-party apps: none would call my services method. But strangely, a proxy icon dropped from the Finder worked fine.
I tried changing public.url to public.item (the base type of the physical hierarchy), but my method is still not called for non-Finder proxy icons.
When the Finder successfully drops a proxy icon on my app, the pboard -types it provides are:
"public.file-url",
"CorePasteboardFlavorType 0x6675726C",
"dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn",
NSFilenamesPboardType,
"dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu",
"Apple URL pasteboard type"
I tried using each of these directly as the "Send Types". "public.file-url" and "NSFilenamesPboardType" highlight the icon as if it'll accept the drop, but don't. The others, unsurprisingly, don't even highlight the dock icon.
I can't find any reference to proxy icons having a different UTI than normal files. Do they? That would be weird.
I know this must be possible, because I can drag proxy icons from any window onto a Terminal window. What am I missing?
UPDATE: From an NSView, if I -registerForDraggedTypes including "public.url", I do get drops of proxy icons from all apps, with exactly the same -types list as from the Finder, listed above. So it's clearly something special to receiving drops via the dock icon. This should still be possible somehow: you can drag a proxy icon from a (non-Finder) window (e.g., an .xcworkspace from Xcode) onto the Terminal dock window, and it catches that just fine.
Swift 4 for your app delegate
func application(_ sender: NSApplication, openFile: String) -> Bool {
Swift.print("sender \(sender) file \(openFile)")
return true
}
func application(_ sender: NSApplication, openFiles: [String]) {
Swift.print("sender \(sender) list \(openFiles)")
// Create a FileManager instance
let fileManager = FileManager.default
for path in openFiles {
do {
let files = try fileManager.contentsOfDirectory(atPath: path)
for file in files {
_ = self.application(sender, openFile: file)// void return
}
}
catch let error as NSError {
print("Yoink \(error.localizedDescription)")
}
}
}
If you implement dragging files to your dock icon using application:openFile: in your NSApplicationDelegate, then dragging proxy icons should work too. The trick to accepting all files is adding a Document Type with extensions of '*'.