SwiftUI: Problems with List inside TabViews inside NavigationView - swift

I want to place a TabView inside a NavigationView with different titles depending on the selected tab. Inside those tabs I want to place a List view. See the code below:
struct ContentView: View {
#State private var selection = 1
var body: some View {
TabView(selection:$selection) {
Page_1()
.tabItem {
Image(systemName: "book")
Text("Page 1")
}
.tag(1)
Page_2()
.tabItem {
Image(systemName: "calendar")
Text("Page 2")
}
.tag(2)
}
}
}
struct Page_1: View {
#State var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("1")
.navigationBarTitle("Page 1 Tab 1")
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("2")
.navigationBarTitle("Page 1 Tab 2")
}
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(.all)
.background()
}
}
}
struct Page_2: View {
#State var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("1")
.navigationBarTitle("Page 2 Tab 1")
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("2")
.navigationBarTitle("Page 2 Tab 2")
}
.tabViewStyle(.page)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea()
.background()
}
}
}
The problem is that when the Pages first appear the lists inside their TabViews seem to be placed slightly too low and then move up. You can see this especially when you switch tabs like here:
After switching back and forth between the tabs they are placed correctly until I freshly start the app again. Would really appreciate your help!:)
Edit
As suggested I tried to put the NavigationViews inside the TabView. That solves the problem with the wrong positioning. However, it leads to the views not being shown at all before I switch back and forth between them. You can see what that looks like in the picture below:

Related

Interacting with a confirmationDialog or alert is causing the parent view to pop

