I'm trying to build an overlay that appears over an AVPlayer when I press pause. The problem I'm having is that while I can overlay buttons just fine I can't get the focus engine in tvOS to focus on them... I'm pretty sure this is happening as the player frame robs focus (it's fullscreen # 1080p) and I can't focus on objects that are inside of its frame.
Pseudo code for what I'm doing:
class MyViewController: UIViewController {
var playerController = AVPlayerViewController()
#IBOutlet weak var button: UIButton! // button centred on screen that requires focus
override func viewDidLoad() {
super.viewDidLoad()
self.playerController.player = AVPlayer(url: myURL)
self.playerController.view.frame = self.view.bounds
self.view.addSubview(self.playerController.view)
self.view.bringSubview(toFront: self.button)
}
// ... functions that enable listeners and show/hide button when self.playerController.player rate changes
}
What I want is to be able to press pause, then swipe up to get focus on the button(s) instead of the seek bar and then swipe back down to the seek bar if needed. Much like the Apple Music application that comes with the tvOS.
Thanks.
You can set desired custom vc with action items to var customOverlayViewController: UIViewController? { get set } of your AVPlayerViewController instance.
(available from tvOS 13)
Official documentation states:
When the transport bar is hidden, the player view controller presents
the overlay view controller’s view when a user swipes up on the Siri
Remote during playback. Use this property to set a custom view
controller instead of installing your own swipe gesture recognizer.
https://developer.apple.com/documentation/avkit/avplayerviewcontroller/3229856-customoverlayviewcontroller
You can also check this video starting from 26:07 to see it in action.
https://developer.apple.com/videos/play/wwdc2019/503
P.S. Don't use var contentOverlayView: UIView? { get } to display interactable content, as official documentation states:
Use the content overlay view to add noninteractive custom views, such
as a logo or watermark, between the video content and the controls.
https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615835-contentoverlayview
My solution is to override preferredFocusEnvironment and add some listeners to the parent VC to change a condition when certain actions are taken...not perfect but good enough.
override var preferredFocusEnvironment {
if self.someCondition {
return [self.button]
}
else {
return [self.playerController.view]
}
}
Related
How do I programmatically hide or show the START SESSION button in a scene's view controller that navigates to the next scene?
create an outlet to your button in the view controller class:
#IBOutlet weak var startButton: UIButton!
and then in whichever function you want you can show hide it, I assume you want it hidden by default So in view did load you can do
override func viewDidLoad()
{
startButton.isHidden = true
}
and then show it somewhere else
func doSomethingAndShowButton()
{
// Do some other stuff
...
// Show the button
startButton.isHidden = false
}
#Doug Null To connect the #IBOutlet. First, follow what #AngrayDuck said. After that do as following:
Go to the storyboard.
Open "Connection inspector" on the right side. Can open through shortcut "Command + Option + 7".
Follow the steps highlighted in the below image.
Hope this helps.
Is there a way in Swift to force the iOS keyboard to not have the top part? By saying the top part I mean the autocomplete and the input field switcher tools that appear at the top.
Some of my views have embedded webViews that run local js and I want the keyboard for the inputs in webView to not have that top part of the keyboard. If it`s not possible to disable these for webView specifically, any other method should be fine as well.
Please take a look at this screenshot to see exactly what part of the keyboard I am talking about.
You can try running the below JS in the webview every time you load the web page
var textFields = document.getElementsByTagName('input');
if (textFields) {
var i;
for( i = 0; i < textFields.length; i++) {
var txtField = textFields[i];
if(txtField) {
txtField.setAttribute('autocomplete','off');
txtField.setAttribute('autocorrect','off');
txtField.setAttribute('autocapitalize','off');
txtField.setAttribute('spellcheck','false');
}
}
}
Additionally write this code to hide the done button accessory view from keyboard
class CustomWebView: WKWebView {
var accessoryView: UIView?
override var inputAccessoryView: UIView? {
return accessoryView
}
}
And use CustomWebView in place of WKWebView wherever this functionality is needed.
Let me know if you need any more help.
Happy Coding :)
I'm working on Apple TV project. The project contains tab bar view controller, normally the tab bar will be appeared when swiping up on remote and hidden when swiping down. But now I reverse that behavior and I want to force focus another view when swiping up(normally focus on tab bar). Any way to do that? Thank you.
In your UIViewController, override shouldUpdateFocusInContext. If you detect an upward navigation into the tab bar, return false to prevent focus from reaching the tab bar. Then use a combination of preferredFocusEnvironments + setNeedsFocusUpdate to redirect focus somewhere else:
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
if let nextView: UIView = context.nextFocusedView{
if ( context.focusHeading == .up && nextView.isDescendant(of: tabBar) ){
changeFocusTo(myView)
return false
}
}
}
internal var viewToFocus: UIView?
func changeFocusTo(_ view:UIView? ){
viewToFocus = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments: [UIFocusEnvironment]{
return viewToFocus != nil ? [viewToFocus!] : super.preferredFocusEnvironments
}
This is a generally useful technique for customizing focus updates. An alternative technique is to use UIFocusGuide. You could insert a focus guide underneath the tab bar or surround the tab bar with a focus guide to redirect focus. Though focus guides are useful for simple cases, I have generally had better results using the technique I am describing instead.
I got the same issue with focus of UITabbarController before and I found the solution in Apple Support
Because UIViewController conforms to UIFocusEnvironment, custom view
controllers in your app can override UIFocusEnvironment delegate
methods to achieve custom focus behaviors. Custom view controllers
can:
Override the preferredFocusedView to specify where focus should start
by default. Override shouldUpdateFocusInContext: to define where focus
is allowed to move. Override
didUpdateFocusInContext:withAnimationCoordinator: to respond to focus
updates when they occur and update your app’s internal state. Your
view controllers can also request that the focus engine reset focus to
the current preferredFocusedView by callingsetNeedsFocusUpdate. Note
that calling setNeedsFocusUpdate only has an effect if the view
controller contains the currently focused view.
For more detail, please check this link
https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/WorkingwiththeAppleTVRemote.html#//apple_ref/doc/uid/TP40015241-CH5-SW14
I'm creating a simple game with a pause button. I want to show another view controller with the pause screen without resetting the game view controller when i return. Every thing i have tried, has resulted in the game view controller being reset, when i press the resume button and return to the game.
Does anybody know how i can do this without resetting the game?
It's a sprite kit and swift iOS app in xcode.
You can show a pop-up based view. You can create .xib and design your pause view as you wish. Then you can resume the game without resetting anything.
Sample code:
// Set Pause Page
var pauseView = PauseView.loadFromNib() // need an extension
#IBAction func Pause(sender: UIButton) {
pauseView.btnResume.addTarget(self, action: #selector(resume(_:)), forControlEvents: .TouchUpInside)
// set place on view
view.addSubview(pauseView)
}
func resume(sender:UIButton) {
pauseView.removeFromSuperview()
}
I created a demo for you:
Full demo available on Github: Get Source Code
Create a variable in your original view controller:
var shouldReset = true
Then, when you perform a segue back, add this in your prepareForSegue:
let vc = destination as! //Your view controller
vc.shouldReset = false
In your viewDidLoad (or wherever you reset your data):
if shouldReset == true {
//Reset
}
I'm making a multiple choice quiz game, and my goal right now is to have four buttons that refresh by spinning around with new answer choices. I think that means I need a subview that animates and re-populates with new buttons--if that's incorrect or not best, please stop me here.
At any rate, I created the subview in my storyboard, put the buttons inside it (background is blue just to see it now):
I dragged that over to my ViewController to make an IBOutlet (buttonContainer) and added this code to my ViewDidLoad:
view.addSubview(buttonContainer)
let buttonTap = UITapGestureRecognizer(target:self, action: Selector("checkAnswer"))
buttonTap.numberOfTapsRequired = 1
buttonContainer.addGestureRecognizer(buttonTap)
buttonContainer.userInteractionEnabled = true
However: When I run it in the simulator, the blue background does not appear at all, but the buttons are still disabled.
Before creating the subview, both the buttons and the function (checkAnswer) they called all worked perfectly.
You don't need any of this code if you are creating everything in storyboard. Just create a new class for the containerview and connect the buttons as an outlet collection.
For example, your button container class might look something like this:
class ButtonContainerView: UIView {
#IBOutlet var answerButtons: [UIButton]!
func rotateButtons() {
for button in answerButtons {
var context = UIGraphicsGetCurrentContext()
UIView.beginAnimations(nil, context: &context)
UIView.setAnimationCurve(UIViewAnimationCurve.Linear)
UIView.setAnimationDuration(5.0)
button.transform = CGAffineTransformRotate(button.transform, CGFloat(M_PI))
UIView.commitAnimations()
}
}
}