hide TabView after clicking on a NavigationLink in SwiftUI - ios

when I have a TabView{} and the first Tab has a NavigationView, when I click on a Row, I want that TabView{} to disappear. How do I do that?
Same Issue here: How to hide the TabBar when navigate with NavigationLink in SwiftUI?
But unfortunately no solution.

There is no way to do that currently. For example, NavigationView responds to the .navigationBarHidden(_:) method on its descendants, but there is not an equivalent for TabView.
If this is something you'd like to see, let Apple know.

there's no way to hide TabView so I had to add TabView inside ZStack as this:
var body: some View {
ZStack {
TabView {
TabBar1().environmentObject(self.userData)
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
TabBar2()
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
}
if self.userData.showFullScreen {
FullScreen().environmentObject(self.userData)
}
}
}
UserData:
final class UserData: ObservableObject {
#Published var showFullScreen = false}
TabBar1:
struct TabBar1: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("TabBar 1")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.green)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
FullScreen:
struct FullScreen: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("FullScreen")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.red)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
check full code on Github
there's also some other ways but it depends on the structure of the views

To solve this limitation, I came out with this approach:
Created an enum to identify the tabs
enum Tabs: Int {
case tab1
case tab2
var title: String {
switch self {
case .tab1: return "Tab 1 Title"
case .tab2: return "Tab 2 Title"
}
}
var imageName: String {
switch self {
case .tab1: return "star" // Example using SF Symbol
case .tab2: return "ellipsis.circle"
}
}
}
Inside the view, such as ContentView.swift, added a property like this:
#State private var selectedTab = Tabs.tab1
Inside the body:
NavigationView {
TabView(selection: $selectedTab) {
ViewExample1()
.tabItem {
Image(systemName: Tabs.tab1.imageName)
Text(Tabs.tab1.title)
}.tag(Tabs.tab1)
ViewExample2()
.tabItem {
Image(systemName: Tabs.tab2.imageName)
Text(Tabs.tab2.title)
}.tag(Tabs.tab2)
}
.navigationBarTitle(selectedTab.title)
}
That's all. I hope this might be helpful.
Note: Just be aware this workaround hides the TabView in any and all child views, if you want to hide in just a particular view, this won't give you the result that you looking for.
Hopefully, Apple implements an (official and proper) option to hide the TabView soon.

Related

SwiftUI TabView inside a NavigationView

So I know it's not really encouraged to put a TabView inside a NavigationView and that you're supposed to do it the other way around. But the way I want my app I don't really see how I can have it another way...
So I have a login screen in which a user inputs their username, then only once I verify that everything is okay with username I wanna bring them over to a TabView(the search button is a navlink) I don't really see any other way to implement this but the problem is with my implementation is once I switch tabs in the tab view, the navigation title doesn't seem to change, and there also doesn't seem to be a navigation bar because when I scroll the old NavigationTitle gets drawn over by a Text View I have.
I'm not sure if adding code would help in this case because it seems this is just kind of a problem with TabViews inside NavigationViews but if someone wants me to show some code I can add an edit with it. I was just wondering if anyone had any ideas for how I could fix something like this or some other way to implement this?
Edit:
struct ContentView: View {
var body: some View {
NavigationView{
NavigationLink{
TabView{
ScrollView{
Text("Some view")
}
.tabItem{
Text("New View")
}
ScrollView{
Text("Another view")
}
.tabItem{
Text("Another view")
}
}
} label: {
Text("Go to new view!")
}
}
}
}
It is perfectly fine to have TabView() inside a NavigationView. Every time we switch between pages in any app, the navigating is mostly expected, so almost every view is inside the NavigtionView for this reason.
You can achieve this design with something like this (see the bottom images also):
struct LoginDemo: View {
#State var username = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your user name", text: $username)
.font(.headline)
.multilineTextAlignment(.center)
.frame(width: 300)
.border(.black)
NavigationLink {
GotoTabView()
} label: {
Text("search")
}
.disabled(username == "Admin" ? false : true)
}
}
.navigationTitle("Hey It's Nav View")
}
}
struct GotoTabView: View {
#State var temp = "status"
#State var selection = "view1"
var body: some View {
TabView(selection: $selection) {
Image("Swift")
.resizable()
.frame(width: 300, height: 300)
.tabItem {
Text("view 1")
}
.tag("view1")
Image("Swift")
.resizable()
.frame(width: 500, height: 500)
.tabItem {
Text("view 2")
}
.tag("view2")
}
.onChange(of: selection){ _ in
if selection == "view1" {
temp = "status"
}
else {
temp = "hero"
}
}
.toolbar{
ToolbarItem(placement: .principal) {
Text(temp)
}
}
}
}
NavigationView:
TabView:

