tvOS how to check if the UITabBar is focused - ios

I'm looking for a way to make a custom transition when changing the focus from or to the UITabBar.
I'm currently trying this by overriding the didUpdateFocus method but I seem to be unable to check if the the tabBar is focused.
The tabBar itself seem to never be in the "focused" state:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinatora)
{
super.didUpdateFocus(in: context, with: coordinator)
print(self.tabBar.isFocused) // always false
}
When checking the UIFocusUpdateContext the currently focused element is a UITabBarButton.
But I'm unable to check if the context is an instance of UITabBarButton because that type isn't available:
context.nextFocusedItem is UITabBarButton // can't compile
I'm really stuck here and would love any suggestions on how to tackle this one.
Thanks.

This answer probably won't help the OP (it was asked more than a year ago), but as I was struggling with the same issue, a presentation from WWDC17 helped me figure it out:
UIView, UIViewController, UIWindow, etc. all conform to the UIFocusEnvironment protocol. The tabBar itself never actually receives direct focus, so the correct way to check if one of its buttons has focus is by checking if its environment contains the UIFocusItem:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let nextFocusedItem = context.nextFocusedItem,
self.tabBar.contains(nextFocusedItem) {
//Do some stuff
}
}
I'm sure the asker figured this out ages ago, but I thought I'd throw this answer up for anyone else that happens to have the same question later.

First add extension for UIView if it has superview:
public func hasSuperView(_ view: UIView) -> Bool {
if self == view {
return true
}
if let superview = superview {
return superview.hasSuperView(view)
}
return false
}
Then add extension for UIFocusUpdateContext to check if view has focus:
public func viewHasFocus(_ view: UIView) -> Bool {
guard let nextFocusedView = nextFocusedView else { return false }
return nextFocusedView.hasSuperView(view) || view.hasSuperView(nextFocusedView)
}
Then use context in this method to check if tabbar has focus:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.viewHasFocus(tabBar) {}
}
Also please override this method in your UITabBarController subclass

I have created extension on UIView which will check if there is focus somewhere in its hierarchy:
func containsFocus() -> Bool {
if self.isFocused { return true }
else {
for subview in self.subviews {
if subview.containsFocus() { return true }
}
return false
}
}
This way you can check if your UITabBar has focus.
Note:
This is very expensive way of checking if your view has focus, because it is recursive function and it will potentially traverse the whole hierarchy of your view.
Tip:
If you are trying to implement custom transition when selecting tabs in UITabBar you should use UITabBarDelegate method:
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem)
If not, just ignore this tip.

Related

CollectionView Touch is now working when placed on top of tabBar Swift

