When starting my app for the first time, a little setup is required. After this setup, the user should then come to the MainView.
Now I want it to be so that the user cannot jump back to the last setup step as soon as he reaches the MainView.
All the solutions I can find is to simply remove the back button. But this is not enough for me, I'm currently working with tvOS, there is a back Button on his remote. If he then presses this, he should ideally just come back to the tvOS home screen.
Is there any possibility to delete the complete navigation stack or something similar with the same result?
You can replace your entire view hierarchy at the parent level (above your NavigationView if a given condition is true:
var body : some View {
if setup {
//parent component of views for once setup is done
MainView()
} else {
//parent component of the setup views (including a NavigationView)
SetupScreens()
}
}
So, on the final setup screen, set setup to true (this should probably be stored in an ObservableObject that each view can access [possible via an .environmentObject]) and the NavigationView will be destroyed and the hierarchy will be replaced with MainView.
Related
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")
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.
As you know, split view controller hides the master view and displays detail view in full screen mode in ipad. In the full screen mode, ios creates a bar button for the master view on the navigation bar. My question is, is it possible to reposition that button to the far right instead of left? Because my detail view is embedded inside a navigation view controller and there are severals views associated with it. It gets confusing when master view is hidden and the detail view has button to go back to the previous view.
In above screencap, "Category" is a button to display the masterview and "List of Events" is a back button. If you have better way to handle this situation, please feel free to suggest.
Yes, you can do it just send a NotificationCenter.default to the split view controller and change self.preferredDisplayMode in your splitview and coming to moving the category buttom u either can use the right bar button in navigationbar or create your custom navigation bar.
Hope this helps
For those who are having the same issue, I found a very simple solution. All you need to do is assign the rightBarButtonItems with leftBarButtonItems value and set the leftBarButtonItems to nil. Voila, that's about it.
if let leftButton = self.navigationItem.leftBarButtonItems {
self.navigationItem.rightBarButtonItems = leftButton
self.navigationItem.leftBarButtonItems = nil
}
I am using a StateManager to control the state of a pop-up modal (e.g., the states are "open.edit", "open.show" and "closed"). I'd like to use a state manager here because the modal is quiet complex and requires it's own transaction (I'm using Ember data).
I am able to set the appropriate data, view and controller on my ModalStateManager.
However, the view (in this case App.ModalView) is never rendered in the DOM. I know this because I've put logging statements in didInsertElement function of my App.ModalView, and those never get logged.
How can I render the view when someone clicks the button to open the modal?
Here's the code that is run on my ModalStateManager when someone clicks to open the modal.
App.ModalStateManager = Ember.State.create({
closed: Ember.State.create({
open: function(manager, modalData) {
var view = App.router.get('applicationController').connectOutlet("modal", modalData);
//this is working
//the view returned is the ModalView; it has a ModalController with expected content
manager.transitionTo('open.show');
}
})
//omitting other states for simplicity
)}
Larger question: How should you build a view that has multiple states and dynamic data, but does not have its own url or state within the router? e.g., Imagine a page with a list of unique items. Clicking an item pops open a modal that shows the item content, allowing the user to edit and save it. The modal doesn't have its own url or state in the router, so its not as easy as setting a dynamic state /:item_id in the router that can be easily wired and updated.
In one of our apps, we have a PanelManager (subclass of StateManager) that handles state for our modals. There is also a PanelController, which has properties that our panel container view binds to for className and visibility. When transitioning from closed to a particular open state (e.g. showingEditPanel), the manager sets the classname and visibility properties and calls connectOutlet on the panelController to show the correct view/controller combo within the panel container. Additional complexity can be modeled with nested states under each open state.
Hiya, I would like to know if there's an example of this anywhere. Basically I would like to push the UITabBar, and keeping with apple's guidelines I can only do this by placing the UITabBar(not the controller) in a viewController. Examples of this are in the Music selection on your iPhone/iTouch when you hit the "Now Playing" nav item, notice the tab bar pushes over.
This is somewhat of the flow I'm trying to accomplish
-----> Table (cell 1)----> Detail View
|
Navigation Controller ----> UITabBar-|----------> view 2
|
-----> view 3
So when the app launches I'm greeted with my tab bar and when I select a cell from the tableView the detail view is pushed onto the stack resulting in a possible customized button bar at the bottom of that view.
Another good example of this functionality is the [B]NYTimes [/B]app (it's free if you want to check it out)
Now I got the basics of this running, but I'm getting crashes when trying to wire IBOutlets to the tab items in IB. Would appreciate some insight on this.
Thx much!
It's pretty easy. The viewcontroller you push on the stack (the one which should hide the tabbar) should have the hidesBottomBarWhenPushed property set to YES.
viewcontrollerbeingpushed.hidesBottomBarWhenPushed = YES;