How to use different tab items for selected/unselected? - swiftui

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
}
}
}

Related

TabView changes selection even with empty custom handler

The code below changes the selection variable. Why? I thought a custom handler allowed me to intercept everything. The behavior I expect is for nothing to happen when I press a tab item. But what it does is set the selection variable and change the view. What gives?
struct ContentView: View {
#State private var selection = 0
var handler: Binding<Int> { Binding(
get: { selection },
set: { log.info("I don't set the selection variable, the tab changes \($0)") }
) }
var body: some View {
ZStack {
TabView(selection: handler) {
Text("view 0")
.tabItem {
Label("Home", systemImage: "house.fill")
}.tag(0)
Text("view 1")
.tabItem {
Label("Arrows", systemImage: "arrow.left.arrow.right")
}.tag(1)
Text("view 3")
.tabItem {
Label("Arrow Up", systemImage: "arrow.up")
}.tag(2)
}
}
}
}

SwiftUI call function on tab view navigation

I want to execute functions when the user switches tabs in a TabView. Strangely, I can't seem to do this with .simultaneousGesture. How can I execute functions on the event of the user switching tabs in a tab view?
Sample Code:
struct ContentView: View {
#State var selectedTab: Int = 0
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Text("View One")
.tabItem {
Text("View One")
}
.simultaneousGesture(TapGesture().onEnded {
print("This isn't printing")
})
Text("View Two")
.tabItem {
Text("View Two")
}
.simultaneousGesture(TapGesture().onEnded {
print("This isn't printing")
})
Text("View Three")
.tabItem {
Text("View Three")
}
.simultaneousGesture(TapGesture().onEnded {
print("This isn't printing")
})
}
}
}
}
EDIT: Updated Code
struct ContentView: View {
#State var selectedTab: Int = 0
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Text("View One")
.tabItem {
Text("View One")
}
Text("View Two")
.tabItem {
Text("View Two")
}
Text("View Three")
.tabItem {
Text("View Three")
}
}
}
.onReceive(Just(selectedTab)) { thing in
print("Tapped!!")
print("\(thing)")
}
}
}
Add a .tag modifier to each tab and use a custom Binding that does the work in the setter.
struct ContentView: View {
#State var selectedTab: Int = 0
var body: some View {
NavigationView {
TabView(selection: Binding(
get: { selectedTab },
set: {
selectedTab = $0
print("switched to tab \(selectedTab)")
}
)) {
Text("View One")
.tabItem { Text("1") }
.tag(1)
Text("View Two")
.tabItem { Text("2") }
.tag(2)
Text("View Three")
.tabItem { Text("3") }
.tag(3)
}
}
}
}

SwiftUI: How to update image to filled / outlined in TabView

I'm trying to use filled image when it is selected and outlined image when it is deselected. I tried to render the images but still filled
So I thought this would work, but it doesn't:
struct ListTabView: View {
#State private var selectedTab = 0
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Text("Tab 1")
.onTapGesture {
self.selectedTab += 1
}
.tabItem {
selectedTab == 0 ? Image(systemName: "star.fill") : Image(systemName: "star")
Text("First")
}
.tag(0)
Text("Tab 2")
.onTapGesture {
self.selectedTab -= 1
}
.tabItem {
selectedTab == 1 ? Image(systemName: "moon.stars.fill") : Image(systemName: "moon.stars")
Text("Second")
}
.tag(1)
}
.accentColor(.pink)
.onAppear {
UITabBar.appearance().barTintColor = .white
}
}
}
}
struct ListTabView_Previews: PreviewProvider {
static var previews: some View {
ListTabView()
}
}
Your code actually works. The issue is something else not documented that I can find. If you use a non .fill variant of an SF Font, the .fill variant will be substituted. Use the following code to test it:
TabView(selection: $selectedTab) {
VStack {
Text("Tab 1")
Text(Image(systemName: "star"))
}
.tabItem {
selectedTab == 0 ? Image(systemName: "star") : Image(systemName: "sun.max")
Text("First")
}
.tag(0)
VStack {
Text("Tab 2")
Text(Image(systemName: "moon.stars"))
}
.tabItem {
selectedTab == 1 ? Image(systemName: "moon.stars") : Image(systemName: "sun.max")
Text("Second")
}
.tag(1)
}
You will note I used plain variants, and yet the filled variant was used. Also, you don't need the .onTap(), but I suspect you added it when the images didn't seem to switch.
You can add this to Image / Label:
Image(systemName: selectedTab == 0 ? "star.fill" : "star")
.environment(\.symbolVariants, selectedTabItemIndex == 0 ? .fill : .none)
It will allow you to set the symbol variants as you would expect it.

