NavigationLink inside a List and VStack does not work after the first pop - swift

I am trying to use the new Pull to Refresh feature in the latest version of SWiftUI which requires a List. Enclosing the VStack in a List causes the NavigationLink to work only once. Below is a simple version of the code without the Pull To Refresh part.
There is a question that was asked 68144891 on stackoverflow and there was a refrence to a known issue link which takes you to a page not found (https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-15-beta-release-notes)
Steps o reproduce
Tap "Press Me 1" or one of the items
Tap "Show Details"
Tap Back at the top
Tap "Press Me" again will not navigate to the next screen. A grey screen blocks when you tap
The app works without the VStack
struct ContentView: View {
var body: some View {
NavigationView {
List {
VStack { // commenting VStack works
Text("Options").font(.largeTitle).bold()
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}
struct SubView: View {
var counter: Int
#State private var showDetails = false
var body: some View {
VStack(alignment: .leading) {
Button("Show details") {
showDetails.toggle()
}
if showDetails {
Text("Clicked")
.font(.largeTitle)
}
}
}
}
Any help appreciated
Thanks much!

... follow-up to my comment
I assume you wanted this
struct ContentView: View {
var body: some View {
NavigationView {
List {
Section(Text("Options").font(.largeTitle).bold()) {
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}

Related

How do I prevent the user from switching to certain tabs in a SwiftUI TabView

I am creating sort of a game in swift that gives the user a new clue every time they guess incorrectly. I would like to have a TabView for the clues, but I don’t want the player to have access to every clue at the start. I was thinking either locking the tabs in the TabView until a function is called or making an array of different views in the TabView that can be edited.
One note before starting my reply: in this community you need to be clear about the issue and show examples of your code, error messages, points where you get stuck. Your answer is quite generic, but I will give you some hints.
Each Tab is a view (almost) like any other one, it can be hidden conditionally.
You can just hide the tabs until the user reaches the point in the game where the clues must be shown. In addition, or in alternative, you can switch to different views to see each clue.
See an example below (there's room for improvement, this is the idea):
struct Tabs: View {
#State private var showTab2 = false // This will trigger the tab to be shown
// It can be stored in your view model
var body: some View {
TabView {
Example(showNextClue: $showTab2)
.tabItem {Label("Tab 1", systemImage: "checkmark")}
if showTab2 { // Tab 2 is hidden until you change the state variable
Tab2()
.tabItem {Label("Tab 2", systemImage: "flag")}
}
Example(showNextClue: $showTab2)
.tabItem {Label("Tab 3", systemImage: "trash")}
}
}
}
struct Tab2: View {
#State private var clueIndex = 0 // What clue will you show now?
var body: some View {
VStack {
switch clueIndex { // Show the applicable clue
// You can iterate through an array of views as you proposed, chose the best method
case 0:
Text("Now you can see this clue number 1")
case 1:
Text("Clue number 2")
default:
Text("Clue \(String(clueIndex))")
}
Button {
clueIndex += 1
} label: {
Text("Show next clue")
}
.padding()
}
}
}
struct Example: View {
#Binding var showNextClue: Bool
var body: some View {
VStack {
Text("This is open")
.padding()
Button {
showNextClue.toggle()
} label: {
Text(showNextClue ? "Now you can see the next clue" : "Click here to see your next clue")
}
}
}
}
Unfortunately you cannot disable the standard TabBar selectors. But you can do your own custom ones, e.g. like this:
struct ContentView: View {
#State private var currentTab = "Home"
#State private var activeTabs: Set<String> = ["Home"] // saves the activated tabs
var body: some View {
VStack {
TabView(selection: $currentTab) {
// Tab Home
VStack(spacing: 20) {
Text("Home Tab")
Button("Unlock Hint 1") { activeTabs.insert("Hint 1")}
Button("Unlock Hint 2") { activeTabs.insert("Hint 2")}
}
.tag("Home")
// Tab 1. Hint
VStack {
Text("Your first Hint")
}
.tag("Hint 1")
// Tab 2. Hint
VStack {
Text("Your Second Hint")
}
.tag("Hint 2")
}
// custom Tabbar buttons
Divider()
HStack {
OwnTabBarButton("Home", imageName: "house.fill")
OwnTabBarButton("Hint 1", imageName: "1.circle")
OwnTabBarButton("Hint 2", imageName: "2.circle")
}
}
}
func OwnTabBarButton(_ label: String, imageName: String) -> some View {
Button {
currentTab = label
} label: {
VStack {
Image(systemName: imageName)
Text(label)
}
}
.disabled(!activeTabs.contains(label))
.padding([.horizontal,.top])
}
}

NavigationLink inside .searchable does not work

I understand its new, but this seems like pretty basic functionality that is not here. When implementing a .searchable in the new iOS 15, it would seem that a NavigationLink does not work, at all.
Ideally, the searchable would produce a filtered list with a ForEach and for each item, a Nav Link could take you to another view based on your selection. The ForEach and list works and looks beautiful, it just wont take you anywhere.
Watching WWDC21, they talk an awful lot about .searchable, but very little demonstration/example is given..
Here is a simple example, with no ForEach loop, that shows it does not work at all.. Am I missing something?
Any insight appreciated:
import SwiftUI
struct ContentView: View {
#State private var term = ""
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.searchable(text: $term) {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Goodbye!")
.padding()
}
}
NavigationLink must always be inside NavigationView, no matter what. If you want to put it outside, like inside .searchable, you should use programmatic navigation with isActive.
struct ContentView: View {
#State private var term = ""
#State private var isPresenting = false
var body: some View {
NavigationView {
/// NavigationView must only contain 1 view
VStack {
Text("Hello, world!")
.padding()
/// invisible NavigationLink
NavigationLink(destination: SecondView(), isActive: $isPresenting) { EmptyView()}
}
}
.searchable(text: $term) {
Button { isPresenting = true } label: {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Goodbye!")
.padding()
}
}
Result:

SwiftUI: Sheet gets dismissed immediately after being presented

I want to have a fullscreen SwiftUI View with a button in the Navigation Bar, which presents a SwiftUI Sheet above.
Unfortunately, the Compiler says: "Currently, only presenting a single sheet is supported.
The next sheet will be presented when the currently presented sheet gets dismissed."
This is my Code:
struct ContentView: View {
var body: some View {
EmptyView().fullScreenCover(isPresented: .constant(true), content: {
FullScreenView.init()
})
}
}
struct FullScreenView: View{
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
#State private var showingSheet = false
var body: some View {
Form {
Section(header: Text("Header")) {
NavigationLink(destination: UIKitView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
AddContentView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: AddContentView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}
}
The Problem
I want to show the SwiftUI View in Fullscreen, so I use fullScreenCover(...). With this first "Sheet", I cannot present a second sheet, my AddContentView() View. Is there any way how I can fix this? I really want to have this sheet above :(
Thanks for any help!!
Feel free to ask for other code or if there are ambiguities. :)
The error message says that the sheet cannot be displayed at the same time(Do not overlap sheets), so if you want to go to view and again to another view, you have to use a NavigationLink and only at the end .sheet()
.sheet(isPresented: $showingSheet){
AddContentView()
}
or fullScreenCover()
.fullScreenCover(isPresented: $showingSheet){
AddContentView()
}
Edited: Sheet is not overlapped twice in this code.
import SwiftUI
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView{
Form {
Section(header: Text("Header")) {
NavigationLink(destination: EmptyView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
EmptyView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: EmptyView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
p.s. fullScreenCover() belongs to the sheet

SwiftUI NavigationView adding extra view onto stack when sheet is dismissed

I have a List with ForEach using a NavigationLink that when tapped displays a detail view. The DetailsView includes a sheet to save the detail information into an array. After the save, the sheet is dismissed but an additional DetailsView is put on the navigation stack, so that I need to tap the back link twice to get back to the listing.
I'm likely doing something incorrect as I'm relatively new to swiftui, but can't determine what.
Three things of interest:
In the ListView, I use .navigationViewStyle(StackNavigationViewStyle()). When removed, the issue goes away but the iPad gets messy for the ListView.
I'm using insert(at: 0) to add data in my array because I want the most recent data at the top of the listing. If I use append instead, the issue does go away. Wanting the most recently saved item at the top of the list, I add a sort, however sorting causes the duplicate issue to reappear.
The issue only seems to occur when selecting the first item created in the list (the last in the array) and then saving a new item into the array.
steps:
click Tap Here First, then tap SAVE, enter a name then click Save.
click tab bar item Saved.
click on the list item from step 1 in the Saved Items listing (nav bar should show "< Saved Items").
click SAVE, enter another name then click Save. At this point, the duplicate view appears with "< Back" as the leading nav bar item, clicking it takes you to the original detail view, then clicking "< Saved Items" takes you to the list view.
What am I doing wrong or what should I be doing better?
xcode 12.4/iOS 14.1
Stripped down code to reproduce:
struct TestModel: Identifiable, Codable {
private(set) var id: UUID
var name: String
}
class AppData: ObservableObject {
#Published var testList = [TestModel]()
}
struct NewView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(item: TestModel(id: UUID(), name: ""))) {
Text("Tap here first")
}.navigationBarTitle("Main View", displayMode: .inline)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ListView: View {
#EnvironmentObject var appData: AppData
var body: some View {
NavigationView {
List {
ForEach(appData.testList) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
.navigationBarTitle("Saved Items", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle()) // remove this and issue goes away, but iPad gets "messy".
}
}
struct DetailView: View {
#State private var isSaveShowing = false
#State var item: TestModel
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 20) {
Text(item.name)
Button(action: {
isSaveShowing = true
}) {
Text("Save".uppercased())
}.sheet(isPresented: $isSaveShowing) {
SaveView(currentItem: item)
}
}
}
}
}
struct SaveView: View {
var currentItem: TestModel
#State private var name = ""
#EnvironmentObject var appData: AppData
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Name ".uppercased())
) {
TextField("Name (required)", text: $name)
}
}
.navigationBarItems(
trailing: Button(action: {
// appData.testList.append(TestModel(id: UUID(), name: name)) // using append instead of insert also resolves issue...
appData.testList.insert(TestModel(id: UUID(), name: name), at: 0)
presentationMode.wrappedValue.dismiss()
}) {
Text("Save")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView: View {
var appData = AppData()
var body: some View {
TabView {
NewView().tabItem {
Image(systemName: "rectangle.stack.badge.plus")
Text("Calculate")
}
ListView().tabItem {
Image(systemName: "tray.and.arrow.down")
Text("Saved")
}
}
.environmentObject(appData)
}
}
Apparently, this must have been a bug in SwiftUI.
Running the same code using Xcode 12.5 beta 3 with iOS 14.5 the issue no longer occurs.

SwiftUI: Dismiss View Within macOS NavigationView

As detailed here (on an iOS topic), the following code can be used to make a SwiftUI View dismiss itself:
#Environment(\.presentationMode) var presentationMode
// ...
presentationMode.wrappedValue.dismiss()
However, this approach doesn't work for a native (not Catalyst) macOS NavigationView setup (such as the below), where the selected view is displayed alongside the List.
Ideally, when any of these sub-views use the above, the list would go back to having nothing selected (like when it first launched); however, the dismiss function appears to do nothing: the view remains exactly the same.
Is this a bug, or expected macOS behaviour?
Is there another approach that can be used instead?
struct HelpView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination:
AboutAppView()
) {
Text("About this App")
}
NavigationLink(destination:
Text("Here’s a User Guide")
) {
Text("User Guide")
}
}
}
}
}
struct AboutAppView: View {
#Environment(\.presentationMode) var presentationMode
public var body: some View {
Button(action: {
self.dismissSelf()
}) {
Text("Dismiss Me!")
}
}
private func dismissSelf() {
presentationMode.wrappedValue.dismiss()
}
}
FYI: The real intent is for less direct scenarios (such as triggering from an Alert upon completion of a task); the button setup here is just for simplicity.
The solution here is simple. Do not use Navigation View where you need to dismiss the view.
Check the example given by Apple https://developer.apple.com/tutorials/swiftui/creating-a-macos-app
If you need dismissable view, there is 2 way.
Create a new modal window (This is more complicated)
Use sheet.
Following is implimenation fo sheet in macOS with SwiftUI
struct HelpView: View {
#State private var showModal = false
var body: some View {
NavigationView {
List {
NavigationLink(destination:
VStack {
Button("About"){ self.showModal.toggle() }
Text("Here’s a User Guide")
}
) {
Text("User Guide")
}
}
}
.sheet(isPresented: $showModal) {
AboutAppView(showModal: self.$showModal)
}
}
}
struct AboutAppView: View {
#Binding var showModal: Bool
public var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Dismiss Me!")
}
}
}
There is also a 3rd option to use ZStack to create a Modal Card in RootView and change opacity to show and hide with dynamic data.