Not sure if I am doing something wrong or if this is just how the TabView in SwiftUI appears now, but it seems to me that the images at the bottom seem much closer to the top border than in previous iOS versions (I am testing on iOS 16).
Here is the code I am using to render this:
struct MainView: View {
var body: some View {
TabView {
NavigationView {
ScrollView {
RoundedRectangle(cornerRadius: 16)
.frame(height: 1000)
.padding()
}
.background(.green)
}
.tabItem {
Image(systemName: "house.fill")
}
.toolbarBackground(.background, for: .tabBar)
NavigationView {
LikesView()
}
.tabItem {
Image(systemName: "heart")
}
NavigationView {
MatchesView()
}
.tabItem {
Image(systemName: "bubble.left")
}
NavigationView {
ProfileView()
}
.tabItem {
Image(systemName: "person")
}
}
.tint(.primary)
}
}
Am I missing something that would cause the images to be higher up in the tab bar or is that just how the standard tab bar looks now?
It looks default to me, just from a brief judgement between a couple other simulators and iOS versions. It does look maybe slightly higher on the 14 Pro Max, compared to something like the 11 Pro Max, since the symbols seem to be a little larger.
There is the ability to add text below the Image with a label which you may be used to seeing:
Label("Messages", systemImage: "bubble.left")
I'm trying to get nested TabViews working in SwiftUI to achieve an onboarding flow prior to the main tabbed app screen but am running in to a non-obvious visual glitch.
I'd like the onboarding portion to be full-screen, ignoring safe areas, but the nested, tabbed, main app screen to honour safe areas.
The code below shows the glitch off: it's full-screen for the first couple of screens, but then - only on initial display - the tabs are below the safe area. If I swipe back and forwards again the tabs do then honour the safe area.
I've tried various combinations of .edgesIgnoringSafeArea(), .frame(), .offset(), as well as trying to make use of the values that GeometryReader provides. Removing the one .edgesIgnoringSafeArea() that is there gives me the (expected) bars below and above my tabs. Due to the swipe transitions I need the tabs to be full-screen.
I've also tried using .overlays to achieve the desired appearance and while this does work it requires more complex state manipulation and just feels wrong.
Finally, I've played around with nesting NavigationViews and TabViews and, as reported elsewhere, that rarely ends well.
I'd be grateful if someone could explain why I'm seeing this glitch (i.e. the gap in my understanding of SwiftUI's rendering/lifecycle, and why the nested tab bar changes position the second time it appears), and if there's a canonical way of achieving what I want. TIA.
// A simple tab view that can self-advance to the next tab.
// Included to simplify the main ContentView
struct SimpleTab: View {
#Binding var tab: Int
let label: String
let backgroundColour: Color
let withNext: Bool
var body: some View {
VStack {
Spacer()
Text(label)
.frame(maxWidth: .infinity, alignment: .center)
if withNext {
Button {
withAnimation {
tab += 1
}
} label: {
Text("Next ->")
}
}
Spacer()
}
.background(backgroundColour
.opacity(0.5))
}
}
struct ContentView: View {
#State var tab = 0
#State var nestedTab = 0
var body: some View {
VStack {
TabView(selection: $tab) {
SimpleTab(tab: $tab, label: "Onboarding 1", backgroundColour: .red, withNext: true)
.tag(0)
SimpleTab(tab: $tab, label: "Onboarding 2", backgroundColour: .green, withNext: true)
.tag(1)
TabView(selection: $nestedTab) {
SimpleTab(tab: $nestedTab, label: "Nested 0", backgroundColour: .blue, withNext: false)
.tabItem {
Label("Nested 0", systemImage: "0.circle")
}
.tag(0)
SimpleTab(tab: $nestedTab, label: "Nested 1", backgroundColour: .blue, withNext: false)
.tabItem {
Label("Nested 1", systemImage: "1.circle")
}
.tag(1)
}
.tabViewStyle(DefaultTabViewStyle())
.tag(2)
}
.edgesIgnoringSafeArea(.all)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.transition(.slide)
}
}
}
I am writing an application using Xcode 14.0.1, and testing on an iPhone 12 mini running iOS 16.0. The current project build is for iOS 14.7. Here is my TabView...
TabView {
ByEyeView()
.tabItem { Label("ByEye", systemImage: "eye") }
ChartView()
.tabItem { Label("Chart", systemImage: "square.grid.4x3.fill") }
ListView()
.tabItem { Label("List", systemImage: "list.bullet") }
EditView()
.tabItem { Label("Edit", systemImage: "square.and.pencil") }
CameraView()
.tabItem { Label("Camera", systemImage: "camera") }
SettingsView()
.tabItem { Label("Settings", systemImage: "gear") }
}
//.labelStyle(TitleAndIconLabelStyle())
//.padding(8)
//.ignoresSafeArea(edges: .bottom)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
This gives a capsule at the bottom of the page with a small version of the icon and no text. I cannot enlarge the Label with .frame, and the .labelStyle() setting is ignored. I take it this is part of PageTabViewStyle() - the index is supposed to be small, and I can probably not change that. But the index sits over the view content, so I need its height if I am to keep buttons clear of it.
Can I find out the index height? Or does PageTabViewStyle assume that the index is small and you should work around it?
The commented-out .ignoreSafeArea() moves the index down while the page remains the same. The .padding() keeps it a bit clear of the bar at the bottom. This is what I am working with for now. This is foul: it will not work with other devices or screen orientations.
The bigger picture:
I have six entries. That does not fit in the default view, so I get a ... More tag which leads to an extra menu. Ugly. I like the PageTabViewStyle method of scrolling, but I want an index with a known height - preferably one that uses the full labels and sits at the bottom of the TabView layout, under the tabbed views.
This was one of those 'Magic Eye' things when you stare at it for days and it makes no sense, and suddenly everything rearranges itself...
Maybe TabPageViewStyle was intended to be for pages where there is no visible index, or overlaying a small index does no harm. This would work for browsing images. All the cunning has gone into making the index view unobtrusive. If you need to know how big it is, then perhaps TabPageViewStyle is not what you want.
What I said I wanted was actually a Scrollable horizontal list, followed by the currently selected list. Something like this...
let tabW = CGFloat(UIScreen.main.bounds.width / 5.0)
enum Page {
case ByEye
case Chart
case List
case Edit
case Camera
case Settings
}
#State private var page = Page.ByEye
func pageButton(_ select: Page, _ icon: String, _ title: String) -> some View {
return Button {
page = select
} label: {
VStack {
Image(systemName: icon)
Text(title)
} .frame(width: tabW)
} .foregroundColor( page == select ? Color.white : Color.gray )
}
var body: some View {
VStack() {
ScrollView(.horizontal) {
HStack() {
pageButton(Page.ByEye, "eye", "ByEye")
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart")
pageButton(Page.List, "list.bullet", "List")
pageButton(Page.Edit, "square.and.pencil", "Edit")
pageButton(Page.Camera, "camera", "Camera")
pageButton(Page.Settings, "gear", "Settings")
}
}
switch page {
case .ByEye:
ByEyeView()
case .Chart:
ChartView()
case .List:
ListView()
case .Edit:
EditView()
case .Camera:
CameraView()
case .Settings:
SettingsView()
}
Spacer()
}
It is not much longer than my original version. It is not as pretty is it could be - when you overflow the title bar you get half an icon, where an ellipsis would be better. But I can fix that later.
The other answer is to write your own index table....
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
pageButton(Page.EyeTest, "eyeglasses", "EyeTest", proxy)
pageButton(Page.Tone, "pause.rectangle", "Tone", proxy)
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart", proxy)
pageButton(Page.ByEye, "eye", "ByEye", proxy)
pageButton(Page.List, "list.bullet", "List", proxy)
pageButton(Page.Camera, "camera", "Camera", proxy)
pageButton(Page.Settings, "gear", "Settings", proxy)
}
}
.onAppear { proxy.scrollTo(page, anchor: .center) }
.onChange(of: page) { page in
withAnimation {
proxy.scrollTo(page, anchor: .center)
}
}
}
This particular one has button-sized icons and text. 'page' is an enum, and also the tags of the TabView. If you stick it in the layout, you can make it fit around the TabView. You will want to hide the TabView index, which you can do with...
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
I have a tabView and I'm trying to change it's color. Using accentColor(:_) works but it's going to be deprecated.
TabView {
AppetizerListView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
AccountView()
.tabItem {
Image(systemName: "person")
Text("Account")
}
OrderView()
.tabItem {
Image(systemName: "bag")
Text("Order")
}
}
.accentColor(Color("brandPrimary"))
Instead I've tried to use .tint(:_) as Apple suggests but is not working (it builds but does not change the color).
TabView {
AppetizerListView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
AccountView()
.tabItem {
Image(systemName: "person")
Text("Account")
}
OrderView()
.tabItem {
Image(systemName: "bag")
Text("Order")
}
}
.tint(Color("brandPrimary"))
I also tried using .tint(_:) in each TabItem but it's also not working.
Any idea of what's going on or which is the correct way of making my code work as expected without using deprecated functions?
Maybe I'm using tint in a wrong way
Thanks!
I've found the solution to the problem, but I'll leave the post here for anyone who has the same problem.
What you have to do is to go to the Assets folder and define the AccentColor (that it has to be already created) as the color that you want your bar to be.
No modifiers have to be added to the tabView and it will automatically be showing the tabView with the color you defined as the AccentColor in your Assets folder.
I'm having a TabView with Navigation Views in it.
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
NavigationLink("Link to my first Navigation Level", destination: MyFirstView())
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView {
Text("Second Nav Page")
}
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(1)
}
}
}
struct MyFirstView: View {
#State var selectedTag: String?
var body: some View {
VStack {
Text("My First View")
NavigationLink("(Working) Link to my second Navigation Level", destination: MySecondView())
Text("But the Button in the Navigation Bar doesn't work")
}
.navigationBarTitle("My First View", displayMode: .inline)
.navigationBarItems(
leading: HStack {
NavigationLink(destination: MySecondView(), tag: "xx", selection: $selectedTag ){
Button(action: {
print("Settings button pressed...")
self.selectedTag = "xx"
}) {
Image(systemName: "gearshape.2.fill").imageScale(.large)
}
}
}
)
}
}
struct MySecondView: View {
var body: some View {
Text("My Second View")
}
}
Now I got a super weired behavior. If I click on "Link to my first Navigation Level" and then on "(Working) Link to my second Naviation Level" the journey works. If I click on "Back" while being in the second Navigation Level it goes back to the first Navigation Level.
Issue:
When I click on the gear symbol in the Navbar on the first Navigation Level it kind of escapes from the nested Navigation and sets it to the top level. This has the consequence that when I click on "Back" it brings me from the Second Navigation Level back to the very root screen but my expected behavior is that it should go back to the first Navigation Level.
Any ideas what I'm doing wrong? I'm using Xcode 12.2 beta 3 and iOS 14.2 (not sure if it's a beta bug).
Many Thanks!
This did the trick (thanks #Asperi). Has to be outside tabBarItems
NavigationLink(destination: AddDetailView(existingItem: nil),
isActive: $addMode) { EmptyView() }