SwiftUI NavigationView, going back if NavigationLink is inside a NavigationBarItem - swiftui-navigationlink

As there are some problems with iOS 13.4 and Xcode 11.4 with presentationMode.wrappedValue.dismiss() I am looking for an alternative approach to go back programmatically. I found this solution from MScottWaller:
iOS SwiftUI: pop or dismiss view programmatically
Unfortunately, in my case it does not work:
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
Text("MasterView")
.navigationBarItems(trailing: HStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Image(systemName: "tag")
.padding(.leading, 4)
}
})
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
If the NavigationLink is inside a navigationBarItem, I can't go back from my DetailView. I don't know if it is a bug or there are other reasons why NavigationLink does not work in the same way inside a navigationBarItem.
As a workaround I use this variant with a empty NavigationLink inside the view. It works, but I don't like this:
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
Text("MasterView")
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
EmptyView()
}
.navigationBarItems(trailing: HStack {
Button(action: { self.showDetail.toggle() }) {
Image(systemName: "tag")
.padding(.leading, 4)
}
})
}
}
}
Any ideas why the NavigationLink does not correct work inside a navigationBarItem?

It's an iOS bug.
https://forums.developer.apple.com/thread/125937
The work around is to toggle a NavigationLink hidden outside of nav bar:
struct Parent: View {
#State private var showingChildView = false
var body: some View {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: Child(),
isActive: self.$showingChildView)
{ Text("HiddenLink").hidden() }
}
.navigationBarItems(trailing: Button(action:{ self.showingChildView = true }) { Text("Next") })
}
}
}

Related

Better way to raise number pad SwiftUI

Upon navigating to a view, I want the number pad to already be raised. Right now I have a solution that works the first time (albeit with a delay) but fails to raise the number pad if the user navigates back a second time. Is there a better way to raise the number pad in SwiftUI (or to have it always up)?
Example Code:
struct ParentView: View {
#FocusState var numberPadFocused: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
Button {
numberPadFocused = true
isActive = true
print("Called")
} label: {
Text("Navigate")
}
NavigationLink(destination: ChildView(focusState: $numberPadFocused), isActive: $isActive) { Color.white }
}
}
}
}
struct ChildView: View {
#State var text: String = ""
#FocusState.Binding var focusState: Bool
var body: some View {
TextField("Enter Number...", text: $text)
.keyboardType(.numberPad)
.focused($focusState)
}
}

How do I change the variable value in a different file in Swiftui

I set a variable in the contentview #State var shouldShowModal = false, i want to change it once i press a button shouldShowModal = false. I keep getting Cannot find 'shouldShowModal' in scope.
Here is a working example, passing the value through #Bindings. Read more about #Binding here, or the official documentation.
This means that you can now do shouldShowModal = false with the binding, which will also update the body of the original view containing #State.
struct ContentView: View {
#State private var shouldShowModal = false
var body: some View {
VStack {
Text("Hello world!")
.sheet(isPresented: $shouldShowModal) {
Text("Modal")
}
OtherView(shouldShowModal: $shouldShowModal)
}
}
}
struct OtherView: View {
#Binding var shouldShowModal: Bool
var body: some View {
VStack {
Text("Should show modal: \(shouldShowModal ? "yes" : "no")")
Toggle("Toggle modal", isOn: $shouldShowModal)
}
}
}

Back button skipping a view in View1 -> View2 -> View3 NavigationLink