When you navigate and open the confirmation dialog. When you select Yes, No or Cancel the page that the app was on is dismissed and it takes you back to the form on the previous page.
We also found this happens with alerts too.
It's a simple enough app structure, top level tabs then a menu which links to sub pages.
Here is a quick demo of the bug:
We put together an example app that demonstrates this.
How can we prevent this from happening while also maintaining the app structure?
import SwiftUI
#main
struct testApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
TabView() {
Form {
NavigationLink(destination: SubPage()) {
Image(systemName: "clock")
Text("Sub Page")
}
// This is where more menu options would be
}
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
struct SubPage: View {
#State private var confirmDialogVisible = false
var body: some View {
VStack {
Button{
confirmDialogVisible = true
} label: {
Text("popup")
}
}
.confirmationDialog("Confirm?", isPresented: $confirmDialogVisible) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
}
We are using XCode 14.1
And running on iOS 16.1
I usually use ViewModifer to keep consistency between tabs. One modifier for the root of the tab and one for the children.
///Modifier that uses `ToolbarViewModifier` and includes a `NavigationView`
struct NavigationViewModifier: ViewModifier{
func body(content: Content) -> some View {
NavigationView{
content
.modifier(ToolbarViewModifier())
}
}
}
///`toolbar` that can be used by the root view of the navigation
///and the children of the navigation
struct ToolbarViewModifier: ViewModifier{
let title: String = "Company Name"
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
VStack{
Image(systemName: "sparkles")
Text(title)
}
}
}
}
}
Then the Views use it something like this.
import SwiftUI
struct CustomTabView: View {
var body: some View {
TabView{
ForEach(0..<4){ n in
CustomChildView(title: n.description)
//Each tab gets a `NavigationView` and the shared toolbar
.modifier(NavigationViewModifier())
.tabItem {
Text(n.description)
}
}
}
}
}
struct CustomChildView: View {
let title: String
#State private var showConfirmation: Bool = false
var body: some View {
VStack{
Text(title)
NavigationLink {
CustomChildView(title: "\(title) :: \(UUID().uuidString)")
} label: {
Text("open child")
}
Button("show confirmation") {
showConfirmation.toggle()
}
.confirmationDialog("Confirm?", isPresented: $showConfirmation) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
//Each child uses the shared toolbar
.modifier(ToolbarViewModifier())
}
}
I stuck with NavigationView since that is what you have in your code but
if we take into consideration the new NavigationStack the possibilities of these two modifiers become exponentially better.
You can include custom back buttons that only appear if the path is not empty, return to the root from anywhere, etc.
Apple says that
Tab bars use bar items to navigate between mutually exclusive panes of content in the same view
Make sure the tab bar is visible when people navigate to different areas in your app
https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars/
Having the NavigationView or NavigationStack on top goes against these guidelines and therefore are the source of endless bugs, Especially when you take into consideration iPadOS.
Simple solution would be to use navigationDestination.
struct testApp: App {
#State var goToSubPage = false
var body: some Scene {
WindowGroup {
NavigationStack {
TabView() {
Form {
VStack {
Image(systemName: "clock")
Text("Sub Page")
}
.onTapGesture {
goToSubPage = true
}
// This is where more menu options would be
}
.navigationDestination(isPresented: $goToSubPage, destination: {
SubPage()
})
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
I tested it and it won't popped off itself anymore.

Taking Top Space When Adding Tab Bar in SwiftUI

I am integrating Side Menu in my SwiftUI app. The side menu is working fine. The problem I am getting is I have a tab bar at the bottom, and when I open my side menu screen, it is taking some space from the top and I am not getting why it is getting that space. This is what I am getting:
And when I open Debug View Hierarchy, it is showing me this:
This is my code for my home screen:
struct HomeView: View {
#State var menuOpen: Bool = false
var body: some View {
Zstack {
ZStack {
HStack {
if !self.menuOpen {
Button {
print("side menu tapped")
self.menuOpen.toggle()
} label: {
Image("sideMenuButton")
.resizable()
.frame(width: 24, height: 24)
}.padding()
}
Spacer()
}
Text("Home")
.font(.custom(Poppins.semiBold.rawValue, size: 17))
.foregroundColor(Color(ColorName.appBlue.rawValue))
}
ZStack {
SideMenu(width: 300,
isOpen: self.menuOpen,
menuClose: self.openMenu)
}
}
}.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
func openMenu() {
self.menuOpen.toggle()
}
}
This is working fine when I use it without tab bar. Without tab bar it looks like this:
My code for tab bar controller is:
struct TabBarControllerView: View {
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
HomeView()
.tabItem {
Label("Home", image: tabSelection == 0 ? ImageName.home.rawValue : ImageName.silverHome.rawValue)
}
.tag(0)
MyAccountView()
.tabItem {
Label("Claims", image: tabSelection == 1 ? ImageName.claims.rawValue : ImageName.silverClaims.rawValue)
}
.tag(1)
}.accentColor(Color(ColorName.appBlue.rawValue))
}
}
struct TabBarControllerView_Previews: PreviewProvider {
static var previews: some View {
TabBarControllerView()
}
}
Does anyone knows why is it getting the top space? I tried .ignoresSafeArea() and also gave padding in negative, it went to top but the side menu did not tapping because it is getting under that space.
I am been stuck in this for 2 days, any help will be appreciated.
TabView comes with Navigation Bar up top. There is a simple solution by adding .navigationBarHidden(true).
See this post: SwiftUI how to hide navigation bar with TabView
Note: Also in the future give a proper minimum amount of code to reproduce the behavior. Your current code given relies on other components and isn't runnable by itself on a fresh project.
Update:
I created a minimum project to test this and this code works fine with me:
import SwiftUI
#main
struct testApp: App {
var body: some Scene {
WindowGroup {
TabBarView()
}
}
}
struct TabBarView: View {
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(0)
}
}
}
struct HomeView: View {
var body: some View {
ZStack {
Rectangle().foregroundColor(.green)
VStack {
HStack {
Spacer()
Button {
print("side menu tapped")
} label: {
Image(systemName: "pencil")
.resizable()
.frame(width: 24, height: 24)
}.padding()
Spacer()
}
Spacer()
}
}
}
}
Given that this code works fine and the issue is not here, minimum working code sample is needed from OP.

SwiftUI TabView Animation

I am currently facing a pb on my app.
I would like to animate the insertion and removal of items that are controlled by SwiftUI TabView.
Here is the simplest view I can come with that reproduce the problem
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
TabView(selection: $selection.animation(),
content: {
Text("Tab Content 1")
.transition(.slide) //could be anything this is for example
.tabItem { Text("tab1") }.tag(1)
.onAppear() {print("testApp")}
.onDisappear(){print("testDis")}
Text("Tab Content 2")
.transition(.slide)
.tabItem { Text("tab2") }.tag(2)
})
}
}
Actually when hitting a tabItem. It switches instantly from "Tab Content 1" to "Tab Content 2" and I would like to animate it (not the tab item button the actuel tab content). The On Appear and onDisapear are corectly called as expected hence all transition should be triggered.
If someone has an idea to start working with I would be very happy
Thanks
1.with .transition() we only specify which transition should happen.
2.Transition occur (as expected), only when explicit animation occurs.
3.Animation occurs when change happened(State, Binding)
here is one of possible approaches.
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
TabView(selection: $selection,
content: {
ItemView(text:"1")
.tabItem { Text("tab1") }.tag(1)
ItemView(text: "2")
.tabItem { Text("tab2") }.tag(2)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ItemView: View {
let text: String
#State var hidden = true
var body: some View {
VStack {
if !hidden {
Text("Tab Content " + text)
.transition(.slide)
}
}
.onAppear() { withAnimation {
hidden = false
}}
.onDisappear(){hidden = true}
}
}

NavigationView inside a TabView Swift UI

I'm just picking up SwiftUI after a long break but I don't understand why I can't place a Navigation View within a Tab View.
I want my Navigation View to be a .tabItem so the view appears as part of the main app navigation so I'm trying this :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
.tabItem {
Text("Home")
}
Text("Tab bar test")
.navigationBarTitle("Page One")
}
}
This doesn't work with error message
Cannot infer contextual base in reference to member 'tabItem'
But when I try this
var body: some View {
TabView {
NavigationView {
Text("Tab bar test")
.navigationBarTitle("Page One")
}
}
.tabItem {
Image(systemName: "1.circle")
Text("Home")
}
}
It builds fine but the tab bar doesn't show up.
My primary question which I'm hoping would be useful to others, is ...
Why can't I make a make a Navigation View a tab bar item by nesting .tabItem directly inside the Navigation View (as per my first example)?
I think it's a similar question to this one but there's no code there. And then quite similar to this one but they seem to be using .tabItem directy with ContentView like I want to with NavigationView but that isn't building!?
I'm probably misunderstanding something simple but I don't get this at all at the moment.
Do this for a better overview:
ContentView:
struct ContentView : View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "folder.fill")
Text("Home")
}
SecondView()
.tabItem {
Image(systemName: "folder.fill")
Text("SecondView")
}
}
}
}
FirstView
struct FirstView: View {
var body: some View {
NavigationView {
Text("FirstView")
.navigationBarTitle("Home")
}
}
}
}
SecondView
struct SecondView: View {
var body: some View {
NavigationView {
Text("SecondView")
.navigationBarTitle("Home")
}
}
}
}
.tabItem should be used as a modifier on the view that represents that tab.
In the first example, this doesn't work because of a syntax error -- you're trying to use it on the opening of a closure in NavigationView {, when instead you want it on the outside of the closing brace: NavigationView { }.tabItem(...)
In the second example, you're using .tabItem as a modifier on the entire TabView instead of the NavigationView.
Both of your examples may have revealed what was going on more obviously if you indent your code so that you can see the hierarchy. Trying selecting your code in Xcode and using Ctrl-I to get Xcode to properly format it for you.
Here's a working version:
struct ContentView : View {
var body: some View {
TabView {
NavigationView {
Text("Tab bar test")
.navigationBarTitle("Page One")
}
.tabItem { //note how this is modifying `NavigationView`
Image(systemName: "1.circle")
Text("Home")
}
}
}
}

Variable 3-Column NavigationView SwiftUI

Problem
I would like to be able to change the number of columns in a Navigation View depending on the sidebar selection. i.e. Most views will have the desired 3-column layout (sidebar > list > detail) but one will have a two column layout (sidebar > detail). I tried to set this up directly in the top layer of the navigation view but this didn't change anything.
NavigationView{
SidebarView()
if selection != .explore {
ListView()
}
DetailView()
}
In the above example, if the selection is 'explore' there should only be a sidebar and a detail view.
Any ideas on how to achieve this?
Code to reproduce
I would want "searchView" to take up the full width. Meaning just a sidebar and search view should appear
Run on macOS or iPadOS
import SwiftUI
enum SidebarSelection {
case library
case notes
case search
}
struct ContentView: View {
#State var selection : SidebarSelection? = SidebarSelection.library
var body: some View {
NavigationView {
List {
NavigationLink(destination: ListView(), tag: SidebarSelection.library, selection: $selection){
Label("Library", systemImage: "book")
}
.tag(SidebarSelection.library)
NavigationLink(destination: ListView(), tag: SidebarSelection.notes, selection: $selection){
Label("Notes", systemImage: "doc.text")
}
.tag(SidebarSelection.notes)
NavigationLink(destination: SearchView(), tag: SidebarSelection.search, selection: $selection){
Label("Search", systemImage: "magnifyingglass")
}
.tag(SidebarSelection.search)
}
.listStyle(SidebarListStyle())
Text("List View")
if selection != .search {
Text("Detail View")
}
}
}
}
struct ListView: View {
var body: some View{
List {
ForEach(0..<10){ index in
NavigationLink(destination: Text("DetailView: \(index)")){
Text("Link to \(index) detail view")
}
}
}
}
}
struct SearchView: View {
var body: some View {
Text("Full width search view")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What you can do is quite easy actually, to achieve this, you just have to be bit more specific for SwiftUI and use the #ViewBuilder property wrapper.
struct ContentView: View {
#ViewBuilder
var body: some View {
if(twoColumns == true){
NavigationView{
SidebarView()
DetailView()
}
} else {
NavigationView{
SidebarView()
ListView()
DetailView()
}
}
}
}