How to disable default keyboard navigation in Mac Catalyst app? - ios

I noticed that I can step through rows in a UITableView in a Mac Catalyst app by pressing the up and down arrow keys on my Mac keyboard. However, this interferes with the existing functionality in one of my view controllers. Is there a way to disable this?
I can't find any reference to this functionality in the UITableView documentation. The Human Interface Guidelines for Mac Catalyst mentions "automatic support for fundamental Mac features, such as ... keyboard navigation," so I guess this is an intentional feature, but I can't find any further reference to it or documentation for it.
I haven't seen any other examples of "automatic" keyboard navigation in my app, but ideally Apple would publish a complete list so we could know how to work with, or if needed, disable, the built-in functionality.

Update as of 2021/11/06
It looks like Apple has been changing how the default focus system works and my previous solution is no longer working or required.
UIKeyCommand has a new wantsPriorityOverSystemBehavior: Bool property which needs to be set to true in order for our subclasses to receive certain types of commands, including the arrow key commands.
As of at least Xcode 13.1 and macOS 11.6, maybe eariler, we can now simply add the following to a UITableViewController subclass to replace the default focus behavior with custom keyboard navigation handing:
class TableViewController: UITableViewController {
override var keyCommands: [UIKeyCommand]? {
let upArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputUpArrow,
modifierFlags: [],
action: #selector(handleUpArrowKeyPress)
)
upArrowCommand.wantsPriorityOverSystemBehavior = true
let downArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputDownArrow,
modifierFlags: [],
action: #selector(handleDownArrowKeyPress)
)
downArrowCommand.wantsPriorityOverSystemBehavior = true
return [
upArrowCommand,
downArrowCommand
]
}
#objc
func handleUpArrowKeyPress () {
}
#objc
func handleDownArrowKeyPress () {
}
}
Previous answer (no longer working or required)
Catalyst automatically assigns UIKeyCommands for the up/down arrows to UITableView instances. This does not happen on iOS. You can see this in action by setting a break point in viewDidLoad() of a UITableViewController and inspecting tableView.keyCommands.
So I created a very simple UITableView subclass and disabled the default keyCommmands by returning nil:
class KeyCommandDisabledTableView: UITableView {
override var keyCommands: [UIKeyCommand]? {
return nil
}
}
I then updated my UITableViewController subclass to use the new KeyCommandDisabledTableView subclass:
class MyTableViewController: UITableViewController {
override func loadView() {
self.view = KeyCommandDisabledTableView(
frame: .zero,
style: .plain // or .grouped
)
}
}
Et voilà! The default arrow key handling is gone and my app's custom arrow key handling is now being called.

Here's another solution I received from Apple DTS. Just add this to the table view delegate:
func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {
return false
}
This works in macOS 11.6 and 12.0. I don't have a 10.15 or 11.5 Mac to test with, so I'll keep my earlier resignFirstResponder solution, too.

I further noticed that the default arrow key navigation only begins after clicking a row in a table, so I guessed the table must be assuming the first responder role. I added this to my table's delegate class:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
#if TARGET_OS_MACCATALYST
[tableView performSelector:#selector(resignFirstResponder) withObject:nil afterDelay:0.1];
#endif
}
That fixed it! Now the default keyboard navigation turns off as soon as it turns on, and doesn't interfere with my app's custom keyboard navigation.
(It didn't work without the delay.)

iOS 14 / macOS 11 makes it much easier to disable this behavior thanks to UITableView and UICollectionView's selectionFollowsFocus property:
tableView.selectionFollowsFocus = false

Related

How to show different static cells for different iOS versions?(Static `UITableView`)

I'm creating a Setting Screen with the help of static UITableVIew.
Now, there are certain cells(Functions) that doesn't work in some iOS version ,so I want to hide them from my tableView in a way that doesn't affect other cell.
E.G. - in the below Image , Change App Icon function won't work in iOS version less than 10.3 so I want to hide it for devices only which have lower iOS version than 10.3 (It should be displayed for iOS 10.3+)
Thanks in Advance
You should use the heightForRowAtIndexPath method and inside it, check the iOS version and return the height of cells based on it.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if #available(iOS 13.0, *) {
return 40
} else {
return ( (indexPath.row == 3) ? 0 : 50)
}
}
As its a static table view, you already know the row and section. So in the above code, we are hiding the third row if the iOS version is below 13.0 by making its height 0. For other rows, it will make height as 50
you can use the available attribute for the same while in cellForRowAtIndexPath function. In your cellForRowAtIndexPath try the following:
if #available(iOS 10.3, *) {
//load all cells here
} else {
// load just the ones you want excluding those which won't work with iOS version less than 10.3
}
Your question isn't clear enough.
How do you implement your cells? If you use a storyboard – you should hide particular cells.
If you use the code, you should check the iOS version at each time of table handling: from cell registering until each delegate method in order to provide the corresponding information.

