Navigating back to main menu View in SwiftUI with Alert Button - swiftui

I have an app with two defined views. I use NavigationView to move between them. I wanted the alert button to switch users back to Main Menu View and I have even found an answer, but after I used it, it produced the screen like this:
How can I let my app navigate back to the main view from the secondary view in a better way?
The code for the second view (first is just a NavigationLink pointing to the second):
import SwiftUI
struct GameView: View {
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
#State private var menuNavigation = false
var body: some View {
NavigationView{
ZStack{
NavigationLink(destination:ContentView(), isActive: $menuNavigation){
Text("")
}
VStack{
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
menuNavigation.toggle()
}
)
)
}
}
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}
Thanks for your help :)

To achieve what you are expecting, you actually need to add the NavigationView inside ContentView, if that's your main view. Because you navigate from ContentView to GameView, and what you are asking here is how to navigate back.
Applying the concept above, you can just dismiss GameView to go back to the in view.
Here is a sample code to achieve that:
Example of main view:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
GameView()
// This is how you hide the "<Back" button, so the user
// can navigate back only when tapping the alert
.navigationBarHidden(true)
} label: {
Text("Go to game view")
}
}
}
}
Example of game view:
struct GameView: View {
// This variable will dismiss the view
#Environment(\.presentationMode) var presentationMode
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
// No need to use this variable
// #State private var menuNavigation = false
var body: some View {
// No need to have a NavigationView
// NavigationView {
// ZStack has no function apparently...
// ZStack{
// No need to have a NavigationLink
// NavigationLink(destination:ContentView(), isActive: $menuNavigation){
// Text("")
//}
VStack {
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
// This is how you go back to ContentView
presentationMode.wrappedValue.dismiss()
}
)
)
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}

I have found a way to make the suggested approach even easier (without using the whole presentationMode syntax):
struct GameView: View {
#Environment(\.dismiss) var dismiss
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
var body: some View {
VStack {
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
dismiss()
}
)
)
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}

Related

SwiftUI onReceive is it possible to get oldValue?

I have a custom TabView and I want to Bind to a State to change tabs. I also want to detect if the user has tapped the same tab again in order to scroll to the top of that view.
didSet isn't called when I use a binding. onChange isn't called because the value hasn't changed, and onReceive doesn't give me the old value to compare.
Any ideas? (Trying to avoid using a published property)
struct ContentView: View {
#State private var scrollToTop1: Bool = false
#State private var scrollToTop2: Bool = false
#State private var selectedTab: Int = 1
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
NavigationView {
View1(scrollToTop: $scrollToTop1)
}
.tag(1)
NavigationView {
View2(scrollToTop: $scrollToTop2)
}
.tag(2)
}
.onReceive(Just(selectedTab)) { [oldValue = selectedTab] newValue in
print("Old: \(oldValue)") //Shows newValue
print("New: \(newValue)")
if oldValue == newValue {
switch selectedTab {
case 1:
scrollToTop1.toggle()
case 2:
scrollToTop2.toggle()
default:
break
}
}
}
TabBar(selectedTab: $selectedTab)
}
}
}
struct TabBar: View {
#Binding var selectedTab: Int
var body: some View {
HStack {
TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
}
.background(Color.green)
}
}
struct TabItem: View {
#Binding var selectedTab: Int
let text: String
let tab: Int
var body: some View {
Button {
selectedTab = tab
} label: {
Text(text)
}
.frame(maxWidth: .infinity)
.frame(height: 50)
}
}
I think this is a great scenario for a custom Binding, where you can intercept the value before its set and compare it:
struct ContentView: View {
#State private var scrollToTop1: Bool = false
#State private var scrollToTop2: Bool = false
#State private var selectedTab: Int = 1
var customBinding: Binding<Int> {
.init {
selectedTab
} set: { newValue in
print("New value: ", newValue)
if newValue == selectedTab {
print("Scroll to top")
}
selectedTab = newValue
}
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: customBinding) {
NavigationView {
Text("1")
}
.tag(1)
NavigationView {
Text("2")
}
.tag(2)
}
TabBar(selectedTab: customBinding)
}
}
}
struct TabBar: View {
#Binding var selectedTab: Int
var body: some View {
HStack {
TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
}
.background(Color.green)
}
}

navigationDestination(isPresented) with a path in swiftui 4

Im trying to pop back to a specific view point or the root view with navigationDestination(isPresented) being used to push views.
Here is a simpler version of the code I am working with
import SwiftUI
struct View1: View {
#State var goToView2 = false
#State var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("View 1")
Button("Go to View 2") {
goToView2 = true
}
}.navigationDestination(isPresented: $goToView2) {
View2(path: $path)
}
}
}
}
struct View2: View {
#State var goToView3 = false
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("View 2")
Button("Go to View 3") {
goToView3 = true
}
}.navigationDestination(isPresented: $goToView3) {
View3(path: $path)
}
}
}
struct View3: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("View 3")
Button("Go to View 1") {
print("Before: \(path.count)")
path = .init()
print("After: \(path.count)")
}
}
}
}
I don't exactly know what else to try. I've tried appending to the path value on change, but as expected, that does not work. Also, the path count is set to 0. Any help is appreciated
Here is the result of the previous code:
https://i.stack.imgur.com/QAWZN.gif

