SwiftUI - Detect which TabView is active onAppear - swiftui

I have a TabView that is showing 4 different views. The whole tabView is set to:
.tabViewStyle(PageTabViewStyle())
(So I can make it Swipe-enabled instead of using the buttons)
I also have an HStack of simple rectangles that should highlight different colors when their respective pages are displayed. (The first rectangle will be showed as blue when the first tab is shown, the second rectangle is blue when the second page is active, etc.) I'm doing this with an onAppear on each of the views.
ViewOne()
.onAppear {
print("1")
activeTab = 1
}
If I only use two views in the tabView, this works fine. But as soon as I add the third, the onAppear goes crazy. When I swipe to the third view the print statement adds to the console in quick succession:
3
1
4
And the blue rectangle is the 4th one. The 4th page throws it off even more and things just gets strange.
My question is this: are the tabViews sort of like the dequeResuableCell of a UIKit TableView, or am I doing something wrong? Might it be a bug? and if so, is there a workaround for this that doesn't use onAppear?
EDIT:
Here's the full code implementation:
VStack {
HStack {
Rectangle()
.foregroundColor((activeTab == 1) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 2) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 3) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 4) ? .blue : .red)
}
.frame(height: 100)
.padding()
TabView(selection: $activeTab) {
// each one of the Views below has the .onAppear attached to them
viewTwo(activeTab: $activeTab)
viewTwo(activeTab: $activeTab)
viewThree(activeTab: $activeTab)
viewFour(activeTab: $activeTab)
}
.tabViewStyle(PageTabViewStyle())
}
}

So it turns out I was overcomplicating this A LOT! Rather than attempting to put an .onAppear on each of the associated views (which I've come to realize is not always perfectly reliable), I just used:
.onChange(of: activeTab, perform: { value in
print(activeTab)
})
This stabilized the code and performs the way I wanted it to.
My full code is below if it might be able to help anyone!
struct EvalView: View {
#State var activeTab = 1
var body: some View {
VStack {
HStack {
Rectangle()
.foregroundColor((activeTab == 1) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 2) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 3) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 4) ? .blue : .red)
}
.frame(height: 100)
.padding()
TabView(selection: $activeTab) {
viewOne(activeTab: $activeTab)
.tag(1)
viewTwo(activeTab: $activeTab)
.tag(2)
viewThree(activeTab: $activeTab)
.tag(3)
viewFour(activeTab: $activeTab)
.tag(4)
}
.tabViewStyle(PageTabViewStyle())
.onChange(of: activeTab, perform: { value in
print(activeTab)
})
}
}
}
struct viewOne: View {
#Binding var activeTab: Int
var body: some View {
Text("1")
}
}
struct viewTwo: View {
#Binding var activeTab: Int
var body: some View {
Text("2")
}
}
struct viewThree: View {
#Binding var activeTab: Int
var body: some View {
Text("3")
}
}
struct viewFour: View {
#Binding var activeTab: Int
var body: some View {
Text("4")
}
}
struct EvalView_Previews: PreviewProvider {
static var previews: some View {
EvalView()
}
}

Related

How can I make a bottom sheet appear behind a tabView?

Does anyone know how I can create a bottom sheet similar to the one in the Diary Queen app. I have tried a few times but every thing I've tried has made the bottom sheet appear above the bottom tabview. I need it to appear from behind the tabview just like in the screenshots.
I've tried a zstack, but every time my bottom view appears above.
import SwiftUI
struct Menu: View {
#State private var showingBottomSheet = true
var body: some View {
TabView{
orderView()
.tabItem{
Image(systemName: "questionmark")
Text("Order")
}
rewardView()
.tabItem{
Image(systemName: "star")
Text("Rewards")
}
dealsView()
.tabItem{
Image(systemName: "tag")
Text("Deals")
}
myDQView()
.tabItem{
Image(systemName: "person")
Text("My DQ")
}
currentOrderView()
.tabItem{
Image(systemName: "bag")
}
}
.padding()
.sheet(isPresented: $showingBottomSheet) {
Text("hello")
//
}
}
}
struct Menu_Previews: PreviewProvider {
static var previews: some View {
Menu()
}
}
The solution to this, is to not use a .sheet, instead you need to have a view that is built and .offset(y:...) off the screen, and have it watch for your boolean value to change. For example:
struct Menu: View {
#State private var showingBottomSheet = false
var body: some View {
TabView{
Group {
ScrollView{}
.tabItem{
Image(systemName: "questionmark")
Text("Order")
}
Text("")
.tabItem{
Image(systemName: "star")
Text("Rewards")
}
Text("")
.tabItem{
Image(systemName: "tag")
Text("Deals")
}
Text("")
.tabItem{
Image(systemName: "person")
Text("My DQ")
}
Text("")
.tabItem{
Image(systemName: "bag")
}
}
.background(
Color.gray.offset(y: showingBottomSheet ? 0 : UIScreen.main.bounds.size.height - 120)
)
}
}
}
Notice that constructing it like this, puts it behind your view. Also in this example, I set the height - 120 soley so if you copied this, you'll see that the offset is indeed behind the other view. I was also forced to use a ScrollView and replace all the other views with Text because you didn't post those views, the principle should still remain the same.
In the order view, create #state variable and using if statement to display the custom view as a sheet
struct Order: View {
#State var isShow: Bool = true
var body: some View {
ZStack(alignment: .bottom) {
VStack {
Button("Display Sheet") {
isShow.toggle()
}
Spacer()
}
if isShow {
RoundedRectangle(cornerRadius: 20)
.fill(Color.gray.opacity(0.2))
.frame(height: UIScreen.main.bounds.height * 0.8)
.transition(.push(from: .bottom))
.animation(.easeInOut(duration: 1))
}
}
}
}

