Swiftui Tabbar: Action for Tapping Tabitem of Currently Selected Tab to Reset View

SwiftUI TabBar: Action for tapping TabItem of currently selected Tab to reset view

Here is possible solution - inject proxy binding around TabView selection state and handle repeated tab tapped before bound value set, like below.

Tested with Xcode 12.1 / iOS 14.1

struct ContentView: View {
@State private var selection = 0

var handler: Binding<Int> { Binding(
get: { self.selection },
set: {
if $0 == self.selection {
print("Reset here!!")
}
self.selection = $0
}
)}

var body: some View {
TabView(selection: handler) {
Text("Tab 1")
.tabItem {
Image(systemName: "star")
Text("One")
}
.tag(0)

Text("Tab 2")
.tabItem {
Image(systemName: "star.fill")
}
.tag(1)
}
}
}

TabView, tabItem: running code on selection or adding an onTapGesture

Here is a solution - you can observe tab selection change and react correspondingly.

Tested with Xcode 12 / iOS 14

import Combine   // << needed for Just publisher below

struct ContentView: View {
@State private var two : String = "Two"
@State private var selection: Int = 1

var body: some View {
TabView(selection: $selection) {
Text("One")
.tabItem {
Text("One")
}.tag(1)
Text("Two")
.tabItem {
Text(two)
}.tag(2)
Text("Three")
.tabItem {
Text("Three")
}.tag(3)
}
// .onChange(selection) { // << if SwiftUI 2.0 min spec
.onReceive(Just(selection)) {
print("Tapped!!")
if $0 == 2 {
self.two = "One"
}
}
}
}

SwiftUI Disable specific tabItem selection in a TabView?

You are pretty close to what you want to achieve. You will just need to preserve the previous selected tab index and reset the current selected tab index with that preserved value at the time of the dismissal of the sheet. That means:

.sheet(isPresented: self.$isPresenting, onDismiss: {
// change back to previous tab selection
self.appState.selectedTab = self.appState.previousSelectedTab
}, content: { self.content })

So how do you keep track of the last selected tab index that stays in sync with the selectedTab property of the AppState? There may be more ways to do that with the APIs from Combine framework itself, but the simplest solution that comes to my mind is:

final class AppState: ObservableObject {
// private setter because no other object should be able to modify this
private (set) var previousSelectedTab = -1
@Published var selectedTab: Int = 0 {
didSet {
previousSelectedTab = oldValue
}
}
}

Caveats:

The above solution of may not be the exact thing as disable specific tab item selection but after you dismiss the sheet it will revert back with a soothing animation to the selected tab prior to presenting the sheet. Here is the result.

Selected tab image for a TabView's TabItem is always blue in SwiftUI

You do not set a selected and not selected icon on your TabView, you only provide one image and it gets rendered in secondary colour when not selected and in accent colour when selected.
If you want your selected tab icons to be black you could set the accent on colour on TabView to black, but it is against Apple's design guidelines and you will change the accent colour of all subviews as well.

Custom Tab Bar with variable number of tabs in SwiftUI

I honestly love @Scott Matthewman's answer! It inspired me to try an implementation – I included Scotts to do points as remarks :)

Model:

struct SinglePage: Identifiable, Equatable {
var id: UUID
var title: String
var image: String

init(title: String, image: String) {
self.id = UUID()
self.title = title
self.image = image
}

static func == (lhs: SinglePage, rhs: SinglePage) -> Bool {
return lhs.id == rhs.id
}
}

class PagesModel: ObservableObject {

// an ordered collection of pages, each with a title and image
@Published var pages: [SinglePage]
// a concept of which page in that collection is active
@Published var selectedPage: SinglePage?

init() {
// Test Data
pages = []
for i in 0..<4 {
let item = SinglePage(title: "Tab \(i)", image: "\(i).circle")
self.pages.append(item)
}
selectedPage = pages.first ?? nil
}

// your user can choose which page should be the active one
func select(page: SinglePage) {
selectedPage = page
}

// they can add a new page to the collection
func add(title: String, image: String) {
let item = SinglePage(title: title, image: image)
self.pages.append(item)
}

// they can remove an existing page from the collection
func delete(page: SinglePage) {
pages.removeAll(where: {$0 == page})
}
}

Views:

struct ContentView: View {

@StateObject var tabs = PagesModel()

var body: some View {
VStack {
// A list of horizontal tabs loops through the ordered collection of pages and renders each one
HStack {
ForEach(tabs.pages) { page in
TabLabelView(page: page)
}
// A separate button, when tapped, can add a new page to the collection
AddTabButton()
}
ActiveTabContentView(page: tabs.selectedPage)
}
.environmentObject(tabs)
}
}

struct TabLabelView: View {

@EnvironmentObject var tabs: PagesModel

let page: SinglePage

var body: some View {
HStack {
// Each tab has a close button which, when tapped, removes it from the pages collection
Button {
tabs.delete(page: page)
} label: {
Image(systemName: "xmark")
}

Text(page.title)
}
.font(.caption)
.padding(5)
// .frame(height: 50)
.background(
Color(page == tabs.selectedPage ? .red : .gray)
)
// Each tab has a button action which, when tapped, sets that button to be the active one
.onTapGesture {
tabs.select(page: page)
}
}
}

// A separate button, when tapped, can add a new page to the collection
struct AddTabButton: View {

@EnvironmentObject var tabs: PagesModel

var body: some View {
Button {
tabs.add(title: "New", image: "star")
} label: {
Label("Add", systemImage: "add")
}
.font(.caption)
.padding(5)
}
}

struct ActiveTabContentView: View {

@EnvironmentObject var tabs: PagesModel

let page: SinglePage?

var body: some View {
if let page = page {
VStack {
Spacer()
Text(page.title)
Image(systemName: page.image)
.font(.largeTitle)
Spacer()
}
}
}
}

TabView resets navigation stack when switching tabs

Here's a simple example of how to preserve state for a navigation stack with a list of items at the root:

struct ContentView: View {

var body: some View {

TabView {

Text("First tab")
.tabItem { Image(systemName: "1.square.fill"); Text("First") }
.tag(0)

SecondTabView()
.tabItem { Image(systemName: "2.square.fill"); Text("Second") }
.tag(1)
}
}
}

struct SecondTabView: View {

private struct ListItem: Identifiable {
var id = UUID()
let title: String
}

private let items = (1...10).map { ListItem(title: "Item #\($0)") }

@State var selectedItemIndex: Int? = nil

var body: some View {

NavigationView {
List(self.items.indices) { index in
NavigationLink(destination: Text(self.items[index].title),
tag: index,
selection: self.$selectedItemIndex) {
Text(self.items[index].title)
}
}
.navigationBarTitle("Second tab", displayMode: .inline)
}
}
}


Related Topics



Leave a reply



Submit