TL;DR: Is there any way to have custom button style (custom pressed state) in SwiftUI on tvOS while it is still working correctly with Accessibility Focus API and therefore its hasFocus works in UI tests?
Please visit dedicated Github repo for a longer version and a sample project with UI tests example.
What is the problem?
To create a custom button in SwiftUI on tvOS and can customize it based on pressed state, you can implement custom ButtonStyle.
Button(...)
.buttonStyle(AnyCustomButtonStyle())
and then use ButtonConfiguration.isPressed in the view.
However, I have found that although the button visually looks focused, it does not really report as Focused in Accessibility API. See the sample project for example and actual test showing the problem.
Among other problems, it makes the button quite difficult to work with in tvOS UI tests. tvOS is relying on focus for navigation and because the button's hasFocus always stays false (even if the button renders in focused appearance), it can prevent a lot of useful APIs to work in tests.
Why do you need custom ButtonStyle on tvOS?
I know Apple provides some custom PrimitiveButtonStyle implementations (like CardButtonStyle), but those don't provide enough flexibility. They all modify your button (e.g. add background).
Not being able to use custom buttonStyle makes it impossible to implement for example Capsule-style buttons like this...
First button focused
First button focused and pressed
Please let's leave aside the discussion if it is a good idea or not đ ... Just trying to find if there is a solution or if it is eventually a bug of SwiftUI.
What is the issue with Accessibility?
Without custom button, the button reports as Focused to Accessibility in UI tests.
Without custom style
(lldb) po app
Attributes: Application, 0x12ed10b30, pid: 61273, label: 'FocusSwiftUI'
Element subtree:
âApplication, 0x12ed10b30, pid: 61273, label: 'FocusSwiftUI'
...
Button, 0x12ed0c660, {{468.0, 477.0}, {298.0, 126.0}}, label: 'Button 1'
Button, 0x12ed0c770, {{453.0, 470.7}, {328.0, 138.7}}, Focused
Button, 0x12ed0c1e0, {{806.0, 477.0}, {303.0, 126.0}}, label: 'Button 2'
Button, 0x12ed0c2f0, {{791.0, 470.8}, {333.0, 138.5}}
...
However, the moment you set buttonStyle to any custom one, the output changes drastically...
(lldb) po app
Attributes: Application, 0x106d0cd40, pid: 70156, label: 'FocusSwiftUI'
Element subtree:
âApplication, 0x106d0cd40, pid: 70156, label: 'FocusSwiftUI'
...
Button, 0x106d0ce50, {{707.0, 505.0}, {142.0, 71.0}}, label: 'Button 1'
Button, 0x106d0cf60, {{885.0, 505.0}, {146.0, 71.0}}, label: 'Button 2'
Button, 0x106d0d180, {{1067.0, 505.0}, {147.0, 71.0}}, label: 'Button 3'
...
Notice there is no more Focused button anywhere... We will never get any button with hasFocus == true in queries for example...
What else did I try?
I tried to experiment with many (probably all) .accessibility... modifiers before asking.
Many different results, but none of them ever had proper focus behavior in UI tests...
.accessibilityChildren: close to default button behavior in terms of accessibility structure (e.g. nested buttons)
.accessibilityRepresentation: button never gets visually highlighted
Temporary workaround
For now there seems to be no solution in sight. I have implemented rather complicated hack to get at least UI tests working for now.
In a nutshell: When the app is running in a context of UI tests (determined through Launch Arg), each affected button adds clear color background (it has no visual or behavioral impact) when focused. The background color then has a constant accessibilityIdentifier (e.g. "MY_FOCUSED", I call it "custom focus marker"). When evaluating if element is focused in UI tests, I then check if the button contains child element where accessibilityIdentifier == "MY_FOCUSED".
It is nasty, but somehow good enough for UI tests and actually works reliably so far. It works thanks to the fact there is always only one focused item at the same time and the "if focused -> set background" takes care of the automatic update of the "custom focus marker".
You can try to set accessibilityRepresentstion - does it help?
Button(..)
.buttonStyle(AnyCustomButtonStyle())
.accessibilityRepresentation {
Button(..)
}
Edit:
Another idea - what about having the âcorrectâ Button in the background of your custom? Something like:
Button(..)
.buttonStyle(AnyCustomButtonStyle())
.background(
Button(..).opacity(0.0001) //it may work even with opacity 0
)
You may additionally improve it with making the visible button hidden to accessibility with .accessibility(hidden: true)
If you're looking for actual VoiceOver accessibility (not using it as a UI test tool I mean) you can use the isFocused environment variable and on change, call UIAccessibility.post(notification: .announcement, argument: "Your message")
Related
When dealing with login screens, I am trying to work out the better approach - either execute navigation "action" to go to login fragment on first use (and hide back button to actual app), or start a new login activity (with its own nav graph). For the first approach (just using navigation components), I do not know the way to remove the back button without a hack "hide". I tried using navoptions, setpopupto etc., but it does not work. Code below:
val navOptions = NavOptions.Builder()
.setPopUpTo(R.id.home_fragment, true)
.build()
host?.navController?.navigate(R.id.action_global_signUpFragment_dest, null, navOptions)
Two questions then:
1) How to properly handle login transition with just navigation component?
2) Is starting a new login activity, with separate nav graph, a better idea?
I think the first approach is better.
To hide the 'back' button on your toolbar inside signUpFragment you can use AppBarConfiguration, and customize which destinations are considered top-level destinations.
For example:
val appBarConfiguration = AppBarConfiguration.Builder(setOf(R.id.home_fragment, R.id.signUpFragment_dest)).build()
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
This way home_fragment and signUpFragment_dest will be considered top-level destinations, and won't have back button on toolbar.
Another option for solving the back button problem is how I did it here. Also, rather than show/hide the bottom nav bar, I have two NavHostFragment, one main full screen one, and one contained within the home fragment (above the bottom nav bar).
When I want to navigate to a full screen view I call this extension function,
fun Fragment.findMainNavController(): NavController =
Navigation.findNavController(activity!!, R.id.nav_host_fragment)
then navigate via the main graph.
This makes sense conceptually to me, to have parent and child nav graphs.
I have my code in Swift 3. I have two UITextFields and a UIButton in my view. I have the textFieldShouldEndEditing delegate of the first UITextField which is fired when I focus on the 2nd UITextField. However when I tap on the UIButton this delegate is not fired. I had thought that when a UIButton gets focus then the UITextFieldâs event will automatically fire since it is losing focus. However, when I tap on the UIButton, the cursor is still blinking in the UITextField, which means it is not losing focus.
Any ideas on why the UIButton is not getting focus will be most appreciated.
Thanks.
This is an infuriating aspect of developing forms on iOS. Button taps don't remove focus from UITextField / UITextView the same way they would in an HTML form.
I'm sure there are more elegant ways to solve this, but if I want to consistently do this across all forms I ensure that the following line of code is run when users tap on any buttons of mine.
- (void)userDidTapButton:(id)sender
{
[[[UITextField new] becomeFirstResponder] resignFirstResponder]
// the above embarrassing line of code basically creates a new textfield
// puts focus in it (thereby removing focus from any existing textfields
// and then removes focus again
// this ensures that delegate events fire on your existing textfields
// finally, run any other button code
}
Actually i'm developing a Apple Tv project in swift3.I faced some problem with focus navigation using remote.
Is there any way to change focus from one object to another object which i want.
I tried below code but not working......
override var preferredFocusedView: UIView? {
get {
return preferredFocusedView1
}
}
In the bellow screenshot i want to move focus from the pink icon to the star button when pressed down button of remote. how it is possible. plz provide me some code in details
I recently migrated my source code to Gtkmm 3.20. In this versiĂłn of gtk appears an automatic popover.
How I can remove this functionality? See image.
This is a new feature of GTK+ 3.20: if the GtkEntry sees touch events, which happens if you use a touchscreen and tap the entry, then it will automatically show that popover, which contains touch-friendly editing buttons (Paste is what you see there; I presume Cut, Copy, and Select All would be available on a non-password GtkEntry as well).
There is no way to turn that off, however it should only show up when you touch the GtkEntry; if you use keyboard or mouse navigation, it shouldn't show up. If it still does, you can report that as a bug to the GNOME Bugzilla.
It seems you are implementing a PIN entry field. I agree that in that case the popover isn't needed. You should state that case directly to the GTK+ developers then; maybe they will provide an API to turn the popover off (but it will not be part of GTK+ 3.20).
I have an MFC sdi app that uses a splitter window to contain a tree control alongside the main view showing the data.
When the user selects something in the tree, that view keeps focus until the user deliberately clicks in the main data window. This means that any toolbar buttons associated with the main view are disabled.
Is there any way to programmatically switch focus back to the main view after the user has clicked the tree control? Or am I doing something fundamentally wrong using a CSplitterWnd and 2 views?
You don't want to bring the focus back to the other view as soon as someone clicks the tree: It would make your app unusable. e.g. It would prevent users from navigating through the tree using the keyboard since the tree would never keep the focus long enough.
I you really want the toolbar to keep reflecting the state of your 2nd view (I'm not sure it's a good idea), you have a few options. Make your pick. 2 come to mind:
Your tree view should NOT be a CView. Use a simple CTreeCtrl. Not very nice because it kind of break the doc/view paradigm (e.g. no more tree's OnUpdate() called whenever an UpdateAllViews() is called).
Prevent the tree from becoming the active view. To do so:
2.a. When you view gets the focus (OnSetFocus()):
STATIC_DOWNCAST(CFrameWnd, AfxGetMainWnd())->SetActiveView(pTheOtherView);
2.b. Derive a CMySplitterWnd class from CSplitterWnd, then override CMySplitterWnd::SetActivePane() to prevent it from setting the treeview as the active view.
In all cases, welcome to the wonderful world of MFC internals where diving into the source code is the mandatory daily sport ;-)