SwiftUI: On tap gesture switches views - ios

I have the following simplified code in a project using XCode 13.2. The intended functionality is to have 2 tabs (Tab1 and Tab2), and the user is able to tap the screen on Tab2 to toggle the background from red to green.
Instead, however, when Tab2 is clicked, the screen displays tab 1, instead of Tab2 with the changed color. I'm wondering why the app goes to Tab1 when Tab2 is tapped.
There are no NavigationLinks or actions that make the user go to Tab1. My only thought is that maybe the app is rebuilding when Tab2 is clicked, which is why it goes back to the first tab? If so, are there any good workarounds for this?
Note: the color still changes on Tab2, but not before the app switches to Tab1.
struct ContentView: View {
var body: some View {
TabView {
Tab1()
.tabItem {
Text("Tab 1")
}
Tab2()
.tabItem {
Text("Tab 2")
}
}
}
}
struct Tab1: View {
var body: some View {
Text("Tab 1")
}
}
struct Tab2: View {
#State var isRed: Bool = false
var body: some View {
if !isRed {
Color.green
.onTapGesture {
isRed = true
}
} else {
Color.red
.onTapGesture {
isRed = false
}
}
}
}

The reason why tapping on Tab2 causes it to jump back to Tab1 is that the structural identity of Tab2 changes whenever you tap on it causing swiftUI to reload the body. See the explanation here.
A simple fix to the example you have above would be to wrap the entire body in a ZStack
var body: some View {
ZStack {
if !isRed {
Color.green
.onTapGesture {
isRed = true
}
} else {
Color.red
.onTapGesture {
isRed = false
}
}
}
}

I have noticed that using if/else in that way causes strange behavior. Refactor Tab2 as shown below and it should work as expected.
struct Tab2: View {
#State var isRed: Bool = false
var body: some View {
ZStack {
Color.red
Color.green
.opacity(isRed ? 0 : 1)
}
.onTapGesture {
isRed.toggle()
}
}
}

Related

SwiftUI - Crash when presenting .sheet in minimal project that includes .toolbar

I've run into a weird crash when presenting a sheet under certain conditions.I've been able to reproduce it in a starter project and have had confirmation from multiple people that it crashes on their end as well. The strange thing is, that for some simulators / users, these steps don't crash the app.
I've filed a Bug Report with Apple (FB9064549), but thought I'd ask it here as well.
The steps to reproduce the crash are as follows:
Click the "Open Detail View" button
Go back
Click the "Open Detail View" button again
Click the "Present Modal"
It will crash
The basic content view has a toolbar with a Menu (or just a normal Text) in it and a NavigationLink to push to a new page. If I comment out the .toolbar, the crash does not happen.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(
destination: DetailView(),
label: {
Text("Open Detail View")
})
}
// If you comment this out, it does not crash
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
ForEach(1...5, id: \.self) { index in
Button {
print("tap menu item")
} label: {
HStack {
Text("Menu Item \(index)")
}
}
}
} label: {
Text("Filter")
}
}
}
}
}
}
The presented DetailView can present a .sheet through a binding item. If I use the FullScreenCover instead, it does not crash.
struct DetailView: View {
#State var modalType: ModalType?
var body: some View {
Button(action: {
modalType = .modalWithTabView
}, label: {
Text("Present Modal")
})
.sheet(item: $modalType, content: { $0 })
}
}
enum ModalType: Identifiable, View {
case modalWithTabView
var id: String {
return "modalWithTabView"
}
var body: some View {
switch self {
case .modalWithTabView:
ModalWithTabView()
}
}
}
The sheet that is presented is a basic TabView pagination. If I don't add the PageTabViewStyle style, it does not crash.
struct ModalWithTabView: View {
#State var currentStep = 0
var body: some View {
TabView(selection: $currentStep) {
ForEach (0 ..< 10) { index in
Text("Page \(index)")
.tag(index)
}
}
// If you comment this out, it does not crash
.tabViewStyle(PageTabViewStyle())
}
}
If anyone has any pointers please let me know!

Custom TabView navigation in SwiftUI

I am trying to create a a custom TabView for my app. I have followed a tutorial for this but i am unavble to get the view to change depending on what button is pressed. My code is below for a button displayed on my TabView. when this is pressed i want HomeView to show and when the Account button is pressed to show AccountView etyc etc.
I was wondering how i can get around this. I have tried using NavLinks but with no luck as i unable to use the animation.
I am new to SwiftUI and trying to learn as i go.
Thank you
Button{
withAnimation{
index = 0
}
}
label: {
HStack(spacing: 8){
Image(systemName: "house.fill")
.foregroundColor(index == 0 ? .white : Color.black.opacity(0.35))
.padding(10)
.background(index == 0 ? Color("BrightGreen") : Color.clear)
.cornerRadius(8)
Text(index == 0 ? "Home" : "")
.foregroundColor(.black)
}
}
You can use #State variable to decide which view should be shown to user. Then depending on what button is tapped set value of this variable. I've made really simple code to show the idea.
// Views to show
struct HomeView: View {
var body: some View {
Text("Home View")
}
}
struct AccountView: View {
var body: some View {
Text("Account View")
}
}
// Enum containing views available in tabbar
enum ViewToDisplay {
case home
case account
}
struct ContentView: View {
#State var currentView: ViewToDisplay = .home
var body: some View {
VStack {
switch currentView{
case .home:
HomeView()
case .account:
AccountView()
}
Spacer()
// Tabbar buttons
HStack {
Button(action: { currentView = .home }) { Text("Home") }
Button(action: { currentView = .account }) { Text("Account") }
}
}
}
}
It works like this:

How to replace the current view in SwiftUI?