SwiftUI tab view display sheet

I'm new to SwiftUI and I tried to build a tab bar that contained a tab that will return a modal(sheet) but not view. After I tried I found sometimes it will work but sometime are not. I want to make the previous tabbed item as the selected tab after the user dismissed the modal. But I can't find what the error. Anyone explains to me what the problem of my code?
import SwiftUI
struct ContentView: View {
#State var isPresenting = false
#State private var selectedItem = 1
#State private var oldSelectedItem = 1
var body: some View {
TabView(selection: $selectedItem){
Text("1")
.tabItem {
Image(systemName: "house")
}.tag(1)
.onAppear {
self.oldSelectedItem = self.selectedItem
}
Text("") // I want this to display the sheet.
.tabItem { Image(systemName: "plus.circle") }
.tag(2)
.onAppear {
self.isPresenting = true
self.selectedItem = self.oldSelectedItem
}
Text("3")
.tabItem {
Image(systemName: "calendar")
}.tag(3)
.onAppear {
self.oldSelectedItem = self.selectedItem
}
}
.sheet(isPresented: $isPresenting) {
testSheet
}
.accentColor(Color.orange)
}
var testSheet : some View {
VStack{
Text("testing")
}
}
}
A possible solution is to use TabView selection to activate sheet programmatically, but do not actually allow this selection to be changed (tested with Xcode 12 / iOS 14).
Update: retested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State var isPresenting = false
#State private var selectedItem = 1
#State private var oldSelectedItem = 1
var body: some View {
TabView(selection: $selectedItem){
Text("1")
.tabItem {
Image(systemName: "house")
}.tag(1)
Text("") // I want this to display the sheet.
.tabItem { Image(systemName: "plus.circle") }
.tag(2)
Text("3")
.tabItem {
Image(systemName: "calendar")
}.tag(3)
}
// .onReceive(Just(selectedItem)) // SwiftUI 1.0 - import Combine for this
.onChange(of: selectedItem) { // SwiftUI 2.0 track changes
if 2 == selectedItem {
self.isPresenting = true
} else {
self.oldSelectedItem = $0
}
}
.sheet(isPresented: $isPresenting, onDismiss: {
self.selectedItem = self.oldSelectedItem
}) {
testSheet
}
.accentColor(Color.orange)
}
var testSheet : some View {
VStack{
Text("testing")
}
}
}
A small change to Martijn Pieters's answer:-
The original code changes the current tab to a blank tab behind the sheet. This update addresses this issue by keeping the last selected tab alive.
struct ContentView: View {
#State var isPresenting = false
#State private var selectedItem = 1
#State private var oldSelectedItem = 1
var body: some View {
TabView(selection: $selectedItem){
Text("1")
.tabItem {
Image(systemName: "house")
}.tag(1)
Text("") // I want this to display the sheet.
.tabItem { Image(systemName: "plus.circle") }
.tag(2)
Text("3")
.tabItem {
Image(systemName: "calendar")
}.tag(3)
}
// .onReceive(Just(selectedItem)) // SwiftUI 1.0 - import Combine for this
.onChange(of: selectedItem) { // SwiftUI 2.0 track changes
if 2 == selectedItem {
self.isPresenting = true
self.selectedItem = self.oldSelectedItem
} else if (isPresented == false) {
self.oldSelectedItem = $0
}
}
.sheet(isPresented: $isPresenting) {
testSheet
}
.accentColor(Color.orange)
}
var testSheet : some View {
VStack{
Text("testing")
}
}
}
After user clicks on the sheet option, the onChange listener restores self.oldselecteditem as the active tab. The dismiss event listener on the sheet has been removed since the last active tab is already active and the listener would serve no purpose.
Add the code below to your TabView
.onChange(of: selectedItem) { newValue in
if newValue == 2 {
isPresenting = true
selectedItem = oldSelectedItem
} else {
oldSelectedItem = newValue
}
}
.sheet(isPresented: $isPresenting) {
// Sheet view
}

SwiftUI: Can I add more Views to TabView then Tab Items?

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)")
}
}