I have added a collectionView on top of a UITabBar but its touch is not working.The screeshot for the tabBar and collectionView
The code is attached below, I want the collectionView to be touchable. Here quickAccessView is the UIView that contains the collectionView. For constraints I'm using snapKit
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.bringSubviewToFront(quickAccessView)
}
private func setupQuickAccessView(){
print("this is tabBar's height", self.tabBar.frame.size.height)
self.tabBar.frame.size.height = 150
print("this is new tabBar's height", self.tabBar.frame.size.height)
self.tabBar.addSubview(quickAccessView)
quickAccessView.clipsToBounds = true
}
private func addQuickAccessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.right.left.equalTo(self.tabBar.safeAreaLayoutGuide)
make.height.equalTo(76)
make.bottom.equalTo(self.tabBar.snp.bottom).offset(-80)
}
}
this is after modification that Aman told
The UITabBarController
final class MainTabBarController: UITabBarController {
private lazy var quickAccessView: QuickAccessView = .fromNib()
var quickAccessSupportedTabBar: QuickAccessSupportedTabBar {
self.tabBar as! QuickAccessSupportedTabBar // Even code is crashing here
}
// Even code is crashing here
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.frame = self.quickAccessView.bounds
setupUI()
}
}
extension MainTabBarController{
private func setupUI(){
setupQuickAcessView()
addQuickAcessViewConstraints()
}
}
// MARK: - Setting Up Quick Access view
extension MainTabBarController {
private func setupQuickAcessView(){
self.quickAccessSupportedTabBar.addSubview(quickAccessView)
}
private func addQuickAcessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.left.right.equalTo(self.quickAccessSupportedTabBar.safeAreaLayoutGuide)
make.height.equalTo(66)
make.bottom.equalTo(self.quickAccessSupportedTabBar.snp.top)
}
}
}
the UItabBar and here it is throwing error and I too am confuse that how to access it and convert it to points
class QuickAccessSupportedTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
I think what may be happening here is that since you've added quickAccessView as tab bar's subview, it is not accepting touches. This would be so because the tabbar's hist test will fail in this scenario.
To get around this, instead of using a UITabBar, subclass UITabBar, let us call it ToastyTabBar for reference. See the code below:
class ToastyTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
Set this as the class of your tabBar (both in the storyboard and the swift file), and then see if that solves it for you.
You'll have to figure out a way to make quickAccessView accessible to the tabbar for the hit test check. I haven't advised on that above because I'm not familiar with your class hierarchy, and how and where you set stuff up, but this should be trivial.
If this solves it for you, please consider marking this as the answer, and if it does not then please share a little more info here about where you're having the problem.
Edit (for someone using a UITabBarController):
In response to your comment about "how to access UITabBar class from UITabBarController" here's how I would go about it.
I'm assuming you have a storyboard with the UITabBarController.
The first step (ignore this step if you already have a UITabBarController custom subclass) is that you need to subclass UITabBarController. Let us call this class ToastyTabBarController for reference. Set this class on the UITabBarController in your storyboard using the identity inspector pane in xcode.
The second step is to set the class of the UITabBar in your storyboard as ToastyTabBar (feel free to name it something more 'professional' 😊).
This is to be done in the same storyboard, in your UITabBarController scene itself. It will show the tabBar under your UITabBarController, and you can set the custom class on it using the identity inspector pane just like earlier.
The next step is to expose a computed property on your custom UITabBarController class, as shown below.
var toastyTabBar: ToastyTabBar {
self.tabBar as! ToastyTabBar
}
And that's it. Now you have a property on your UITabBarController subclass which is of ToastyTabBar type and you can use this new property, toastyTabBar, however you require.
Hope this helps.

iPhone X hide home indicator on view controller

I have a view controller that takes up the whole screen from top to bottom. I would like to hide the home bar indicator on the bottom of the screen on iPhone X devices.
How can I do this in iOS 11?
You should override prefersHomeIndicatorAutoHidden in your view controller to achieve that:
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
There is another alternative. If you are looking for the behavior where the indicator dims, then when the user swipes up it activates, and when they swipe up again the home action is invoked (I.E., two swipes are needed to invoke), then the answer is here: iPhone X home indicator behavior. The short of it is to override on your UIViewController:
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return UIRectEdge.bottom
}
prefersHomeIndicatorAutoHidden only hides the indicator, but will not suppress the gesture.
And you will get what you want (If I understand your comments correctly - your question and the selected answer seem to imply the other answer).
If your window?.rootViewController is a UITabBarController or UINavigationController, just inherit it and add two function as follows,
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
//#available(iOS 11, *)
override var childViewControllerForHomeIndicatorAutoHidden: UIViewController? {
return nil
}
Implement -(BOOL)prefersHomeIndicatorAutoHidden in your UIViewController and return YES.
Read more https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden.
I tried to set it and return true only when I am in full-screen :
override var prefersHomeIndicatorAutoHidden: Bool { isNavigationBarAndTabBarHidden }
but it doesn't seem to work... isNavigationBarAndTabBarHidden is a custom variable tied to my fullscreen extension.
Edit: We need to call setNeedsUpdateOfHomeIndicatorAutoHidden every time we update prefersHomeIndicatorAutoHidden's value.
var isNavigationBarAndTabBarHidden = false {
didSet {
setNeedsUpdateOfHomeIndicatorAutoHidden()
}
}
override func prefersHomeIndicatorAutoHidden() -> Bool {
return true
}
I suppose you can add this method in your AppDelegate for hide home indicator on all of your ViewControllers.

Different size classes for iPad portrait and landscape modes with containerviews

