SwiftUI: Wrong TabView's Tab Bar height in landscape mode - swiftui-tabview

This is a simple example of programmatic Tab Bar height calculation for TabView:
import SwiftUI
struct ContentView: View {
let tabbarHeight = UITabBarController().tabBar.frame.height
var body: some View {
TabView {
VStack {
Text("First View")
Text("tabbar height: \(tabbarHeight)")
}
.tabItem {
Label("First", systemImage: "house")
}
Text("Second View")
.tabItem {
Label("Second", systemImage: "chart.bar")
}
}
.overlay {
VStack {
Spacer()
Rectangle().fill(.green)
.frame(width: 50, height: tabbarHeight)
}
}
.onAppear {
let tabBarAppearance = UITabBarAppearance()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
tabBarAppearance.backgroundColor = .purple
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Everything is ok When running in portrait mode:
But when launching in landscape mode, tabar height is being calculated wrong (49 instead of 32):
Is it a bug?
Xcode 14.1/iPhone 13/iOS 16.1

Related

How can I make a side bar in SwiftUI in navigationstack

I am trying to make a sidebar in swiftUI that is triggered in and out from the side with a button
I have been able to make it pop in and out from the bottom using a side modifier like this
struct sideBarExample: View {
#State var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.blue)
.overlay(Text("Main View"))
}
var sideBar: some View{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
}
var body: some View {
NavigationStack{
mainView
.sheet(isPresented: $showSideBar, content: {
sideBar
})
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
showSideBar.toggle()
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
But ideally it should be from the side
For iPhone you have to build your own sidebar, just overlay it in a ZStack and animate in with .transition.
struct ContentView: View {
#State private var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.gray)
.overlay(Text("Main View"))
}
var sideBar: some View{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
.frame(width: 200)
}
var body: some View {
NavigationStack{
ZStack(alignment: .leading) {
mainView
if showSideBar {
sideBar
.transition(.move(edge: .leading))
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
showSideBar.toggle()
}
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
You can use the offset view modifier to move the sidebar around
Here is an example
struct SideBarExample: View {
#State var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.blue)
.overlay(Text("Main View"))
}
var sideBar: some View{
HStack{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
.frame(width:250)
Spacer()
}
}
var body: some View {
NavigationStack{
ZStack{
mainView
sideBar
.offset(CGSize(width: showSideBar ? 0:-250, height: 0))
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
showSideBar.toggle()
}
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
Here is what that looks like

SwiftUI Toolbar item getting clipped when back button is pressed

I've run in to a strange behavior in SwiftUI that I can't seem to work around.
Given the following simple example app I experience this behavior: The toolbar item renders correctly on the initial run, but navigating away and returning it gets clipped.
Sample code to recreate this:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("Hello, world!")
.padding()
.navigationTitle("View 1")
.toolbar {
Circle()
.fill(Color.red)
.frame(width: 150, height: 150, alignment: .center)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View2.swift
import SwiftUI
struct View2: View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
View2()
}
}
It is clipped by navigation bar as seen on view debug below so it is just rendering issue (should be clipped always):
A possible solution is to use that widget (Circle in the case) above NavigationView and align it with toolbar item.
Here is main part:
.toolbar {
Color.clear
.frame(width: 150)
.overlay(GeometryReader {
Color.clear.preference(key: ViewPointKey.self,
value: [$0.frame(in: .global).center])
})
}
//...
Circle().fill(Color.red)
.frame(width: 150, height: 150)
.position(x: pos.x, y: pos.y) // << here !!
//...
.onPreferenceChange(ViewPointKey.self) {
pos = $0.first ?? .zero
}
Complete findings and code is here

SwiftUI: Button with offset not working in ScrollView

I'm trying to make this Profile Button to work inside a ScrollView, that when tapped, a modal view should come up. But for some reason, when I add some offset to the Button, to make it inline with the Navigation Title, the Button stops working.
Does anyone know a fix for this?
Here's my code:
import SwiftUI
struct TestView: View {
#State var showMenu = false
var body: some View {
NavigationView {
ScrollView {
VStack {
HStack {
Spacer()
Image(systemName: "person.crop.circle")
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 44, height: 44)
.onTapGesture {
showMenu.toggle()
}
.offset(y: -64)
}
Spacer()
}
.padding()
.sheet(isPresented: $showMenu) {
MenuView()
}
}
.navigationTitle("Browse")
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Hiding tab bar on a specific page in SwiftUI

I am working with a camera in my application. The app allows you to navigate to this camera view with the help of the TabView in SwiftUI. However, the problem is, when I am on the camera view, I would like to make the TabView hidden. So far I've been trying to find a solution but I cannot seem to find any.
Here is a screenshot of the code and the view with the tabview
Note: The screenshot contains an image if the preview. The camera works fine when I run it on a real device. The problem is that I need the tab bar to be hidden once I've entered the camera view.
And here is an example of the code that I am using:
import SwiftUI
struct AppView: View {
var body: some View {
TabView{
Home()
.tabItem {
// Add icon
Text("Home")
}
MomentsCam()
.tabItem {
// Add icon
Text("Camera")
}.navigationBarHidden(true)
Moments()
.tabItem{
//Add icon
Text("Moments")
}
}
}
}
I updated my solution with TabView for your situation. The same idea: you're using ZStack and #State var selection. And the idea is to use .opacity of TabView and YourCameraView (which is just Image(systemName: "plus.circle") in my example):
struct TabViewModel: View {
#State var selection: Int = 0
var body: some View {
ZStack {
GeometryReader { geometry in
TabView(selection: self.$selection) {
Text("list")
.tabItem {
Image(systemName: "list.bullet.below.rectangle")
}.tag(0)
Text("plus")
.tabItem {
Image(systemName: "camera")
}.tag(1)
Text("more categories!")
.tabItem {
Image(systemName: "square.grid.2x2")
}.tag(2)
}
.opacity(self.selection == 1 ? 0.01 : 1)
Image(systemName: "plus.circle")
.resizable()
.frame(width: 60, height: 60)
.shadow(color: .gray, radius: 2, x: 0, y: 5)
.offset(x: geometry.size.width / 2 - 30, y: geometry.size.height - 80)
.onTapGesture {
self.selection = 0
}
.opacity(self.selection == 1 ? 1 : 0)
}
}
}
}
when you tap on camera tabItem TabView becomes invisible
You may try the following code according to your needs.
struct MomentsCam: View {
var body: some View {
Text("Cam")
}
}
struct Moments: View {
var body: some View {
Text("Moments Cam")
}
}
struct AppView: View {
#State var showCamera = false
var body: some View {
GeometryReader{ p in
ZStack{
TabView{
Home()
.tabItem {
// Add icon
Text("Home")
}
Text("holder")
.tabItem {
// Add icon
Text("Camera")
}.navigationBarHidden(true).onAppear{
self.showCamera = true
print(p.size)
}
Moments()
.tabItem{
//Add icon
Text("Moments")
}
}
if self.showCamera{
MomentsCam().frame(width: p.size.width, height: p.size.height).background(Color.white)
}
}
}
}
}

NavigationView and NavigationLink on button click in SwiftUI?

I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}

Resources