Can't do a Simple Navigate to View and back SwiftUI Navigation Bar Button - xcode

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.

Related

SwiftUI: How to change visibility of details NavigationSplitView?

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)
}
}
}

Animation between views not working when using NavigationLink isActive

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.

Is there a reliable workaround for onDisappear() not working within .sheet() or .popover() in SwiftUI on macOS?

I'm building an app that shares quite a bit of SwiftUI code between its iOS and macOS targets. On iOS, onDisappear seems to work reliably on Views. However, on macOS, onDisappear doesn't get called if the View is inside a sheet or popover.
The following code illustrates the concept:
import SwiftUI
struct ContentView: View {
#State private var textShown = true
#State private var showSheet = false
#State private var showPopover = false
var body: some View {
VStack {
Button("Toggle text") {
self.textShown.toggle()
}
if textShown {
Text("Text").onDisappear {
print("Text disappearing")
}
}
Button("Toggle sheet") {
self.showSheet.toggle()
}.sheet(isPresented: $showSheet, onDismiss: {
print("On dismiss")
}) {
VStack {
Button("Close sheet") {
self.showSheet = false
}
}.onDisappear {
print("Sheet disappearing")
}
}
Button("Toggle popover") {
self.showPopover.toggle()
}.popover(isPresented: $showPopover) {
VStack {
Text("popover")
}.onDisappear {
print("Popover disappearing")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Note that onDisappear works fine on the Text component at the beginning of the VStack but the other two onDisappear calls don't get executed on macOS.
One workaround I've found is to attach an ObservableObject to the View and use deinit to call cleanup code. However, this isn't a great solution for two reasons:
1) With the popover example, there's a significant delay between the dismissal of the popover and the deist call (although it works quickly on sheets)
2) I haven't had any crashes on macOS with this approach, but on iOS, deinit have been unreliable in SwiftUI doing anything but trivial code -- holding references to my data store, app state, etc. have had crashes.
Here's the basic approach I used for the deinit strategy:
class DeinitObject : ObservableObject {
deinit {
print("Deinit obj")
}
}
struct ViewWithObservableObject : View {
#ObservedObject private var deinitObj = DeinitObject()
var body: some View {
Text("Deinit view")
}
}
Also, I would have thought I could use the onDismiss parameter of the sheet call, but that doesn't get called either on macOS. And, it's not an available parameter of popover.
All of this is using Xcode 11.4.1 and macOS 10.15.3.
Any solutions for good workarounds?

SwiftUI: Action of custom button not triggered

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))

Select SwiftUI cell in Popover

I am trying to make a popover in SwiftUI using a UIHostingController with a list that can be tapped. First, the user name and password should be filled in, and then the user role should be tapped in the list, and the popover should be dismissed when the save button is tapped.
Also, the save button in the navigation bar should be disabled until the user information has been verified.
The Xcode playground for this can be fetched from my GitHub repository https://github.com/imyrvold/Popover
To be able to use the AddUserView as a rootView in UIHostingController, I had to use an Xcode storyboard, and add it to the Resources in the Xcode Playground.
import SwiftUI
import Combine
public struct AddUserView : View {
#ObjectBinding public var loginInfo: LoginInfo
#EnvironmentObject var viewModel: RoleViewModel
#State var selectedRole: Role? = nil
#Environment(\.isPresented) var isPresented: Binding<Bool>?
public var body: some View {
NavigationView {
VStack {
TextField(self.$loginInfo.firstName, placeholder: Text("First Name"))
TextField(self.$loginInfo.lastName, placeholder: Text("Last Name"))
TextField(self.$loginInfo.email, placeholder: Text("Email"))
SecureField(self.$loginInfo.password, placeholder: Text("Password"))
Divider()
List(self.viewModel.roles) { role in
RoleCell(role: role).tapAction {
self.selectedRole = role
}
}
}
.padding()
.navigationBarTitle(Text("Add User"))
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
})//.disabled(!self.loginInfo.isValid)
}
}
// MARK:- Action methods
func saveAction() {
}
}
The first problem I have is that when I uncomment the disabled(!self.loginInfo.isValid), all the TextField's are also disabled. Not sure if that is a bug in SwiftUI?
I also want to have the rolecell set the checkmark on the cell when tapped, but so far I have been unable to figure out how to do that.
And how can I dismiss the Popover when the save button is tapped?
(When running the playground, have to click the start playground a second time to run properly, the first time the Save popover doesn't work).
Have you tried this
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
}.disabled(!self.loginInfo.isValid))

Resources