Adding animation to TabViews in SwiftUI when switching between tabs - swift

The question might look familiar, but I went through all solutions on this topic but none had a working approach for the latest versions of SwiftUI and iOS.
So here is my tab view, I am trying to animate when switching between the tabs. I tried binding animations by adding animation to the binding and that does not work. I also tried attaching the onChange modifier on the TabView itself which prints the correctly selected tab but it does not animate so neither approach works could someone point to the correct implementation of this?
struct MainTabScreen: View {
#State private var selectedTab = 0
var body: some View {
// The binding animation does not animate
TabView (selection: $selectedTab.animation(
.easeInOut(duration: 1.0))
) {
Home()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.tag(0)
Dollar()
.tabItem {
Image(systemName: "dollarsign.circle")
Text("Dollar")
}
.tag(1)
Menu()
.tabItem {
Image(systemName: "plus")
Text("Menu")
}
.tag(2)
}
.onChange(of: selected, perform: { tab in
print("TAPPED ON :\(tab)") // prints correct tab
withAnimation(.easeInOut(duration: 2)) {
selectedTab = tab // does not animate
}
})
}
}
Most solutions online advise using .animation(.easeInOut) to the tab view itself, but this is now deprecated. Looking for a solution that works with iOS 15 and Swift 5.

SwiftUI.TabView is one of those Views that just offer the basic Apple look.
You can easily substitute that SwiftUI.TabView with your own so you can add any animations, transitions, colors that work for you app.
import SwiftUI
struct MainTabScreen: View {
#State private var selectedTab: Tabs = .home
var body: some View {
VStack{
//Present only the View that is selected
selectedTab.view()
// You can also apply transitions if you want
//.transition(.slide)
//Have the selected View take up all the available space
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//This is the "TabView"
HStack{
ForEach(Tabs.allCases, id:\.rawValue){ tab in
tab.label()
//Change the color for the selected case
.foregroundColor(selectedTab == tab ? Color.blue : nil)
//Select the animation and change the tab using your desired abimation
.onTapGesture {
withAnimation(.easeInOut){
selectedTab = tab
}
}
//Stretch the Views so they take up the entire bottom
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
//Set the height of the TabView
.frame( height: 80, alignment: .center)
}
}
//Enum that keeps track of the New TabView's Views
enum Tabs:String,CaseIterable,CustomStringConvertible{
case home
case dollar
case menu
///Formatted name for the View
var description: String{
rawValue.capitalized
}
///Image that represents the View
#ViewBuilder func image() -> some View{
switch self {
case .home:
Image(systemName: "house")
case .dollar:
Image(systemName: "dollarsign.circle")
case .menu:
Image(systemName: "plus")
}
}
///Primary View for the tab
#ViewBuilder func view() -> some View{
switch self {
case .home:
Text("Home()")
case .dollar:
Text("Dollar()")
case .menu:
Text("Menu()")
}
}
///Label for the TabView
#ViewBuilder func label() -> some View{
switch self {
default:
VStack{
image()
Text(description)
}
}
}
}
}
struct MainTabScreen_Previews: PreviewProvider {
static var previews: some View {
MainTabScreen()
}
}

Related

How can I track scrolling with a ScrollView linked to a Custom PageControl - SwiftUI

I want to create a Carousel with SwiftUI(without using TabView)
with a matching/linked Page Control in SwiftUI
So far I have both views and can update the pageControl view with a
#State var pagecontrolTracker updated with a DragGesture() .onChanged but it doesn't update the PageControl if I scroll fast, or sometimes doesn't update at all 😭.
If I Scroll slow tho, the Page Control does update sometimes as expected.
Is there a better way to update this faster and smoother?
I saw .updating modifier for DragGesture() but this doesn't work either
Full View:
struct ContentView: View {
#State var pagecontrolTracker: Int = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0...3, id: \.self) { index in
PagingRow()
.gesture(DragGesture().onChanged({ _ in
pagecontrolTracker = index
}))
}
}
}
PagingControls(pagecontrolTracker: $pagecontrolTracker)
}
.padding()
}
}
Inside Custom SwiftUI Row View
struct PagingRow: View {
var body: some View {
VStack {
HStack {
Image(systemName: "globe")
Text("Test Title")
}
.padding()
Button {
print("Test action")
} label: {
Text("Tap Me")
}
.buttonStyle(.borderedProminent)
.padding()
}
.background(Color.orange)
.frame(width: 200)
.cornerRadius(8)
}
}
Custom PageControl in SwiftUI
struct PagingControls: View {
#Binding var pagecontrolTracker: Int
var body: some View {
HStack {
ForEach(0...3, id: \.self) { pagingIndex in
Circle()
.fill(pagecontrolTracker == pagingIndex ? .orange : .black)
.frame(width: 8, height: 8)
}
}
}
}
Note: I don't want to use TabView since I want to be able to show the next upcoming card in the scrollView
A TabView would only show one card per page

