NavigationStack and TabView in Swiftui iOS 16: bug or improper usage? - swiftui

[Xcode 14.1, iOS 16.1]
I have a NavigationStack with a navigationTitle and a TabView with 2 Views. Each View has a ScrollView (see image below):
NavigationStack and TabView problem image
When I tap on Tab1 (#1 in red on the image above), then swipe up, the behavior is as expected (#2), i.e. the big navigationTitle move to the center, and my view passes below and becomes blurry. Perfect.
However, when I tap ton Tab2 (#3) and then swipe up (#4), the big title stays big, and the view doesn't become blurry.
Then I tap on Tab1 again (#5) and it works as expected.
Please help!
Here is my code:
ContentView:
import SwiftUI
struct ContentView: View {
#State private var selection: Tab = .tab1
enum Tab {
case tab1
case tab2
}
#State private var mainTitle = "Tab1"
var body: some View {
NavigationStack {
TabView(selection: $selection) {
Tab1(mainTitle: $mainTitle)
.tabItem {
Label("Tab1", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab1)
Tab2(mainTitle: $mainTitle)
.tabItem {
Label("Tab2", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab2)
} .navigationTitle(mainTitle)
}
}
}
Tab1:
import SwiftUI
struct Tab1: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 1")
.padding(.all,100)
.background(.blue)
} .onAppear {
mainTitle = "Tab1"
}
}
}
Tab2:
import SwiftUI
struct Tab2: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 2")
.padding(.all,100)
.background(.green)
} .onAppear {
mainTitle = "Tab2"
}
}
}
I tried a hack that is supposed to fix the transparency bug for Tab bars, but it doesn't work.
.onAppear {
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
}

TabViews are designed to sit at the top of the navigation hierarchy. They're intended to allow users to switch between independent sections of your app at any time.
You would generally put a separate navigation stack within each tab that then handles pushing and popping of views. And then, you can use the navigationTitle modifier to manage the screen's title.
So your structure (which might be split over multiple custom views) should look something like:
TabView {
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 1")
}
.tabItem { Label("Tab1", ...) }
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 2")
}
.tabItem { Label("Tab2", ...) }
}
This structure is by design, to align with Apple's Human Interface Guidelines. It's worth reading the HIG to get a handle on where Apple are coming from, and how working on the same principles can really help your app feel like it belongs on your users' device.

Related

SwiftUI how to align the navigation bar title on center when user taps on a navigation link?

I have a TabView in a struct. I add a NavigationView and a NavigationLink. When the view shows up the navigation bar is centered. When i tap on the navigation link the text is on the leading side. How can i make the navigation bar text align to center after i tap the navigation link in iOS 16 and above? I have searched stackoverflow and nothing seems to work in iOS 16 with this specific implementation i am posting below. Any help appreciated.
import Foundation
import SwiftUI
public struct TabViewTest: View {
#State var selectedTab: Int = 0
public var body: some View {
TabView (selection: $selectedTab) {
NavigationView {
NavigationLink(destination: Text("destination"))
{
FeedView(selectedTab: $selectedTab)
.navigationBarTitle("Feed", displayMode: .inline)
.toolbar {
ToolbarItemGroup {
Image(systemName :"magnifyingglass.circle")
.onTapGesture {
selectedTab = 1
}
}
}
}
}
.tabItem {
Text("Feed")
.foregroundColor(.white)
}
.tag(0)
}
}
struct ProfileView: View {
#Binding var selectedTab: Int
var body: some View {
ZStack {
Color.white.ignoresSafeArea()
VStack {
Text("Nav link test tap anywhere")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}

How do I switch views by using a touch gesture on SwiftUI?

Hi I am pretty new to using SwiftUI. I have been having trouble with adding a tap gesture to my welcome page that will allow the user to move on to another view with drop down question boxes. I currently have this:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "Globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Welcome to WOD planner!")
.font(.title)
.multilineTextAlignment(.center)
.onTapGesture{ action:{
//go to question page
}
}
}
.padding()
}
}
I made another view labeled QuestionPage. Im just confused how to code this gesture to make it change and isolate the two different views.
Thanks!
I solve this by having a State variable that will track the View that I want to show. Within the WindowGroup in my App, I now present the View that the viewState refers to.
This sample code should do the trick, it will change Views if you tap on the text in the middle of the screen. I personally would rather use a button, but it can be done with onTapGesture:
enum ViewState {
case welcomeView
case questionView
}
#main
struct MyApp: App {
#State var currentView: ViewState = .welcomeView
var body: some Scene {
WindowGroup {
switch (currentView) {
case .welcomeView:
WelcomeView(viewState: $currentView)
case .questionView:
QuestionView(viewState: $currentView)
}
}
}
}
struct WelcomeView: View {
#Binding var viewState: ViewState
var body: some View {
// your view here
Text("Welcome")
.onTapGesture { viewState = .questionView }
}
}
struct QuestionView: View {
#Binding var viewState: ViewState
var body: some View {
// your view here
Text("Some questions")
.onTapGesture { viewState = .welcomeView }
}
}

Changing EnvironmentObject published property changes TabView

