SwiftUI how to keep track of page index in PageTabViewStyle? - ios

I'm trying to keep track of what page the user is on in a TabView that is PageTabViewStyle in SwiftUI but I can't figure out the best way to keep track of the page index? Using .onAppear doesn't work well as it gets called multiple times and pages 2 and 3 get called even when not on the screen. 🤔
#State var pageIndex = 0
var body: some View {
VStack {
Text("current page = \(0) ")
TabView {
Text("First")
.onAppear {
pageIndex = 0
}
Text("Second")
.onAppear {
pageIndex = 1
}
Text("Third")
.onAppear {
pageIndex = 2
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}

You can pass a selection binding to the TabView and use tag to identify the pages:
struct ContentView: View {
#State var pageIndex = 0
var body: some View {
VStack {
Text("current page = \(pageIndex) ")
TabView(selection: $pageIndex) {
Text("First").tag(0)
Text("Second").tag(1)
Text("Third").tag(2)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}
Note that in your original code, it was always going to say current page = 0 because you weren't interpolating the pageIndex variable into the string

If you want to be able to get the current page/index and act on that data, what I found useful is using the .onChange() modifier on the view, like so.
TabView(selection: $currentIndex) {
Text("First view")
.tag(0)
Text("Middle view")
.tag(1)
Text("Last view")
.tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.onChange(of: currentIndex) { newValue in
print("New page: \(newValue)")
}
Hope that helps!

Related

SwiftUI TabView inside a NavigationView

So I know it's not really encouraged to put a TabView inside a NavigationView and that you're supposed to do it the other way around. But the way I want my app I don't really see how I can have it another way...
So I have a login screen in which a user inputs their username, then only once I verify that everything is okay with username I wanna bring them over to a TabView(the search button is a navlink) I don't really see any other way to implement this but the problem is with my implementation is once I switch tabs in the tab view, the navigation title doesn't seem to change, and there also doesn't seem to be a navigation bar because when I scroll the old NavigationTitle gets drawn over by a Text View I have.
I'm not sure if adding code would help in this case because it seems this is just kind of a problem with TabViews inside NavigationViews but if someone wants me to show some code I can add an edit with it. I was just wondering if anyone had any ideas for how I could fix something like this or some other way to implement this?
Edit:
struct ContentView: View {
var body: some View {
NavigationView{
NavigationLink{
TabView{
ScrollView{
Text("Some view")
}
.tabItem{
Text("New View")
}
ScrollView{
Text("Another view")
}
.tabItem{
Text("Another view")
}
}
} label: {
Text("Go to new view!")
}
}
}
}
It is perfectly fine to have TabView() inside a NavigationView. Every time we switch between pages in any app, the navigating is mostly expected, so almost every view is inside the NavigtionView for this reason.
You can achieve this design with something like this (see the bottom images also):
struct LoginDemo: View {
#State var username = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your user name", text: $username)
.font(.headline)
.multilineTextAlignment(.center)
.frame(width: 300)
.border(.black)
NavigationLink {
GotoTabView()
} label: {
Text("search")
}
.disabled(username == "Admin" ? false : true)
}
}
.navigationTitle("Hey It's Nav View")
}
}
struct GotoTabView: View {
#State var temp = "status"
#State var selection = "view1"
var body: some View {
TabView(selection: $selection) {
Image("Swift")
.resizable()
.frame(width: 300, height: 300)
.tabItem {
Text("view 1")
}
.tag("view1")
Image("Swift")
.resizable()
.frame(width: 500, height: 500)
.tabItem {
Text("view 2")
}
.tag("view2")
}
.onChange(of: selection){ _ in
if selection == "view1" {
temp = "status"
}
else {
temp = "hero"
}
}
.toolbar{
ToolbarItem(placement: .principal) {
Text(temp)
}
}
}
}
NavigationView:
TabView:

Is there a way to change a tabItem's image when selected in SwiftUI when using ForEach?

Considering the simplest possible tab view app in SwiftUI:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
Text("First View")
.font(.title)
.tabItem {
VStack {
Image("first")
Text("First")
}
}
.tag(0)
Text("Second View")
.font(.title)
.tabItem {
VStack {
Image("second")
Text("Second")
}
}
.tag(1)
}
}
}
Whenever a tab is tapped, the tab items are reloaded and you have the opportunity to change the image for a selected tab if you needed by comparing the selection with the tag. However, if you have a dynamic number of tabs in a variable and use a ForEach to display them, that doesn't work:
struct ContentView: View {
#State private var selection = 0
private var items: [AnyView] {
return [
AnyView(Text("First View")
.font(.title)
.tabItem {
VStack {
Image("first1")
Text("First1")
}
}
.tag(0)),
AnyView(Text("Second View")
.font(.title)
.tabItem {
VStack {
Image("second")
Text("Second")
}
}
.tag(1))
]
}
var body: some View {
TabView(selection: $selection){
ForEach(0..<self.items.count) { index in
self.items[index]
}
}
}
}
The body of the ForEach is not called when the view reloads. Is there a way to accomplish changing an image while also using a ForEach in your TabView?
Try the following:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
Text("First View")
.font(.title)
.tabItem {
VStack {
Image(systemName: selection == 0 ? "xmark" : "plus")
Text("First")
}
}
.tag(0)
Text("Second View")
.font(.title)
.tabItem {
VStack {
Image(systemName: selection == 1 ? "xmark" : "minus")
Text("Second")
}
}
.tag(1)
}
}
}
The answer is in the docs! When using a ForEach with a range:
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
So to make this work, use ForEach(_:id:content:).

