Manage Focus with multiple controls on Apple TV - ios

I have multiple controls on my screen. A collection view on right-top, then a button at left-center and besides the button, i have another collection view. Please refer the attached image for it
I am able to move the focus from button to bottom collection view and vice versa. I have created a focus guide for the same as below:
focusGuide.preferredFocusedView = self.btn
self.view.addLayoutGuide(self.focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(collectionViewHeader.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(collectionViewBottom.topAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(collectionViewBottom.leadingAnchor).active = true
self.focusGuide.widthAnchor.constraintEqualToAnchor(collectionViewBottom.widthAnchor).active = true
and in didUpdateFocusInContext: , I have write :
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
guard let nextFocusedView = context.nextFocusedView else { return }
if(nextFocusedView .isKindOfClass(bottomCell)) {
self.focusGuide.preferredFocusedView = self.btn
} else {
self.focusGuide.preferredFocusedView = self.collectionViewBottom
}
}
But, I am not able to move focus from button to top collection view. I may need multiple focus guide for this, but I do not know what should come there. Can anyone help me on this?
Thank you

I had similar problems in many parts of my UI so ended up defining a custom view to create larger areas of the screen that represent groups of focusable controls. Because I setup these larger areas to cover empty regions that don't contain controls, they intercept the focus movement and transfer to focus to the controls within the area independently of vertical or horizontal alignment with the original control where focus started from.
This is the custom view. You use it by placing one or more controls inside it in IB.
Note that it has additional features such as forcing focus to a specific control without having to override preferredFocusView but you can drop those if you don't need them).
class FocusGroup : UIView
{
weak private var nextFocusView:UIView? = nil
weak var initialView:UIView? = nil
var captureFocus:Bool = false
func focusOnView(view:UIView, now:Bool=false)
{
if not(view.isDescendantOfView(self))
|| view === self
{ return }
nextFocusView = view
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
func resetFocus(now now:Bool=false)
{
nextFocusView = nil
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
override func canBecomeFocused() -> Bool
{
if nextFocusView != nil { return true }
if containsFocus { return false }
return firstFocusableSubView() != nil
}
func firstFocusableSubView() -> UIView?
{
return findSubview({
$0.canBecomeFocused()
&& $0.userInteractionEnabled
&& $0.visible
&& ( not($0 is UIButton)
|| ($0 as! UIButton).enabled )
})
}
override var preferredFocusedView: UIView?
{
if let viewToFocus = ( nextFocusView ?? initialView ) ?? firstFocusableSubView()
{
return viewToFocus
}
return nil
}
override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool
{
// when capturing focus, prevent navigation outside of grouped subviews
if captureFocus
&& containsFocus
&& context.previouslyFocusedView!.isDescendantOfView(self)
&& (
context.nextFocusedView == nil
|| context.nextFocusedView === self
|| not(context.nextFocusedView!.isDescendantOfView(self))
)
{ return false }
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
// give focus to specific view as requested
if nextFocusView != nil
{
if context.nextFocusedView === nextFocusView
|| not(nextFocusView!.canBecomeFocused())
{ nextFocusView = nil }
return
}
}
}

Just insert a focus guide above the button and to the left of the top collection view, redirecting focus to the top collection view:
focusGuide.preferredFocusedView = self.topView
self.view.addLayoutGuide(focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(topView.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(topView.bottomAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(btn.leadingAnchor).active = true
self.focusGuide.trailingAnchor.constraintEqualToAnchor(btn.trailingAnchor).active = true
You may also want to insert a focus guide to the right of the button and below the top collection view so that navigating down from the top row redirects focus to the button instead of to the bottom row.

Related

How to identify each UIView uniquely?

I have a class like this:
class foo: UIView
{
static let childView: UIView
static func addView(onView: UIView)
{
//add childView onto onView
}
static func removeView(onView: UIView)
{
//remove childView from onView
}
}
I want to identify each onView separately, i.e.
If I do like this in a viewController
foo.addView(onView: self.view)
foo.addView(onView: self.view)
It should not remove childView only by calling removeView(onView: self.view) once, in-fact it should also call like this in order to remove child view
removeView(onView: self.view)
removeView(onView: self.view)
For this purpose, I need to identify each UIView separately.
How to identify each onView separately, and the number of times the child view add function is called for it ?
p.s I had tried this with accessibilityIdentifier and set a string tag when add view function is called, and increase the tag if onView has an associated tag with it. And in remove view, I am removing the child view, only if the tag value goes 0.
This somehow achieved what I want, but I am doubting on accessibilityIdentifier, will this disturb some built in functionality.
what apple says is: Here
But don't know what UI Automation Interfaces is, and what is accessibility label.
Edit: (Code inside functions)
static func addView(onView: UIView)
{
if let iden = onView.accessibilityIdentifier,
let identifier = Int(iden)
{
if identifier == 0
{
onView.accessibilityIdentifier = "1"
showHud(onView: onView)
}
else
{
onView.accessibilityIdentifier = "\(identifier + 1)"
}
}
else
{
onView.accessibilityIdentifier = "1"
showHud(onView: onView)
}
}
static func removeView(fromView: UIView)
{
if let iden = fromView.accessibilityIdentifier,
let identifier = Int(iden)
{
if identifier == 1
{
fromView.accessibilityIdentifier = "0"
childView.removeFromSuperview()
}
else
{
if identifier > 1
{
fromView.accessibilityIdentifier = "\(identifier - 1)"
}
}
}
else
{
fromView.accessibilityIdentifier = "0"
childView.removeFromSuperview()
}
}

SWIFT: Force focus engine to go to button when quitting collection in TVOS

I have implemented programmatically this:
List of tableView cells.
Each tableViewCell contains a
collectionView and below it a Button.
What's happening now:
Focus engine is on a collection View
User scrolls down
Focus engine goes to next collection view unless I'm on the last item of the focused collection view, then it goes to VoirTout button.
What I want is:
Focus engine is on a collection View
User scrolls down
Focus engine goes to Voir Tout Button
I have seen a couple of answers that make use of preferredFocusEnvironments like this:
// Trying to force focus on button
var voirToutButton: CustomButton? = CustomButton(color: .red, titleString: "");
override var preferredFocusEnvironments : [UIFocusEnvironment] {
return [voirToutButton!]
}
And then calling these inside viewDidLoad:
// TRYING TO FORCE FOCUS ON VOIR TOUT BUTTON
setNeedsFocusUpdate()
updateFocusIfNeeded()
But, in my case that had no effect.
I solved it like this. I'm not sure this is the most optimal way but it works:
class TableViewCell: UITableViewCell {
var voirToutButton: CustomButton? = CustomButton(color: .red, titleString: "");
var shouldRefocusToVoirToutButton:Bool = false
override var preferredFocusEnvironments : [UIFocusEnvironment] {
return [voirToutButton!]
}
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
if (self.shouldRefocusToVoirToutButton){
// Resetting this to false in order to avoid the focus getting stuck on voirToutButton
self.shouldRefocusToVoirToutButton = false
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
return true
}
}
extension TableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let previouslyFocusedItemParent = context.previouslyFocusedItem?.parentFocusEnvironment as? UICollectionView, let nextFocusedItemParent = context.nextFocusedItem?.parentFocusEnvironment as? UICollectionView {
let isPreviousFocusInCurrentCollectionView = previouslyFocusedItemParent == self.moviesCollectionView
let isNextFocusInCurrentCollectionView = nextFocusedItemParent == self.moviesCollectionView
let isFocusLeavingCollectionView = !(isPreviousFocusInCurrentCollectionView == isNextFocusInCurrentCollectionView)
let isFocusMovingDown:Bool = !(context.focusHeading == .up)
if (isFocusLeavingCollectionView && isFocusMovingDown) {
self.shouldRefocusToVoirToutButton = true
}
return true
}
// I remove this else it says that I need to return a bool (!(
else {
print("If I remove this else it says that I need to return a bool (!(")
return true
}
}
}

How to "find" your own constraint?

Say I have a UIView,
class CleverView: UIView
In the custom class, I want to do this:
func changeWidth() {
let c = ... find my own layout constraint, for "width"
c.constant = 70 * Gameinfo.ImportanceOfEnemyFactor
}
Similarly I wanna be able to "find" like that, the constraint (or I guess, all constraints, there could be more than one) attached to one of the four edges.
So, to look through all the constraints attached to me, and find any width/height ones, or indeed any relevant to a given (say, "left") edge.
Any ideas?
It's perhaps worth noting this question
Please, note that (obviously) I am asking how to do this dynamically/programmatically.
(Yes, you can say "link to the constraint" or "use an ID" - the whole point of the QA is how to find them on the fly and work dynamically.)
If you are new to constraints, note that .constraints just gives you the ends stored "there".
There are really two cases:
Constraints regarding a view's size or relations to descendant views are saved in itself
Constraints between two views are saved in the views' lowest common ancestor
To repeat. For constraints which are between two views. iOS does, in fact, always store them in the lowest common ancestor. Thus, a constraint of a view can always be found by searching all ancestors of the view.
Thus, we need to check the view itself and all its superviews for constraints. One approach could be:
extension UIView {
// retrieves all constraints that mention the view
func getAllConstraints() -> [NSLayoutConstraint] {
// array will contain self and all superviews
var views = [self]
// get all superviews
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
// transform views to constraints and filter only those
// constraints that include the view itself
return views.flatMap({ $0.constraints }).filter { constraint in
return constraint.firstItem as? UIView == self ||
constraint.secondItem as? UIView == self
}
}
}
You can apply all kinds of filters after getting all constraints about a view, and I guess that's the most difficult part. Some examples:
extension UIView {
// Example 1: Get all width constraints involving this view
// We could have multiple constraints involving width, e.g.:
// - two different width constraints with the exact same value
// - this view's width equal to another view's width
// - another view's height equal to this view's width (this view mentioned 2nd)
func getWidthConstraints() -> [NSLayoutConstraint] {
return getAllConstraints().filter( {
($0.firstAttribute == .width && $0.firstItem as? UIView == self) ||
($0.secondAttribute == .width && $0.secondItem as? UIView == self)
} )
}
// Example 2: Change width constraint(s) of this view to a specific value
// Make sure that we are looking at an equality constraint (not inequality)
// and that the constraint is not against another view
func changeWidth(to value: CGFloat) {
getAllConstraints().filter( {
$0.firstAttribute == .width &&
$0.relation == .equal &&
$0.secondAttribute == .notAnAttribute
} ).forEach( {$0.constant = value })
}
// Example 3: Change leading constraints only where this view is
// mentioned first. We could also filter leadingMargin, left, or leftMargin
func changeLeading(to value: CGFloat) {
getAllConstraints().filter( {
$0.firstAttribute == .leading &&
$0.firstItem as? UIView == self
}).forEach({$0.constant = value})
}
}
// edit: Enhanced examples and clarified their explanations in comments
I guess you can work with constraints property of UIView. constraints basically returns an array of constraint directly assigned to UIView. It will not be able to get you the constraints held by superview such as leading, trailing, top or bottom but width and height constraints are held by View itself. For superview's constraints, you can loop through superview's constraints. Lets say the clever view has these constraints:
class CleverView: UIView {
func printSuperViewConstriantsCount() {
var c = 0
self.superview?.constraints.forEach({ (constraint) in
guard constraint.secondItem is CleverView || constraint.firstItem is CleverView else {
return
}
c += 1
print(constraint.firstAttribute.toString())
})
print("superview constraints:\(c)")
}
func printSelfConstriantsCount() {
self.constraints.forEach { (constraint) in
return print(constraint.firstAttribute.toString())
}
print("self constraints:\(self.constraints.count)")
}
}
Output:
top
leading
trailing
superview constraints:3
height
self constraints:1
Basically, you can look at NSLayoutConstraint class to get the info out about a particular constraint.
To print the name of constraints, we can use this extension
extension NSLayoutAttribute {
func toString() -> String {
switch self {
case .left:
return "left"
case .right:
return "right"
case .top:
return "top"
case .bottom:
return "bottom"
case .leading:
return "leading"
case .trailing:
return "trailing"
case .width:
return "width"
case .height:
return "height"
case .centerX:
return "centerX"
case .centerY:
return "centerY"
case .lastBaseline:
return "lastBaseline"
case .firstBaseline:
return "firstBaseline"
case .leftMargin:
return "leftMargin"
case .rightMargin:
return "rightMargin"
case .topMargin:
return "topMargin"
case .bottomMargin:
return "bottomMargin"
case .leadingMargin:
return "leadingMargin"
case .trailingMargin:
return "trailingMargin"
case .centerXWithinMargins:
return "centerXWithinMargins"
case .centerYWithinMargins:
return "centerYWithinMargins"
case .notAnAttribute:
return "notAnAttribute"
}
}
}
stakri's answer is OK, but we can do better by using
sequence(first:next:):
extension UIView {
var allConstraints: [NSLayoutConstraint] {
sequence(first: self, next: \.superview)
.flatMap(\.constraints)
.lazy
.filter { constraint in
constraint.firstItem as? UIView == self || constraint.secondItem as? UIView == self
}
}
}
Then, if we check both implementations by swift-benchmark by Google we can see that Sequence implementation is much faster (almost +50k iterations for the ±same time).
running Find All Constraints: Stakri... done! (1778.86 ms)
running Find All Constraints: Sequence... done! (1875.20 ms)
name time std iterations
---------------------------------------------------------------
Find All Constraints.Stakri 3756.000 ns ± 96.67 % 291183
Find All Constraints.Sequence 3727.000 ns ± 117.42 % 342261
Might save someone some typing.......
Based on stakri's bounty-winning answer, here is exactly how to get
all constraints of the type "fractional width of another view"
all constraints of the type "fixed point width"
all constraints of the type "your x position"
So ..
fileprivate extension UIView {
func widthAsPointsConstraints()->[NSLayoutConstraint] {}
func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {}
func xPositionConstraints()->[NSLayoutConstraint]
}
Full code below. Of course, you can do "height" the same way.
So, use them like this...
let cc = someView.widthAsFractionOfAnotherViewConstraints()
for c in cc {
c.changeToNewConstraintWith(multiplier: 0.25)
}
or
let cc = someView.widthAsPointsConstraints()
for c in cc {
c.constant = 150.0
}
Also, at the bottom I pasted in a simple demo code, example output...
Here's the code. V2 ...
fileprivate extension UIView { // experimental
func allConstraints()->[NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { constraint in
return constraint.firstItem as? UIView == self ||
constraint.secondItem as? UIView == self
}
}
func widthAsPointsConstraints()->[NSLayoutConstraint] {
return self.allConstraints()
.filter({
( $0.firstItem as? UIView == self && $0.secondItem == nil )
})
.filter({
$0.firstAttribute == .width && $0.secondAttribute == .notAnAttribute
})
}
func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {
func _bothviews(_ c: NSLayoutConstraint)->Bool {
if c.firstItem == nil { return false }
if c.secondItem == nil { return false }
if !c.firstItem!.isKind(of: UIView.self) { return false }
if !c.secondItem!.isKind(of: UIView.self) { return false }
return true
}
func _ab(_ c: NSLayoutConstraint)->Bool {
return _bothviews(c)
&& c.firstItem as? UIView == self
&& c.secondItem as? UIView != self
&& c.firstAttribute == .width
}
func _ba(_ c: NSLayoutConstraint)->Bool {
return _bothviews(c)
&& c.firstItem as? UIView != self
&& c.secondItem as? UIView == self
&& c.secondAttribute == .width
}
// note that .relation could be anything: and we don't mind that
return self.allConstraints()
.filter({ _ab($0) || _ba($0) })
}
func xPositionConstraints()->[NSLayoutConstraint] {
return self.allConstraints()
.filter({
return $0.firstAttribute == .centerX || $0.secondAttribute == .centerX
})
}
}
extension NSLayoutConstraint {
// typical routine to "change" multiplier fraction...
#discardableResult
func changeToNewConstraintWith(multiplier:CGFloat) -> NSLayoutConstraint {
//NSLayoutConstraint.deactivate([self])
self.isActive = false
let nc = NSLayoutConstraint(
item: firstItem as Any,
attribute: firstAttribute,
relatedBy: relation,
toItem: secondItem,
attribute: secondAttribute,
multiplier: multiplier,
constant: constant)
nc.priority = priority
nc.shouldBeArchived = self.shouldBeArchived
nc.identifier = self.identifier
//NSLayoutConstraint.activate([nc])
nc.isActive = true
return nc
}
}
Just an example demo...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_teste()
delay(5) {
print("changing any 'fraction fo another view' style widths ...\n\n")
let cc = self.animeHolder.widthAsFractionOfAnotherViewConstraints()
for c in cc {
c.changeToNewConstraintWith(multiplier: 0.25)
}
self._teste()
}
delay(10) {
print("changing any 'points' style widths ...\n\n")
let cc = self.animeHolder.widthAsPointsConstraints()
for c in cc {
c.constant = 150.0
}
self._teste()
}
}
func _teste() {
print("\n---- allConstraints")
for c in animeHolder.allConstraints() {
print("\n \(c)")
}
print("\n---- widthAsPointsConstraints")
for c in animeHolder.widthAsPointsConstraints() {
print("\n \(c)\n \(c.multiplier) \(c.constant)")
}
print("\n---- widthAsFractionOfAnotherViewConstraints")
for c in animeHolder.widthAsFractionOfAnotherViewConstraints() {
print("\n \(c)\n \(c.multiplier) \(c.constant)")
}
print("\n----\n")
}

iOS 11 & iPhone X: UINavigationBar's toolbar spacing incorrect when embedded in UITabBarController

I am experiencing an annoying problem testing the newest iOS 11 on the iPhone X simulator.
I have an UITabBarController and inside each tab there is a UINavigationController, each UINavigationBar has defined also a bottom toolBar (setToolbarHidden:), and by default they show up at the bottom, just over the tabBar.
It has been working fine so far and seems to work fine also in the upcomming iPhone 8 and 8 Plus models, but on the iPhone X there is a gap between the toolBar and the tabBar. My guess is that the toolBar doesn't realize that is displayed inside a tabBar and then leaves the accommodating space at the bottom.
I guess the only way to fix it would be using a custom toolbar and display/animate it myself instead of using the defaults UINavigationBar, but I would like to hear other options :)
This is how it looks on iPhone 8.
And here is the problem on iPhone X.
I filed this as radr://problem/34421298, which was closed as a duplicate of radr://problem/34462371. However, in the latest beta of Xcode 9.2 (9C32c) with iOS 11.2, this seems to be fixed. Here's an example of my app running in the simulator of each device, with no changes in between.
This isn't really a solution to your problem, other than that some patience may solve it without needing to resort to UI trickery. My assumption is that iOS 11.2 will be out before the end of the year, since it's needed to support HomePod.
If you don't consider rotations you can try to manipulate the layer of the toolbar as a very hacky yet fast workaround.
class FixNavigationController: UINavigationController
{
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateTollbarPosition()
}
func updateTollbarPosition() {
guard let tabbarFrame = tabBarController?.tabBar.frame else {
return
}
let gapHeight = tabbarFrame.origin.y-toolbar.frame.origin.y-toolbar.frame.size.height
var
frame = toolbar.layer.frame
frame.origin.y += gapHeight
toolbar.layer.frame = frame
}
}
Unfortunately, rotation animation doesn't look good when it comes to this approach. In this case, adding the custom toolbar instead of the standard one will be a better solution.
I have found only one workaround: add toolbar directly to the view controller
iOS 11.1 and iPhone X are released and this bug/feature isn't fixed yet. So I implemented this workaround. This code works in iOS 9.0+.
Just set this class in your storyboard as navigation controller's class. It will use custom toolbar in iPhone X with correct layout constraints, and falls back to native one in other devices. The custom toolbar is added to navigation controller's view instead of your view controller, to make transitions smoother.
Important Note: You have to call updateItems(animated:) manually after setting toolbarItems of your view controller to update interface. If you set toolbarItems property of navigation controller, you can ignore this step.
It simulates all native toolbar behavior (including changing toolbar height in portrait/landscape modes), except push/pop animations.
import UIKit
class FixNavigationController: UINavigationController {
private weak var alterToolbarHeightConstraint: NSLayoutConstraint?
private var _alterToolbar: UIToolbar?
private func initAlretToolbar() {
_alterToolbar = UIToolbar()
_alterToolbar!.isTranslucent = true
_alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(_alterToolbar!)
if view.traitCollection.verticalSizeClass == .compact {
alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
} else {
alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
}
let bottomAnchor: NSLayoutConstraint
if #available(iOS 11.0, *) {
bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
} else {
bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
}
NSLayoutConstraint.activate([
_alterToolbar!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
_alterToolbar!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomAnchor,
alterToolbarHeightConstraint!
])
self.view.updateFocusIfNeeded()
self.view.layoutIfNeeded()
}
private var alterToolbarInSuper: UIToolbar? {
var superNavigationController = self.navigationController as? FixNavigationController
while superNavigationController != nil {
if superNavigationController?._alterToolbar != nil {
return superNavigationController?._alterToolbar
}
superNavigationController = superNavigationController?.navigationController as? FixNavigationController
}
return nil
}
private var alterToolbar: UIToolbar! {
get {
if let t = alterToolbarInSuper {
return t
}
if _alterToolbar == nil {
initAlretToolbar()
}
return _alterToolbar
}
}
// This is the logic to determine should use custom toolbar or fallback to native one
private var shouldUseAlterToolbar: Bool {
// return true if height is iPhone X's one
return UIScreen.main.nativeBounds.height == 2436
}
/// Manually call it after setting toolbar items in child view controllers
func updateItems(animated: Bool = false) {
if shouldUseAlterToolbar {
(_alterToolbar ?? alterToolbarInSuper)?.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: animated)
}
}
override var isToolbarHidden: Bool {
get {
if shouldUseAlterToolbar {
return _alterToolbar == nil && alterToolbarInSuper == nil
} else {
return super.isToolbarHidden
}
}
set {
if shouldUseAlterToolbar {
if newValue {
super.isToolbarHidden = newValue
_alterToolbar?.removeFromSuperview()
_alterToolbar = nil
self.view.updateFocusIfNeeded()
self.view.layoutIfNeeded()
// TODO: Animation when push/pop
alterToolbarHeightConstraint = nil
var superNavigationController = self.navigationController as? FixNavigationController
while let superNC = superNavigationController {
if superNC._alterToolbar != nil {
superNC._alterToolbar?.removeFromSuperview()
superNC._alterToolbar = nil
superNC.view.updateFocusIfNeeded()
superNC.view.layoutIfNeeded()
}
superNavigationController = superNC.navigationController as? FixNavigationController
}
} else {
alterToolbar.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: false)
}
} else {
super.isToolbarHidden = newValue
}
}
}
override func setToolbarItems(_ toolbarItems: [UIBarButtonItem]?, animated: Bool) {
super.setToolbarItems(toolbarItems, animated: animated)
updateItems(animated: animated)
}
override var toolbarItems: [UIBarButtonItem]? {
get {
return super.toolbarItems
}
set {
super.toolbarItems = newValue
updateItems()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
guard let _alterToolbar = _alterToolbar else {
return
}
self.alterToolbarHeightConstraint?.isActive = false
let height: CGFloat = (view.traitCollection.verticalSizeClass == .compact) ? 32.0 : 44.0
let alterToolbarHeightConstraint = _alterToolbar.heightAnchor.constraint(equalToConstant: height)
alterToolbarHeightConstraint.isActive = true
self.alterToolbarHeightConstraint = alterToolbarHeightConstraint
}
}
Apple still has not yet fixed this bug in iOS 11.2. Derived from Mousavian's solution, here is a simpler approach that I took.
I took this approach because I have just one UITableViewController where this bug happens. So in my case, I just added the following code listed below to my ViewController (which is UITableViewController) where this bug happens.
Advantages are:
This fix just takes over in case of an iPhone X. No side effects to expect on other devices
Works with any transition
Works regardless of other parent/child controllers having Toolbars or not
Simple
And here is the code:
1.Add startFixIPhoneXToolbarBug to your viewWillAppear like this:
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
startFixIPhoneXToolbarBug()
}
2.Add endFixIPhoneXToolbarBug to your viewWillDisappear like this:
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
endFixIPhoneXToolbarBug()
}
3.Implement start/endFixIPhoneXToolbarBug in your viewController like this:
private var alterToolbarHeightConstraint: NSLayoutConstraint? = nil
private var alterToolbar: UIToolbar? = nil
func startFixIPhoneXToolbarBug()
{
// Check if we are running on an iPhone X
if UIScreen.main.nativeBounds.height != 2436
{
return // No
}
// See if we have a Toolbar
if let tb:UIToolbar = self.navigationController?.toolbar
{
// See if we already added our own
if alterToolbar == nil
{
// Should always be the case
if let tbView = tb.superview
{
// Create a new Toolbar and apply correct constraints
alterToolbar = UIToolbar()
alterToolbar!.isTranslucent = true
alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
tb.isHidden = true
tbView.addSubview(alterToolbar!)
if tbView.traitCollection.verticalSizeClass == .compact
{
alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
}
else
{
alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
}
let bottomAnchor: NSLayoutConstraint
if #available(iOS 11.0, *)
{
bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: tbView.safeAreaLayoutGuide.bottomAnchor)
}
else
{
bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
}
NSLayoutConstraint.activate([
alterToolbar!.leadingAnchor.constraint(equalTo: tbView.leadingAnchor),
alterToolbar!.trailingAnchor.constraint(equalTo: tbView.trailingAnchor),
bottomAnchor,
alterToolbarHeightConstraint!
])
tbView.updateFocusIfNeeded()
tbView.layoutIfNeeded()
}
}
// Add the original items to the new toolbox
alterToolbar!.setItems(tb.items, animated: false)
}
}
func endFixIPhoneXToolbarBug()
{
if alterToolbar != nil
{
alterToolbar!.removeFromSuperview()
alterToolbar = nil
alterToolbarHeightConstraint = nil
if let tb:UIToolbar = self.navigationController?.toolbar
{
tb.isHidden = false
}
}
}

