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
Swift Safely Unwrapping Optinal Strings and Ints
iOS 13, Custom Image, Title and Subtitle in the Presented Uiactivityviewcontroller
How to Record My MAC's Internal Sound, Not the Microphone!, Using Avcapturesession
Calculate Time Difference in Swift 4
Realm, Avoid to Store Some Property
Swiftui Sheet Not Animating Dismissal on MACos Big Sur
iOS Swift Didbegincontact Not Being Called
Swift 3:Appdelegate Does Not Conform to Protocol Gidsignindelegate
Realm: Map JSON to Realm-Objects with Alamofire
Swift Pattern Matching with Enum and Optional Tuple Associated Values
How to Check Whether an Object Is Kind of a Dynamic Class Type in Swift
Converting Audiobuffer to Cmsamplebuffer with Accurate Cmtime
Create CSV File in Swift and Write to File
Swiftui MACos Scroll a List with Arrow Keys While a Textfield Is Active