SpriteKit App Using Excessive CPU - sprite-kit

I wrote a SpriteKit app last year targeting 10.10 (Yosemite). Everything ran fine, but when I upgraded to El Capitan this year it freezes in one particular spot. It's a tough problem to diagnose because there is a lot of code so I'll try to be as descriptive as possible. I've also created a YOUTUBE screen recording of the issue.
App's Purpose
The app is basically a leaderboard that I created for a tournament at the school that I teach at. When the app launches, it goes to the LeaderboardScene scene and displays the leaderboard.
The app stays in this scene for the rest of the time. The sword that says "battle" is a button. When it is pressed it creates an overlay and shows the two students that will be facing each other in video form (SKVideoNode).
The videos play continuously and the user of the app eventually clicks on whichever student wins that match and then the overlay is removed from the scene and the app shows the leaderboard once again.
Potential Reasons For High CPU
Playing video: Normally the overlay shows video, but I also created an option where still images are loaded instead of video just in case I had a problem. Whether I load images or video, the CPU usage is super high.
Here's some of the code that is most likely causing this issue:
LeaderboardScene.m
//when the sword button is pressed it switches to the LB_SHOW_VERSUS_SCREEN state
-(void) update:(NSTimeInterval)currentTime {
switch (_leaderboardState) {
...
case LB_SHOW_VERSUS_SCREEN: { //Case for "Versus Screen" overlay
[self showVersusScreen];
break;
}
case LB_CHOOSE_WINNER: {
break;
}
default:
break;
}
}
...
//sets up the video overlay
-(void) showVersusScreen {
//doesn't allow the matchup screen to pop up until the producer FLASHING actions are complete
if ([_right hasActions] == NO) {
[self addChild:_matchup]; //_matchup is an object from the Matchup.m class
NSArray *producers = #[_left, _right];
[_matchup createRound:_round WithProducers:producers VideoType:YES]; //creates the matchup with VIDEO
//[_matchup createRound:_round WithProducers:producers VideoType:NO]; //creates the matchup without VIDEO
_leaderboardState = LB_CHOOSE_WINNER;
}
}
Matchup.m
//more setting up of the overlay
-(void) createRound:(NSString*)round WithProducers:(NSArray*)producers VideoType:(bool)isVideoType {
SKAction *wait = [SKAction waitForDuration:1.25];
[self loadSoundsWithProducers:producers];
[self runAction:wait completion:^{ //resets the overlay
_isVideoType = isVideoType;
[self removeAllChildren];
[self initBackground];
[self initHighlightNode];
[self initOutline];
[self initText:round];
if (_isVideoType)
[self initVersusVideoWithProducers:producers]; //this is selected
else
[self initVersusImagesWithProducers:producers];
[self animationSequence];
_currentSoundIndex = 0;
[self playAudio];
}];
}
...
//creates a VersusSprite object which represents each of the students
-(void) initVersusVideoWithProducers:(NSArray*)producers {
Producer *left = (Producer*)[producers objectAtIndex:0];
Producer *right = (Producer*)[producers objectAtIndex:1];
_leftProducer = [[VersusSprite alloc] initWithProducerVideo:left.name LeftSide:YES];
_leftProducer.name = left.name;
_leftProducer.zPosition = 5;
_leftProducer.position = CGPointMake(-_SCREEN_WIDTH/2, _SCREEN_HEIGHT/3);
[self addChild:_leftProducer];
_rightProducer = [[VersusSprite alloc] initWithProducerVideo:right.name LeftSide:NO];
_rightProducer.name = right.name;
_rightProducer.zPosition = 5;
_rightProducer.xScale = -1;
_rightProducer.position = CGPointMake(_SCREEN_WIDTH + _SCREEN_WIDTH/2, _SCREEN_HEIGHT/3);
[self addChild:_rightProducer];
}
VersusSprite.m
-(instancetype) initWithProducerVideo:(NSString*)fileName LeftSide:(bool)isLeftSide {
if (self = [super init]) {
_isVideo = YES;
_isLeftSide = isLeftSide;
self.name = fileName;
[self initVideoWithFileName:fileName]; //creates videos
[self addProducerLabel];
}
return self;
}
...
//creates the videos for the VersusSprite
-(void) initVideoWithFileName:(NSString*)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES);
NSString *desktopPath = [paths objectAtIndex:0];
NSString *resourcePath = [NSString stringWithFormat:#"%#/vs", desktopPath];
NSString *videoPath = [NSString stringWithFormat:#"%#/%#.mp4", resourcePath, fileName];
NSURL *fileURL = [NSURL fileURLWithPath:videoPath];
AVPlayer *avPlayer = [[AVPlayer alloc] initWithURL:fileURL];
_vid = [SKVideoNode videoNodeWithAVPlayer:avPlayer];
//[_vid setScale:1];
[self addChild:_vid];
[_vid play];
avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[avPlayer currentItem]];
}
//used to get the videos to loop
- (void)playerItemDidReachEnd:(NSNotification *)notification {
AVPlayerItem *p = [notification object];
[p seekToTime:kCMTimeZero];
}
UPDATE
The issue has been identified and is very specific to my project, so it probably won't help anyone else unfortunately. When clicking on the "sword" icon that says "Battle", the scene gets blurred and then the overlay is put on top of it. The blurring occurs on a background thread as you'll see below:
[self runAction:[SKAction waitForDuration:1.5] completion:^{
[self blurSceneProgressivelyToValue:15 WithDuration:1.25];
}];
I'll have to handle the blur in another way or just remove it altogether.

