How to auto scroll a WebView after DOM changes in Cocoa / WebKit - objective-c

I'm building a small Mac Application that gets continuously supplied with data via a web socket. After processing each data segment, the data is displayed in a WebView. New data never replaces any data in the WebView instead new data is always appended to the WebView's content using DOM manipulations. The code sample below should give you an idea of what I'm doing.
DOMDocument *doc = self.webview.mainFrame.DOMDocument;
DOMHTMLElement *container = (DOMHTMLElement *)[doc createElement:#"div"];
NSString *html = [NSString stringWithFormat:#"... omitted for the sake of brevity ... "];
[container setInnerHTML:html];
[doc.body appendChild:container];
The rendering of the WebView apparently happens asynchronously. Is there a way to tell when the DOM manipulation finished and the content has been drawn? I want to use something like [webview scrollToEndOfDocument:self] to implement auto scrolling. Listening for DOM Events didn't help since they seem to be triggered when the DOM was modified but before these changes have been rendered. The code I'm using so far is similar to the following
[self.webview.mainFrame.DOMDocument addEventListener:#"DOMSubtreeModified" listener:self useCapture:NO];
in conjunction with
- (void)handleEvent:(DOMEvent *)event
{
[self.webview scrollToEndOfDocument:self];
}
The problem with this code is that the scrolling happens too early. I'm basically always one data segment behind. Can I register for a callback / notification of any kind that is triggered when the content was drawn?

Using timers
Auto scrolling can be implemented using an NSTimer. The challenge this solution bears is to figure out when to disable the timer to allow manual scrolling. I wasn't able to do the latter. Anyways, here is the code that enables autoscrolling using a timer:
self.WebViewAutoScrollTimer =
[NSTimer scheduledTimerWithTimeInterval:1.0/30.0
target:self
selector:#selector(scrollWebView:)
userInfo:nil
repeats:YES];
scrollWebView: simply being a method that calls scrollToEndOfDocument: on the web view.
Using notifications
Listing to NSViewFrameDidChangeNotification emitted by the web view allows to only scroll in the event of frame size changes. These changes occur when new content is added but also when the size of the encapsulating view changes, e.g. the window is resized. My implementation does not distinguish between those two scenarios.
NSClipView *contentView = self.webView.enclosingScrollView.contentView;
[contentView setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserverForName:NSViewFrameDidChangeNotification
object:contentView
queue:nil
usingBlock:^(NSNotification *notification) {
[self.webView scrollToEndOfDocument:self];
}];
Note: It is important that you instruct the content view of the web view's scroll view – think about this a couple of times and it will start to make sense – to post notifications when its frame size changes because NSView instances do not do this by default. This is accomplished using the first two lines of code in the example above.

Related

NSScrubber pan animation end notification

The touchbar-specific NSScrubber control scrolls with inertia on a pan gesture. I want to be notified of this animation's end to perform some function.
Try 1
The NSScrubberDelegate has a - didFinishInteractingWithScrubber: method which I implemented. However, soon after I stop manipulating the scrubber directly -- lifts the finger off the touchbar -- I get a callback, but the scroll continues to happen due to inertia. The final item that gets selected is NOT the one when this delegate method was called back.
Try 2
Digging further, I came across NSAnimation. Though it isn't documented clearly, I gather that scrubber is also a NSAnimatablePropertyContainer, as its selectedIndex property documentation says one can animate the selection through the animator proxy thus: scrubber.animator.selectedIndex = i. By that virtue, assuming that the animated property for the smooth panning is the boundsOrigin, I tried querying it.
I was able to get a CAAnimation by doing this
CAAnimation* a = [NSScrubber defaultAnimationForKey:#"boundsOrigin"];
// returns the same pointer value as above
// a = [myScrubber animationForKey:#"boundsOrigin"];
a.delegate = self;
...
- (void)animationDidStop:(CAAnimation *)anim
finished:(BOOL)flag {
if (flag == YES)
NSLog(#"Animation ended!\n");
}
I get a valid pointer value for a. However, I get numerous calls to animationDidStop with all of them having flag = YES; as the scrubber scrolls I keep getting these calls and when the scroll stops the calls stop. This feels closest to what I want but I dunno why so many calls come instead of just one when the animation ends.
Since NSScrubber's NSView or NSScrollView aren't exposed, I'm not sure if I'm querying the right object to get to the right NSAnimation.
Try 3
I also tried the hacky route of doing this on manipulation end code in vain
-(void)didFinishInteractingWithScrubber:(NSScrubber *)scrubber {
NSLog(#"Manipulation ended\n");
NSAnimationContext*c = NSAnimationContext.currentContext;
[c setCompletionHandler:^{
NSLog(#"Inertial scrolling stopped!\n");
}];
}
The completion handler is called almost immediately, before the inertial scroll stops :(
Ask
Is there anyway to know when the scrubber's pan gesture inertial animation ends?
I finally found a way to register a callback for the pan gesture's inertial scroll animation end.
Like any other scroll view, this also has the NSScrollViewDidEndLiveScrollNotification. Use the notification centre to register for the callback!
NSScrollView *sv = myScrubber.enclosingScrollView;
// register for NSScrollViewWillStartLiveScrollNotification if start is also needed
[[NSNotificationCenter defaultCenter] addObserverForName:NSScrollViewDidEndLiveScrollNotification
object:sv
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
NSLog(#"Scroll complete");
}];
Thanks to this answer for showing this approach.

How to remove or hide background in MKMapView when using custom tiles

Having tried many methods I still haven't found a good and full-proof way of preventing the usual "maps" from being shown behind custom map tiles that I am using. Ultimately I want my app to have a map page consisting only of a custom map.
I am really looking for a solution that is pure iOS and doesn't require any 3rd party software but it would appear difficult.
I have tried 3 methods already:
Number 1, hiding the background map via it's view:
NSArray *views = [[[self.mapView subviews] objectAtIndex:0] subviews];
[[views objectAtIndex:0] setHidden:YES];
this however doesn't work on a certain new operating system coming out very soon! The whole screen goes blank. The Apple Developer Forum hasn't provided a solution either
Number 2, Using another blank overlay (e.g. MKCircle) to cover the background map. This works however when scrolling or zooming out quickly, sometimes the overlay flickers off and you can briefly see the background map behind so not ideal.
Number 3, and this is what I have been working on for a few days now is to simply prevent the user from zooming out. Most documented methods tend to use regionDidChangeAnimated or regionWillChangeAnimated, however these do not seem to suddenly stop the map zooming out when pinching - they wait until the pinch movement has finished before taking effect so again it means the background map can be viewed briefly.
So now I am stumped, unless of course I have missed something with these other two methods.
So any help would be much appreciated!
Add this:
-(void)viewDidAppear:(BOOL)animated
{
MKTileOverlay *overlay = [[MKTileOverlay alloc] init];// initWithURLTemplate:tileTemplate];
overlay.canReplaceMapContent = YES;
[map addOverlay:overlay];
overlay = nil;
}
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *tile =nil;
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay: (id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MKTileOverlay class]])
{
MKTileOverlayRenderer *renderer = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
[renderer setAlpha:0.5];
return renderer;
}
}
It will replace the map content from the background. It worked very well in my case where I am adding an overlay on the whole map and hiding the real map from the user.
You can't do this in current releases without a third-party library like MapBox. However, the future OS release that you speak of lets you do this.

Force NSScrollView to use NSScrollerStyleOverlay independently from input device

How to force NSScrollView to use scrollers like that:
independently from input device, because now if computer has wired mouse connected or app is running on laptop (macbook) it uses scrollers like this:
witch is always visible. It is possible to achieve my desired result if user changes settings in system preferences. But I need to have that result without changing anything. Or maybe there is some easy method to use custom NSScrollers?
Hiding scrollers would be pretty acceptable result, but when I hide them I have some scrolling issues. I use custom NSClipView witch adjusts documentView to be centered when adjusting its frame size. But when I adjust frame size, and it is scrolled to center, if I scroll anywhere it jumps to bottom left corner and then works as it should.
I'm using table view and I just put this code in awakeFormNib:
[[self.tableView enclosingScrollView] setScrollerStyle:NSScrollerStyleOverlay];
I also tried to hide scrollers but then I couldn't scroll the content using trackpad (no problem using mouse). In the other project I have similar table view with the same settings and hiding scrollers doesn't cause any issues.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notify:) name:NSPreferredScrollerStyleDidChangeNotification object:nil];
- (void)notify:(NSNotification*)
{
__weak typeof(self) weakself = self;
dispatch_async(dispatch_get_main_queue(), ^{
weakself.scrollView.scrollerStyle = NSScrollerStyleOverlay;
});
}

MPMoviePlayerController adding UIButton to view that fades with controls

I am trying to add a UIButton to the view of a MPMoviePlayerController along with the standard controls. The button appears over the video and works as expected receiving touch events, but I would like to have it fade in and out with the standard controls in response to user touches.
I know I could accomplish this by rolling my own custom player controls, but it seems silly since I am just trying to add one button.
EDIT
If you recursively traverse the view hierarchy of the MPMoviePlayerController's view eventually you will come to a view class called MPInlineVideoOverlay. You can add any additional controls easily to this view to achieve the auto fade in/out behavior.
There are a few gotchas though, it can sometimes take awhile (up to a second in my experience) after you have created the MPMoviePlayerController and added it to a view before it has initialized fully and created it's MPInlineVideoOverlay layer. Because of this I had to create an instance variable called controlView in the code below because sometimes it doesn't exist when this code runs. This is why I have the last bit of code where the function calls itself again in 0.1 seconds if it isn't found. I couldn't notice any delay in the button appearing on my interface despite this delay.
-(void)setupAdditionalControls {
//Call after you have initialized your MPMoviePlayerController (probably viewDidLoad)
controlView = nil;
[self recursiveViewTraversal:movie.view counter:0];
//check to see if we found it, if we didn't we need to do it again in 0.1 seconds
if(controlView) {
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[controlView addSubview:backButton];
} else {
[self performSelector:#selector(setupAdditionalControls) withObject:nil afterDelay:0.1];
}
}
-(void)recursiveViewTraversal:(UIView*)view counter:(int)counter {
NSLog(#"Depth %d - %#", counter, view); //For debug
if([view isKindOfClass:NSClassFromString(#"MPInlineVideoOverlay")]) {
//Add any additional controls you want to have fade with the standard controls here
controlView = view;
} else {
for(UIView *child in [view subviews]) {
[self recursiveViewTraversal:child counter:counter+1];
}
}
}
It isn't the best solution, but I am posting it in case someone else is trying to do the same thing. If Apple was to change the view structure or class names internal to the control overlay it would break. I am also assuming you aren't playing the video full screen (although you can play it fullscreen with embeded controls). I also had to disable the fullscreen button using the technique described here because the MPInlineVideoOverlay view gets removed and released when it is pressed: iPad MPMoviePlayerController - Disable Fullscreen
Calling setupAdditionalControls when you receive the fullscreen notifications described above will re-add your additional controls to the UI.
Would love a more elegant solution if anyone can suggest something other than this hackery I have come up with.
My solution to the same problem was:
Add the button as a child of the MPMoviePlayerController's view;
fade the button in and out using animation of its alpha property, with the proper durations;
handle the player controller's touchesBegan, and use that to toggle the button's visibility (using its alpha);
use a timer to determine when to hide the button again.
By trial-and-error, I determined that the durations that matched the (current) iOS ones are:
fade in: 0.1s
fade out: 0.2s
duration on screen: 5.0s (extend that each time the view is touched)
Of course this is still fragile; if the built-in delays change, mine will look wrong, but the code will still run.

How to generate grid in PDF using Cocoa?

I'm a beginner to Cocoa and Objective-C.
I want to make a Cocoa application that will generate a grid of boxes (used for practicing Chinese calligraphy) to export as a PDF, similar to this online generator: http://incompetech.com/graphpaper/chinesequarter/.
How should I generate the grid? I've tried to use Quartz with a CustomView, but didn't manage to get very far. Also, once the grid is drawn in the CustomView, what is the method for "printing" that to a PDF?
Thanks for the help.
How should I generate the grid?
Implement a custom view that draws it.
I've tried to use Quartz with a CustomView, …
That's one way; AppKit drawing is the other. Most parts of them are very similar, though; AppKit is directly based on PostScript, while Quartz is indirectly based on PostScript.
… but didn't manage to get very far.
You should ask a more specific question about your problem.
Also, once the grid is drawn in the CustomView, what is the method for "printing" that to a PDF?
Send it a dataWithPDFInsideRect: message, passing its bounds.
Note that there is no “once the grid is drawn in the CustomView”. Though there may be some internal caching, conceptually, a view does not draw once and hold onto it; it draws when needed, every time it's needed, into where it's needed. When the window needs to be redrawn, Cocoa will tell any views that are in the dirty area to (re)draw, and they will draw ultimately to the screen. When you ask for PDF data, that will also tell the view to draw, and it will draw into a context that records PDF data. This allows the view both to be lazy (draw only when needed) and to draw differently in different contexts (e.g., when printing).
Oops, you were asking about Cocoa and this is Cocoa Touch, but I'll leave it here as it may be some use (at least to others who find this later).
You can draw things in the view and then put what's there into a pdf.
This code will take what's drawn in a UIView (called sheetView here), put it into a pdf, then put that as an attachment in an email (so you can see it for now). You'll need to reference the protocol MFMailComposeViewControllerDelegate in your header.
if ([MFMailComposeViewController canSendMail]) {
//set up PDF rendering context
NSMutableData *pdfData = [NSMutableData data];
UIGraphicsBeginPDFContextToData(pdfData, sheetView.bounds, nil);
UIGraphicsBeginPDFPage();
//tell our view to draw (would normally use setNeedsDisplay, but need drawn now).
[sheetView drawRect:sheetView.bounds];
//remove PDF rendering context
UIGraphicsEndPDFContext();
//send PDF data in mail message as an attachment
MFMailComposeViewController *mailComposer = [[[MFMailComposeViewController alloc] init] autorelease];
mailComposer.mailComposeDelegate = self;If
[mailComposer addAttachmentData:pdfData mimeType:#"application/pdf" fileName:#"SheetView.pdf"];
[self presentModalViewController:mailComposer animated:YES];
}
else {
if (WARNINGS) NSLog(#"Device is unable to send email in its current state.");
}
You'll also need this method...
#pragma mark -
#pragma mark MFMailComposeViewControllerDelegate protocol method
//also need to implement the following method, so that the email composer can let
//us know that the user has clicked either Send or Cancel in the window.
//It's our duty to end the modal session here.
-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
[self dismissModalViewControllerAnimated:YES];
}