I have a an app I'm writing (first one) that has developed an issue. I have a tabbed view with multiple views, one of which is settings. On the settings view, various elements (mostly Toggles) bind to the global environment object appData. When I select something on the settings view, the TabView changes to the first tab away from the settings view.
The playground example below shows the issue. It does it the first time but not subsequent times. In my application, it does it every time. Not sure why it's different. To see the issue, load the playground code, click on settings and click the "Setting" toggle. It immediately jumps to MainView. The value for isSet in the application data does appear to get set.
I also see this on the emulator (set for iPhone SE) for my application.
I'm using XCode 13.4.
import SwiftUI
import Combine
import Foundation
import PlaygroundSupport
class ApplicationData: ObservableObject
{
#Published var isSet: Bool = false
}
struct MainView: View
{
var body: some View
{
Text("Main View")
}
}
struct SettingsView: View
{
#EnvironmentObject var appData : ApplicationData
var body: some View
{
Toggle("Setting", isOn: $appData.isSet)
}
}
struct TabbedView: View
{
#StateObject var appData = ApplicationData()
#State var selection: Int = 0
var body: some View
{
TabView(selection: $selection)
{
MainView().tabItem({Label("Main", systemImage: "menucard")})
SettingsView().tabItem({Label("Settings", systemImage: "gear")})
}
.environmentObject(appData)
}
}
let view = TabbedView()
let hostingVC = UIHostingController(rootView: view)
PlaygroundPage.current.liveView = hostingVC
I think the issue comes from TabView(selection:) itself. As you don't provide tags to identify the tabs, the view looses track when it is redrawn.
Either use TabView without selection:
TabView // here
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
}
Or – if with selection – you have to provide a .tag() so selection can track what is selected:
TabView(selection: $selection)
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
.tag(0) // here
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1) // here
}

Navigation + Tabview + Sheet broken in iOS 15

It looks like Navigation + TabView + Sheet is broken in iOS 15.
When I do this:
ContentView -> DetailView -> Bottom Sheet
When the bottom sheet comes up, the Detail view is automatically popped off the stack:
https://www.youtube.com/watch?v=gguLptAx0l4
I expect the Detail view to stay there even when the bottom sheet appears. Does anyone have any idea on why this happens and how to fix it?
Here is my sample code:
import Combine
import SwiftUI
import RealmSwift
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItemView(num: 1)
.tabItem {
Text("One")
}
TabItemView(num: 2)
.tabItem {
Text("Two")
}
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)")) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
This works on iOS 14 btw
UPDATE 1:
Tried #Sebastian's suggestion of putting NavigationView inside of TabView. While this fixed the nav bug, it fundamentally changed the behavior (I don't want to show the tabs in DetailView).
Also tried his suggestion of using Introspect to set navigationController.hidesBottomBarWhenPushed = true on the NavigationLink destination, but that didn't do anything:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}.tabItem {
Text("Two")
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)").introspectNavigationController { navigationController in
navigationController.hidesBottomBarWhenPushed = true
}) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
You need to flip how you nest TabView & NavigationView. Instead of nesting several TabView views inside a NavigationView, use the TabView as the parent component, with a NavigationView for each tab.
This is how the updated ContentView would look like:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}
.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}
.tabItem {
Text("Two")
}
}
}
}
This makes sense and is more correct: The tabs should always be visible, but you want to show a different navigation stack with different content in each tab.
That it worked previously doesn't make it more correct - SwiftUI probably just changed its mind on dealing with unexpected situations. That, and the lack of error messages in these situations, is the downside of using a framework that tries to render anything you throw at it!
If the goal is specifically to hide the tabs when pushing a new view on a NavigationView (e.g., when tapping on a conversation in a messaging app), you have to use a different solution. Apple added the UIViewController.hidesBottomBarWhenPushed property to UIKit to support this specific use case.
This property is set on the UIViewController that, when presented, should not show a toolbar. In other words: Not the UINavigationController or the UITabBarController, but the child UIViewController that you push onto the UINavigationController.
This property is not supported in SwiftUI natively. You could set it using SwiftUI-Introspect, or simply write the navigation structure of your application using UIKit and write the views inside in SwiftUI, linking them using UIHostingViewController.

View resizing when hiding tab bar with Introspect in SwiftUI

I'm using Introspect to hide the tab bar on child navigation link pages. However, I've noticed some odd behavior when the app is backgrounded and then brought back to the foreground.
It seems like initially, the hidden tab bar is still taking up some space, but this disappears when cycling the app back to the foreground. I'm not sure if this is SwiftUI behavior or has to do with how I'm using Introspect / UIKit.
It's causing layout issues in my app, so I'd like to make the spacing consistent if possible.
Here's a minimal example that shows the behavior:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { tabBarController in
tabBarController.tabBar.isHidden = true
}
}
}
Here is the late answer. Basically add tabbar height to current view frame. And onDissappear restore view frame size
import SwiftUI
import Introspect
#State var uiTabarController: UITabBarController?
#State var tabBarFrame: CGRect?
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { (UITabBarController) in
uiTabarController = UITabBarController
self.tabBarFrame = uiTabarController?.view.frame
uiTabarController?.tabBar.isHidden = true
uiTabarController?.view.frame = CGRect(x:0, y:0, width:tabBarFrame!.width, height:tabBarFrame!.height+UITabBarController.tabBar.frame.height);
}
.onDisappear {
if let frame = self.tabBarFrame {
self.uiTabarController?.tabBar.isHidden = false
uiTabarController?.view.frame = frame
}
}
}
}