Related

Why isn't GameCenter waiting long enough to fill all the player slots?

So I'm working with multiplayer for the first time and I'm confused about the whole minplayer/maxplayer options. When I set minplayer=2 and maxplayer=4 and test the code, it connects 2 players just fine, but jumps directly into the game scene without waiting for players 3-4. How do I keep the code from progressing to the main game scene before all the slots are filled? The code works fine if I set minPlayers=maxPlayers. I know match.expectedPlayerCount==0 is supposed to fire once minPlayers is satisfied, but it isn't waiting at all for additional players to join. What am I missing here?
GKMatchRequest * matchRequest = [[[GKMatchRequest alloc] init] autorelease];
matchRequest.minPlayers = 2;
matchRequest.maxPlayers = 4;
gameCenterManager.matchController = [[GKMatchmakerViewController alloc] initWithMatchRequest:matchRequest];
gameCenterManager.matchController.matchmakerDelegate = self;
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[delegate.viewController presentViewController:gameCenterManager.matchController animated:YES completion:nil];
Find Match Code
-(void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match
{
TXGameCenterManager *gameCenterManager = [TXGameCenterManager sharedTXGameCenterManager];
gameCenterManager.multiplayerMatch = match;
// The delegate of the match is HelloWorldLayer
gameCenterManager.multiplayerMatch.delegate = self;
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[delegate.viewController dismissModalViewControllerAnimated:NO];
if( match.expectedPlayerCount==0 )
{
// Launching the game without waiting for connection change messages
NSLog(#"Begin game without waiting for match connection change messages");
// Determine the host, local or remote
NSArray * playerIds = match.playerIDs;
NSLog(#"Number of players: %d", [playerIds count]);
NSLog(#"ID of player: %#", [playerIds lastObject]);
NSLog(#"I got the player ids");
[GKPlayer loadPlayersForIdentifiers:playerIds withCompletionHandler:^(NSArray *players, NSError * error)
{
//bunch of code that gets player aliases and set host player
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
ChangeState code
-(void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
{
NSArray * playerIds = [NSArray arrayWithObject:playerID];
switch (state)
{
case GKPlayerStateConnected:
// handle a new player connection.
NSLog(#"Player connected!");
[GKPlayer loadPlayersForIdentifiers:playerIds withCompletionHandler:^(NSArray *players, NSError * error)
{
//bunch of code that gets player aliases and set host player
if (match.expectedPlayerCount==0)
{
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
}];
break;
case GKPlayerStateDisconnected:
// a player just disconnected.
NSLog(#"Player disconnected!");
break;
}
-(void)StartMultiplayerGame
{
[[CCDirector sharedDirector] replaceScene:[HelloWorldLayer node]];
}
if (match.expectedPlayerCount==0)
{
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
You said it yourself, if minPlayers players have joined (which is 2 in your case) then expectedPlayerCount is 0. So as soon as 2 players have joined, you're starting the game. This is not Game Center's fault.
You could wait for a longer amount of time once expectedPlayerCount is 0 to allow other players to join.
Your code also does not consider that a second player might join, then leave again. So in that case you would be starting the game with just one player.

video playing from application background to foreground takes 2 seconds lagging from pause to play in iOS 6.0

I have VideoController which plays video. Now VideoController is pushed in navigationController.
VideoController *ObjVideoController = [[VideoController alloc] init];
ObjVideoController.strVideoURL = [AnimationArr objectAtIndex:SequenceCount];
[self.navigationController pushViewController:ObjVideoController animated:NO];
[ObjVideoController play:[AnimationArr objectAtIndex:SequenceCount]];
Play method in VideoController is like this:
- (void)play:(NSString*)videoFile {
playbaktime = 0.0;
NSBundle *main = [NSBundle mainBundle];
NSURL *url = [NSURL fileURLWithPath:[[main resourcePath] stringByAppendingPathComponent:videoFile]];
if (!self.ctl)
{
self.ctl = nil;
self.ctl = [[MPMoviePlayerController alloc]init];
[self.view addSubview:self.ctl.view];
}
[self.ctl prepareToPlay];
self.ctl.contentURL = url;
self.ctl.controlStyle = 0;
//self.ctl.useApplicationAudioSession = NO;
[self.ctl.view setUserInteractionEnabled:NO];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
[self.ctl.view setFrame:CGRectMake(0,0,480,320)];
else
[self.ctl.view setFrame:CGRectMake(0,0,1024,768)];
self.ctl.backgroundView.backgroundColor = [UIColor whiteColor];
[self.ctl play];
}
Now observer has been add for UIApplicationWillResignActive, UIApplicationDidBecomeActive. Its selector are given below:
-(void)Pause_Video:(NSNotification*)notification {
Pausevideo = 1;
playbaktime = self.ctl.currentPlaybackTime;
[self.ctl pause];
}
-(void)Play_Video:(NSNotification*)notification {
if(self.ctl.loadState == MPMovieLoadStateUnknown)
{
[self play:self.strVideoURL];
Pausevideo = 0;
}
else{
if (self.ctl.playbackState == MPMoviePlaybackStatePaused) {
[self.ctl play];
Pausevideo = 0;
}
else
{
[self.ctl setInitialPlaybackTime:playbaktime];
[self.ctl play];
Pausevideo = 0;
}
}
}
Hope u understood question and help will be appreciated.
You seem to be calling prepareToPlay every time before playing, this is not necessary, just call it once when you load the file. There is no need to call it after you pause.
Additionally, you are incurring a lag because notifications do not fire instantly. Specifically, UIApplicationDidBecomeActive only fires well after the app has entered the foreground. What you want instead is to observe the UIApplicationWillEnterForegroundNotification, which will fire as the app is entering the foreground, and allow you to decrease the lag significantly.

AVCaptureSession - Stop Running - take a long long time

I use ZXing for an app, this is mainly the same code than the ZXing original code except that I allow to scan several time in a row (ie., the ZXingWidgetController is not necesseraly dismissed as soon as something is detected).
I experience a long long freeze (sometimes it never ends) when I press the dismiss button that call
- (void)cancelled {
// if (!self.isStatusBarHidden) {
// [[UIApplication sharedApplication] setStatusBarHidden:NO];
// }
[self stopCapture];
wasCancelled = YES;
if (delegate != nil) {
[delegate zxingControllerDidCancel:self];
}
}
with
- (void)stopCapture {
decoding = NO;
#if HAS_AVFF
if([captureSession isRunning])[captureSession stopRunning];
AVCaptureInput* input = [captureSession.inputs objectAtIndex:0];
[captureSession removeInput:input];
AVCaptureVideoDataOutput* output = (AVCaptureVideoDataOutput*)[captureSession.outputs objectAtIndex:0];
[captureSession removeOutput:output];
[self.prevLayer removeFromSuperlayer];
/*
// heebee jeebees here ... is iOS still writing into the layer?
if (self.prevLayer) {
layer.session = nil;
AVCaptureVideoPreviewLayer* layer = prevLayer;
[self.prevLayer retain];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 12000000000), dispatch_get_main_queue(), ^{
[layer release];
});
}
*/
self.prevLayer = nil;
self.captureSession = nil;
#endif
}
(please notice that the dismissModalViewController that remove the view is within the delegate method)
I experience the freeze only while dismissing only if I made several scans in a row, and only with an iPhone 4 (no freeze with a 4S)
Any idea ?
Cheers
Rom
According to the AV Cam View Controller Example calling startRunning or stopRunning does not return until the session completes the requested operation. Since you are sending these messages to the session on the main thread, it freezes all the UI until the requested operation completes. What I would recommend is that you wrap your calls in an Asynchronous dispatch so that the view does not lock-up.
- (void)cancelled
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self stopCapture];
});
//You might want to think about putting the following in another method
//and calling it when the stop capture method finishes
wasCancelled = YES;
if (delegate != nil) {
[delegate zxingControllerDidCancel:self];
}
}

Emulating splash video in iOS application

OK, there are very few options to emulate the splash video in iOS. All we can do is wait till application is fully loaded and then create Media Player and load video in it.
I implemented it with following code:
-(void) moviePlayBackDidFinish:(NSNotification*)notification
{
NSLog(#"Intro video stopped");
[mMoviePlayer release];
}
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSURL* mMovieURL;
NSBundle *bundle = [NSBundle mainBundle];
if(bundle != nil)
{
NSString *moviePath = [bundle pathForResource:#"intro" ofType:#"mp4"];
if (moviePath)
{
mMovieURL = [NSURL fileURLWithPath:moviePath];
[mMovieURL retain];
}
}
mMoviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:mMovieURL];
[mMovieURL release];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:mMoviePlayer];
mMoviePlayer.controlStyle = MPMovieControlStyleNone;
[mMoviePlayer.backgroundView addSubview:[[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Splash/background.png"]] autorelease]];
mMoviePlayer.scalingMode = MPMovieScalingModeFill;
[window addSubview:mMoviePlayer.view];
[mMoviePlayer setFullscreen:YES animated:NO];
[window makeKeyAndVisible];
[mMoviePlayer play];
<... other stuff ...>
}
My video is only 1 MB. But this code do something different then I'd like to see:
First of all user can see a static splash screen for a few seconds;
Then a black screen appears for 1 or 2 seconds. I think this is happening because the media player is loaded.
Video start playing.
Main interface loads.
As you understand I don't like the pause with black screen - it looks ugly.
As far as I can see in my Console log the problem is that mediaplayer is waiting till the main view controller is fully loaded.
Few words about main view: i'm writing an application for iPad and the main view consists of several subviews with multiple images. Every image and every subview in main view loads some data from Internet Web service via ASIHTTPRequest lib.
I think that Media Player is waiting for all initial connections to finish and only then it's starting the video...
How can I force the video to play before main view is loaded? Or maybe I can delay the loading of main XIB?
You cannot get rid of the static splash image. While it is shown, the OS is loading the application and instantiating stuff until it is ready to call your UIApplicationDelegate. So all you can do is either use no splash (black screen for a few seconds) or make your movie start exactly with the shown splash screen so it looks like the static image would suddenly animate.
To get rid of the black screen while the movie loads, you can try to make the player transparent and have an UIImageView behind the player that shows the splash image. The behavior would be this:
Splash screen is shown (static image).
Application is loaded. You see the UIImageView, also showing the splash screen. On top of it is the transparent movie player.
Movie player finally has loaded the move and starts playing it.
At least in theory, this should cause the effect that the static image suddenly starts animating.
But if you don't use a splash screen at all (a lot of games do that), then it doesn't matter that the movie player is showing a black screen at first, you wouldn't notice.
Regarding showing the splash screen in an UIImageView: unfortunately, you have to test the interface rotation and load the image manually, there's no way to query which splash screen was shown. If you only support one interface orientation (again, a lot of games do this) you don't have this problem, of course.
There is a better solution now, assuming you are using UIViewControllers.
Instead of using MPMoviePlayerController, use MPMoviePlayerViewController. Here is some sample code, adapted from the question:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSURL* mMovieURL;
NSBundle *bundle = [NSBundle mainBundle];
if(bundle != nil)
{
NSString *moviePath = [bundle pathForResource:#"intro" ofType:#"mp4"];
if (moviePath)
{
mMovieURL = [NSURL fileURLWithPath:moviePath];
[mMovieURL retain];
}
}
mMoviePlayer = [[MPMoviePlayerViewController alloc] initWithContentURL:mMovieURL];
[mMovieURL release];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayBackDidFinish)
name:MPMoviePlayerPlaybackDidFinishNotification
object:mMoviePlayer.moviePlayer];
mMoviePlayer.moviePlayer.controlStyle = MPMovieControlStyleNone;
[mMoviePlayer.moviePlayer.backgroundView addSubview:[[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"SplashCopy.png"]] autorelease]];
mMoviePlayer.moviePlayer.scalingMode = MPMovieScalingModeFill;
[window.rootViewController.view addSubview:mMoviePlayer.moviePlayer.view];
[mMoviePlayer.moviePlayer setFullscreen:YES animated:NO];
[mMoviePlayer.moviePlayer play];
}
Emulating splash video in iOS application
this code for swift4.0
var mMovieURL: URL?
let bundle = Bundle.main
if bundle != nil {
let moviePath: String? = bundle.path(forResource: "intro", ofType: "mp4")
if moviePath != nil {
mMovieURL = URL(fileURLWithPath: moviePath ?? "")
}
}
mMoviePlayer = MPMoviePlayerController(contentURL: mMovieURL!)
NotificationCenter.default.addObserver(self, selector: #selector(self.moviePlayBackDidFinish), name: .MPMoviePlayerPlaybackDidFinish, object: mMoviePlayer)
mMoviePlayer.controlStyle = .none
mMoviePlayer.backgroundView.addSubview(UIImageView(image: UIImage(named: "Splash.png")))
mMoviePlayer.scalingMode = .fill
window?.addSubview(mMoviePlayer.view)
// window?.rootViewController?.view.addSubview(mMoviePlayer.view)
mMoviePlayer.setFullscreen(true, animated: false)
window?.makeKeyAndVisible()
mMoviePlayer.play()
return true
}
#objc func moviePlayBackDidFinish(_ notification: Notification) {
mMoviePlayer.view.removeFromSuperview()
}

UIWebView - Enabling Action Sheets on <img> tags

Is it just me or has the action sheet on <img> tags been disabled in UIWebView? In Safari, e.g, when you want to save an image locally, you touch and hold on the image to get an action sheet shown. But it's not working in my custom UIWebView. I mean, it is still working for <a> tags, i.e, when I touch and hold on html links, an action sheet shows up. But not for the <img> tags.
I've tried things like putting img { -webkit-touch-callout: inherit; } in css, which didn't work. On the other hand, when I double-tap and hold on the images, a copy-balloon shows up.
So the question is, has the default action sheet callout for <img> tags been disabled for UIWebView? Is so, is there a way to re-enable it? I've googled around and saw many Q&As on how to disable it in UIWebView, so is it just me who aren't seeing the popup?
Thanks in advance!
Yes apple has disabled this feature (among others) in UIWebViews and kept it for Safari only.
However you can recreate this yourself by extending this tutorial, http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/.
Once you've finished this tutorial you'll want to add a few extra's so you can actually save images (which the tutorial doesn't cover).
I added an extra notification called #"tapAndHoldShortNotification" after 0.3 seconds which calls a method with just the disable callout code in it (to prevent both the default and your own menu popping while the page is still loading, a little bug fix).
Also to detect images you'll need to extend the JSTools.js, here's mine with the extra functions.
function MyAppGetHTMLElementsAtPoint(x,y) {
var tags = ",";
var e = document.elementFromPoint(x,y);
while (e) {
if (e.tagName) {
tags += e.tagName + ',';
}
e = e.parentNode;
}
return tags;
}
function MyAppGetLinkSRCAtPoint(x,y) {
var tags = "";
var e = document.elementFromPoint(x,y);
while (e) {
if (e.src) {
tags += e.src;
break;
}
e = e.parentNode;
}
return tags;
}
function MyAppGetLinkHREFAtPoint(x,y) {
var tags = "";
var e = document.elementFromPoint(x,y);
while (e) {
if (e.href) {
tags += e.href;
break;
}
e = e.parentNode;
}
return tags;
}
Now you can detect the user clicking on images and actually find out the images url they are clicking on, but we need to change the -(void)openContextualMenuAtPoint: method to provide extra options.
Again here's mine (I tried to copy Safari's behaviour for this):
- (void)openContextualMenuAt:(CGPoint)pt{
// Load the JavaScript code from the Resources and inject it into the web page
NSString *path = [[NSBundle mainBundle] pathForResource:#"JSTools" ofType:#"js"];
NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[webView stringByEvaluatingJavaScriptFromString:jsCode];
// get the Tags at the touch location
NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
[NSString stringWithFormat:#"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
NSString *tagsHREF = [webView stringByEvaluatingJavaScriptFromString:
[NSString stringWithFormat:#"MyAppGetLinkHREFAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
NSString *tagsSRC = [webView stringByEvaluatingJavaScriptFromString:
[NSString stringWithFormat:#"MyAppGetLinkSRCAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
selectedLinkURL = #"";
selectedImageURL = #"";
// If an image was touched, add image-related buttons.
if ([tags rangeOfString:#",IMG,"].location != NSNotFound) {
selectedImageURL = tagsSRC;
if (sheet.title == nil) {
sheet.title = tagsSRC;
}
[sheet addButtonWithTitle:#"Save Image"];
[sheet addButtonWithTitle:#"Copy Image"];
}
// If a link is pressed add image buttons.
if ([tags rangeOfString:#",A,"].location != NSNotFound){
selectedLinkURL = tagsHREF;
sheet.title = tagsHREF;
[sheet addButtonWithTitle:#"Open"];
[sheet addButtonWithTitle:#"Copy"];
}
if (sheet.numberOfButtons > 0) {
[sheet addButtonWithTitle:#"Cancel"];
sheet.cancelButtonIndex = (sheet.numberOfButtons-1);
[sheet showInView:webView];
}
[selectedLinkURL retain];
[selectedImageURL retain];
[sheet release];
}
(NOTES: selectedLinkURL and selectedImageURL are declared in the .h file to let them be accessed throughout the class, for saving or opening the link latter.
So far we've just been going back over the tutorials code making changes but now we will move into what the tutorial doesn't cover (it stops before actually mentioning how to handle saving the images or opening the links).
To handle the users choice we now need to add the actionSheet:clickedButtonAtIndex: method.
-(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:#"Open"]){
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:selectedLinkURL]]];
}
else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:#"Copy"]){
[[UIPasteboard generalPasteboard] setString:selectedLinkURL];
}
else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:#"Copy Image"]){
[[UIPasteboard generalPasteboard] setString:selectedImageURL];
}
else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:#"Save Image"]){
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(saveImageURL:) object:selectedImageURL];
[queue addOperation:operation];
[operation release];
}
}
This checks what the user wants to do and handles /most/ of them, only the "save image" operation needs another method to handle that. For the progress I used MBProgressHub.
Add an MBProgressHUB *progressHud; to the interface declaration in the .h and set it up in the init method (of whatever class you're handling the webview from).
progressHud = [[MBProgressHUD alloc] initWithView:self.view];
progressHud.customView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Tick.png"]] autorelease];
progressHud.opacity = 0.8;
[self.view addSubview:progressHud];
[progressHud hide:NO];
progressHud.userInteractionEnabled = NO;
And the -(void)saveImageURL:(NSString*)url; method will actually save it to the image library.
(A better way would be to do the download through an NSURLRequest and update the progress hud in MBProgressHUDModeDeterminate to deflect how long it'll actually take to download, but this is a more hacked together implementation then that)
-(void)saveImageURL:(NSString*)url{
[self performSelectorOnMainThread:#selector(showStartSaveAlert) withObject:nil waitUntilDone:YES];
UIImageWriteToSavedPhotosAlbum([UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:url]]], nil, nil, nil);
[self performSelectorOnMainThread:#selector(showFinishedSaveAlert) withObject:nil waitUntilDone:YES];
}
-(void)showStartSaveAlert{
progressHud.mode = MBProgressHUDModeIndeterminate;
progressHud.labelText = #"Saving Image...";
[progressHud show:YES];
}
-(void)showFinishedSaveAlert{
// Set custom view mode
progressHud.mode = MBProgressHUDModeCustomView;
progressHud.labelText = #"Completed";
[progressHud performSelector:#selector(hide:) withObject:[NSNumber numberWithBool:YES] afterDelay:0.5];
}
And of cause add [progressHud release]; to the dealloc method.
Hopefully this shows you how to add some of the options to a webView that apple left out.
Of cause though you can add more things to this like a "Read Later" option for instapaper or a "Open In Safari" button.
(looking at the length of this post I'm seeing why the original tutorial left out the finial implementation details)
Edit: (updated with more info)
I was asked about the detail I glossed over at the top, the #"tapAndHoldShortNotification", so this is clarifying it.
This is my UIWindow subclass, it adds the second notification to cancel the default selection menu (this is because when I tried the tutorial it showed both menus).
- (void)tapAndHoldAction:(NSTimer*)timer {
contextualMenuTimer = nil;
UIView* clickedView = [self hitTest:CGPointMake(tapLocation.x, tapLocation.y) withEvent:nil];
while (clickedView != nil) {
if ([clickedView isKindOfClass:[UIWebView class]]) {
break;
}
clickedView = clickedView.superview;
}
if (clickedView) {
NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:tapLocation.x],#"x",
[NSNumber numberWithFloat:tapLocation.y],#"y",nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"TapAndHoldNotification" object:coord];
}
}
- (void)tapAndHoldActionShort:(NSTimer*)timer {
UIView* clickedView = [self hitTest:CGPointMake(tapLocation.x, tapLocation.y) withEvent:nil];
while (clickedView != nil) {
if ([clickedView isKindOfClass:[UIWebView class]]) {
break;
}
clickedView = clickedView.superview;
}
if (clickedView) {
NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:tapLocation.x],#"x",
[NSNumber numberWithFloat:tapLocation.y],#"y",nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"TapAndHoldShortNotification" object:coord];
}
}
- (void)sendEvent:(UIEvent *)event {
NSSet *touches = [event touchesForWindow:self];
[touches retain];
[super sendEvent:event]; // Call super to make sure the event is processed as usual
if ([touches count] == 1) { // We're only interested in one-finger events
UITouch *touch = [touches anyObject];
switch ([touch phase]) {
case UITouchPhaseBegan: // A finger touched the screen
tapLocation = [touch locationInView:self];
[contextualMenuTimer invalidate];
contextualMenuTimer = [NSTimer scheduledTimerWithTimeInterval:0.8 target:self selector:#selector(tapAndHoldAction:) userInfo:nil repeats:NO];
NSTimer *myTimer;
myTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(tapAndHoldActionShort:) userInfo:nil repeats:NO];
break;
case UITouchPhaseEnded:
case UITouchPhaseMoved:
case UITouchPhaseCancelled:
[contextualMenuTimer invalidate];
contextualMenuTimer = nil;
break;
}
} else { // Multiple fingers are touching the screen
[contextualMenuTimer invalidate];
contextualMenuTimer = nil;
}
[touches release];
}
The notification is then handled like this:
// in -viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(stopSelection:) name:#"TapAndHoldShortNotification" object:nil];
- (void)stopSelection:(NSNotification*)notification{
[webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.style.webkitTouchCallout='none';"];
}
It's only a little change but it fixes the annoying little bug where you get 2 menus appear (the standard one and yours).
Also you could easily add iPad support by sending the touches location as the notification fires and then showing the UIActionSheet from that point, though this was written before the iPad so doesn't include support for that.
After struggling for, like 2 or 3 days non-stop on this problem, it seems like the position is computed "relatively" to the UIWebView's "TOP-LEFT" corner (I am programing for iOS 7).
So, to make this work, when you get the position, on the controller where your WebView is (i'll put a snippet of my code below), don't add the "scroll-offset"
SNIPPET - ContextualMenuAction:
- (void)contextualMenuAction:(NSNotification*)notification {
// Load javascript
[self loadJavascript];
// Initialize the coordinates
CGPoint pt;
pt.x = [[[notification object] objectForKey:#"x"] floatValue];
pt.y = [[[notification object] objectForKey:#"y"] floatValue];
// Convert point from window to view coordinate system
pt = [self.WebView convertPoint:pt fromView:nil];
// Get PAGE and UIWEBVIEW dimensions
CGSize pageDimensions = [self.WebView documentSize];
CGSize webviewDimensions = self.WebView.frame.size;
/***** If the page is in MOBILE version *****/
if (webviewDimensions.width == pageDimensions.width) {
}
/***** If the page is in DESKTOP version *****/
else {
// convert point from view to HTML coordinate system
CGSize viewSize = [self.WebView frame].size;
// Contiens la portion de la page visible depuis la webview (en fonction du zoom)
CGSize windowSize = [self.WebView windowSize];
CGFloat factor = windowSize.width / viewSize.width;
CGFloat factorHeight = windowSize.height / viewSize.height;
NSLog(#"factor: %f", factor);
pt.x = pt.x * factor; // ** logically, we would add the offset **
pt.y = pt.y * factorHeight; // ** logically, we would add the offset **
}
NSLog(#"x: %f and y: %f", pt.x, pt.y);
NSLog(#"WINDOW: width: %f height: %f", [self.WebView windowSize].width, [self.WebView windowSize].height);
NSLog(#"DOCUMENT: width: %f height: %f", pageDimensions.width, pageDimensions.height);
[self openContextualMenuAt:pt];
}
SNIPPET - in openContextualMenuAt:
To load the correct JS function:
- (void)openContextualMenuAt:(CGPoint)pt {
// Load javascript
[self loadJavascript];
// get the Tags at the touch location
NSString *tags = [self.WebView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"getHTMLTagsAtPoint(%li,%li);",(long)pt.x,(long)pt.y]];
...
}
SNIPPET - in JSTools.js:
This is the function I use to get the element touched
function getHTMLTagsAtPoint(x,y) {
var tags = ",";
var element = document.elementFromPoint(x,y);
while (element) {
if (element.tagName) {
tags += element.tagName + ',';
}
element = element.parentNode;
}
return tags;
}
SNIPPET - loadJavascript
I use this one to inject my JS code in the webview
-(void)loadJavascript {
[self.WebView stringByEvaluatingJavaScriptFromString:
[NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"JSTools" ofType:#"js"] encoding:NSUTF8StringEncoding error:nil]];
}
This part (everything I did to overrride the default UIActionSheet) is HEAVILY (should I say completely) based on
this post
#Freerunning's answer is complete (i did almost everything he said in my other classes, like on the post my code is based on), the snippets i posted is just to show you more "completely" how my code is.
Hope this helps! ^^
First of all thanks to Freerunnering for the great solution!
But you can do this with an UILongPressGestureRecognizer instead of a custom LongPressRecognizer. This makes things a bit easier to implement:
In the Viewcontroller Containing the webView:
Add UIGestureRecognizerDelegate to your ViewController
let mainJavascript = "function MyAppGetHTMLElementsAtPoint(x,y) { var tags = \",\"; var e = document.elementFromPoint(x,y); while (e) { if (e.tagName) { tags += e.tagName + ','; } e = e.parentNode; } return tags; } function MyAppGetLinkSRCAtPoint(x,y) { var tags = \"\"; var e = document.elementFromPoint(x,y); while (e) { if (e.src) { tags += e.src; break; } e = e.parentNode; } return tags; } function MyAppGetLinkHREFAtPoint(x,y) { var tags = \"\"; var e = document.elementFromPoint(x,y); while (e) { if (e.href) { tags += e.href; break; } e = e.parentNode; } return tags; }"
func viewDidLoad() {
...
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(CustomViewController.longPressRecognizerAction(_:)))
self.webView.scrollView.addGestureRecognizer(longPressRecognizer)
longPressRecognizer.delegate = self
...
}
func longPressRecognizerAction(sender: UILongPressGestureRecognizer) {
if sender.state == UIGestureRecognizerState.Began {
let tapPostion = sender.locationInView(self.webView)
let tags = self.webView.stringByEvaluatingJavaScriptFromString("MyAppGetHTMLElementsAtPoint(\(tapPostion.x),\(tapPostion.y));")
let href = self.webView.stringByEvaluatingJavaScriptFromString("MyAppGetLinkHREFAtPoint(\(tapPostion.x),\(tapPostion.y));")
let src = self.webView.stringByEvaluatingJavaScriptFromString("MyAppGetLinkSRCAtPoint(\(tapPostion.x),\(tapPostion.y));")
print("tags: \(tags)\nhref: \(href)\nsrc: \(src)")
// handle the results, for example with an UIDocumentInteractionController
}
}
// Without this function, the customLongPressRecognizer would be replaced by the original UIWebView LongPressRecognizer
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
And thats it!