UIPickerView, detect "rolling wheel" start and stop?

I just discovered that if I do the following:
Click the button that animates a UIPickerView into my view
Quickly start the wheel rolling towards, then past, the last item
Dismiss the view with a button
Then it has not yet selected the last item yet.
I tried this by simply outputting to the console whenever the didSelectRow method was fired, and it fires when the wheel stabilizes on the last item.
Can I detect that the wheel is still rolling, so that I can delay checking it for a selected value until it stabilizes?
If it matters, I'm programming in MonoTouch, but I can read Objective-C code well enough to reimplement it, if you have a code example that is.
As animation keys don't work, I wrote this simple function that works for detecting if a UIPickerView is currently moving.
-(bool) anySubViewScrolling:(UIView*)view
{
if( [ view isKindOfClass:[ UIScrollView class ] ] )
{
UIScrollView* scroll_view = (UIScrollView*) view;
if( scroll_view.dragging || scroll_view.decelerating )
{
return true;
}
}
for( UIView *sub_view in [ view subviews ] )
{
if( [ self anySubViewScrolling:sub_view ] )
{
return true;
}
}
return false;
}
It ends up returning true five levels deep.
Swift 4 (updated) version with extension of #DrainBoy answers
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
}
Since animationKeys seems to not work anymore, I have another solution. If you check the subviews of UIPickerView, you'll see that there is a UIPickerTableView for each component.
This UIPickerTableView is indeed a subclass of UITableView and of course of UIScrollView. Therefore, you can check its contentOffset value to detect a difference.
Besides, its scrollViewDelegate is nil by default, so I assume you can safely set an object of yours to detect scrollViewWillBeginDragging, scrollViewDidEndDecelerating, etc.
By keeping a reference to each UIPickerTableView, you should be able to implement an efficient isWheelRolling method.
Expanded #iluvatar_GR answer
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
func waitTillDoneScrolling (completion: #escaping () -> Void) {
var isMoving = true
DispatchQueue.global(qos: .background).async {
while isMoving == true {
isMoving = self.isScrolling()
}
DispatchQueue.main.async {
completion()}
}
}
}
Expanded #iluvatar_GR, #Robert_at_Nextgensystems answer
Used Gesture, UIScrollView isDragging or isDecelerating.
// Call it every time when Guesture action.
#objc func respondToSwipeGesture(gesture: UIGestureRecognizer) {
// Changes the button name to scrolling at the start of scrolling.
DispatchQueue.main.async {
self._button.setTitle("Scrolling...", for: .normal)
self._button.isEnabled = false
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "FF8FAE")
}
// Indication according to scrolling status
_datePicker.waitTillDoneScrolling(completion: {
print("completion")
DispatchQueue.main.async {
self._button.setTitle("Completion", for: .normal)
self._button.isEnabled = true
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "7CB0FF")
}
})
}
[SWIFT4] Share Example Source link!
enter Sample Source link
Reference : How to recognize swipe in all 4 directions
I think you can just check if the UIPickerView is in the middle of animating and wait for it to stop. This was answered here link
You can use a SwipeGestureRecognizer on the picker.
I assume this is not a perfect solution at all.
- (void)viewDidLoad
{
[super viewDidLoad];
_pickerSwipeGestureRecognizer.delegate = self;
[_pickerSwipeGestureRecognizer setDirection:(UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp)];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if([gestureRecognizer isEqual:_pickerSwipeGestureRecognizer]){
NSLog(#"start");
}
}
- (void)pickerView:(UIPickerView *)thePickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
NSLog(#"end");
}

Resources