How can i create TabView with Headers on top not bottom in SwiftUI

How can i make a SwiftUI TabView with headers aligned to top rather than the bottom.
Thanks
This is not directly supported by SwiftUI but you could make your own custom view and here is a simple example on how to do that:
struct ContentView: View {
#State var selectedTab = Tabs.FirstTab
var body: some View {
VStack {
HStack {
Spacer()
VStack {
Image(systemName: "airplane")
.foregroundColor(selectedTab == .FirstTab ? Color.red : Color.black)
Text("First tab")
}
.onTapGesture {
self.selectedTab = .FirstTab
}
Spacer()
VStack {
Image(systemName: "person.fill")
.foregroundColor(selectedTab == .SecondTab ? Color.red : Color.black)
Text("Second tab")
}
.onTapGesture {
self.selectedTab = .SecondTab
}
Spacer()
VStack {
Image(systemName: "cart.fill")
.foregroundColor(selectedTab == .ThirdTab ? Color.red : Color.black)
Text("Third tab")
}
.onTapGesture {
self.selectedTab = .ThirdTab
}
Spacer()
}
.padding(.bottom)
.background(Color.green.edgesIgnoringSafeArea(.all))
Spacer()
if selectedTab == .FirstTab {
FirstTabView()
} else if selectedTab == .SecondTab {
SecondTabView()
} else {
ThirdTabView()
}
}
}
}
struct FirstTabView : View {
var body : some View {
VStack {
Text("FIRST TAB VIEW")
}
}
}
struct SecondTabView : View {
var body : some View {
Text("SECOND TAB VIEW")
}
}
struct ThirdTabView : View {
var body : some View {
Text("THIRD TAB VIEW")
}
}
enum Tabs {
case FirstTab
case SecondTab
case ThirdTab
}
NOTE: I haven't put much effort in aligning the tabs perfectly and used Spacers for simplicity (because this is not relevant to the question). Also I have put all the code so that you could create new empty project and copy-paste it to try and understand how it works.
Lets go through it:
The Tabs enum is created for simplicity
The selectedTab variable is keeping track of which tab is currently selected
I am using if-else construction to display the selected view (as of SwiftUI 2.0 there is also a switch-statement, but since you didn't specify what versions are you using I am using if-else)
The tab view itself is nothing more than Image & Text views aligned to look like the TabView
The foregroundColor modifier using ternary operator is simulating TabView's accent color
When tapped, each Text + Image combination (which replaces buttons on the normal TabView) changes the state to select its view as visible (you could also use buttons with action instead of .onTapGesture I used this for the simplicity of the example)
Here is the result:

SwiftUI: animating between Single and HStack views

Goal:
1- Shows a view (Blue) that covers entire screen
2- When tapped on bottom (top right corner), it shows an HStack animating right side HStack (Green) "Slide Offset animation".
import SwiftUI
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
if showgreen {
Rectangle()
.foregroundColor(.green)
.offset(x: showgreen ? 0 : UIScreen.main.bounds.width)
.animation(.easeInOut)
}
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
The code works, however I cannot get the Green "Slide Offset animation" to work. Really appreciate any help! : )
Instead of using the if conditional, you need the green rectangle to be already present, and just offscreen. When showgreen toggles, you need to shrink the size of the blue rectangle, which will make room for the green rectangle.
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: showgreen ? UIScreen.main.bounds.width / 2 : UIScreen.main.bounds.width)
.animation(.easeInOut)
Rectangle()
.foregroundColor(.green)
.animation(.easeInOut)
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}

hide Navigation Bar only on first page - Swift UI