SwiftUI: NavigationView focus on last selected NavigationLink

maybe a very simple problem:
I use a navigation with a long list of entries. If the user returns from the navigationLink the list starts on the first item. How can I set the focus on the last selected navigationLink so the user don't need to scroll from the beginning again.
My app is for blind people so the scrolling from above isn't an easy thing.
´´´
struct CategoryDetailView: View {
#EnvironmentObject var blindzeln: BLINDzeln
#AppStorage ("version") var version: Int = 0
#State var shouldRefresh: Bool = false
#State private var searchText = ""
let categoryTitle: String
let catID: Int
var body: some View {
VStack{
List {
ForEach(blindzeln.results.filter { searchText.isEmpty || ($0.title.localizedCaseInsensitiveContains(searchText) || $0.textBody.localizedCaseInsensitiveContains(searchText)) }, id: \.entryID){ item in
NavigationLink(destination: ItemDetailViewStandard(item: item, isFavorite: false, catID: catID)) {DisplayEntryView(item: item, catID: catID)}.listRowSeparatorTint(.primary).listRowSeparator(.hidden)
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "") {}
.navigationTitle(categoryTitle)
.navigationBarTitleDisplayMode(.inline)
.listStyle(.inset)
}
.task(){
await blindzeln.decodeCategoryData(showCategory: categoryTitle)
}
.onAppear(){
blindzeln.resetData()
}
}
}
´´´
you could try this approach, using the List with selection, such
as in this example code. It does not scroll back to the beginning of the list
after selecting a destination.
struct ContentView: View {
#State private var selections = Set<Thing>()
#State var things: [Thing] = []
var body: some View {
NavigationStack {
List(things, selection: $selections){ thing in
NavigationLink(destination: Text("destination-\(thing.val)")) {
Text("item-\(thing.val)")
}
}
}
.onAppear {
(0..<111).forEach{things.append(Thing(val: $0))}
}
}
}
EDIT-1:
Since there are so many elements missing from you code, I can only guess
and suggest something like this:
struct CategoryDetailView: View {
#EnvironmentObject var blindzeln: BLINDzeln
#AppStorage ("version") var version: Int = 0
#State var shouldRefresh: Bool = false
#State private var searchText = ""
#State private var selections = Set<Thing>() // <-- same type as item in the List
let categoryTitle: String
let catID: Int
var body: some View {
VStack {
// -- here
List(blindzeln.results.filter { searchText.isEmpty || ($0.title.localizedCaseInsensitiveContains(searchText) || $0.textBody.localizedCaseInsensitiveContains(searchText)) },
id: \.entryID,
selection: $selections){ item in
NavigationLink(destination: ItemDetailViewStandard(item: item, isFavorite: false, catID: catID)) {
DisplayEntryView(item: item, catID: catID)
}
.listRowSeparatorTint(.primary).listRowSeparator(.hidden)
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "") {}
.navigationTitle(categoryTitle)
.navigationBarTitleDisplayMode(.inline)
.listStyle(.inset)
.task{
await blindzeln.decodeCategoryData(showCategory: categoryTitle)
}
.onAppear{
blindzeln.resetData()
}
}
}

SwiftUI: button in ToolbarItem(placement: .principal) not work after change it's label

Xcode 12 beta 6
There is a button in toolbar, its label text is binding to a state var buttonTitle. I want to tap this button to trigger a sheet view, select to change the binding var.
After back to content view, the button's title is updated. But if you tap the button again, it not work.
Code:
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: $buttonTitle)
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#Binding var buttonTitle: String
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
}
}
It is known toolbar-sheet layout issue, see also here. You can file another feedback to Apple.
Here is a workaround for your case - using callback to update toolbar item after sheet closed. Tested with Xcode 12b5.
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: buttonTitle) {
self.buttonTitle = $0
}
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#State private var buttonTitle: String
let callback: (String) -> ()
init(buttonTitle: String, callback: #escaping (String) -> ()) {
_buttonTitle = State(initialValue: buttonTitle)
self.callback = callback
}
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
.onDisappear {
callback(buttonTitle)
}
}
}
Move sheet(...) outside of ToolbarItem scope like this:
NavigationView {
..
}.sheet(...)

How to navigate out of a ActionSheet?

how to navigate out of a ActionSheet where I can only Pass a Text but not a NavigationLink?
Sample Code:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
Text("Test")
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [
.default(Text("How to navigate from here to HelpView???")),
])
}
}
}
}
You would need something like this:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
#State private var showingHelp = false
var body: some View {
NavigationView {
VStack {
Text("Test")
Button("Tap me") { self.showingSheet = true }
NavigationLink(destination: HelpView(isShowing: $showingHelp),
isActive: $showingHelp) {
EmptyView()
}
}
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [.cancel(),
.default(Text("Go to help")) {
self.showingSheet = false
self.showingHelp = true
}])
}
}
}
You have another state that programmatically triggers a NavigationLink (you could also do it using .sheet and modal presentation). You would also need to pass showingHelp as a #Binding to help view to be able to reset it.
struct HelpView: View {
#Binding var isShowing: Bool
var body: some View {
Text("Help view")
.onDisappear() { self.isShowing = false }
}
}