Why onAppear called again after onDisappear while switching tab in TabView in SwiftUI?

I am calling API when tab item is appeared if there is any changes. Why onAppear called after called onDisappear?
Here is the simple example :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
Text("Home")
.navigationTitle("Home")
.onAppear {
print("Home appeared")
}
.onDisappear {
print("Home disappeared")
}
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView {
Text("Account")
.navigationTitle("Account")
.onAppear {
print("Account appeared")
}
.onDisappear {
print("Account disappeared")
}
}
.tabItem {
Image(systemName: "gear")
Text("Account")
}.tag(1)
}
}
}
Just run above code and we will see onAppear after onDisappear.
Home appeared
---After switch tab to Account---
Home disappeared
Account appeared
Home appeared
Is there any solution to avoid this?
It's very annoying bug, imagine this scenario:
Home view onAppear method contains a timer which is fetching data repeatedly.
Timer is triggered invisibly by switching to the Account view.
Workaround:
Create a standalone view for every embedded NavigationView content
Pass the current selection value on to standalone view as #Binding parameter
E.g.:
struct ContentView: View {
#State var selected: MenuItem = .HOME
var body: some View {
return TabView(selection: $selected) {
HomeView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(MenuItem.HOME)
AccountView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "gear")
Text("Account")
}
}
.tag(MenuItem.ACCOUNT)
}
}
}
enum MenuItem: Int, Codable {
case HOME
case ACCOUNT
}
HomeView:
struct HomeView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Home")
.onAppear(perform: {
if MenuItem.HOME == selectedMenuItem {
print("-> HomeView")
}
})
}
}
AccountView:
struct AccountView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Account")
.onAppear(perform: {
if MenuItem.ACCOUNT == selectedMenuItem {
print("-> AccountView")
}
})
}
}
To whom it may help.
Because this behaviour I only could reproduce on iOS 14+, I end up using https://github.com/NicholasBellucci/StatefulTabView (which properly only get called when showed; but don't know if it's a bug or not, but it works with version 0.1.8) and TabView on iOS 13+.
I'm not sure why you are seeing that behaviour in your App. But I can explain why I was seeing it in my App.
I had a very similar setup to you and was seeing the same behaviour running an iOS13 App on iOS14 beta. In my Home screen I had a custom Tab Bar that would animate in and out when a detail screen was displayed. The code for triggering the hiding of the Tab Bar was done in the .onAppear of the Detail screen. This was triggering the Home screen to be redrawn and the .onAppear to be called. I removed the animation and found a much better set up due to this bug and the Home screen .onAppear stopped being called.
So if you have something in your Account Screen .onAppear that has a visual effect on the Home Screen then try commenting it out and seeing if it fixes the issue.
Good Luck.
I have been trying to understand this behavior for a number of days now. If you are working with a TabView, all of your onAppears() / onDisapear() will fire immediately on app init and never again. Which actually makes since I guess?
This was my solution to fix this:
import SwiftUI
enum TabItems {
case one, two
}
struct ContentView: View {
#State private var selection: TabItems = .one
var body: some View {
TabView(selection: $selection) {
ViewOne(isSelected: $selection)
.tabBarItem(tab: .one, selection: $selection)
ViewTwo(isSelected: $selection)
.tabBarItem(tab: .two, selection: $selection)
}
}
}
struct ViewOne: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View One")
.onChange(of: isSelected) { _ in
if isSelected == .one {
// Do something
}
}
}
}
struct ViewTwo: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View Two")
.onChange(of: isSelected) { _ in
if isSelected == .two {
// Do something
}
}
}
}
View Modifier for custom TabView
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifer: ViewModifier {
let tab: TabBarItem
#Binding var selection: TabBarItem
func body(content: Content) -> some View {
content
.opacity(selection == tab ? 1.0 : 0.0)
.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
modifier(TabBarItemViewModifer(tab: tab, selection: selection))
}
}