Hiding everything in the iOS navbar, including icons

I've managed to hide the navbar itself, but I want it all gone - charge icon, connection bars, clock - absolutely everything.
I'm aware this may create a 'dead end' for my app, but that's ok for my purposes.
I read here that it's apparently not legal. Is that still true?
The app is not for distribution, so I'd still be interested to hear solutions even if Apple doesn't like it.
Thank you.
I assume you want to hide status bar.
You can use prefersStatusBarHidden property to hide status bar in Swift 4.2:
class ViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
return hideStatusBar
}
override func viewDidLoad() {
super.viewDidLoad()
}
}

UITableView is resetting its background color before view appears

I'm using probably a little bit exotic way of initialization of my UI components. I create them programmatically and among them is a UITableView instance, I set its background color immediately upon initialization, like this:
class MyViewController: UIViewController {
...
let tableView = UITableView().tap {
$0.backgroundColor = .black
$0.separatorStyle = .none
}
...
}
where tap is extension function:
func tap(_ block: (Self) -> Void) -> Self {
block(self)
return self
}
This worked very well in my previous project which was created in Xcode 8 and then migrated to Xcode 9 without breaking anything. But now I've created brand new project in Xcode 9 and copy-pasted above-mentioned extension to it, but seems like something went wrong. When my view controller appears on screen table has white background and default separator insets.
This seems to affect only some of the properties because others are working as they should have (e.g. $0.register(nib: UINib?, forCellReuseIdentifier: String) registers required cell class and $0.showsVerticalScrollIndicator = false hides scroll indicator).
Perhaps some of you, guys, could give me an idea what's the heart of the matter.
Here's full code, to reproduce the issue simply create a new project and replace ViewController.swift's content. As you can see, table has correct rowHeight (160) but resets its background color.
As for "before view appears" statement: I've printed table's background color in viewDidLoad, viewWillAppear and viewDidAppear like this:
print(#function, table.backgroundColor.debugDescription)
– it changes its color only in the last debug print:
viewDidLoad() Optional(UIExtendedGrayColorSpace 0 1)
viewWillAppear Optional(UIExtendedGrayColorSpace 0 1)
viewDidAppear Optional(UIExtendedSRGBColorSpace 1 1 1 1)
I ended up moving the initialization to lazy var's function – turns out initializing UITableView during the initialization of it's view controller has some side effects.

UI Tests - isSelected is always returning false

We have updated out Swift 2.3 project to Swift 3 recently using Xcode 8.2.1 (8C1002), and now most of our UI Tests related with tableViews and the isSelected property aren't working. It's always returning false, even when the object is selected (we can see it in the iOS Simulator).
Has anyone experienced similar issues? Our code used to work normally in Swift 2.3 before the conversion. Here is how we retrieve a tableView cell:
let cell = app.tables.cells.element(at: 4)
Note: app is a XCUIApplication.
And here is how we check if it's selected or not:
XCTAssert(cell.isSelected)
Another observation is that we are sure that the object exists because waitForExpectations is returning true:
let existsPredicate = NSPredicate(format: "exists = 1")
expectation(for: existsPredicate, evaluatedWith: cell, handler: nil)
waitForExpectations(timeout: 20, handler: nil)
EDIT: In order to replace isSelected, I've tried to use NSPredicate with selected = 1 and with isSelected = 1. None worked. I also tried to use acessibilityValue based in other question's answer, however it wasn't that simple since sometimes the items in my table view are selected/unselected programatically. Also, that method involved adding test code to the app, which isn't a good practice.
EDIT AFTER BOUNTY END: Since no one could find a solution for that problem and that's obviously a bug in Xcode, I've submitted a bug report to Apple. I will comment here when they release an Xcode version with the fix.
EXTRA EDIT: One day after my last edit, dzoanb came with a functional answer.
I made a few tests and a little research. You can check out the app created for this purpose >>here<<. It would be great if you could check it out (it required a little bit of work). There are also UI tests to prove it works. Also, two options are available, one is vanilla XCTest and one library with a lot of helpers I'm creating with my colleagues AutoMate. But that's not the point.
Here is what I found out:
1) isSelected property of XCUIElement depends on accessibilityTrait. Element to be selected in XCTest has to have UIAccessibilityTraitSelected set.
2) I couldn't reproduce Your problem but I was able to control isSelected property.
3) Yes, it requires a little bit of code, but should work well with VoiceOver if it is important for You.
All necessary code is in Your custom UITableViewCell subclass. And uses overriding UIAccessibilityElement accessibilityTraits property.
private var traits: UIAccessibilityTraits = UIAccessibilityTraitNone
// MARK: UITableViewCell life cycle
override func awakeFromNib() {
super.awakeFromNib()
traits = super.accessibilityTraits
}
// MARK: UIAccessibilityElement
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return traits | UIAccessibilityTraitSelected
}
return traits
}
set {
traits = newValue
}
}
Hope it helps.
Couldn't get that code to compile under Swift 4.
This worked for me.
public override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits.union(.selected)
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}
Have you tried making a break point before and after the tap, and check the value of the cell? Like the WWDC video here: https://youtu.be/7zMGf-0OnoU
(See from 10 minutes in)
isSelected only works on views which inherit from UIControl. UIControl.isSelected informs XCUIElement.isSelected.
Since UITableViewCell does not inherit from UIControl, you aren't seeing the value you want in your tests when you observe cell.isSelected.
I suggest that if you want this to be testable via UI tests that you file a feature request with Apple to make UIControl a protocol, which you could then extend your cells to conform to, or add UITableViewCell.isSelected to the properties that inform XCUIElement.isSelected.
#dzoanb solution can work without adding a private var:
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits | UIAccessibilityTraitSelected
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}