I am developing an app with SwiftUI.
I have a NavigationView and I have buttons on the navigation bar. I want to replace the current view (which is a result of a TabView selection) with another one.
Basically, when the user clicks "Edit" button, I want to replace the view with another view to make the edition and when the user is done, the previous view is restored by clicking on a "Done" button.
I could just use a variable to dynamically choose which view is displayed on the current tab view, but I feel like this isn't the "right way to do" in SwiftUI. And this way I could not apply any transition visual effect.
Some code samples to explain what I am looking for.
private extension ContentView {
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 3 {
Button(action: {
print("Edit pressed")
// Here I want to replace the tabSelection 3 view by another view temporarly and update the navigation bar items
}) {
Text("Edit")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
ContactPage()
.tabItem {
Text("1")
}
.tag(1)
Text("Chats")
.tabItem() {
Text("2")
}
.tag(2)
SettingsView()
.tabItem {
Text("3")
}
.tag(3)
}.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
Thank you
EDIT
I have a working version where I simply update a toggle variable in my button action that makes my view display one or another thing, it is working but I cannot apply any animation effect on it, and it doesn't look "right" in SwiftUI, I guess there is something better that I do not know.
If you just want to add animations you can try:
struct ContentView: View {
...
#State var showEditView = false
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
view3
.tabItem {
Text("3")
}
.tag(3)
}
.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
private extension ContentView {
var view3: some View {
VStack {
if showEditView {
FormView()
.background(Color.red)
.transition(.slide)
} else {
Text("View 3")
.background(Color.blue)
.transition(.slide)
}
}
}
}
struct FormView: View {
var body: some View {
Form {
Text("test")
}
}
}
A possible alternative is to use a ViewRouter: How To Navigate Between Views In SwiftUI By Using An #EnvironmentObject.

SwiftUI button inactive inside NavigationLink item area

I have a view for a list item that displays some news cards within a navigationLink.
I am supposed to add a like/unlike button within each news card of navigationLink, without being took to NavigationLink.destination page.
It seems like a small button inside a big button.
When you click that small one, execute the small one without executing the bigger one.
(note: the click area is covered by the two buttons, smaller one has the priority)
(In javascript, it seems like something called .stopPropaganda)
This is my code:
var body: some View {
NavigationView {
List {
ForEach(self.newsData.newsList, id:\.self) { articleID in
NavigationLink(destination: NewsDetail(articleID: articleID)) {
HStack {
Text(newsTitle)
Button(action: {
self.news.isBookmarked.toggle()
}) {
if self.news.isBookmarked {
Image(systemName: "bookmark.fill")
} else {
Image(systemName: "bookmark")
}
}
}
}
}
}
}
}
Currently, the button action (like/dislike) will not be performed as whenever the button is pressed, the navigationLink takes you to the destination view.
I have tried this almost same question but it cannot solve this problem.
Is there a way that makes this possible?
Thanks.
as of XCode 12.3, the magic is to add .buttonStyle(PlainButtonStyle()) or BorderlessButtonStyle to the button, when said button is on the same row as a NavigationLink within a List.
Without this particular incantation, the entire list row gets activated when the button is pressed and vice versa (button gets activated when NavigationLink is pressed).
This code does exactly what you want.
struct Artcle {
var text: String
var isBookmarked: Bool = false
}
struct ArticleDetail: View {
var article: Artcle
var body: some View {
Text(article.text)
}
}
struct ArticleCell: View {
var article: Artcle
var toggle: () -> ()
#State var showDetails = false
var body: some View {
HStack {
Text(article.text)
Spacer()
Button(action: {
self.toggle()
}) {
Image(systemName: article.isBookmarked ? "bookmark.fill" : "bookmark").padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.overlay(
NavigationLink(destination: ArticleDetail(article: article), isActive: $showDetails) { EmptyView() }
)
.onTapGesture {
self.showDetails = true
}
}
}
struct ContentView: View {
#State var articles: [Artcle]
init() {
_articles = State(initialValue: (0...10).map { Artcle(text: "Article \($0 + 1)") })
}
func toggleArticle(at index: Int) {
articles[index].isBookmarked.toggle()
}
var body: some View {
NavigationView {
List {
ForEach(Array(self.articles.enumerated()), id:\.offset) { offset, article in
ArticleCell(article: article) {
self.toggleArticle(at: offset)
}
}
}
}
}
}

SwiftUI Modal Sheet Re-Opening After Dismiss

I have a list in a Navigation View, with a trailing navigation button to add a list item. The button opens a modal sheet. When I dismiss the sheet (by pulling it down), the sheet pops right back up again automatically and I can't get back to the first screen. Here's my code.
struct ListView: View {
#ObservedObject var listVM: ListViewModel
#State var showNewItemView: Bool = false
init() {
self.listVM = ListViewModel()
}
var body: some View {
NavigationView {
List {
ForEach(listVM.items, id: \.dateCreated) { item in
HStack {
Text(item.name)
Spacer()
Image(systemName: "arrow.right")
}
}
}
.navigationBarTitle("List Name")
.navigationBarItems(trailing: AddNewItemBtn(isOn: $showNewItemView))
}
}
}
struct AddNewItemBtn: View {
#Binding var isOn: Bool
var body: some View {
Button(
action: { self.isOn.toggle() },
label: { Image(systemName: "plus.app") })
.sheet(
isPresented: self.$isOn,
content: { NewItemView() })
}
}
I am getting this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c603b7c22SheetHostingControllerVS_7AnyView_: 0x7fc5e0c1f8f0> on which is already presenting (null)
I've tried toggling the bool within "onDismiss" on the button itself, but that doesn't work either. Any ideas?
Turns out putting the button in the navigationBarItems(trailing:) modifier is the problem. I just put the button in the list itself instead of in the nav bar and it works perfectly fine. Must be some kind of bug.

Resources