I've found this question and I've tried to implement the solution that has been given. However I run into a problem.
My initial view controller has two container views who both have their own view controller. I've created a root view controller that is assigned to the initial view controller. The code in this class looks like this.
class RootViewController: UIViewController {
var willTransitionToPortrait: Bool!
var traitCollection_CompactRegular: UITraitCollection!
var traitCollection_AnyAny: UITraitCollection!
override func viewDidLoad() {
super.viewDidLoad()
setupReferenceSizeClasses()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
willTransitionToPortrait = size.height > size.width
}
func setupReferenceSizeClasses(){
let traitCollection_hCompact = UITraitCollection(horizontalSizeClass: .Compact)
let traitCollection_vRegular = UITraitCollection(verticalSizeClass: .Regular)
traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact, traitCollection_vRegular])
let traitCollection_hAny = UITraitCollection(horizontalSizeClass: .Unspecified)
let traitCollection_vAny = UITraitCollection(verticalSizeClass: .Unspecified)
traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny, traitCollection_vAny])
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = ((willTransitionToPortrait) != nil) ? traitCollection_CompactRegular : traitCollection_AnyAny
return traitCollectionForOverride;
}
However when I run it the size class won't respons like it should. One of the container view controllers will start acting weird in both landscape and portrait mode like can be seen below.
When I don't assign the rootviewcontroller it will look like this
While it should look like this in portrait mode
Does anyone know what might be going wrong here? Why it doesn't change the size class like desired.
EDIT
Like #Muhammad Yawar Ali asked here are screenshots from the position of all the size classes I've set. I have no warnings or errors on any constraints so these screenshots contain the updated views.
I hope this shows everything that is needed.
EDIT:
for some reason I'm unable to put in all the screenshots
On the viewWillTransitionToSize you need to call also super, to pass the event to the next responder (your rootviewcontroller)...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
willTransitionToPortrait = size.height > size.width
}
Realize this is over two years old but...
I just ran across what I think is a similar issue. What you may be forgetting is that 'overrideTraitCollectionForChildViewController' only overrides the views children, so this method won't do anything with the containers since they are located at the root.
I solved this putting my two containers in a UIStackView in Interface Builder and made a property of this stack in code and then updated the axis depending on the orientation. For example, in Objective-C:
#property (weak, nonatomic) IBOutlet UIStackView *rootStack;
// ...
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
self.rootStack.axis = UILayoutConstraintAxisVertical;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else {
self.rootStack.axis = UILayoutConstraintAxisHorizontal;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
If you have any constraints that are different between portrait and landscape you will need to adjust those in code as well.
I suppose you could also solve this by embedding the view controller with the containers in another view controller.
I have cloned your code from repository : https://github.com/MaikoHermans/sizeClasses.git
And editted code put the below code in you controller it will work fine & will not effect your design in iPads.
import UIKit
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Unspecified)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
}
You can try with this code but There is an issue i believe its not updating traits properly for ipads and view layout remains same but looks good. I have tried multiple ways but not succeeded yet will update my answer.

Make UILabel focusable and tappable (tvOS)