My app running on Swift UI, and my main page is Home(), In the home page there is NavigationView and NavigationLink(destination: SaveThePlanet()), I have hide the Navigation View on the main page "Home", its also hide in SaveThePlanet().
How can I unhide the navigation back button in the SaveThePlanet() page?
import SwiftUI
struct Home: View {
#State var show = false
#State var showSaveThePlanet = false
var body: some View {
NavigationView {
ZStack {
Color.gray
ContentView()
.blur(radius: show ? 10 : 0)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: showSaveThePlanet ? 10 : 0)
.scaleEffect(showSaveThePlanet ? 0.90 : 1)
.animation(.default)
leftIcon(show: $show)
.offset(x: 0, y: showSaveThePlanet ? 300 : 70)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.animation(.easeInOut)
SaveThePlanet()
.background(Color("Bg"))
.cornerRadius(10)
.shadow(color: Color("Green-Sh"), radius: 10, x: 0, y: 0)
.animation(.spring())
.offset(y: showSaveThePlanet ? 120 : UIScreen.main.bounds.height)
.padding()
rightIcon(show: $showSaveThePlanet)
.offset(x: 0, y: 70)
.animation(.easeInOut)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.opacity(showSaveThePlanet ? 0 : 1)
rightIconClose(show: $showSaveThePlanet)
.offset(x: 0, y: 70)
.animation(.easeInOut)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.opacity(showSaveThePlanet ? 1 : 0)
MenuView(show: $show)
}
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("Home")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(false)
}
}
}
What worked for me : have an #State property on your first view, that determines whether or not you can show the navigation bar. Then pass that property on to all subsequent views via #Binding, so that it is the 'single source of truth' for whether or not the navigation bar should show.
#State private var navBarHidden = false
Then on your main view, reference that property for the navBarHidden property, and set the title. Also add an onAppear closure, which will set that hidden property for when this view re-appears, ie if we pop back here from a detail view.
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(navBarHidden: self.$navBarHidden)
) {
Text("Go to detail view")
}
}
.navigationBarTitle("")
.navigationBarHidden(self.navBarHidden)
.onAppear(perform: {
self.navBarHidden = true
})
}
Then on a subsequent detail view, pass that navBarHidden property on as an #Binding (its passed in above)
#Binding var navBarHidden : Bool
var body: some View {
Text("Hello Detail View!")
.navigationBarTitle("Detail")
.onAppear() {
self.navBarHidden = false
}
}
And when the onAppear() is called above in the detail view, it sets that original property to false for hidden, which shows the nav bar. And when you click back to return to the home view, the onAppear() of the home view is called again, which sets it back to hidden = true.
I'm answering because I think the solution nowadays is pretty easier. So for people having the same problem just add a
.navigationBarHidden(true)
on your Home() component and you should be fine. This solution works for sure in Swift 5.5, there is to do the onAppear and onDisappear trick of the other answers. It will hide only the navigation bar on the view you specified
It's a little hard to tell based on the code you've posted, but it looks like you are trying to present a view that slides up from the bottom when showSaveThePlanet is true, and also show the navigation bar only when that view appears.
This can be accomplished by setting .navigationBarHidden(!showSaveThePlanet) anywhere in your body property. Note that your code does not use NavigationLink anywhere to push a new view onto the NavigationView stack, so you would not get a back button. You can add your own button to dismiss the sheet using .navigationBarItems(leading:)
Here is a simplified example showing what I mean.
struct ContentView: View {
#State private var detailShowing = false
var body: some View {
NavigationView {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
Color.gray.edgesIgnoringSafeArea(.all)
// A card-like view that is initially offscreen,
// and slides on when detailShowing == true
DetailView()
.offset(x: 0, y: detailShowing ? 120 : UIScreen.main.bounds.height)
.animation(.spring())
// Just here to change state
Button("Toggle") {
self.detailShowing.toggle()
}
.padding()
.offset(x: 0, y: detailShowing ? 0 : 44)
.animation(.none)
}
// This is the key modifier
.navigationBarHidden(!detailShowing)
.navigationBarTitle("Detail View", displayMode: .inline)
.navigationBarItems(leading: Button("Close") {
self.detailShowing = false
})
}
}
}
struct DetailView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
RoundedRectangle(cornerRadius: 15).fill(Color.secondary).frame(width: 300, height: 500)
Text("Detail Content")
.padding()
}
}
}
Best way that I found is toggling the navBarHidden in destination views. This way incomplete swipe to pop gestures will not remove the navigation bar.
So in your main view you would write
#State private var navBarHidden = true
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(navBarHidden: self.$navBarHidden)
) {
Text("Go to detail view")
}
}
.navigationBarTitle("")
.navigationBarHidden(self.navBarHidden)
}
And in your destination view:
#Binding var navBarHidden : Bool
var body: some View {
Text("Hello Detail View!")
.navigationBarTitle("Detail")
.onAppear {
self.navBarHidden = false
}
.onDisappear {
self.navBarHidden = true
}
}