Presenting a modal view sheet from a Sub view

I am trying to present a sheet from a sub view selected from the menu item on the navigation bar but the modal Sheet does does not display. I spent a few days trying to debug but could not pin point the problem.
I am sorry, this is a little confusing and will show a simplified version of the code to reproduce. But in a nutshell, the problem seems to be a sheet view that I have as part of the main view. Removing the sheet code from the main view displays the sheet from the sub view. Unfortunately, I don't have the freedom to change the Mainview.swift
Let me show some code to make it easy to understand....
First, before showing the code, the steps to repeat the problem:
click on the circle with 3 dots in the navigation bar
select the second item (Subview)
click on the "Edit Parameters" button and the EditParameters() view will not display
ContentView.swift (just calls the Mainview()). Included code to copy for reproducing issue :-)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Mainview()
}
}
}
}
Mainview.swift. This is a simplified version of the actual App which is quite complex and I don't have leeway to change much here unfortunately!
fileprivate enum CurrentView {
case summary, sub
}
enum OptionSheet: Identifiable {
var id: Self {self}
case add
}
struct Mainview: View {
#State private var currentView: CurrentView? = .summary
#State private var showSheet: OptionSheet? = nil
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
//Removing the below sheet view will display the sheet from the subview but with this sheet here, it the sheet from subview does not work. This is required as these action items are accessed from the second menu item (circle and arrow) navigation baritem
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
}) {
Label("Add Elements", systemImage: "plus")
}
} label: {
Image(systemName: "cursorarrow.click").resizable()
}
}
var trailingBarItems: some View {
Menu {
Button(action: {currentView = .summary}) {
Label("Summary", systemImage: "list.bullet.rectangle")
}
Button(action: {currentView = .sub}) {
Label("Subview", systemImage: "circle")
}
} label: {
Image(systemName: "ellipsis.circle").resizable()
}
}
#ViewBuilder
func sheetContent(for mode: OptionSheet) -> some View {
switch mode {
case .add:
AddElements()
}
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .summary:
SummaryView()
case .sub:
SubView()
}
}
}
}
Subview.swift. This is the file that contains the button "Edit Parameters" which does not display the sheet. I am trying to display the sheet from this view.
struct SubView: View {
#State private var editParameters: Bool = false
var body: some View {
VStack {
Button(action: {
editParameters.toggle()
}, label: {
HStack {
Image(systemName: "square.and.pencil")
.font(.headline)
Text("Edit Parameters")
.fontWeight(.semibold)
.font(.headline)
}
})
.padding(10)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.cornerRadius(20)
.sheet(isPresented: $editParameters, content: {
EditParameterView()
})
.padding()
Text("Subview....")
}
.padding()
}
}
EditParameters.swift. This is the view it should display when the Edit Parameters button is pressed
struct EditParameterView: View {
var body: some View {
Text("Edit Parameters...")
}
}
Summaryview.swift. Nothing special here. just including for completeness
struct SummaryView: View {
var body: some View {
Text("Summary View")
}
}
In SwiftUI, you can't have 2 .sheet() modifiers on the same hierarchy. Here, the first .sheet() modifier is on one of the parent views to the second .sheet(). The easy solution is to move one of the .sheets() so it's own hierarchy.
You could either use ZStacks:
var body: some View {
ZStack {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
}
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
or more elegantly:
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
.background(
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
)
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}

NavigationTitle visual glitches - transparent and not changing state from .large to .inline on scroll

