I've come to SwiftUI from UIKit and I'm having trouble with a NavigationLink not animating when presenting a new View.
I've set up the view structure to include the NavigationLink when the following property is non-nil:
#State private var retrievedDeviceIdentity: Proteus.DeviceIdentity?
The Proteus.DeviceIdentity type is a basic data struct. This property is populated by a successful asynchronous closure, rather than a direct user interaction. Hence, the view structure is set up like so, using NavigationLink's destination:isActive:label: initialiser:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
if let deviceIdentity = retrievedDeviceIdentity {
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
}
}
When retrievedDeviceIdentity is populated to be non-nil the new View is indeed presented. However, there is no slide transition to that View; it just changes immediately. When in that new view, tapping on the back button does do the slide transition back to this view.
Any ideas how to fix this? As I'm pretty new to SwiftUI if I've set the new structure up wrong then I'd welcome feedback on that too.
(I'm using Xcode 12.3 on macOS Big Sur 11.0.1.)
#Asperi got close, but moving the NavigationLink led to the view not presenting at all.
What did work was removing the if brace unwrapping retrievedDeviceIdentity:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
This required AddDeviceLinkDeviceForm's deviceIdentity property to be made optional to accept the wrapped value.
I think it is due to conditional injection, try instead to have it persistently included in view hierarchy (and so be registered in NavigationView), like
VStack {
Form {
// Form building
}
}
.background(
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: retrievedDeviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
)
Note: I'm not sure about your expectation for .onDisappear and why do you need it, probably it will be needed to move in some other place or under different modifier.
Related
I'd like to hide/show the details split view of a NavigationSplitView on macOS.
However NavigationSplitViewVisibility does not seem to have such option. Changing .navigationSplitViewColumnWidth() or .frame() has no effect on the details view although it works well with the content and list view.
NavigationSplitView {
List(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
Did Apple forget to implement such a feature? :/
Trying to figure out an answer to the same question for myself, I have come to this conclusion:
A NavigationSplitView is meant to display a hierarchy where each next level (sidebar, content, detail) is a sub-level of of the previous one. In such a structure you might always want to show a detail view, even it is empty.
In any case, even if that is not the logic, the way to make the "detail" part hidable would be by implementing a two-column navigation with NavigationSplitView and adding a DetailView, enclosing all of these in an HStack and making the DetailView visibility conditional:
struct MyView: View {
#State var showingDetail: Bool = true
var body: some View {
HStack {
NavigationSplitView {
SidebarView()
} detail: {
ContentView()
}
if showingDetail {
DetailView()
}
}
.toolbar {
Toggle(isOn: $showingDetail) {
Image(systemName: "sidebar.trailing")
}
.toggleStyle(.button)
}
}
}
By default the first list added to a view seems to be set to sidebar. Even if you don't specifically call .listStyle(SidebarListStyle()).
Is there any way that the list you set on the view (even if it's the first view) is not set to a sidebar? There is no indication on the Apple's documentation, and anything I have tried to style the list is not working.
So instead of the list looking like this:
It should look like:
Edit:
Yes, code. Any simple code will do.
struct ContentView: View {
var body: some View {
List {
NavigationLink(
destination: TextEView(),
) {
Text("One")
}
}
}
}
struct TextEView: View {
#State private var fullText: String = "This is some editable text..."
var body: some View {
TextEditor(text: $fullText)
}
}
For your case try
List {
NavigationLink(
destination: TextEView(),
) {
Text("One")
}
}
.listStyle(.plain) // << here !!
I am currently developing an app for watchOS 6 (independent app) using Swift/SwiftUI in XCode 11.5 on macOS Catalina.
Before a user can use my app, a configuration process is required. As the configuration process consists of several different views which are shown one after each other, I implemented this by using navigation links.
After the configuration process has been finished, the user should click on a button to return to the "main" app (main view). For controlling views which are on the same hierarchical level, my plan was to use an EnvironmentObject (as far as I understood, an EnvironmentObject once injected is handed over to the subviews and subviews can use the EnvironmentObject) in combination with a "controlling view" which controls the display of the views. Therefore, I followed the tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
This is my code:
ContentView.swift
struct ContentView: View {
var body: some View {
ContentViewManager().environmentObject(AppStateControl())
}
}
struct ContentViewManager: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
if(appStateControl.callView == "AppConfig") {
AppConfig()
}
if(appStateControl.callView == "AppMain") {
AppMain()
}
}
}
}
AppStateControl.swift
class AppStateControl: ObservableObject {
#Published var callView: String = "AppConfig"
}
AppConfig.swift
struct AppConfig: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Main")
NavigationLink(destination: DetailView1().environmentObject(appStateControl)) {
Text("Show Detail View 1")
}
}
}
}
struct DetailView1: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 1")
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
}
}
}
struct DetailView2: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 2")
Button(action: {
self.appStateControl.callView = "AppMain"
}) {
Text("Go to main App")
}
}
}
}
AppMain.swift
struct AppMain: View {
var body: some View {
Text("Main App")
}
}
In a previous version of my code (without the handing over of the EnvironmentObject all the time) I got a runtime error ("Thread 1: Fatal error: No ObservableObject of type AppStateControl found. A View.environmentObject(_:) for AppStateControl may be missing as an ancestor of this view.") caused by line 41 in AppConfig.swift. In the internet, I read that this is probably a bug of NavigationLink (see: https://www.hackingwithswift.com/forums/swiftui/environment-object-not-being-inherited-by-child-sometimes-and-app-crashes/269, https://twitter.com/twostraws/status/1146315336578469888). Thus, the recommendation was to explicitly pass the EnvironmentObject to the destination of the NavigationLink (above implementation). Unfortunately, this also does not work and instead a click on the button "Go to main App" in "DetailView2" leads to the view "DetailView1" instead of "AppMain".
Any ideas how to solve this problem? To me, it seems that a change of the EnvironmentObject in a view called via a navigation link does not refresh the views (correctly).
Thanks in advance.
One of the solutions is to create a variable controlling whether to display a navigation stack.
class AppStateControl: ObservableObject {
...
#Published var isDetailActive = false // <- add this
}
Then you can use this variable to control the first NavigationLink by setting isActive parameter. Also you need to add .isDetailLink(false) to all subsequent links.
First link in stack:
NavigationLink(destination: DetailView1().environmentObject(appStateControl), isActive: self.$appStateControl.isDetailActive) {
Text("Show Detail View 1")
}
.isDetailLink(false)
All other links:
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
.isDetailLink(false)
Then just set isDetailActive to false to pop all your NavigationLinks and return to the main view:
Button(action: {
self.appStateControl.callView = "AppMain"
self.appStateControl.isDetailActive = false // <- add this
}) {
Text("Go to main App")
}
Testing some stuff on tvOS with SwiftUI. When i add a custom style to a button, the "action" of the button is not getting triggered. In this example, i wanna print "pressed", but adding functions is not working either.
Is it possible to implement an custom action trigger aswell or what am i doing wrong?
For clarification why i wanna have a custom button style.
When i dont use any buttonstyle, the "action" is working, but the "onFocusChange" function is never getting called. WHICH I NEED!
But.. when i use a buttonstyle, the onFocusChange is working but the action is not.....
struct CustomButton: View {
#State private var buttonFocus: Bool = false
var body: some View {
VStack(alignment: .center){
Button(action: {
print("pressed")
})
{
Text("Save")
}
.buttonStyle(TestButtonStyle(focused: buttonFocus))
.focusable(true, onFocusChange: { (changed) in
self.buttonFocus.toggle()
})
}
}
}
Buttonstyle:
struct TestButtonStyle: ButtonStyle {
let focused: Bool
public func makeBody(configuration: TestButtonStyle.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.red))
.scaleEffect(focused ? 1.5 : 1.0)
}
}
Its working with SwiftUI 2.0 now.
You can use the following environment Object on your View you want to be focused.
Also, add a State or Binding to bubble up to your parent view.
#Environment(\.isFocused) var isFocused: Bool
#Binding var focusedValue: Bool
Then, you can call the following modifier, which gets called when the View is getting focused or not. Here you change your Binding or State variable.
.onChange(of: isFocused, perform: { value in
self.focusedValue = value
})
Finally, you can use your Binding or State to modify your View.
.scaleEffect(self.focused ? 1.1 : 1)
.animation(.linear(duration: 0.1))
I'm trying to do a simple SwiftUI navigation from one view to another and back using
a bar button item. I have tried three different approaches to calling a new view.
Using a Button in the body view works, but using NavigationBarItems in the navigation
bar fails in two different ways.
Here's the start view:
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
VStack {
Text("This is the content view")
.navigationBarTitle("Nav Title")
//this works ONCE only:
.navigationBarItems(trailing: Button(action: {self.showSecondView.toggle()}) {
Text("SecondView")
})
//this always fails on return to contentview with error:
//Tried to pop to a view controller that doesn't exist
// .navigationBarItems(trailing:
// NavigationLink(destination: SecondView()) {
// Text("SecondNav")
// }
// )
//This always works:
Button(action: {self.showSecondView.toggle()} ) {
Text("Call Modal Second View")
}.padding()
Text(self.showSecondView ? "true" : "false")
}.sheet(isPresented: $showSecondView) {
SecondView()
}
}
}
}
If I use a NavigationLink in the NavigationBarItems, the SecondView is displayed, but
on return to the ContentView, it crashes with the error: "Tried to pop to a view
controller that doesn't exist"
If I use a Button in the NavigationBarItems, the transition to the SecondView works
once and only once. The return to ContentView works but the button no longer functions.
Interestingly, If the first action taken is with the Button in the Body, the
NavigationBarItem does not work even once.
And the simple SecondView:
struct SecondView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
NavigationView {
VStack{
Text("This is the second view")
Button(action: { self.presentation.wrappedValue.dismiss()}) {
Text("Dismiss Modal")
}.padding()
}
}
}
}
I'm confused. Any guidance would be appreciated. Xcode 11.2 (11B44), Catalina 10.15
This is still an issue for me, I am having the same issue with popover (Modal) presentation and pushing Second controller via NavigationLink in navigationBarItems.
This is a really serious bug. The only way it works correctly is when the NavigationLink is inside NavigationView content and not navigationBarItems.
This is a really breaking as NavigationBar buttons are suppose to work that way.
I came across this issue today when I updated my Xcode to 11.2. According to this post it seems to be a bug with 13.2. I tested it on my actual iPhone X, which is still running 13.1.2, and it works just fine there.