I'm trying to implement 6 lines high description label and I want it to be focusable. Ideally that would mean extending UILabel class to make a custom component. I tried that by implementing canBecomeFocused and didUpdateFocusInContext but my UILabel doesn't seem to get any focus.
I also tried replacing UILabel with UIButton, but buttons aren't really optimised for this sort of thing. Also that would mean I'd need to change buttonType on focus from custom to plain.. and buttonType seems to be a ready-only property.
In reality I'd like to have exact same text label implemented by Apple in Apple TV Movies app. For movie description they have a text label that displays a few lines of text and a "more". When focused it looks like a button (shadows around) and changed background color. When tapped - it opens up a modal window with entire movie description.
Any suggestions? Or maybe someone has already implemented this custom control for tvOS? Or event better - there is a component from Apple that does this and I'm missing something.
P.S: Swift solution would be welcome :)
Ok, answering my own question :)
So it appears that some some views are "focusable" on tvOS out-of-the-box, and other have to be instructed to do so.
I finally ended up using UITextView, which has a selectable property, but if not one of these focusable views by default. Editing of TextView has to be disabled to make it look like UILabel. Also, currently there is a bug which prevents you from using selectable property from Interface Builder but works from code.
Naturally, canBecomeFocused() and didUpdateFocusInContext had to be implemented too. You'll also need to pass a UIViewController because UITextView is not capable of presenting a modal view controller. Bellow is what I ended up creating.
class FocusableText: UITextView {
var data: String?
var parentView: UIViewController?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: "tapped:")
tap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.addGestureRecognizer(tap)
}
func tapped(gesture: UITapGestureRecognizer) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let descriptionView = storyboard.instantiateViewControllerWithIdentifier("descriptionView") as? DescriptionViewController {
if let view = parentView {
if let show = show {
descriptionView.descriptionText = self.data
view.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
view.presentViewController(descriptionView, animated: true, completion: nil)
}
}
}
}
override func canBecomeFocused() -> Bool {
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
} else if context.previouslyFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
}
}
As for making a UILabel focusable:
class MyLabel: UILabel {
override var canBecomeFocused: Bool {
return true
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator)
backgroundColor = context.nextFocusedView == self ? .blue:.red
}
}
IMPORTANT!!!
As stated on the apple developer portal:
The value of this property is true if the view can become focused; false otherwise.
By default, the value of this property is false. This property informs the focus engine if a view is capable of being focused. Sometimes even if a view returns true, a view may not be focusable for the following reasons:
The view is hidden.
The view has alpha set to 0.
The view has userInteractionEnabled set to false.
The view is not currently in the view hierarchy.
Use a collection view with just one cell and add transform to cell and change cell background color in didUpdateFocusInContext when focus moves to cell.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.focused {
self.transform = CGAffineTransformMakeScale(1.01, 1.01)
self.backgroundColor = UIColor.whiteColor()
self.textLabel.textColor = .blackColor()
}
else {
self.transform = CGAffineTransformMakeScale(1, 1)
self.backgroundColor = UIColor.clearColor()
self.textLabel.textColor = .whiteColor()
}
}, completion: nil)
}
As an additional step you could try to extract the color of the image if you are using the image as background like iTunes and use that for Visual effect view behind the cell.
Also you can apply transform to the collectionView in the video controller to make it look like in focus
You can use system button, and set the background image in storyboard to an image that contains the color you would like

ViewController orientation change isn't animated

I'm trying to adjust some views when the iPhone changes it's orientation from portrait to landscape and vice versa. Everything works fine on iOS8 but unfortunately the changes aren't animated, but happen immediately, on iOS9. Here is the code that I have in a modal UIViewController presented via a custom transition:
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.AllButUpsideDown
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return .Portrait
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let changeToPortrait = size.width < size.height
coordinator.animateAlongsideTransition({ (context) -> Void in
//show portrait
if changeToPortrait {
//do view changes...
}
//show landscape
else {
//undo view changes...
}
}) { (context) -> Void in
print("done")
}
}
If I print coordinator.isAnimated() it says false and therefore the coordinator.transitionDuration() is also 0.0.
What do I have to do to animate the transition changes?
Thanks for your help!
This behavior is likely because you set a custom UIWindow subclass to the window property of the AppDelegate inside the application:didFinishLaunchingWithOptions: method, as telomere mentioned in another answer.
If you need to use a UIWindow subclass in iOS 9, implement a custom getter for the window property of the AppDelegate to maintain orientation change animations.
Apple's documentation for UIApplicationDelegate's window property says:
"...you must implement the getter method of this property and use it
to create and return your custom window."
Common practice is often to set the window property directly in application:didFinishLaunchingWithOptions:. Instead, in your AppDeleate, implement the custom getter like this (Thanks to Tomas Camin for the code, found here):
Objective-C
- (MyCustomWindow *)window
{
static MyCustomWindow *customWindow = nil;
if (!customWindow) customWindow = [[MyCustomWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
return customWindow;
}
Swift
var customWindow: MyCustomWindow?
var window: UIWindow? {
get {
customWindow = customWindow ?? MyCustomWindow(frame: UIScreen.mainScreen().bounds)
return customWindow
}
set { }
}
Have a look at your application:didFinishLaunchingWithOptions: handler. It seems that if you manually create your application's UIWindow object, this triggers the iOS9 rotation issue.
I'm not sure of the exact reason, but in our app we were able to fix the issue by removing the code which manipulated the UIWindow and instead let the storyboard system initialise it.

Resources