Disabling Dynamic Type in Swift

I have a Sprite Kit based game that uses a UIView within one of the scenes, and I do that so that I can take advantage of the UITableViewController to present a game settings screen.
The difficulty I am running into is that when a user sets their iPad system accessibility settings to use (extra) large type, the text within the UITableView is too large for the cells and it looks just plain silly.
What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
I have found another similar posting (here) but the response offers an Objective-C response:
#import <objc/runtime.h>
#implementation AppDelegate
NSString* swizzled_preferredContentSizeCategory(id self, SEL _cmd) {
return UIContentSizeCategoryLarge; // Set category you prefer, Large being iOS' default.
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
Method method = class_getInstanceMethod([UIApplication class], #selector(preferredContentSizeCategory));
method_setImplementation(method, (IMP)swizzled_preferredContentSizeCategory);
...
}
I need to do this in Swift.
What is the correct way to do this same thing in Swift in Xcode 7+ ?
Thanks #zeeple for the solution.
Here is the answer to the original question:
"preferredContentSizeCategory" in Objective-C is a method, but in Swift it is a read-only variable.
So in your AppDelegate is like this:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// MARK: - UIApplicationDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.classInit
self.window = UIWindow(frame: UIScreen.main.bounds)
...
self.window?.makeKeyAndVisible()
return true
}
}
// MARK: - Fix Dynamic Type
extension UIApplication {
static let classInit: Void = {
method_exchangeImplementations(
class_getInstanceMethod(UIApplication.self, #selector(getter: fixedPreferredContentSizeCategory))!,
class_getInstanceMethod(UIApplication.self, #selector(getter: preferredContentSizeCategory))!
)
}()
#objc
var fixedPreferredContentSizeCategory: UIContentSizeCategory {
return .large
}
}
Okay, first let me say this: while I am happy that I was able to quickly find a way to accommodate the dynamic text provided by the iOS accessibility settings (which I will show the code for in a sec) I think it is still important to get an answer to the original question.
That said, here is what I did to the table view code to respect the larger type that some users need. It was a two step process. First, add:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
to the viewDidLoad method. Then, in the cellForRowAtIndexPath method, add the following before you return the cell:
cell.textLabel!.numberOfLines = 0
Good luck folks, and please add an answer to the original question if you have one :)
What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
Dynamic Type only works for text with implemented text styles.
So, if you always want to disable Dynamic Type and display the same sized type in the cells, don't use text styles nor image size adjustment in them.
However, if you do want to use the text styles, never tick Automatically Adjusts Font for each text element in Interface Builder (equivalent to adjustsFontForContentSizeCategoryin code).
Hi All Frustrated Devs,
Here is perfect solution to disable Dynamic Type is :
Since iOS 15 you can set limits on the minimum and maximum sizes of dynamic type. It works for both UIKit and SwiftUI.
// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityExtraLarge
// SwiftUI
ContentView()
.dynamicTypeSize(.medium ... .accessibility3)
Also if you want to directly disable throughout the whole Application, You should create a Base Class which should be parent of your all VCs. In BaseVC you can set up code.

Resources