Disable to Change Page On Swipe of Tab View in SwiftUI

I am using Tab View in my SwiftUI app. I want the changing of page disabled, while swiping left or right. And I had achieved it from this. This works fine but the issue I am facing is I have a button on the bottom of every view, and when I try to swipe from the button, it is swiping left right. I want to disable it too but don't know how to do it. Here is my code:
struct TabViewTesting: View {
#State var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
firstView().tag(0).contentShape(Rectangle()).gesture(DragGesture())
secondView().tag(1).contentShape(Rectangle()).gesture(DragGesture())
thirdView().tag(2).contentShape(Rectangle()).gesture(DragGesture())
}.tabViewStyle(.page(indexDisplayMode: .never))
}
}
And this is the code for the Views:
extension TabViewTesting {
func firstView() -> some View {
VStack {
Text("First screen")
Spacer()
Button {
self.tabSelection = 1
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 2nd view")
.foregroundColor(.white)
}
}.padding()
}.background(.green)
}
func secondView() -> some View {
VStack {
Text("second screen")
Spacer()
Button {
self.tabSelection = 2
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 3rd view")
.foregroundColor(.white)
}
}.padding()
}.background(.red)
}
func thirdView() -> some View {
VStack {
Text("Third screen")
Spacer()
Button {
self.tabSelection = 0
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to first view")
.foregroundColor(.white)
}
}.padding()
}.background(.yellow)
}
}
And this is what happening:
I actually found an answer in the comments of this question.
The issue is: any view that has an "onTapGesture" will ignore ".gesture(DragGesture())".
The solution is to use ".simulataneousGesture(DragGesture())" instead to ensure the gesture is capture and handled by both view/modifier.
It worked perfectly in my case after changing it. The only exception is for 2 finger drag gesture.

Taking Top Space When Adding Tab Bar in SwiftUI

I am integrating Side Menu in my SwiftUI app. The side menu is working fine. The problem I am getting is I have a tab bar at the bottom, and when I open my side menu screen, it is taking some space from the top and I am not getting why it is getting that space. This is what I am getting:
And when I open Debug View Hierarchy, it is showing me this:
This is my code for my home screen:
struct HomeView: View {
#State var menuOpen: Bool = false
var body: some View {
Zstack {
ZStack {
HStack {
if !self.menuOpen {
Button {
print("side menu tapped")
self.menuOpen.toggle()
} label: {
Image("sideMenuButton")
.resizable()
.frame(width: 24, height: 24)
}.padding()
}
Spacer()
}
Text("Home")
.font(.custom(Poppins.semiBold.rawValue, size: 17))
.foregroundColor(Color(ColorName.appBlue.rawValue))
}
ZStack {
SideMenu(width: 300,
isOpen: self.menuOpen,
menuClose: self.openMenu)
}
}
}.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
func openMenu() {
self.menuOpen.toggle()
}
}
This is working fine when I use it without tab bar. Without tab bar it looks like this:
My code for tab bar controller is:
struct TabBarControllerView: View {
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
HomeView()
.tabItem {
Label("Home", image: tabSelection == 0 ? ImageName.home.rawValue : ImageName.silverHome.rawValue)
}
.tag(0)
MyAccountView()
.tabItem {
Label("Claims", image: tabSelection == 1 ? ImageName.claims.rawValue : ImageName.silverClaims.rawValue)
}
.tag(1)
}.accentColor(Color(ColorName.appBlue.rawValue))
}
}
struct TabBarControllerView_Previews: PreviewProvider {
static var previews: some View {
TabBarControllerView()
}
}
Does anyone knows why is it getting the top space? I tried .ignoresSafeArea() and also gave padding in negative, it went to top but the side menu did not tapping because it is getting under that space.
I am been stuck in this for 2 days, any help will be appreciated.
TabView comes with Navigation Bar up top. There is a simple solution by adding .navigationBarHidden(true).
See this post: SwiftUI how to hide navigation bar with TabView
Note: Also in the future give a proper minimum amount of code to reproduce the behavior. Your current code given relies on other components and isn't runnable by itself on a fresh project.
Update:
I created a minimum project to test this and this code works fine with me:
import SwiftUI
#main
struct testApp: App {
var body: some Scene {
WindowGroup {
TabBarView()
}
}
}
struct TabBarView: View {
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(0)
}
}
}
struct HomeView: View {
var body: some View {
ZStack {
Rectangle().foregroundColor(.green)
VStack {
HStack {
Spacer()
Button {
print("side menu tapped")
} label: {
Image(systemName: "pencil")
.resizable()
.frame(width: 24, height: 24)
}.padding()
Spacer()
}
Spacer()
}
}
}
}
Given that this code works fine and the issue is not here, minimum working code sample is needed from OP.

