MPMoviePlayerController adding UIButton to view that fades with controls - objective-c

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.

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 make blur effect when view controller slides out

I've got a program with slide out menu. There's Menu button(BarButtonItem) in the right position. When the view controller loads I'm doing next
_menuBarButton.target = self.revealViewController;
_menuBarButton.action = #selector(revealToggle:);
So, when I click on the this button the View Controller slides out to right and I see another View Controller.
So, I want to make the main view controller blur when it slides out. I've got a code how to do it blur, but I can't implement this code because when I tap on the bat button it runs revealToggle: selector.
I've tried:
1. To set action for bar button. And firstly blur view controller:
- (IBAction)menuBarButtonTapped:(UIBarButtonItem *)sender {
[self setBlurEffect];
_menuBarButton.target = self.revealViewController;
_menuBarButton.action = #selector(revealToggle:);
[self.menuBarButton.target performSelector:#selector(revealToggle:)];
}
But app crashes with "unrecognized selector" (the solutions of this problem doesn
t help too).
I've wanted to use willDisapear method but it doesn't run, because the main view controller doesn't disapear. It just slides out.
So, could you help me with this problem?
P.S. I'll be happy if you propose any other effects for main view controller except blur.
P.P.S. Sorry for many mistakes in question.
I've found a good answer for me. I don't use several ViewControllers to implement menu panel.
Now I'm using another UIView calls menuPanel. It contains tableView(UITableView) and blurView(UIView). The second one is under first one.
You can blur this view in to ways:
From the code using next method.
(void) setBlurEffect {// Add blur view
CGRect boundsView = self.someView.bounds;
UIVisualEffectView *tableViewVisualEffect = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.tableViewVisualEffect.frame = boundsView;
self.tableViewVisualEffect.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableViewVisualEffect.backgroundColor = [UIColor clearColor];
[UIView transitionWithView:self.view
duration:0.3
options:UIViewAnimationOptionTransitionCrossDissolve
animations: ^ {
[self.someView addSubview:self.tableViewVisualEffect];
}
completion: nil ];
// Here you can add visual effects to any UIView control.
// Replace custom view with navigation bar in above code to add effects to custom view.
}
Where someView is view you want to do blur(or any other effect)
From the story board with Visual Effect View.(I've chosen)
After all I've set tableView and blurView into menuPanelView as pinned to all sides of the menuPanel with 0 distance.
And the last thing I've done I'm changing the position of the menuPanelView with animation:
[self.menuPanelView setFrame:self.visibleTableViewFramePosition];
[UIView animateWithDuration:0.8
animations:^{
[self.menuPanelView setFrame:self.invisibleTableViewFramePosition];
} completion:nil];
Where invisibleTableViewFramePosition is CGRect variable contains position of the menuPanelView when it has to be invisible. You also need to create visibleTableViewFramePosition.
That's all. Hope it may be helpful for someone=)

Flipping a view only works when hiding, but not when showing

I'm using the iCarousel library to show a coverflow-like UI. Animating the iCarousel subviews directly tends to blow up, so I create a wrapper for my animation and stick a subview inside of it which looks like the tapped cover. Then I hide the actual iCarousel view, and animate the fake cover on top of it.
I'm using UIView's transitionWithView:duration:options:animations:completion: method, but I'm running into some trouble. When animating another view onscreen for the first time, the view appears without any animation. When animating out, the view correctly flips and hides.
My view hierarchy is as follows:
main view, loaded from a nib
wrapper view
view to transition from
view to transition to
The view I'm transitioning to is a UINavigationController's view which contains a UITableViewController subclass. Instead of the initial animation, the UINavigationController appears and then the view grows upward a little, as if it's taking over the space otherwise occupied by a status bar.
Any idea why the table view might be animating like this? (I suspect containment APIs and/or wantsFullscreen, although I'm not explicitly using them. I simply install the views into the wrapper via addSubview:.)
Here's my "flip in" code, that animates every time but the first:
- (void) flipInWithCompletion:(MBTransitionCompletion)completion {
BOOL displayingPrimary = [self isDisplayingPrimaryView];
UIView *frontView = [self frontView];
UIView *backView = [self backView];
UIView *wrapperView = [self wrapperView];
[wrapperView addSubview:frontView];
[UIView transitionWithView:wrapperView
duration:0.8
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^{
[wrapperView addSubview:backView];
[frontView removeFromSuperview];
}
completion:^(BOOL finished) {
[self setIsDisplayingPrimaryView:!displayingPrimary];
if (completion) {
completion();
}
}
];
}
What might cause the table view to grow instead of allowing the wrapper to flip?
Edit:
I've made a video demoing the exact problem.
Sounds like the view isn't properly loaded before it starts to animate in. I think I remember having a similar problem before. Try adding it to your view first, then remove it, and try to animate it into place to see if that fixes it.
That's not really a solution though, but it should give you a clue as to what might be wrong.

How to prevent manual zooming in a UIScrollView

Hopefully someone can help with this issue. I have a class derived from UIScrollView and I'd like to prevent the user from being able to zoom or scroll via manual pinch and swipe gestures. All view navigation will instead be controlled by programmatic means in response to where a user taps (think of an ebook reader where tapping on the left or right sides of the display causes the view to scroll by exactly one page width). Any suggestions on how to implement this?
On your - (void)viewDidLoad; you should be able to just disable whatever gesture recognizer you want. In this case:
UIPinchGestureRecognizer *pinchRecognizer = self.pinchGestureRecognizer;
pinchRecognizer.enabled = NO;
or
UIPanGestureRecognizer *panRecognizer = self.scrollView.panGestureRecognizer;
panRecognizer.enabled = NO;
I sometimes do this from view controllers that contain UIScrollViews. I just target the scroll view (self.scrollView.pinchGestureRecognizer) and temporarily disable gestures when the app. is in a certain state.
To prevent user-controller zooming and panning but still allow programmatic zooming and panning of a scrollview, the best approach is to override the UIScrollView's -addGestureRecognizer: method in a subclass.
In my use I wanted to block all the recognizers and control the viewable area completely from my view controller, I did so like this:
-(void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
//Prevent any of the default panning and zooming controls from working
gestureRecognizer.enabled = NO;
[super addGestureRecognizer:gestureRecognizer];
}
Each gesture recognizer is simply disabled, for finer control (allowing the pan control but only allow zooming via a double tap for instance) you'd simply check the incoming gesture recognizer via -isKindOfClass: and disabling as appropriate.
-(void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
//Prevent zooming but not panning
if ([gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]]) {
gestureRecognizer.enabled = NO;
}
[super addGestureRecognizer:gestureRecognizer];
}
I used this method in a comic reading app that uses guided navigation to animate between cropped panels on a page with the full page being contained in a UIScrollView.I can smoothly zoom in and out on a selected area by simply setting the view bounds to the region I want to display.
A quick note here. It seems UIScrollView's panGestureRecognizer and pinchGestureRecognizer are both enabled the first time a view controller is added to a window.
Basically what that means is setting them to enabled = NO in viewDidLoad won't work in some cases. I moved my enabled = NO to viewWillAppear: and it stuck. :)
I don't have too much experience with UIScrollViews, but looking at the docs, it looks like you can set maximumZoomScale, minimumZoomScale, and scrollEnabled to disable everything you want to disable.
Here are the docs: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIScrollView_Class/Reference/UIScrollView.html
From the docs:
scrollEnabled:
If the value of this property is YES ,
scrolling is enabled, and if it is NO
, scrolling is disabled. The default
is YES.
When scrolling is disabled, the scroll
view does not accept touch events; it
forwards them up the responder chain.
maximumZoomScale:
This value determines how large the
content can be scaled. It must be
greater than the minimum zoom scale
for zooming to be enabled. The default
value is 1.0.
In your UIScrollView subclass overwrite also the setZoomScale: method which automatically re-disables the gesture
- (void)setZoomScale:(CGFloat)zoomScale {
[super setZoomScale:zoomScale];
self.pinchGestureRecognizer.enabled = NO;
}

Why doesn't UIView.exclusiveTouch work?

In one of my iPhone projects, I have three views that you can move around by touching and dragging. However, I want to stop the user from moving two views at the same time, by using two fingers. I have therefore tried to experiment with UIView.exclusiveTouch, without any success.
To understand how the property works, I created a brand new project, with the following code in the view controller:
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 460)];
UIButton* a = [UIButton buttonWithType:UIButtonTypeInfoDark];
[a addTarget:self action:#selector(hej:) forControlEvents:UIControlEventTouchUpInside];
a.center = CGPointMake(50, 50);
a.multipleTouchEnabled = YES;
UIButton* b = [UIButton buttonWithType:UIButtonTypeInfoDark];
[b addTarget:self action:#selector(hej:) forControlEvents:UIControlEventTouchUpInside];
b.center = CGPointMake(200, 50);
b.multipleTouchEnabled = YES;
a.exclusiveTouch = YES;
[self.view addSubview:a];
[self.view addSubview:b];
}
- (void)hej:(id)sender
{
NSLog(#"hej: %#", sender);
}
When running this, hej: gets called, with different senders, when pressing any of the buttons - even though one of them has exclusiveTouch set to YES. I've tried commenting the multipleTouchEnabled-lines, to no avail. Can somebody explain to me what I'm missing here?
Thanks,
Eli
From The iPhone OS Programming Guide:
Restricting event delivery to a single view:
By default, a view’s exclusiveTouch property is set to NO. If you set
the property to YES, you mark the view so that, if it is tracking
touches, it is the only view in the window that is tracking touches.
Other views in the window cannot receive those touches. However, a
view that is marked “exclusive touch” does not receive touches that
are associated with other views in the same window. If a finger
contacts an exclusive-touch view, then that touch is delivered only if
that view is the only view tracking a finger in that window. If a
finger touches a non-exclusive view, then that touch is delivered only
if there is not another finger tracking in an exclusive-touch view.
It states that the exclusive touch property does NOT affect touches outside the frame of the view.
To handle this in the past, I use the main view to track ALL TOUCHES on screen instead of letting each subview track touches. The best way is to do:
if(CGRectContainsPoint(thesubviewIcareAbout.frame, theLocationOfTheTouch)){
//the subview has been touched, do what you want
}
I was encountering an issue like this where taps on my UIButtons were getting passed through to a tap gesture recognizer that I had attached to self.view, even though I was setting isExclusiveTouch to true on my UIButtons. Upon reviewing the materials here so far, I decided to put some code in my tap gesture code that checks if the tap location is contained in any UIButton frame and if that frame is also visible on the screen at the same time. If both of those conditions are true, then the UIButton will already have handled the tap, and the event triggered in my gesture recognizer can then be ignored as a pass through of the event. My logic allows me to loop over all subviews, checking if they are of type UIButton, and then checking if the tap was in that view and the view is visible.
#objc func singleTapped(tap: UITapGestureRecognizer)
{
anyControlsBreakpoint()
let tapPoint = tap.location(in: self.view)
// Prevent taps inside of buttons from passing through to singleTapped logic
for subview in self.view.subviews
{
if subview is UIButton {
if pointIsInFrameAndThatFrameIsVisible(view: subview, point: tapPoint)
{
return // Completely ignores pass through events that were already handled by a UIButton
}
}
}
Below is the code that checks if point was inside a visible button. Note that I hide my buttons by setting their alpha to zero. If you are using the isHidden property, your logic might need to look for that.
func pointIsInFrameAndThatFrameIsVisible(view : UIView, point : CGPoint) -> Bool
{
return view.frame.contains(point) && view.alpha == 1
}