The .navigationTitle on some views seem to be having some problems. On some views (and only some of the time), the .navigationTitle will not change from .large to .inline as would be expected. Instead, the title stays in place when scrolling up, and the navigation bar is completely invisible (as outlined in the video below). This is all reproducible every time.
Video of reproducible .navigationTitle bugs
I haven't found any people on stack overflow or the Apple Developer forums who have run into this exact issue. There have some people who have produced similar results as this, but those were all fixed by removing some stylizing code to the .navigationbar, of which I am not making any modifications to it anywhere in my code.
Below are some snippets of my code:
import SwiftUI
struct WelcomeUI: View {
var body: some View {
NavigationView {
VStack {
//NavigationLink(destination: SignupUI(), label: {
//Text("Sign Up")
//}
NavigationLink(destination: LoginUI(), label: {
Text("Log In")
})
}
}
}
}
struct LoginUI: View {
var body: some View {
VStack {
NavigationLink(destination: MainUI(), label: { Text("Log In") })
//Button(action: { ... }
}
.navigationBarHidden(false)
}
}
struct MainUI: View {
#State var selectedTab: Views = .add
var body: some View {
TabView(selection: $selectedTab) {
SpendingView()
.tabItem {
Image(systemName: "bag.circle")
Text("Spending")
}.tag(Views.spending)
Text("Adding View")
.tabItem {
Image(systemName: "plus")
Text("Add")
}.tag(Views.add)
Text("Edit View")
.tabItem {
Image(systemName: "pencil")
Text("Edit")
}.tag(Views.edit)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(Views.settings)
}
.navigationBarTitle(Text(selectedTab.rawValue))
.navigationBarBackButtonHidden(true)
}
}
enum Views: String {
case spending = "Spending"
case add = "Add"
case edit = "Edit"
case settings = "Settings"
}
struct SettingsView: View {
var body: some View {
VStack{
ZStack {
Form {
Section(header: Text("Section Header")) {
NavigationLink(destination: WelcomeUI()) {
Text("Setting Option")
}
}
Section {
//Button("Log Out") {
//self.logout()
//}
Text("Log Out")
}
}
Button("say-high", action: {print("Hi")})
}
}
}
}
struct SpendingView: View {
var body: some View {
ScrollView{
Text("SpendingView")
NavigationLink("subSpending", destination: SubSpendingView())
}.padding()
}
}
struct SubSpendingView: View {
var body: some View {
ScrollView{
Text("SubSpendingView")
}.navigationBarTitle("SubSpending")
}
}
It almost seems like a bug in SwiftUI itself just because the fact that bringing down the control centre makes it kind of work, but with no animation (as seen in the video). Also, changing which view is selected first in #State var selectedTab: Views seems to let the view selected to work as expected, but lets the rest of the tabs mess up.
When I build and run the app on my iPad, it behaves as expected with no bugs, it's only when run on my iPhone and the iOS simulator on Mac that it does this, any way to fix this?
For this to work flawlessly the ScrollView needs to be the direct child of the NavigationView. I ran into a similar issue with wanting to dismiss the TabView when I navigating but SwiftUI won't let that happen. Each tab needs to be a NavigationView and you need to dismiss the TabView creatively if that is what you want.
TabView {
NavigationView {
ScrollView {
// your view here
}
}.tabItem {
// tab label
}
// etc
}
Essentially the navigation view needs to be a child (in the brackets) of the tab view and the scrollview needs to be the direct child of the navigation view.
Use navigationBarTitle("Title") and navigationBarBackButtonHidden(true) on the TabView's sub-view, not on itself.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
}
.navigationBarTitle("Title")
.navigationBarBackButtonHidden(true)
}
}
}

Why onAppear called again after onDisappear while switching tab in TabView in SwiftUI?

I am calling API when tab item is appeared if there is any changes. Why onAppear called after called onDisappear?
Here is the simple example :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
Text("Home")
.navigationTitle("Home")
.onAppear {
print("Home appeared")
}
.onDisappear {
print("Home disappeared")
}
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView {
Text("Account")
.navigationTitle("Account")
.onAppear {
print("Account appeared")
}
.onDisappear {
print("Account disappeared")
}
}
.tabItem {
Image(systemName: "gear")
Text("Account")
}.tag(1)
}
}
}
Just run above code and we will see onAppear after onDisappear.
Home appeared
---After switch tab to Account---
Home disappeared
Account appeared
Home appeared
Is there any solution to avoid this?
It's very annoying bug, imagine this scenario:
Home view onAppear method contains a timer which is fetching data repeatedly.
Timer is triggered invisibly by switching to the Account view.
Workaround:
Create a standalone view for every embedded NavigationView content
Pass the current selection value on to standalone view as #Binding parameter
E.g.:
struct ContentView: View {
#State var selected: MenuItem = .HOME
var body: some View {
return TabView(selection: $selected) {
HomeView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(MenuItem.HOME)
AccountView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "gear")
Text("Account")
}
}
.tag(MenuItem.ACCOUNT)
}
}
}
enum MenuItem: Int, Codable {
case HOME
case ACCOUNT
}
HomeView:
struct HomeView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Home")
.onAppear(perform: {
if MenuItem.HOME == selectedMenuItem {
print("-> HomeView")
}
})
}
}
AccountView:
struct AccountView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Account")
.onAppear(perform: {
if MenuItem.ACCOUNT == selectedMenuItem {
print("-> AccountView")
}
})
}
}
To whom it may help.
Because this behaviour I only could reproduce on iOS 14+, I end up using https://github.com/NicholasBellucci/StatefulTabView (which properly only get called when showed; but don't know if it's a bug or not, but it works with version 0.1.8) and TabView on iOS 13+.
I'm not sure why you are seeing that behaviour in your App. But I can explain why I was seeing it in my App.
I had a very similar setup to you and was seeing the same behaviour running an iOS13 App on iOS14 beta. In my Home screen I had a custom Tab Bar that would animate in and out when a detail screen was displayed. The code for triggering the hiding of the Tab Bar was done in the .onAppear of the Detail screen. This was triggering the Home screen to be redrawn and the .onAppear to be called. I removed the animation and found a much better set up due to this bug and the Home screen .onAppear stopped being called.
So if you have something in your Account Screen .onAppear that has a visual effect on the Home Screen then try commenting it out and seeing if it fixes the issue.
Good Luck.
I have been trying to understand this behavior for a number of days now. If you are working with a TabView, all of your onAppears() / onDisapear() will fire immediately on app init and never again. Which actually makes since I guess?
This was my solution to fix this:
import SwiftUI
enum TabItems {
case one, two
}
struct ContentView: View {
#State private var selection: TabItems = .one
var body: some View {
TabView(selection: $selection) {
ViewOne(isSelected: $selection)
.tabBarItem(tab: .one, selection: $selection)
ViewTwo(isSelected: $selection)
.tabBarItem(tab: .two, selection: $selection)
}
}
}
struct ViewOne: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View One")
.onChange(of: isSelected) { _ in
if isSelected == .one {
// Do something
}
}
}
}
struct ViewTwo: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View Two")
.onChange(of: isSelected) { _ in
if isSelected == .two {
// Do something
}
}
}
}
View Modifier for custom TabView
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifer: ViewModifier {
let tab: TabBarItem
#Binding var selection: TabBarItem
func body(content: Content) -> some View {
content
.opacity(selection == tab ? 1.0 : 0.0)
.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
modifier(TabBarItemViewModifer(tab: tab, selection: selection))
}
}