I'm fairly new to Swift and it may be I'm going about this all wrong, but I have a bug I can't work out - within a NavigationView, a NavigationLink in View1 opens to View 2, then that view has a NavigationLink to View3. View3 shows the title from View1, which is obviously wrong, and when the back button is pressed it also goes back to View1 rather than View2. Am I using these links wrong or is it a SwiftUI issue? Simplified example of what I'm doing below.
struct FirstView: View {
var body: some View {
Text("Hello, World #1!")
.navigationBarItems(trailing:
NavigationLink(destination: SecondView()){
Image(systemName: "folder.badge.plus")
}
)
}
}
struct SecondView: View {
var body: some View {
Text("Hello, World #2!")
.navigationBarItems(trailing:
NavigationLink(destination: ThirdView()){
Image(systemName: "folder.badge.plus")
}
)
}
}
struct ThirdView: View {
var body: some View {
Text("Hello, World #3!")
}
}
Funny enough I had the same problem with the navigation skipping one view. Your problem is different though. The first view only will have the NavigationView declared. After that you just use navigation links.
Refactoring:
import SwiftUI
struct FirstView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, World #1!")
.padding()
NavigationLink(destination: SecondView()){
Image(systemName: "folder.badge.plus")
}
}
// a navigationBarTitle modifier inside!
.navigationBarTitle(Text("Hello"))
}
}
}
struct SecondView: View {
var body: some View {
VStack {
Text("Hello, World #2!")
.padding()
NavigationLink(destination: ThirdView()){
Image(systemName: "folder.badge.plus")
}
}
}
}
struct ThirdView: View {
var body: some View {
Text("Hello, World #3!")
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}

SwiftUI doubly nested NavigationLink views not responding to changing ObservedObject

I've created three views. I'm passing ObservedObject state to each view in sequence. When I change the state in the last view (AnotherView2), my application does not show the Text view with 'YES IT IS FINISHED!'. However, if I uncomment this line in AnotherView
self.userDefaultsManager.setupComplete = true
it works as I expected by showing the text.
struct ContentView: View {
#ObservedObject var userDefaultsManager = UserDefaultsManager()
#State var showAnotherView: Bool = false
var body: some View {
NavigationView {
VStack {
if !userDefaultsManager.setupComplete {
Button(action: {
self.showAnotherView.toggle()
}) {
Text("Show Another View")
}
NavigationLink(destination: AnotherView(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView, label: {
EmptyView()
})
} else {
Text("YES IT IS FINISHED!")
}
}
}
}
}
struct AnotherView: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
#State var showAnotherView2: Bool = false
var body: some View {
VStack {
Button(action: {
//self.userDefaultsManager.setupComplete = true
self.showAnotherView2 = true
}, label: {
Text("Press")
})
NavigationLink(destination: AnotherView2(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView2, label: {
EmptyView()
})
}
}
}
struct AnotherView2: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
var body: some View {
Button(action: {
self.userDefaultsManager.setupComplete = true
}, label: {
Text("Just Do It")
})
}
}
class UserDefaultsManager: ObservableObject {
#Published var setupComplete: Bool = UserDefaults.standard.bool(forKey: "setupComplete") {
didSet { UserDefaults.standard.set(self.setupComplete, forKey: "setupComplete") }
}
}
Can someone help me understand what is wrong with my code or the API that it won't work on double nested calls to show views in this manner?
EDIT: Using XCode 11.3 & iOS 13.3.1
I suppose here is a layout that gives required behaviour
var body: some View {
Group {
if !userDefaultsManager.setupComplete {
NavigationView {
VStack {
Button(action: {
self.showAnotherView.toggle()
}) {
Text("Show Another View")
}
NavigationLink(destination: AnotherView(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView, label: {
EmptyView()
})
}
}
} else {
Text("YES IT IS FINISHED!")
}
}
}

How to disable ScrollView Bounce In SwiftUI

Any Modifier available to stop bounce of ScrollView in swiftUI ?
struct RoomDetailsView: View {
var body: some View {
ScrollView(showsIndicators: false) {
Image("test")
Text("Hello Text")
...
...
}
}
}
I tried below code but it not work for me. looks like it deprecated
ScrollView(alwaysBounceVertical: true) {
Image("test")
Text("Hello Text")
...
...
}
try using this line of code:
UIScrollView.appearance().bounces = false
You can use it like this:-
struct RoomDetailsView: View {
init() {
UIScrollView.appearance().bounces = false
}
var body: some View {
ScrollView(showsIndicators: false) {
Image("test")
Text("Hello Text")
...
...
}
}
}
Or you can write this line in AppDelegate to apply this behaviour throughout into your app.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIScrollView.appearance().bounces = false
}
You may use SwiftUI-Introspect library:
ScrollView {
// some content
}
.introspectScrollView { scrollView in
scrollView.alwaysBounceVertical = false
}
Set the ScrollView's showsIndicators parameter equal to false so the user interacting with your view doesn't activate the scroll indicator (it can happen even without scrolling enabled).
ScrollView(showsIndicators: false)
In the onAppear modifier for the ScrollView, add this line.
UIScrollView.appearance().bounces = false
In the onDisappear modifier for the ScrollView, add this line.
UIScrollView.appearance().bounces = true
If you set UIScrollView.appearance().bounces equal to false in the init, it will prevent all of your app's ScrollViews from bouncing. By setting it equal to true in the onAppear modifier and equal to false in the onDisappear modifier, you ensure that it only effects the one ScrollView.
Your ScrollView should look something like this.
ScrollView(showsIndicators: false) {
...
...
...
}
.onAppear {
UIScrollView.appearance().bounces = false
}
.onDisappear {
UIScrollView.appearance().bounces = true
}
A better solution would be to use viewbuilder and create your own scrollview that doesn't bounce when the content size is less than scrollview frame size.
import SwiftUI
struct BounceVerticalDisableScrollView<Content: View>: View {
#State private var alwaysBounceVertical = false
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
GeometryReader { scrlViewGeometry in
ScrollView {
content
.background(
GeometryReader {
// calculate height by clear background
Color.clear.preference(key: SizePreferenceKey.self,
value: $0.frame(in: .local).size.height)
}.onPreferenceChange(SizePreferenceKey.self) {
self.alwaysBounceVertical = $0 < scrlViewGeometry.size.height
}
)
}
// disable scroll when content size is less than frame of scrollview
.disabled(self.alwaysBounceVertical)
}
}
}
// return size
public struct SizePreferenceKey: PreferenceKey {
public static var defaultValue: CGFloat = .zero
public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
So, that you can use the scrollview as:
var body: some View {
VStack (alignment: .leading, spacing: 10) {
BounceVerticalDisableScrollView{
VStack (alignment: .leading, spacing: 10) {
....
....
}.padding()
}
}
}
Apple introduced an new modifier named scrollBounceBehavior with iOS 16.4 that can be used to prevent the ScrollView from bouncing when the content is smaller than the screen.
https://developer.apple.com/documentation/swiftui/view/scrollbouncebehavior(_:axes:)
struct RoomDetailsView: View {
var body: some View {
ScrollView(showsIndicators: false) {
Image("test")
Text("Hello Text")
...
...
}
.scrollBounceBehavior(.basedOnSize)
}
}