hide TabView after clicking on a NavigationLink in SwiftUI

when I have a TabView{} and the first Tab has a NavigationView, when I click on a Row, I want that TabView{} to disappear. How do I do that?
Same Issue here: How to hide the TabBar when navigate with NavigationLink in SwiftUI?
But unfortunately no solution.
There is no way to do that currently. For example, NavigationView responds to the .navigationBarHidden(_:) method on its descendants, but there is not an equivalent for TabView.
If this is something you'd like to see, let Apple know.
there's no way to hide TabView so I had to add TabView inside ZStack as this:
var body: some View {
ZStack {
TabView {
TabBar1().environmentObject(self.userData)
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
TabBar2()
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
}
if self.userData.showFullScreen {
FullScreen().environmentObject(self.userData)
}
}
}
UserData:
final class UserData: ObservableObject {
#Published var showFullScreen = false}
TabBar1:
struct TabBar1: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("TabBar 1")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.green)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
FullScreen:
struct FullScreen: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("FullScreen")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.red)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
check full code on Github
there's also some other ways but it depends on the structure of the views
To solve this limitation, I came out with this approach:
Created an enum to identify the tabs
enum Tabs: Int {
case tab1
case tab2
var title: String {
switch self {
case .tab1: return "Tab 1 Title"
case .tab2: return "Tab 2 Title"
}
}
var imageName: String {
switch self {
case .tab1: return "star" // Example using SF Symbol
case .tab2: return "ellipsis.circle"
}
}
}
Inside the view, such as ContentView.swift, added a property like this:
#State private var selectedTab = Tabs.tab1
Inside the body:
NavigationView {
TabView(selection: $selectedTab) {
ViewExample1()
.tabItem {
Image(systemName: Tabs.tab1.imageName)
Text(Tabs.tab1.title)
}.tag(Tabs.tab1)
ViewExample2()
.tabItem {
Image(systemName: Tabs.tab2.imageName)
Text(Tabs.tab2.title)
}.tag(Tabs.tab2)
}
.navigationBarTitle(selectedTab.title)
}
That's all. I hope this might be helpful.
Note: Just be aware this workaround hides the TabView in any and all child views, if you want to hide in just a particular view, this won't give you the result that you looking for.
Hopefully, Apple implements an (official and proper) option to hide the TabView soon.

Handle Tab Selection SwiftUI

Following tutorials, I have the following code to show a tab view with 3 tab items all with an icon on them, When pressed they navigate between the three different views. This all works fine, however, I want to be able to handle the selection and only show views 2 or 3 if certain criteria are met.
Is there a way to get the selected value and check it then check my own criteria and then show the view is criteria is met, or show an alert if it is not saying they can't use that view at the moment.
Essentially I want to be able to intercept the selection value before it switches out the view, maybe I need to rewrite all of this but this is the functionality I'm looking for as this is how I had my previous app working using the old framework.
#State private var selection = 1
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
}
}
You can do it by changing the value of selection on tap. You can use .onAppear() method for a particular tab to check your condition:
#State private var selection = 1
var conditionSatisfied = false
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
}
}

Resources