I consider whether there is possibility to add more Views to TabView in SwiftUI then there is place for TabItems.
I have done something like this:
TabView(selection: $selectedTab) {
Text("Hello World 1")
.tabItem {
Image(systemName: "1.circle")
Text("Item 1")
}.tag(0)
Text("Hello World 2")
.tabItem {
Image(systemName: "2.circle")
Text("Item 2")
}.tag(1)
Text("Hello World 3")
.tabItem {
Image(systemName: "3.circle")
Text("Item 3")
}.tag(2)
Text("Hello World 4")
.tabItem {
Image(systemName: "4.circle")
Text("Item 4")
}.tag(3)
Text("Hello World 5")
.tabItem {
Image(systemName: "5.circle")
Text("")
}.tag(4)
Text("Hello World 5")
.tabItem {
Image(systemName: "6.circle")
Text("")
}.tag(5)
}
And there is More 3-dots button displayed automatically. But I would like to not show this additional tab items in tab bar just first 4 or 5 items and other items will be only programatically navigated. I would like to do it this way to add then Hamburger Menu with buttons that will be switching this other views.
I know that Hamburger/Navigation Drawer/Side Menu is not what apples recommends but such design will fit my application requirements great. :)
I hope the following approach would be useful. The idea is to have dynamic range which shows tab items depending on currently selected one.
For this demo selection, and including visible tabs, are changed depending on preview/next button, but it is not important - the of selection might be different, it just need update range of visible tabs depending on selected tab. That is it.
Here is how the demo behaves:
struct ContentView: View {
static let maxTabs = 8
#State var selectedTab = 2
#State var visibleTabs = [0, 1, 2, 3]
var body: some View {
VStack {
self.selectorView
Divider()
TabView(selection: $selectedTab) {
ForEach(visibleTabs, id: \.self) { i in
self.viewForTab(i)
.tabItem {
Image(systemName: "\(i).circle")
Text("Item \(i)")
}.tag(i)
}
}
}
}
var selectorView: some View {
HStack {
Button(action: {
let prev = self.selectedTab - 1
if prev >= 0 {
if prev < self.visibleTabs.min()! {
self.visibleTabs = self.visibleTabs.map { $0 - 1 }
}
self.selectedTab = prev
}
}) {
Text("< Prev").padding([.top, .horizontal])
}.disabled(self.selectedTab == 0)
Button(action: {
let next = self.selectedTab + 1
if next < Self.maxTabs {
if next > self.visibleTabs.max()! {
self.visibleTabs = self.visibleTabs.map { $0 + 1 }
}
self.selectedTab = next
}
}) {
Text("Next >").padding([.top, .horizontal])
}.disabled(self.selectedTab == Self.maxTabs - 1)
}
}
private func viewForTab(_ tag: Int) -> some View {
// Provide your view for requested tab tag
Text("Hello World \(tag)")
}
}
Related
I am wanting to add a TabView with a List, inside another List. The issue is that adding a list with a TabView that has a list inside makes it so that the view is small. Can't figure out if this scenario is possible.
The reason it needs two List is because i am going to add the Tab Selector as a sticky header, which comes built in by using Section inside the List.
#State private var selectedTab = 1
var body: some View {
List {
// TAB SELECTOR
HStack {
Spacer()
Button(action: {selectedTab = 1}) {
Text("First Tab")
}
Spacer()
Button(action: {selectedTab = 2}) {
Text("Second Tab")
}
Spacer()
Button(action: {selectedTab = 3}) {
Text("Thrid Tab")
}
Spacer()
}
// TABS
TabView(selection: $selectedTab) {
Text("First Tab View")
.tag(1)
VStack {
List {
ForEach(0 ..< 20) { index in
Text("This is index \(index)")
}
}
}
.tag(2)
Text("Third Tab View")
.tag(3)
}
}
}
In my example code below, I have two tabs and within the main tab (Tab A) there are two "buttons" on the front page to allow the user to navigate to two views (View 1 or View 2) using navigationlinks. Within each of view 1 and view 2 there are further navigation links so my code (purposefully) resets the navigation stack for each view when you switch tabs and then return to the tab. Also, if you navigate to view 1 or view 2 (while still on the main tab (tab A)), tapping the main tab button (tab A) again brings you back to the front page (which presents the two buttons). This behaviour is what I need. The problem I now have is that the navigation links (to view 1 and view 2) don't work as intended. Sometimes clicking the view 1 button takes you to view 2 and sometimes it takes you to view 1. The same happens with the view 2 button. This must have something to do with how I reset the navigation stack for my other needs, but I'm not sure how to solve this. I include some minimal code to show this. Does anyone have an idea how to solve this? Thanks
struct ContentView: View {
#State var activeView: Int = 0
#State var showNavigation: Bool = false
let items = ["View1", "View2"]
var body: some View {
TabView(selection: Binding<Int> (
get: {
activeView
}, set: {
activeView = $0
showNavigation = false
}))
{
NavigationView {
HStack {
VStack {
NavigationLink(
destination: View1(), isActive: $showNavigation, label: {
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[0])
}
VStack{
NavigationLink(
destination: View2(), isActive: $showNavigation, label: {
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[1])
}
}.navigationTitle("")
.navigationBarHidden(true)
}
.tabItem {
Image(systemName: "a.circle")
Text("Main")
}
.tag(0)
View3()
.padding()
.tabItem {
Image(systemName: "b.circle")
Text("View 3")
}
.tag(1)
}
}
}
The problem seems to be that you're using the same showNavigation variable for both NavigationLinks.
I've changed the variable around a bit to hold an id and added a custom Binding to keep track of which NavigationLink should be active:
struct ContentView: View {
#State var activeView: Int = 0
#State var activeNavigationLink: Int = 0 //<-- Here
let items = ["View1", "View2"]
func navigationLinkBinding(id: Int) -> Binding<Bool> { //<-- Here
.init { () -> Bool in
activeNavigationLink == id
} set: { (newValue) in
if newValue {
activeNavigationLink = id
} else {
activeNavigationLink = 0
}
}
}
var body: some View {
TabView(selection: Binding<Int> (
get: {
activeView
}, set: {
activeView = $0
activeNavigationLink = 0 //<-- Here
}))
{
NavigationView {
HStack {
VStack {
NavigationLink(
destination: Text("View 1"), isActive: navigationLinkBinding(id: 1), label: { //<-- Here
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[0])
}
VStack{
NavigationLink(
destination: Text("View 2"), isActive: navigationLinkBinding(id: 2), label: { //<-- Here
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[1])
}
}.navigationTitle("")
.navigationBarHidden(true)
}
.tabItem {
Image(systemName: "a.circle")
Text("Main")
}
.tag(0)
Text("View 3")
.padding()
.tabItem {
Image(systemName: "b.circle")
Text("View 3")
}
.tag(1)
}
}
}
How can I make a different tab item for selected and unselected tabs? For example, I would like to use a different image and make the selected text bold.
This is what I have:
struct ContentView: View {
#SceneStorage("selectedTab") private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
Text("Content 1")
.tabItem {
Label("First", systemImage: "alarm")
.accessibilityHint("Something 1")
}
Text("Content 2")
.tabItem {
Label("Second", systemImage: "calendar")
.accessibilityHint("Something 2")
}
}
}
}
Is there a way a built in way to do this since inside the tab I can't figure out if it is the selected one or not.
Use selectedTab with tag and change your tab image by using the selectedTab condition.
And for Font you can use UITabBarAppearance().
struct ContentView: View {
#State private var selectedTab = 0
init() {
// Set font here of selected and normal
let appearance = UITabBarAppearance()
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 10)]
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15)]
UITabBar.appearance().standardAppearance = appearance
}
var body: some View {
TabView(selection: $selectedTab) {
Text("Content 1")
.tabItem {
Label("First", systemImage: selectedTab == 0 ? "alarm" : "alarm_unselected") //<-- Here
.accessibilityHint("Something 1")
}.tag(0) //<-- Here
Text("Content 2")
.tabItem {
Label("Second", systemImage: selectedTab == 1 ? "calendar" : "calendar_unselected") //<-- Here
.accessibilityHint("Something 2")
}.tag(1) //<-- Here
}
}
}
How can you dynamically change SwiftUI Navigation Bar Items?
I have a TabView within a NavigationView, and I would like the Navigation Bar Items to change depending on tab that is selected. However, I am having a hard time determining how to change this with .onAppear(), assuming that is even what you are suppose to do.
My code is currently laid out as follows:
var body: some View {
NavigationView {
TabView {
contentWithNavigationButtons()
.tabItem {
Image(systemName: "house")
Text("Buttons")
}
contentWithoutNavigationButtons()
.tabItem {
Image(systemName: "person")
Text("No Buttons")
}
.onAppear {
//Navigation Bar Items should be removed
}
}
.navigationBarItems(trailing: someButton)
}
Here is a demo of possible solution - add tracking selection for tabs and make button depending of tab selection. Tested with Xcode 12 / iOS 14
struct DemoView: View {
#State private var selection = 0 // << here !!
var body: some View {
NavigationView {
TabView(selection: $selection) {
contentWithNavigationButtons()
.tabItem {
Image(systemName: "house")
Text("Buttons")
}.tag(0) // << here !!
contentWithoutNavigationButtons()
.tabItem {
Image(systemName: "person")
Text("No Buttons")
}.tag(1)
}
.navigationBarItems(trailing: Group {
if selection == 0 { // << here !!
Button("Some"){}
}
})
}
}
}
I learned how to create a tabBar like UIKit tabBar in swiftUI. And I want to move the center tabItem to top . Is there any way I can achieve this?
TabView code
TabView {
ViewTasks()
.tabItem {
Image(systemName: "list.bullet.below.rectangle")
Text("View Tasks")
}
AddTask()
.tabItem {
Image(systemName: "plus.circle").font(.system(size: 60))
}
CategoriesTasks()
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Categories")
}
}
Visual Example
First idea is to use ZStack so you can cover tabItem with your own tappable image. Here is example:
struct TabViewModel: View {
#State var showActionSheet: Bool = false
var body: some View {
ZStack {
GeometryReader { geometry in
TabView {
Text("list")
.tabItem {
Image(systemName: "list.bullet.below.rectangle")
}
Text("plus")
.tabItem {
Text(" ")
}
Text("categories")
.tabItem {
Image(systemName: "square.grid.2x2")
}
}
Image(systemName: "plus.circle")
.resizable()
.frame(width: 40, height: 40)
.shadow(color: .gray, radius: 2, x: 0, y: 5)
.offset(x: geometry.size.width / 2 - 20, y: geometry.size.height - 80)
.onTapGesture {
self.showActionSheet.toggle()
}
}
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("some actions"))
}
}
}
so some image will cover center tabView item, which will be invisible (Text(" ")):
update
if you still need to switch between these 3 views you can use #State var selection (don't forget to tag(...) tabItems):
struct TabViewModel: View {
#State var selection: Int = 0
var body: some View {
ZStack {
GeometryReader { geometry in
TabView(selection: self.$selection) {
//...
Text("plus")
.tabItem {
Text(" ")
}.tag(1)
//...
.onTapGesture {
self.selection = 1
}
// ...
}
SWIFTUI 2
.onTapGesture usage sometimes causes unexpected behavior such as not displaying the overlay view (alert, sheets or fullscreen) or displaying it when you click another tab. The safer way is to set the selection value with the .onChange(of:) method:
struct TabViewModel: View {
#State var selection: Int = 0
var body: some View {
ZStack {
GeometryReader { geometry in
TabView(selection: self.$selection) {
//...
Text("plus")
.tabItem {
Text(" ")
}.tag(1)
//...
.onChange(of: selection) { _ in
self.selection = 1
}
// ...
}