Why SwiftUI context menu show all row view in preview?

I have a complex view in List row:
var body: some View {
VStack {
VStack {
FullWidthImageView(ad)
HStack {
Text("\(self.price) \(self.ad.currency!)")
.font(.headline)
Spacer()
SwiftUI.Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
Where FullWidthImageView is view with defined contexMenu modifier.
But when I long-press on an image I see not the only image in preview, but all row view.
There is no other contextMenu on any element.
How to make a preview in context with image only?
UPD. Here is a simple code illustrating the problem
We don't have any idea why in your case it doesn't work, until we see your FullWidthImageView and how you construct the context menu. Asperi's answer is working example, and it is correctly done! But did it really explain your trouble?
The trouble is that while applying .contextMenu modifier to only some part of your View (as in your example) we have to be careful.
Let see some example.
import SwiftUI
struct FullWidthImageView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
Image(systemName: model.toggle ? "pencil.and.outline" : "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
}.contextMenu(ContextMenu {
Button(action: {
self.model.toggle.toggle()
}) {
HStack {
Text("toggle image to?")
Image(systemName: model.toggle ? "trash" : "pencil.and.outline")
}
}
Button("No") {}
})
}
}
class Model:ObservableObject {
#Published var toggle = false
}
let modelStore = Model()
struct ContentView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
FullWidthImageView()
Text("Long press the image to change it").bold()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
while running, the "context menu" modified View seems to be "static"!
Yes, on long press, you see the trash image, even though it is updated properly while you dismiss the context view. On every long press you see trash only!
How to make it dynamic? I need that the image will be the same, as on my "main View!
Here we have .id modifier. Let see the difference!
First we have to update our model
class Model:ObservableObject {
#Published var toggle = false
var id: UUID {
UUID()
}
}
and next our View
FullWidthImageView().id(model.id)
Now it works as we expected.
For another example, where "standard" state / binding simply doesn't work check SwiftUI hierarchical Picker with dynamic data crashes
UPDATE
As a temporary workaround you can mimic List by ScrollView
import SwiftUI
struct Row: View {
let i:Int
var body: some View {
VStack {
Image(systemName: "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu {
Button("A") {}
Button("B") {}
})
Text("I don’t want to show in preview because I don’t have context menu modifire").bold()
}.padding()
}
}
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
ForEach(0 ..< 20) { (i) in
VStack {
Divider()
Row(i: i)
}
}
}
}
}
}
It is not optimal, but in your case it should work
Here is a code (simulated possible your scenario) that works, ie. only image is shown for context menu preview (tested with Xcode 11.3+).
struct FullWidthImageView: View {
var body: some View {
Image("auto")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu() {
Button("Ok") {}
})
}
}
struct TestContextMenu: View {
var body: some View {
VStack {
VStack {
FullWidthImageView()
HStack {
Text("100 $")
.font(.headline)
Spacer()
Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
}
}
}
}
It's buried in the replies here, but the key discovery is that List is changing the behavior of .contextMenu -- it creates "blocks" that pop up with the menu instead of attaching the menu to the element specified. Switching out List for ScrollView fixes the issue.

Resources