I have a collection view with a standard horizontal layout. Upon presenting a view controller and then dismissing it, the collection view reset focus back to the first cell, even though the last focused cell was not that one.
I've set
self.collectionView.remembersLastFocusedIndexPath = YES;
What's weird is that this only happens when I push a view controller on my navigation controller.
So if I do
[self.navigationController pushViewController:controller animated:YES];
and then dismiss, remembersLastFocusedIndexPath does not work properly.
However, if I:
[self presentViewController:controller animated:YES completion:nil];
Then it works as expected.
Any idea why it wouldn't work via a navigation controller?
What worked for me was ensuring that preferredFocusView on the viewController was the collection view:
override weak var preferredFocusedView: UIView? {
return collectionView
}
After this remembersLastFocusedIndexPath seems to work.
make sure that you don't reloadData() on viewWillAppear of your collectionView or TableView. Otherwise rememberedFocus will be reseted to default (for collection view in my case it was the centre visible cell)
By default, the system remembers the position. You should probably fix this issue by setting remembersLastFocusedIndexPath to false.
You could also implement the UICollectionViewDelegate method:
func indexPathForPreferredFocusedViewInCollectionView(collectionView: UICollectionView) -> NSIndexPath?
The documentation tells us:
The functionality of this delegate method is equivalent to overriding the UICollectionView class’s preferredFocusedView method in the UIFocusEnvironment protocol. If the collection view’s remembersLastFocusedIndexPath method is set to YES, this method defines the index path that gets focused when the collection view is focused for the first time.
But for your issue, setting remembersLastFocusedIndexPath to false should fix it.
You can also try to implement similar behaviour. Just remember last focused cell in
func collectionView(collectionView: UICollectionView, didUpdateFocusInContext context: UICollectionViewFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
and use this information in
func collectionView(collectionView: UICollectionView, canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool
For 2022, this is still a severe Apple bug.
Simply does not work:
navigationController?.pushViewController(details, animated: false)
Works perfectly:
present(details, animated: false)
There are no workarounds, currently:
It seems in the far past some workarounds would make it work. There are no workarounds presently. It is just "utterly broken".
Unfortunately, all you can do is file it under the usual heading "You must be joking, Apple."
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.
I'm using a WKWebview on my story board. I need a way for clicking on specific text to perform an action.
Assume I have a webkit added:
#IBOutlet weak var webkit: WKWebView!
And then later on I have:
webkit.loadHTMLString("Click here or there for different actions", baseURL: nil)
How could I set this up so that clicking on the word "here" would perform code of my choosing, and clicking on "there" would perform different code of my choosing.
It needs to be set up in a webkit like this because I need to use html (the above is just an example).
I had thought about trying to do this through linking. For example:
webkit.loadHTMLString("Click <a href=''>here</a> or there for different actions", baseURL: nil)
And then run something like I found in this question's answer:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == WKNavigationType.linkActivated {
print("run code")
decisionHandler(WKNavigationActionPolicy.cancel)
return
}
decisionHandler(WKNavigationActionPolicy.allow)
}
However this seems messy as I don't actually need website links. And this only works for one specific action. Clicking on any link would allow me to run some kind of code, but I want specific code for specific words.
Any guidance would be greatly appreciated!
I have implemented a custom viewController transition whereby I present a new ViewController as a pop-over. This resembles something like an alertViewController. I have written the transition handler and set my presenting viewController as the delegate and than preset this presented view controller like this:
#IBAction func pickOption(sender: UIButton) {
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewControllerWithIdentifier("OptionVC")
vc.modalPresentationStyle = .OverCurrentContext
vc.transitioningDelegate = self
self.presentViewController(vc, animated: true, completion: nil)
}
Here is where I am confused about proceeding with the layout. I wish to have this presented ViewController take up a small fraction of the frame of the presenting ViewController's View, preferably using Autolayout to set margin constraints. However, both the ViewController's in this are separate scenes in Interface Builder.
In the transition handler, the canonical thing to do seems to be adding the presented ViewController's view to a "context" view and handle animations there. Then call transitionContext.completeTransition(). Now that the presented ViewController's view is a subview of the presenting ViewController's view, I would like the autolayout constraints to be present but as I previously stated this isn't obvious how to do given both views are in separate scenes in IB.
Thanks in advance for any advice for guidance.
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 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! :))