SwiftUI NavigationView trying to pop to missing destination (Monoceros?)

I'm using Xcode 12 with deployment for iOS 14.0.
My home screen has a NavigationView
Within the NavigationView there is a TabView (with 4 tabs)
Within each tab are subviews that have buttons and NavigationLinks
The navigation on the app is functioning correctly (when I click a NavigationLink on one of the subviews, it navigates to the correct view and when I click the back button, it dismisses the view.) However, when I click the back button, the console prints the following error:
Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-103/Shared/NavigationBridge_PhoneTV.swift:337
Aside from the error log, the app is functioning fine, so I'm planning to just ignore the error for now... but I'm wondering what it means? I don't have anything within my code named "Monoceros". I'm guessing it has something to do with the TabView being a subview of the NavigationView?
EDIT:
Several months later, this issue still persists. Here is reproducible code. Open the ContentView(), on the FirstScreen() click on the NavigationLink, then click the back button. It will print out Monoceros lol
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
FirstScreen()
.tabItem {
Text("One")
Image(systemName: "house.fill")
}
Text("Second Screen")
.tabItem {
Text("Two")
Image(systemName: "heart.fill")
}
}
}
}
}
struct FirstScreen: View {
var body: some View {
NavigationLink("Click here", destination: Text("Final Screen"))
// Click the back button on FinalScreen prints:
//Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-120/Shared/NavigationBridge_PhoneTV.swift:341
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Unfortunately this is an active issue with a TabView placed inside NavigationView.
The error would not manifest itself if you placed your NavigationView inside the TabView, but this of course would lead to the tabs being displayed from within your Final Screen, which you are probably trying to avoid.
                                      
There is currently no work-around for this, and as of to date we need to wait for Apple to properly implement the corresponding .navigationBarHidden() of TabViews as well.
Problems and unexpected behaviours have been reported when embedding a TabView into a NavigationView, however if you have tested your app thoroughly and found no particular problems, it is safe to say that you can stick to this method.
Alternatively you will have to build a TabView component manually, like below:
import SwiftUI
enum Tab {
case house, heart
}
struct TabView: View {
#Binding var tabIdx: Tab
var body: some View {
HStack {
Group {
Spacer()
Button (action: {
self.tabIdx = .house
}) {
VStack{
Image(systemName: "house.fill")
Text("House")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .house ? .blue : .secondary)
Spacer()
Button (action: {
self.tabIdx = .heart
}) {
VStack{
Image(systemName: "heart.fill")
Text("Heart")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .heart ? .blue : .secondary)
Spacer()
}
}
.padding(.bottom, 30)
.padding(.top, 10)
.background(Color(red: 0.1, green: 0.1, blue: 0.1))
.font(.system(size: 30))
.frame(height: 80)
}
}
struct FirstScreen: View {
var body: some View {
NavigationLink("Click here", destination: Text("Final Screen"))
.font(.system(size:20))
}
}
struct ContentView: View {
#State var tabIdx: Tab = .house
var body: some View {
NavigationView {
VStack(spacing: 20) {
Spacer()
if tabIdx == .house {
FirstScreen()
} else if tabIdx == .heart {
Text("Second Screen")
}
Spacer(minLength: 0)
TabView(tabIdx: self.$tabIdx)
}
.ignoresSafeArea()
}
}
}
The above bug is well detailed in this blog post, which you could consult for further reference and more examples.

Adding Segmented Style Picker to SwiftUI's NavigationView

The question is as simple as in the title. I am trying to put a Picker which has the style of SegmentedPickerStyle to NavigationBar in SwiftUI. It is just like the native Phone application's history page. The image is below
I have looked for Google and Github for example projects, libraries or any tutorials and no luck. I think if nativa apps and WhatsApp for example has it, then it should be possible. Any help would be appreciated.
SwiftUI 2 + toolbar:
struct DemoView: View {
#State private var mode: Int = 0
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Color", selection: $mode) {
Text("Light").tag(0)
Text("Dark").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
}
You can put a Picker directly into .navigationBarItems.
The only trouble I'm having is getting the Picker to be centered. (Just to show that a Picker can indeed be in the Navigation Bar I put together a kind of hacky solution with frame and Geometry Reader. You'll need to find a proper solution to centering.)
struct ContentView: View {
#State private var choices = ["All", "Missed"]
#State private var choice = 0
#State private var contacts = [("Anna Lisa Moreno", "9:40 AM"), ("Justin Shumaker", "9:35 AM")]
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.contacts, id: \.self.0) { (contact, time) in
ContactView(name: contact, time: time)
}
.onDelete(perform: self.deleteItems)
}
.navigationBarTitle("Recents")
.navigationBarItems(
leading:
HStack {
Button("Clear") {
// do stuff
}
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 130)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, (geometry.size.width / 2.0) - 130)
},
trailing: EditButton())
}
}
}
func deleteItems(at offsets: IndexSet) {
contacts.remove(atOffsets: offsets)
}
}
struct ContactView: View {
var name: String
var time: String
var body: some View {
HStack {
VStack {
Image(systemName: "phone.fill.arrow.up.right")
.font(.headline)
.foregroundColor(.secondary)
Text("")
}
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
Text("iPhone")
.foregroundColor(.secondary)
}
Spacer()
Text(self.time)
.foregroundColor(.secondary)
}
}
}
For those who want to make it dead center, Just put two HStack to each side and made them width fixed and equal.
Add this method to View extension.
extension View {
func navigationBarItems<L, C, T>(leading: L, center: C, trailing: T) -> some View where L: View, C: View, T: View {
self.navigationBarItems(leading:
HStack{
HStack {
leading
}
.frame(width: 60, alignment: .leading)
Spacer()
HStack {
center
}
.frame(width: 300, alignment: .center)
Spacer()
HStack {
//Text("asdasd")
trailing
}
//.background(Color.blue)
.frame(width: 100, alignment: .trailing)
}
//.background(Color.yellow)
.frame(width: UIScreen.main.bounds.size.width-32)
)
}
}
Now you have a View modifier which has the same usage of navigationBatItems(:_). You can edit the code based on your needs.
Usage example:
.navigationBarItems(leading: EmptyView(), center:
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}, trailing: EmptyView())
UPDATE
There was the issue of leading and trailing items were violating UINavigationBarContentView's safeArea. While I was searching through, I came across another solution in this answer. It is little helper library called SwiftUIX. If you do not want install whole library -like me- I created a gist just for navigationBarItems. Just add the file to your project.
But do not forget this, It was stretching the Picker to cover all the free space and forcing StatusView to be narrower. So I had to set frames like this;
.navigationBarItems(center:
Picker(...) {
...
}
.frame(width: 150)
, trailing:
StatusView()
.frame(width: 70)
)
If you need segmentcontroll to be in center you need to use GeometryReader, below code will provide picker as title, and trailing (right) button.
You set up two view on the sides left and right with the same width, and the middle view will take the rest.
5 is the magic number depends how width you need segment to be.
You can experiment and see the best fit for you.
GeometryReader {
Text("TEST")
.navigationBarItems(leading:
HStack {
Spacer().frame(width: geometry.size.width / 5)
Spacer()
picker
Spacer()
Button().frame(width: geometry.size.width / 5)
}.frame(width: geometry.size.width)
}
But better solution is if you save picker size and then calculate other frame sizes, so picker will be same on ipad & iphone
#State var segmentControllerWidth: CGFloat = 0
var body: some View {
HStack {
Spacer()
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
.background(Color.red)
segmentController
.fixedSize()
.background(PreferenceViewSetter())
profileButton
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
}
.onPreferenceChange(PreferenceViewKey.self) { preferences in
segmentControllerWidth = preferences.width
}
}
struct PreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: PreferenceViewKey.self,
value: PreferenceViewData(width: geometry.size.width))
}
}
}
struct PreferenceViewData: Equatable {
let width: CGFloat
}
struct PreferenceViewKey: PreferenceKey {
typealias Value = PreferenceViewData
static var defaultValue = PreferenceViewData(width: 0)
static func reduce(value: inout PreferenceViewData, nextValue: () -> PreferenceViewData) {
value = nextValue()
}
}
Simple answer how to center segment controller and hide one of the buttons.
#State var showLeadingButton = true
var body: some View {
HStack {
Button(action: {}, label: {"leading"})
.opacity(showLeadingButton ? true : false)
Spacer()
Picker(selection: $selectedStatus,
label: Text("SEGMENT") {
segmentValues
}
.id(UUID())
.pickerStyle(SegmentedPickerStyle())
.fixedSize()
Spacer()
Button(action: {}, label: {"trailing"})
}
.frame(width: UIScreen.main.bounds.width)
}