With UIButton, tvOS produces UIControlEvents.PrimaryActionTriggered (not .TouchUpInside, as it does on iOS) when the button gets focus and the user taps on the remote.
UIControl's don't seem to produce either of these events, however. I can't see that it produces any events, in fact, when the user focuses on the control and taps on the remote.
How does one use a custom UIControl for tvOS?
A UIControl doesn't emit any control events of its own accord. Your subclass is responsible for emitting events (generally by sending itself sendActionsForControlEvents:).
Since UIControl isn't currently documented for tvOS, it might be that Apple doesn't want you to subclass it.
Anyway, I've only played around with it a little, but apparently to implement your own UIControl subclass, you must override canBecomeFocused to return YES, and you must override pressesEnded:withEvent: to act on the user's press (presumably by sending yourself sendActionsForControlEvents:.).
You might also want to override pressesBegan:withEvent: to highlight your control.
Because UIControl conforms to the UIFocusEnvironment protocol (by way of UIView), you can override didUpdateFocusInContext:withAnimationCoordinator: to update your control's appearance depending on whether it has focus.
I spent the last couple weeks adding tvOS support for the widgets I package with my SVG rendering library SVGgh, so I have somewhat of a feeling for this. The issue is that there is what I think is a new or newish kind of action: UIControlEventPrimaryActionTriggered that gets sent out when the control wants to communicate a commitment of action, as in this response to the completion of a UIPress in my GHButton class. Remember to wire up a selector to this new action in your storyboard.
-(void) pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
{
[self sendActionsForControlEvents:UIControlEventPrimaryActionTriggered];
self.beingPressed = NO;
self.selected = self.selected;
}
But in a segmented control analog, the action isn't set on button push, but when the focused segment gets changed, as in the following (heavily edited for clarity) code from my GHSegmentedControl class:
- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
{
BOOL result = NO;
if(context.nextFocusedView == self)
{
result = YES;
[self highlight:YES];
if(context.focusHeading == UIFocusHeadingRight)
{
[self incrementValue];
[self.parentContent.control setNeedsFocusUpdate];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
else if(context.focusHeading == UIFocusHeadingLeft)
{
[self decrementValue];
[self.parentContent.control setNeedsFocusUpdate];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
return result;
}
Now that I think of it, it's possible I should be sending out a UIControlEventPrimaryActionTriggered action in the segmented control as well but UIControlEventValueChanged seems to work the way I expect.
Related
I try to add a skip-intro-button (like Netflix) to the AVPlayerViewController of a tvOS movie app. I added it as subview to the contentOverlayView and when I let it appear I force giving it the focus with preferredFocusEnvironments. Everything's fine...
Until the user navigates somewhere else (e.g. seek bar or video asset info view). The button then loses focus (expected) and never can be focused again by user interaction.
I tried to:
add a UIFocusGuide()
put my button directly on the AVPlayerViewController's view
add a own subview, which contains the buttons to the AVPlayerViewController's view
add a own subview, which contains the buttons to the contentOverlayView
add several other buttons next to, below, above my button on the same subview (for each of the cases above)
The last approach shows that none of the other buttons can ever get focus by user interaction, so it seems, that, for the same reason, the skip-intro-button cannot be focused by the user. But what is this reason?
What is the right practice to add custom interaction elements to AVPlayerViewController?
Anyone any ideas?
Try to answer my main question "What is the right practice to add custom interaction elements to AVPlayerViewController?" myself:
Since I posted this question, 1.5 years ago, I didn't change the implementation of the skip intro feature very much. So whenever the user uses the remote, the button will lose focus and the only thing I changed is, that I hide the button whenever this happens, by implementing didUpdateFocus(in:with:), similar to this:
if let previouslyFocusedView = context.previouslyFocusedView {
if previouslyFocusedView == skipIntroButton {
changeSkipIntroButtonVisibility(to: 0.0)//animates alpha value
}
}
(I'm not completely sure, why I don't set it to isHidden = true)
However, in the meantime I had to implement a more complex overlay to our player, i.e. a "Start Next Video / Watch Credits" thing, with a teaser, some buttons, a countdown/progress bar and more. With the problems described above, it is obvious, that I couldn't go with the contentOverlayView approach.
So I decided to implement it the "traditional way", by presenting a complete UIViewController on top of the player's view, like this:
func showNextVideoOverlay() {
guard let nextVideoTeaser = nextVideoTeaser else { return }
let nextVideoOverlay = NextVideoAnnouncementViewController(withTeaser: nextVideoTeaser, player: player)
nextVideoOverlay.nextVideoAnnouncementDelegate = self
nextVideoOverlay.modalPresentationStyle = .overFullScreen
present(nextVideoOverlay, animated: true, completion: nil)
}
Of course the NextVideoAnnouncementViewController is transparent, so video watching is still possible. I turned out that this straight forward approach works pretty well and I really don't know, why I haven't thought about it, when implementing skip intro.
My colleagues in QA found one tricky thing, you should be aware of (and I think, I remember, that this is different with different Devices and different remotes and on different tvOS versions - try it):
The overlying view controller blocks most of the commands coming from the remote, respectively you can navigate inside the view controller without affecting the player - except the play/pause button. That one will pause the player, but it's not possible to resume from there. This is, because a playing (av)player always listens to this button, while a paused one doesn't. So I also had to implement something like this:
func addRemoteButtonRecognizer() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(playPauseButtonPressed))
tapRecognizer.allowedPressTypes = [NSNumber(value: UIPress.PressType.playPause.rawValue)]
self.view.addGestureRecognizer(tapRecognizer)
}
#objc func playPauseButtonPressed(sender: AnyObject) {
nextVideoAnnouncementDelegate?.remotePlayButtonPressed()
}
func remotePlayButtonPressed() {
if player?.playerStatus == .paused {
player?.play()
} else {
player?.pause()
}
}
I hope this helps some of you, who come by here and find no other answer.
in watchOS I used presentControllerWithName to show a View Controller and to pass the context in this way
presentControllerWithName("NameOfTheViewController", context:"PassedContext")
Which is the equivalent in tvOS?
Best Regards
As noted in other answers, the way to programmatically show another view controller in tvOS (or iOS) is performSegueWithIdentifier:sender:. (Or presentViewController:animated:completion: if you're not getting your VCs from a storyboard flow.)
But you might not need to do it programmatically. In watchOS it's sometimes easiest to do it that way, but in iOS & tvOS, it's common to make controls directly perform storyboard transitions entirely from Interface Builder. Just control-drag (right-click-drag) from the button to another view controller. (More step-by-step instructions in Xcode Help.)
Unlike watchOS, the view controller transitions in iOS & tvOS don't include a way to pass context information. Not as part of the API, at least — you have to include a bit of glue code yourself to do that. How to do that is a pretty common question.
If you're using storyboard segues (generally, you should), the prepareForSegue:sender: method is typically where you do this — you get a reference to the new view controller that's about to be shown, and use some function or property you've defined on that view controller to pass it some context. It often looks something like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == mySegueIdentifier {
guard let destination = segue.destinationViewController as? MyViewControllerClass
else { abort("unexpected storyboard segue") }
destination.someProperty = someValue
}
}
You can find good examples of this when you create a new Xcode project with the Master-Detail App template.
tvOS is more similar to iOS than it is to watchOS, although they all have some similarities. In tvOS (like in iOS) you can use both performSegueWithIdentifier:sender: or presentViewController:animated:completion: depending on your situation.
For more on this, you can check out the UIViewController class reference.
I have one swing code written by other person. For swing tabbed pane, he has added both change and container listener and both calls the same method:
addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent theEvent ) {
someMethod();
}
} );
addContainerListener(new ContainerAdapter() {
public void componentAdded(ContainerEvent theEvent) {
someMethod();
}
public void componentRemoved(ContainerEvent theEvent) {
someMethod();
}
} );
Whenever tab is removed from this tabbed pane, it internally calls JTabbedPane.removeTabAt(int index), which in turn calls fireStateChanged() causing new change event listened by change listener.
Now as new component (tab) is removed from tabbed pane, it also calls componentRemoved(ContainerEvent theEvent) method of container listener.
Both change even and container events, then calls same method someMethod(), which does set background and foreground colors.
I would like to know, if this kind code might cause some issues. Recently we are facing random IndexOutOfBoundException exeptions. I am just wondering, if this is causing this issue.
Also as per my understanding in swing, once event is listened, logic inside it should be executed using worker thread (e.g. SwingWorker). Please let me know if this is correct.
I am new to swing, thus any hint would be appreciated.
Thanks.
Whenever tab is removed from this tabbed pane, it internally calls
JTabbedPane.removeTabAt(int index), which in turn calls
fireStateChanged() causing new change event listened by change
listener.
This is true if the removed tab is also the selected tab. In the other cases, you won't be notified.
You need to choose what event you want to listen to:
Addition/Removal of components?--> go for ContainerListener
Selected tab? --> go for ChangeListener
I would like to know, if this kind code might cause some issues.
Recently we are facing random IndexOutOfBoundException exeptions. I am
just wondering, if this is causing this issue.
Since there is no line in your sample code that could throw that Exception, it is impossible to answer your question. Post an SSCCE that shows your issue.
Also as per my understanding in swing, once event is listened, logic
inside it should be executed using worker thread (e.g. SwingWorker).
Please let me know if this is correct.
It depends:
If you need to modify anything in the UI, anything related to Swing, it needs to be executed on the EDT (Event Dispatching Thread) and thus, SwingWorker is not an option.
If you need to perform business logic operations, and especially if they can be lengthy, then you should indeed use a SwingWorker or any other mechanism to execute that code in another thread than the EDT. Consider visiting the Swing tag wiki on "Concurrency"
I got a very interesting problem here. My iPhone app has an UITabbarController as rootViewController in the AppDelegate.
If the app is opened the first time, it must be configured basically. For this purpose I create an UINavigationController and tell the tabbarController to present it modally:
firstRun = [[firstRunViewController alloc] init];
navCtrl = [[UINavigationController alloc] initWithRootViewController:firstRun];
[[self tabBarController] presentModalViewController:navCtrl animated:NO];
When the configuration is done, I'd like to get rid of the firstRunViewController. I'm using this technique very often, using -dismissModalViewControllerAnimated:.
But in this constellation this doesn't work. It doesn't matter from what controller I'm calling the dismiss.
I tried it via the tabbarController, the rootViewController, the currently active viewController, of cause self and several other controllers.
EVERY TIME I call -dismissModalViewControllerAnimated: I get this exception:
'UIViewControllerHierarchyInconsistency', reason: 'presentedViewController for controller is itself on dismiss for: <UINavigationController:…
Can anybody help? Thanks in advance, with kind regards, Julian
EDIT
In my AppDelegate I'm using a UITabbarController as rootViewController for the main window:
self.window.rootViewController = self.tabBarController;
[self.window makeKeyAndVisible];
Then I'm creating an UINavigationController and tell the UITabbarController to present the modalViewController:
UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:firstRun];
[[self tabBarController] presentModalViewController:navCtrl animated:NO];
When I now call -dismissModalViewControllerAnimated: on the firstViewController I'm getting the error from above.
In my opinion you are abusing UITabbarController. This class, even though a subclass of UIViewController, does not really use much of the UIViewController infrastructure.
What you want is a slight extension of what you have now. Create a new UIViewController subclass in your appDelegate, and add it as the single object to an array, and set the tabBar's viewControllers to this array. Set your subclass' hidesBottomBarWhenPushed to YES so it hides the tab bar when it becomes visible.
Now your app will launch and your UIViewController subclass will become the frontmost view. You can make this view the one you wanted to present modally, or you can present that view from your subclass using some kind of animation. Oh, and if you use the launch view as the background image for your subclass, you can really make this a smooth transition - I do this now.
When your modal view is done, then you can instantiate whatever views you want to then display, and set the UITabBarController to use those views with tabBarController.viewControllers (or the animated version). Poof, you UIViewController will get replaces (and under ARC just disappear).
I don't have a chance to test my hypothesis, but I suspect that this issue could depend on the fact that you are presenting the modal view too early, whereby too early means before the main window has had the chance to set up the tab bar controller. So, I would suggest this changes:
create a method to instantiate your navigation controller:
- (void)initializeAndPresentNavigationController {
UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:firstRun];
[[self tabBarController] presentModalViewController:navCtrl animated:NO];
}
instead of presenting the navigation controller directly from appDidFinishLaunching, call the above method asynchronously:
[self performSelector:#selector(initializeAndPresentNavigationController) withObject:nil afterDelay:0.0];
Here the trick of calling the method as I do in 2 is that the call to initializeAndPresentNavigationController will be simply pushed on the main loop, and executed after your app has had the possibility to build its initial UI.
Hope it works for you.
I finally found the answer myself!
I just couldn't see the wood for the trees! I'm quite happy right now! :)
I did really silly things: In the last viewController of the setup viewControllers I had to change the tabars viewControllers corresponding to whether the user is administrator or not. So I did:
appDelegate.tabBarController.viewControllers = [NSArray arrayWithObjects:appDelegate.readState,
appDelegate.navCtrl,
appDelegate.settings, nil];
You can see that I was adding the AppDelegate's "navCtrl" to the tabbar's viewControllers. So I was trying to dismiss a viewController I just added to the parentViewControllers (UITabbarController) sub-controllers.
Dismissing something I want to present just in the same moment is NOT advisable! :))
I want to switch between 2 UIViewController classes programmatically without any extra UI control like a UITabBarController that adds a UI to the application.
My main loads the first view controller with addSubView.
vc1 = new viewc1();
window.AddSubview(vc1.View);
window.MakeKeyAndVisible ();
I can load my second viewcontroller from the first one with PresentModalViewController
vc2 = new viewc2();
PresentModalViewController(vc2, true);
but i need to switch back and forth, and to release the old viewControllers to save memory.
What is the best way to do this?
DismissModalViewControllerAnimated(false); in the 2nd view controller isnt releasing memory and I dont want modal "windows" as it doesnt seem optimal. I have a custom UI so the tabbar controller is not wanted.
You can do it in simple code. But you can't release the view controllers as it required to handle user interactions such as button tap events etc. Adding a view to window will only preserve view instance. If you release your view controller instance, you could get a bad access error or unrecognized selector error.
So let your main code be
if(vc1==nil)
vc1 = new viewC1();
window.addSubView(vc1.view);
window.MakeKeyAndVisible ();
And your switch code will be
if(vc2==nil)
vc2 = new viewC2();
if(vc1.view.superview!=nil){
vc1.view.removefromSuperView();
window.addsubview(vc2.view);
} else {
vc2.view.removeFromSuperView();
window.addsubview(vc1.view);
}
Now in dealloc method add
vc1.release();
vc2.release();
Thats it...Hope this helps...
I just followed your syntax
You can use a UINavigationController still, you don't need the extra UI that it provides. You can use the .Hidden property of the UINavigationBar to hide that. To switch between views you can just use PushViewController(controller, animated) to push the new view. If you want to release the old controller then you can just set the UINavigationController's .ViewControllers property using:
navigationController.ViewControllers = new UIViewController[1]{ vc2 };
this will remove the reference of the first controller and make the second controller the root. (this will also work the other way around!)